import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:food_ai/l10n/app_localizations.dart'; import 'package:intl/intl.dart'; import '../../shared/models/meal_type.dart'; import '../profile/profile_provider.dart'; import 'menu_provider.dart'; import 'plan_menu_sheet.dart'; /// Bottom sheet that collects the user's date (or date range) and meal type /// choices, then triggers partial menu generation. class PlanDatePickerSheet extends ConsumerStatefulWidget { const PlanDatePickerSheet({ super.key, required this.mode, required this.defaultStart, }); final PlanMode mode; /// First date to pre-select. Typically tomorrow or the day after the last /// planned date. final DateTime defaultStart; @override ConsumerState createState() => _PlanDatePickerSheetState(); } class _PlanDatePickerSheetState extends ConsumerState { late DateTime _selectedDate; late DateTime _rangeStart; late DateTime _rangeEnd; bool _rangeSelectingEnd = false; // For singleMeal mode: selected meal type String? _selectedMealType; bool _loading = false; @override void initState() { super.initState(); _selectedDate = widget.defaultStart; _rangeStart = widget.defaultStart; final windowDays = widget.mode == PlanMode.week ? 6 : 2; _rangeEnd = widget.defaultStart.add(Duration(days: windowDays)); } List get _userMealTypes { final profile = ref.read(profileProvider).valueOrNull; return profile?.mealTypes ?? const ['breakfast', 'lunch', 'dinner']; } // ── Helpers ──────────────────────────────────────────────────────────────── String _formatDate(DateTime date) { final locale = Localizations.localeOf(context).toLanguageTag(); return DateFormat('d MMMM', locale).format(date); } List _buildDateList() { if (widget.mode == PlanMode.singleMeal || widget.mode == PlanMode.singleDay) { return [_formatApiDate(_selectedDate)]; } final dates = []; var current = _rangeStart; while (!current.isAfter(_rangeEnd)) { dates.add(_formatApiDate(current)); current = current.add(const Duration(days: 1)); } return dates; } List _buildMealTypeList() { if (widget.mode == PlanMode.singleMeal) { return _selectedMealType != null ? [_selectedMealType!] : []; } return _userMealTypes; } String _formatApiDate(DateTime date) => '${date.year}-${date.month.toString().padLeft(2, '0')}-' '${date.day.toString().padLeft(2, '0')}'; bool get _canSubmit { if (widget.mode == PlanMode.singleMeal && _selectedMealType == null) { return false; } if ((widget.mode == PlanMode.days || widget.mode == PlanMode.week) && !_rangeEnd.isAfter(_rangeStart.subtract(const Duration(days: 1)))) { return false; } return !_loading; } Future _submit() async { final dates = _buildDateList(); final mealTypes = _buildMealTypeList(); if (dates.isEmpty || mealTypes.isEmpty) return; setState(() => _loading = true); try { await ref.read(planMenuServiceProvider).generateForDates( dates: dates, mealTypes: mealTypes, ); if (mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context)!.planSuccess), behavior: SnackBarBehavior.floating, ), ); } } catch (_) { if (mounted) setState(() => _loading = false); } } // ── Range picker interactions ────────────────────────────────────────────── void _onDayTapped(DateTime date) { if (date.isBefore(DateTime.now().subtract(const Duration(days: 1)))) return; if (widget.mode == PlanMode.week) { // Sliding 7-day window anchored to the tapped day. setState(() { _rangeStart = date; _rangeEnd = date.add(const Duration(days: 6)); _rangeSelectingEnd = false; }); return; } // days mode: first tap = start, second tap = end. if (!_rangeSelectingEnd) { setState(() { _rangeStart = date; _rangeEnd = date.add(const Duration(days: 2)); _rangeSelectingEnd = true; }); } else { if (date.isBefore(_rangeStart)) { setState(() { _rangeStart = date; _rangeSelectingEnd = true; }); } else { setState(() { _rangeEnd = date; _rangeSelectingEnd = false; }); } } } // ── Build ────────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final theme = Theme.of(context); return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: SafeArea( child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Title Text( _sheetTitle(l10n), style: theme.textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 20), // Date selector if (widget.mode == PlanMode.singleMeal || widget.mode == PlanMode.singleDay) _DateStripSelector( selected: _selectedDate, onSelected: (date) => setState(() => _selectedDate = date), ) else _CalendarRangePicker( rangeStart: _rangeStart, rangeEnd: _rangeEnd, onDayTapped: _onDayTapped, ), const SizedBox(height: 16), // Meal type selector (only for singleMeal mode) if (widget.mode == PlanMode.singleMeal) ...[ Text(l10n.planSelectMealType, style: theme.textTheme.titleSmall), const SizedBox(height: 8), _MealTypeChips( mealTypeIds: _userMealTypes, selected: _selectedMealType, onSelected: (id) => setState(() => _selectedMealType = id), ), const SizedBox(height: 16), ], // Summary line for range modes if (widget.mode == PlanMode.days || widget.mode == PlanMode.week) Padding( padding: const EdgeInsets.only(bottom: 12), child: Text( '${_formatDate(_rangeStart)} – ${_formatDate(_rangeEnd)}' ' (${_rangeEnd.difference(_rangeStart).inDays + 1})', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, ), ), // Generate button FilledButton( onPressed: _canSubmit ? _submit : null, child: _loading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text(l10n.planGenerateButton), ), ], ), ), ), ), ); } String _sheetTitle(AppLocalizations l10n) => switch (widget.mode) { PlanMode.singleMeal => l10n.planOptionSingleMeal, PlanMode.singleDay => l10n.planOptionDay, PlanMode.days => l10n.planOptionDays, PlanMode.week => l10n.planOptionWeek, }; } // ── Date strip (horizontal scroll) ──────────────────────────────────────────── class _DateStripSelector extends StatefulWidget { const _DateStripSelector({ required this.selected, required this.onSelected, }); final DateTime selected; final void Function(DateTime) onSelected; @override State<_DateStripSelector> createState() => _DateStripSelectorState(); } class _DateStripSelectorState extends State<_DateStripSelector> { late final ScrollController _scrollController; // Show 30 upcoming days (today excluded, starts tomorrow). static const _futureDays = 30; static const _itemWidth = 64.0; DateTime get _tomorrow => DateTime.now().add(const Duration(days: 1)); List get _dates => List.generate( _futureDays, (index) => _tomorrow.add(Duration(days: index))); @override void initState() { super.initState(); _scrollController = ScrollController(); } @override void dispose() { _scrollController.dispose(); super.dispose(); } bool _isSameDay(DateTime a, DateTime b) => a.year == b.year && a.month == b.month && a.day == b.day; @override Widget build(BuildContext context) { final theme = Theme.of(context); final locale = Localizations.localeOf(context).toLanguageTag(); return SizedBox( height: 72, child: ListView.separated( controller: _scrollController, scrollDirection: Axis.horizontal, itemCount: _dates.length, separatorBuilder: (_, __) => const SizedBox(width: 6), itemBuilder: (context, index) { final date = _dates[index]; final isSelected = _isSameDay(date, widget.selected); return GestureDetector( onTap: () => widget.onSelected(date), child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: _itemWidth, decoration: BoxDecoration( color: isSelected ? theme.colorScheme.primary : theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( DateFormat('E', locale).format(date), style: theme.textTheme.labelSmall?.copyWith( color: isSelected ? theme.colorScheme.onPrimary : theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 2), Text( '${date.day}', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: isSelected ? theme.colorScheme.onPrimary : theme.colorScheme.onSurface, ), ), Text( DateFormat('MMM', locale).format(date), style: theme.textTheme.labelSmall?.copyWith( color: isSelected ? theme.colorScheme.onPrimary : theme.colorScheme.onSurfaceVariant, ), ), ], ), ), ); }, ), ); } } // ── Calendar range picker ────────────────────────────────────────────────────── class _CalendarRangePicker extends StatefulWidget { const _CalendarRangePicker({ required this.rangeStart, required this.rangeEnd, required this.onDayTapped, }); final DateTime rangeStart; final DateTime rangeEnd; final void Function(DateTime) onDayTapped; @override State<_CalendarRangePicker> createState() => _CalendarRangePickerState(); } class _CalendarRangePickerState extends State<_CalendarRangePicker> { late DateTime _displayMonth; @override void initState() { super.initState(); _displayMonth = DateTime(widget.rangeStart.year, widget.rangeStart.month); } void _prevMonth() { final now = DateTime.now(); if (_displayMonth.year == now.year && _displayMonth.month == now.month) { return; // don't go into the past } setState(() { _displayMonth = DateTime(_displayMonth.year, _displayMonth.month - 1); }); } void _nextMonth() { setState(() { _displayMonth = DateTime(_displayMonth.year, _displayMonth.month + 1); }); } bool _isInRange(DateTime date) { final dayOnly = DateTime(date.year, date.month, date.day); final start = DateTime(widget.rangeStart.year, widget.rangeStart.month, widget.rangeStart.day); final end = DateTime(widget.rangeEnd.year, widget.rangeEnd.month, widget.rangeEnd.day); return !dayOnly.isBefore(start) && !dayOnly.isAfter(end); } bool _isRangeStart(DateTime date) => date.year == widget.rangeStart.year && date.month == widget.rangeStart.month && date.day == widget.rangeStart.day; bool _isRangeEnd(DateTime date) => date.year == widget.rangeEnd.year && date.month == widget.rangeEnd.month && date.day == widget.rangeEnd.day; bool _isPast(DateTime date) { final today = DateTime.now(); return date.isBefore(DateTime(today.year, today.month, today.day)); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final locale = Localizations.localeOf(context).toLanguageTag(); final monthLabel = DateFormat('MMMM yyyy', locale).format(_displayMonth); // Build the grid: first day of the month offset + days in month. final firstDay = DateTime(_displayMonth.year, _displayMonth.month, 1); // ISO weekday: Mon=1, Sun=7; leading empty cells before day 1. final leadingBlanks = (firstDay.weekday - 1) % 7; final daysInMonth = DateUtils.getDaysInMonth(_displayMonth.year, _displayMonth.month); final totalCells = leadingBlanks + daysInMonth; return Column( children: [ // Month navigation Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon(Icons.chevron_left), onPressed: _prevMonth, ), Text(monthLabel, style: theme.textTheme.titleMedium), IconButton( icon: const Icon(Icons.chevron_right), onPressed: _nextMonth, ), ], ), // Day-of-week header Row( children: ['M', 'T', 'W', 'T', 'F', 'S', 'S'] .map( (dayLabel) => Expanded( child: Center( child: Text( dayLabel, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), ), ), ) .toList(), ), const SizedBox(height: 4), // Calendar grid GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 7, mainAxisSpacing: 2, crossAxisSpacing: 2, childAspectRatio: 1, ), itemCount: totalCells, itemBuilder: (context, index) { if (index < leadingBlanks) { return const SizedBox.shrink(); } final dayNumber = index - leadingBlanks + 1; final date = DateTime( _displayMonth.year, _displayMonth.month, dayNumber); final inRange = _isInRange(date); final isStart = _isRangeStart(date); final isEnd = _isRangeEnd(date); final isPast = _isPast(date); Color bgColor = Colors.transparent; Color textColor = theme.colorScheme.onSurface; if (isPast) { // ignore: deprecated_member_use textColor = theme.colorScheme.onSurface.withOpacity(0.3); } else if (isStart || isEnd) { bgColor = theme.colorScheme.primary; textColor = theme.colorScheme.onPrimary; } else if (inRange) { bgColor = theme.colorScheme.primaryContainer; textColor = theme.colorScheme.onPrimaryContainer; } return GestureDetector( onTap: isPast ? null : () => widget.onDayTapped(date), child: Container( decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: Text( '$dayNumber', style: theme.textTheme.bodySmall?.copyWith( color: textColor, fontWeight: (isStart || isEnd) ? FontWeight.bold : FontWeight.normal, ), ), ), ); }, ), ], ); } } // ── Meal type chips ──────────────────────────────────────────────────────────── class _MealTypeChips extends StatelessWidget { const _MealTypeChips({ required this.mealTypeIds, required this.selected, required this.onSelected, }); final List mealTypeIds; final String? selected; final void Function(String) onSelected; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Wrap( spacing: 8, runSpacing: 6, children: mealTypeIds.map((mealTypeId) { final option = mealTypeById(mealTypeId); final label = '${option?.emoji ?? ''} ${mealTypeLabel(mealTypeId, l10n)}'.trim(); return ChoiceChip( label: Text(label), selected: selected == mealTypeId, onSelected: (_) => onSelected(mealTypeId), ); }).toList(), ); } }