glp-companion/lib/features/meal_planner/presentation/widgets/assign_slot_dialog.dart

189 lines
5.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../nutrition/domain/entities/meal_type.dart';
import '../../domain/entities/meal_plan_slot.dart';
import '../../domain/entities/recipe.dart';
import '../controllers/meal_plan_calendar_controller.dart';
/// Opens the quick-assign modal for a specific day/meal slot.
///
/// Shows the saved recipes to pick from, plus a free-text override for
/// meals without a saved recipe. Resolves to `true` when a slot was
/// written, otherwise `null`/`false`.
Future<bool?> showAssignSlotDialog(
BuildContext context, {
required DateTime day,
required MealType mealType,
required List<Recipe> savedRecipes,
MealPlanSlot? existingSlot,
}) {
return showDialog<bool>(
context: context,
builder: (context) => AssignSlotDialog(
day: day,
mealType: mealType,
savedRecipes: savedRecipes,
existingSlot: existingSlot,
),
);
}
/// Modal used by the weekly calendar's "+" affordance: assign a saved
/// recipe (via dropdown) or type a quick custom description for the slot.
class AssignSlotDialog extends ConsumerStatefulWidget {
const AssignSlotDialog({
super.key,
required this.day,
required this.mealType,
required this.savedRecipes,
this.existingSlot,
});
final DateTime day;
final MealType mealType;
final List<Recipe> savedRecipes;
final MealPlanSlot? existingSlot;
@override
ConsumerState<AssignSlotDialog> createState() => _AssignSlotDialogState();
}
class _AssignSlotDialogState extends ConsumerState<AssignSlotDialog> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late final TextEditingController _descriptionController;
Recipe? _selectedRecipe;
bool _isSaving = false;
@override
void initState() {
super.initState();
_descriptionController = TextEditingController(
text: widget.existingSlot?.customDescription ?? '',
);
// Pre-select the currently assigned recipe, if any.
final recipeId = widget.existingSlot?.recipeId;
if (recipeId != null) {
for (final recipe in widget.savedRecipes) {
if (recipe.id == recipeId) {
_selectedRecipe = recipe;
break;
}
}
}
}
@override
Widget build(BuildContext context) {
final dayLabel = MaterialLocalizations.of(context).formatCompactDate(widget.day);
return AlertDialog(
title: Text('Assign ${widget.mealType.displayName} — $dayLabel'),
content: SizedBox(
width: 420,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DropdownButtonFormField<Recipe>(
initialValue: _selectedRecipe,
decoration: const InputDecoration(
labelText: 'Saved recipe',
border: OutlineInputBorder(),
),
items: [
for (final recipe in widget.savedRecipes)
DropdownMenuItem<Recipe>(
value: recipe,
child: Text(
recipe.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
onChanged: (recipe) => setState(() => _selectedRecipe = recipe),
),
const SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Description (optional)',
hintText: 'e.g. Leftover chicken & greens',
border: OutlineInputBorder(),
),
textCapitalization: TextCapitalization.sentences,
validator: _validate,
),
],
),
),
),
),
actions: [
TextButton(
onPressed: _isSaving ? null : () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton.icon(
onPressed: _isSaving ? null : _assign,
icon: _isSaving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.event_available_outlined),
label: const Text('Assign'),
),
],
);
}
String? _validate(String? value) {
// A description is only required when no saved recipe is selected.
if (_selectedRecipe == null && (value == null || value.trim().isEmpty)) {
return 'Select a recipe or enter a description';
}
return null;
}
Future<void> _assign() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
setState(() => _isSaving = true);
final controller = ref.read(mealPlanCalendarControllerProvider.notifier);
final description = _descriptionController.text.trim();
final bool saved;
final recipe = _selectedRecipe;
if (recipe != null) {
saved = await controller.assignRecipe(
day: widget.day,
mealType: widget.mealType,
recipeId: recipe.id,
note: description.isEmpty ? null : description,
);
} else {
saved = await controller.saveCustomEntry(
day: widget.day,
mealType: widget.mealType,
description: description,
);
}
if (!mounted) return;
Navigator.of(context).pop(saved);
}
@override
void dispose() {
_descriptionController.dispose();
super.dispose();
}
}