import 'package:flutter/material.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'; /// Responsive 7-day meal-plan grid. /// /// On wide layouts (>= 900 dp) all seven day columns are visible at once; /// compact widths fall back to a tabbed pager with one day per tab. Each /// column header shows the day's scheduled-protein rollup against the daily /// target, and every meal slot offers a "+ Assign Recipe" action that opens /// the searchable Recipe Vault sheet. class WeeklyCalendarView extends StatelessWidget { const WeeklyCalendarView({ super.key, required this.state, required this.onSlotTapped, required this.onTogglePrepared, required this.onClearSlot, }); final MealPlanCalendarState state; /// The slot was tapped — open the searchable assign sheet. final void Function(DateTime day, MealType mealType) onSlotTapped; final void Function(MealPlanSlot slot) onTogglePrepared; final void Function(MealPlanSlot slot) onClearSlot; @override Widget build(BuildContext context) { final days = state.days; return LayoutBuilder( builder: (context, constraints) { final wide = constraints.maxWidth >= 900; if (wide) { return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (final day in days) SizedBox( width: constraints.maxWidth / 7, // Each column scrolls vertically so short windows never // trigger RenderFlex overflow stripes. child: SingleChildScrollView( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 8, ), child: _DayColumn( day: day, state: state, onSlotTapped: onSlotTapped, onTogglePrepared: onTogglePrepared, onClearSlot: onClearSlot, ), ), ), ], ), ); } return _CompactWeekPager( days: days, state: state, onSlotTapped: onSlotTapped, onTogglePrepared: onTogglePrepared, onClearSlot: onClearSlot, ); }, ); } } /// Tabbed pager used below the 900 dp breakpoint: one tab per day. class _CompactWeekPager extends StatefulWidget { const _CompactWeekPager({ required this.days, required this.state, required this.onSlotTapped, required this.onTogglePrepared, required this.onClearSlot, }); final List days; final MealPlanCalendarState state; final void Function(DateTime day, MealType mealType) onSlotTapped; final void Function(MealPlanSlot slot) onTogglePrepared; final void Function(MealPlanSlot slot) onClearSlot; @override State<_CompactWeekPager> createState() => _CompactWeekPagerState(); } class _CompactWeekPagerState extends State<_CompactWeekPager> with SingleTickerProviderStateMixin { late final TabController _tabController = TabController(length: widget.days.length, vsync: this); @override void dispose() { _tabController.dispose(); super.dispose(); } static const List _weekdayLabels = [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', ]; @override Widget build(BuildContext context) { return DefaultTabController( length: widget.days.length, child: Column( children: [ TabBar( controller: _tabController, isScrollable: true, tabAlignment: TabAlignment.start, tabs: [ for (final day in widget.days) Tab(text: '${_weekdayLabels[day.weekday - 1]} ${day.day}'), ], ), Expanded( child: TabBarView( controller: _tabController, children: [ for (final day in widget.days) SingleChildScrollView( padding: const EdgeInsets.all(8), child: _DayColumn( day: day, state: widget.state, onSlotTapped: widget.onSlotTapped, onTogglePrepared: widget.onTogglePrepared, onClearSlot: widget.onClearSlot, ), ), ], ), ), ], ), ); } } /// One day of the weekly grid: header (date + protein rollup indicator) /// followed by the four meal slots. class _DayColumn extends StatelessWidget { const _DayColumn({ required this.day, required this.state, required this.onSlotTapped, required this.onTogglePrepared, required this.onClearSlot, }); final DateTime day; final MealPlanCalendarState state; final void Function(DateTime day, MealType mealType) onSlotTapped; final void Function(MealPlanSlot slot) onTogglePrepared; final void Function(MealPlanSlot slot) onClearSlot; static const List _weekdayLabels = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; @override Widget build(BuildContext context) { final theme = Theme.of(context); final protein = state.proteinForDay(day); final target = state.dailyProteinTargetGrams; final progress = state.proteinProgressForDay(day).clamp(0.0, 1.0); final reachedGoal = protein >= target; final roundedProtein = protein.roundToDouble(); final proteinLabel = roundedProtein == protein ? roundedProtein.toStringAsFixed(0) : protein.toStringAsFixed(1); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( _weekdayLabels[day.weekday - 1], style: theme.textTheme.labelLarge ?.copyWith(fontWeight: FontWeight.bold), ), ), Text( reachedGoal ? '✓ $proteinLabel g' : '$proteinLabel g', style: theme.textTheme.labelSmall?.copyWith( fontWeight: FontWeight.w600, color: reachedGoal ? Colors.green.shade700 : theme.colorScheme.outline, ), ), ], ), const SizedBox(height: 4), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: progress, minHeight: 6, backgroundColor: theme.colorScheme.surfaceContainerHighest, color: reachedGoal ? Colors.green : theme.colorScheme.primary, ), ), ], ), ), const Divider(height: 12), for (final mealType in MealType.values) ...[ _DaySlotCard( mealType: mealType, slot: state.slotFor(day, mealType), recipe: _recipeForSlot(state.slotFor(day, mealType)), onTap: () => onSlotTapped(day, mealType), onTogglePrepared: onTogglePrepared, onClearSlot: onClearSlot, ), const SizedBox(height: 8), ], ], ); } Recipe? _recipeForSlot(MealPlanSlot? slot) => slot == null ? null : state.recipeFor(slot); } /// A single meal slot: tap target for the searchable assign sheet; shows /// the assigned recipe (with protein badge) or a free-text note, a /// prepared checkbox, and a clear action. class _DaySlotCard extends StatelessWidget { const _DaySlotCard({ required this.mealType, required this.slot, required this.recipe, required this.onTap, required this.onTogglePrepared, required this.onClearSlot, }); final MealType mealType; final MealPlanSlot? slot; final Recipe? recipe; final VoidCallback onTap; final void Function(MealPlanSlot slot) onTogglePrepared; final void Function(MealPlanSlot slot) onClearSlot; static IconData _iconFor(MealType type) { switch (type) { case MealType.breakfast: return Icons.free_breakfast_outlined; case MealType.lunch: return Icons.lunch_dining_outlined; case MealType.dinner: return Icons.dinner_dining_outlined; case MealType.snack: return Icons.cookie_outlined; } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final currentSlot = slot; final filled = currentSlot != null && !currentSlot.isEmpty; return Material( color: theme.colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(12), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: onTap, child: Container( constraints: const BoxConstraints(minHeight: 64), padding: const EdgeInsets.all(8), child: _buildContent(theme, currentSlot, filled), ), ), ); } Widget _buildContent( ThemeData theme, MealPlanSlot? currentSlot, bool filled, ) { final assignedRecipe = recipe; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(_iconFor(mealType), size: 16), const SizedBox(width: 6), Expanded( child: Text( mealType.displayName, style: theme.textTheme.labelMedium ?.copyWith(fontWeight: FontWeight.w600), ), ), if (currentSlot != null && filled) ...[ Checkbox( value: currentSlot.isPrepared, onChanged: (_) => onTogglePrepared(currentSlot), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, ), PopupMenuButton( padding: EdgeInsets.zero, icon: const Icon(Icons.more_vert, size: 18), onSelected: (value) { if (value == 'clear') onClearSlot(currentSlot); }, itemBuilder: (context) => const [ PopupMenuItem(value: 'clear', child: Text('Clear slot')), ], ), ] else Icon(Icons.add, size: 18, color: theme.colorScheme.outline), ], ), if (currentSlot != null && filled) ...[ const SizedBox(height: 2), Text( assignedRecipe?.title ?? currentSlot.customDescription ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( fontWeight: FontWeight.w500, decoration: currentSlot.isPrepared ? TextDecoration.lineThrough : null, ), ), if (assignedRecipe != null) ...[ const SizedBox(height: 4), Row( children: [ _MiniProteinBadge( proteinGrams: assignedRecipe.macrosPerServing.proteinGrams, ), const SizedBox(width: 6), Expanded( child: Text( '${assignedRecipe.macrosPerServing.calories} kcal', style: theme.textTheme.labelSmall, overflow: TextOverflow.ellipsis, ), ), ], ), ], ] else Row( children: [ Icon(Icons.add_circle_outline, size: 14, color: theme.colorScheme.primary), const SizedBox(width: 4), Expanded( child: Text( 'Assign Recipe', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.labelSmall?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.primary, ), ), ), ], ), ], ); } } /// Tiny protein badge used inside calendar slots. class _MiniProteinBadge extends StatelessWidget { const _MiniProteinBadge({required this.proteinGrams}); final double proteinGrams; @override Widget build(BuildContext context) { final rounded = proteinGrams.roundToDouble(); final label = rounded == proteinGrams ? '${rounded.toStringAsFixed(0)}g P' : '${proteinGrams.toStringAsFixed(1)}g P'; return Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(6), ), child: Text( label, style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.green.shade800, ), ), ); } }