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 searchable "Assign Recipe" modal sheet for a day/meal slot. /// /// Populated from the Recipe Vault catalog; resolves to `true` when a slot /// was written, otherwise `null`/`false`. Future showAssignRecipeSheet( BuildContext context, { required DateTime day, required MealType mealType, required List savedRecipes, MealPlanSlot? existingSlot, }) { return showModalBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, builder: (context) => AssignRecipeSheet( day: day, mealType: mealType, savedRecipes: savedRecipes, existingSlot: existingSlot, ), ); } /// Modal bottom sheet: search field over the Recipe Vault plus a free-text /// custom entry fallback for meals without a saved recipe. class AssignRecipeSheet extends ConsumerStatefulWidget { const AssignRecipeSheet({ super.key, required this.day, required this.mealType, required this.savedRecipes, this.existingSlot, }); final DateTime day; final MealType mealType; final List savedRecipes; final MealPlanSlot? existingSlot; @override ConsumerState createState() => _AssignRecipeSheetState(); } class _AssignRecipeSheetState extends ConsumerState { final TextEditingController _searchController = TextEditingController(); final TextEditingController _customController = TextEditingController(); String _query = ''; bool _isSaving = false; @override void initState() { super.initState(); _customController.text = widget.existingSlot?.customDescription ?? ''; } @override void dispose() { _searchController.dispose(); _customController.dispose(); super.dispose(); } List get _filtered { final query = _query.trim().toLowerCase(); if (query.isEmpty) return widget.savedRecipes; return widget.savedRecipes .where((recipe) => recipe.title.toLowerCase().contains(query)) .toList(growable: false); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final dayLabel = MaterialLocalizations.of(context).formatCompactDate(widget.day); final bottomInset = MediaQuery.of(context).viewInsets.bottom; final filtered = _filtered; return Padding( padding: EdgeInsets.only(left: 16, right: 16, bottom: bottomInset + 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Assign ${widget.mealType.displayName} — $dayLabel', style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), TextField( controller: _searchController, decoration: const InputDecoration( labelText: 'Search Recipe Vault', hintText: 'e.g. chicken', border: OutlineInputBorder(), isDense: true, prefixIcon: Icon(Icons.search), ), onChanged: (value) => setState(() => _query = value), ), const SizedBox(height: 12), if (widget.savedRecipes.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Text( 'No saved recipes yet — import one from the Recipe Vault.', style: theme.textTheme.bodySmall ?.copyWith(fontStyle: FontStyle.italic), ), ) else if (filtered.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Text( 'No recipes match "$_query".', style: theme.textTheme.bodySmall ?.copyWith(fontStyle: FontStyle.italic), ), ) else ConstrainedBox( constraints: const BoxConstraints(maxHeight: 280), child: ListView.separated( shrinkWrap: true, itemCount: filtered.length, separatorBuilder: (context, index) => const Divider(height: 1), itemBuilder: (context, index) { final recipe = filtered[index]; final protein = recipe.macrosPerServing.proteinGrams.round(); return ListTile( dense: true, enabled: !_isSaving, leading: const Icon(Icons.menu_book_outlined), title: Text( recipe.title, maxLines: 1, overflow: TextOverflow.ellipsis, ), trailing: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(8), ), child: Text( '${protein}g P', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: Colors.green.shade800, ), ), ), onTap: () => _assignRecipe(recipe), ); }, ), ), const SizedBox(height: 12), const Divider(), TextField( controller: _customController, enabled: !_isSaving, decoration: const InputDecoration( labelText: 'Custom entry (no recipe)', hintText: 'e.g. Leftover chicken & greens', border: OutlineInputBorder(), isDense: true, ), textCapitalization: TextCapitalization.sentences, ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: _isSaving ? null : () => Navigator.of(context).pop(), child: const Text('Cancel'), ), const SizedBox(width: 8), FilledButton.icon( onPressed: _isSaving ? null : _saveCustom, icon: _isSaving ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.edit_outlined), label: const Text('Save Custom'), ), ], ), ], ), ); } Future _assignRecipe(Recipe recipe) async { setState(() => _isSaving = true); final saved = await ref .read(mealPlanCalendarControllerProvider.notifier) .assignRecipe( day: widget.day, mealType: widget.mealType, recipeId: recipe.id, ); if (!mounted) return; Navigator.of(context).pop(saved); } Future _saveCustom() async { final description = _customController.text.trim(); if (description.isEmpty) return; setState(() => _isSaving = true); final saved = await ref .read(mealPlanCalendarControllerProvider.notifier) .saveCustomEntry( day: widget.day, mealType: widget.mealType, description: description, ); if (!mounted) return; Navigator.of(context).pop(saved); } }