import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:social_app/core/l10n/l10n.dart'; import '../../../../app/di/injection.dart'; import '../../../../app/router/app_routes.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../features/calendar/data/repositories/calendar_repository.dart'; import '../../../../shared/widgets/app_pressable.dart'; import '../../../../shared/widgets/bottom_dock.dart'; import '../../../../shared/state/calendar_state_manager.dart'; import '../../../../app/router/home_return_policy.dart'; import '../calendar_time_utils.dart'; import '../utils/event_color_resolver.dart'; import '../../../../features/calendar/data/models/schedule_item_model.dart'; class CalendarMonthScreen extends StatefulWidget { final bool resetToToday; const CalendarMonthScreen({super.key, this.resetToToday = false}); @override State createState() => _CalendarMonthScreenState(); } class _CalendarMonthScreenState extends State with WidgetsBindingObserver { late final CalendarStateManager _calendarManager; late DateTime _currentMonth; late DateTime _selectedDate; final Map> _eventsByDay = {}; ColorScheme get _colorScheme => Theme.of(context).colorScheme; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _calendarManager = sl(); if (widget.resetToToday) { _calendarManager.resetToToday(); } final savedDate = _calendarManager.selectedDate; _selectedDate = savedDate; _currentMonth = DateTime(savedDate.year, savedDate.month, 1); _loadMonthEvents(); } Future _loadMonthEvents({bool forceRefresh = false}) async { final events = await sl().getMonthEvents( _currentMonth, forceRefresh: forceRefresh, ); if (!mounted) { return; } _eventsByDay.clear(); for (final event in events.where( (e) => e.status != ScheduleStatus.archived, )) { final key = formatYmd(event.startAt); _eventsByDay[key] = [...(_eventsByDay[key] ?? const []), event]; } setState(() {}); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _loadMonthEvents(forceRefresh: true); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _colorScheme.surface, body: PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { if (!didPop) { returnToHomePreserveState(context, forceGoHome: true); } }, child: SafeArea( child: Column( children: [ _buildHeader(), Expanded( child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.only(bottom: 84), child: _buildMonthContent(), ), ), ), _buildBottomDock(), ], ), ), ), ); } Widget _buildHeader() { final l10n = context.l10n; final today = DateTime.now(); final isNotToday = !isSameDay(_selectedDate, today); return SizedBox( height: 76, child: Padding( padding: const EdgeInsets.only(left: 16, right: 16, top: 14, bottom: 6), child: Column( children: [ SizedBox( height: 56, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ AppPressable( borderRadius: BorderRadius.circular(AppRadius.md), onTap: _showMonthPicker, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 160), switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeOut, child: Text( l10n.calendarMonthHeader(_currentMonth.month), key: ValueKey(_currentMonth.month), style: TextStyle( fontSize: 22, fontWeight: FontWeight.w700, color: _colorScheme.onSurface, ), ), ), const SizedBox(width: 6), Icon( LucideIcons.chevronDown, size: 16, color: _colorScheme.onSurface, ), ], ), ), const Spacer(), if (isNotToday) AppPressable( borderRadius: BorderRadius.circular(AppRadius.xl), onTap: _goToToday, child: Container( height: 36, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all( color: _colorScheme.outlineVariant, ), ), child: Center( child: Text( l10n.calendarMonthToday, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: _colorScheme.onSurface, ), ), ), ), ), if (isNotToday) const SizedBox(width: 8), AppPressable( borderRadius: BorderRadius.circular(AppRadius.full), onTap: () async { final changed = await context.push( AppRoutes.calendarEventCreate, ); if (changed == true) { await _loadMonthEvents(forceRefresh: true); } }, child: Container( width: 36, height: 36, decoration: BoxDecoration( color: _colorScheme.primary, borderRadius: BorderRadius.circular(AppRadius.full), ), child: Icon( LucideIcons.plus, size: 20, color: _colorScheme.onPrimary, ), ), ), ], ), ), ], ), ), ); } void _goToToday() { final today = DateTime.now(); final targetMonth = DateTime(today.year, today.month, 1); setState(() { _selectedDate = today; if (_currentMonth.year != targetMonth.year || _currentMonth.month != targetMonth.month) { _currentMonth = targetMonth; } }); _calendarManager.setSelectedDate(today); _loadMonthEvents(forceRefresh: true); } Widget _buildMonthContent() { return Column( children: [ _buildWeekdayHeader(), Container(height: 1, color: _colorScheme.outlineVariant), ..._buildWeeks(), ], ); } Widget _buildWeekdayHeader() { final l10n = context.l10n; final List weekdays = [ l10n.calendarMonthWeekdaySunShort, l10n.calendarMonthWeekdayMonShort, l10n.calendarMonthWeekdayTueShort, l10n.calendarMonthWeekdayWedShort, l10n.calendarMonthWeekdayThuShort, l10n.calendarMonthWeekdayFriShort, l10n.calendarMonthWeekdaySatShort, ]; return SizedBox( height: 40, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: weekdays .map( (day) => SizedBox( width: 36, child: Center( child: Text( day, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: _colorScheme.onSurfaceVariant, ), ), ), ), ) .toList(), ), ), ); } List _buildWeeks() { final firstDayOfMonth = DateTime( _currentMonth.year, _currentMonth.month, 1, ); final lastDayOfMonth = DateTime( _currentMonth.year, _currentMonth.month + 1, 0, ); final startWeekday = firstDayOfMonth.weekday % 7; final daysInMonth = lastDayOfMonth.day; final totalCells = ((daysInMonth + startWeekday) / 7).ceil() * 7; final weeks = []; for (var weekStart = 0; weekStart < totalCells; weekStart += 7) { weeks.add(_buildWeekRow(weekStart, startWeekday, daysInMonth)); if (weekStart + 7 < totalCells) { weeks.add(Container(height: 1, color: _colorScheme.outlineVariant)); } } return weeks; } Widget _buildWeekRow(int weekStart, int startWeekday, int daysInMonth) { return Padding( padding: const EdgeInsets.only(left: 16, right: 16, top: 14, bottom: 0), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate(7, (index) { final dayIndex = weekStart + index - startWeekday + 1; if (dayIndex < 1 || dayIndex > daysInMonth) { return SizedBox(width: 36, height: 36); } final date = DateTime( _currentMonth.year, _currentMonth.month, dayIndex, ); final isSelected = isSameDay(_selectedDate, date); return AppPressable( borderRadius: BorderRadius.circular(AppRadius.full), onTap: () { setState(() { _selectedDate = date; }); _calendarManager.setSelectedDate(date); _calendarManager.setViewType(CalendarViewType.month); context.push( '${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}', ); }, child: Container( width: 36, height: 36, decoration: BoxDecoration( color: isSelected ? _colorScheme.primaryContainer : _colorScheme.surface.withValues(alpha: 0), borderRadius: BorderRadius.circular(AppRadius.full), ), child: Center( child: Text( '$dayIndex', style: TextStyle( fontSize: 15, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, color: isSelected ? _colorScheme.primary : _colorScheme.onSurface, ), ), ), ), ); }), ), const SizedBox(height: 10), _buildWeekEvents(weekStart, startWeekday, daysInMonth), ], ), ); } Widget _buildWeekEvents(int weekStart, int startWeekday, int daysInMonth) { final firstDayOfMonth = DateTime( _currentMonth.year, _currentMonth.month, 1, ); final weekFirstDate = firstDayOfMonth.add( Duration(days: weekStart - startWeekday), ); return SizedBox( height: 70, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate(7, (index) { final dayIndex = weekStart + index - startWeekday + 1; if (dayIndex < 1 || dayIndex > daysInMonth) { return const SizedBox(width: 38, height: 1); } final date = weekFirstDate.add(Duration(days: index)); final events = _eventsByDay[formatYmd(date)] ?? const []; final displayEvents = events.take(2).toList(); final remainingCount = events.length - 2; return SizedBox( width: 38, height: 70, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ...displayEvents.map((event) { final color = resolveEventColor( status: event.status, colorHex: event.metadata?.color, ); return AppPressable( borderRadius: BorderRadius.circular(AppRadius.sm), onTap: () async { _calendarManager.setSelectedDate(date); final changed = await context.push( '/calendar/events/${event.id}', ); if (changed == true) { await _loadMonthEvents(forceRefresh: true); } }, child: Container( margin: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2, ), decoration: BoxDecoration( color: color.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: Text( event.title, style: TextStyle( fontSize: 9, color: color, fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ); }), if (remainingCount > 0) AppPressable( borderRadius: BorderRadius.circular(AppRadius.sm), onTap: () { _calendarManager.setSelectedDate(date); _calendarManager.setViewType(CalendarViewType.day); context.push( '${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}', ); }, child: Text( '+$remainingCount', style: TextStyle( fontSize: 9, color: _colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, ), ), ), ], ), ); }), ), ); } void _showMonthPicker() { final l10n = context.l10n; var selectedYear = _currentMonth.year; var selectedMonth = _currentMonth.month; showModalBottomSheet( context: context, backgroundColor: _colorScheme.surface, builder: (context) { return StatefulBuilder( builder: (context, setSheetState) { return SizedBox( height: 300, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(l10n.commonCancel), ), TextButton( onPressed: () { Navigator.pop(context); setState(() { _currentMonth = DateTime( selectedYear, selectedMonth, 1, ); _selectedDate = DateTime( selectedYear, selectedMonth, 1, ); }); _calendarManager.setSelectedDate(_selectedDate); _loadMonthEvents(); }, child: Text(l10n.commonConfirm), ), ], ), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: CupertinoPicker( itemExtent: 40, scrollController: FixedExtentScrollController( initialItem: _currentMonth.year - 2020, ), onSelectedItemChanged: (index) { setSheetState(() { selectedYear = 2020 + index; }); }, children: List.generate(20, (index) { return Center( child: Text( l10n.calendarMonthYearLabel(2020 + index), ), ); }), ), ), Expanded( child: CupertinoPicker( itemExtent: 40, scrollController: FixedExtentScrollController( initialItem: _currentMonth.month - 1, ), onSelectedItemChanged: (index) { setSheetState(() { selectedMonth = index + 1; }); }, children: List.generate(12, (index) { return Center( child: Text( l10n.calendarMonthHeader(index + 1), ), ); }), ), ), ], ), ), ], ), ); }, ); }, ); } Widget _buildBottomDock() { return BottomDock( activeTab: DockTab.calendar, onTodoTap: () { _calendarManager.setViewType(CalendarViewType.month); context.push(AppRoutes.todoList); }, onCalendarTap: () {}, onHomeTap: () => returnToHomePreserveState(context, forceGoHome: true), ); } }