import 'package:christian_period_tracker/models/scripture.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:table_calendar/table_calendar.dart'; import '../../models/user_profile.dart'; import '../../models/cycle_entry.dart'; import '../../providers/user_provider.dart'; import '../../services/cycle_service.dart'; import '../../theme/app_theme.dart'; import '../log/log_screen.dart'; import 'package:uuid/uuid.dart'; import '../../widgets/protected_wrapper.dart'; class CalendarScreen extends ConsumerStatefulWidget { final bool readOnly; const CalendarScreen({ super.key, this.readOnly = false, }); @override ConsumerState createState() => _CalendarScreenState(); } enum PredictionMode { short, regular, long } class _CalendarScreenState extends ConsumerState { DateTime _focusedDay = DateTime.now(); DateTime? _selectedDay; CalendarFormat _calendarFormat = CalendarFormat.month; PredictionMode _predictionMode = PredictionMode.regular; @override Widget build(BuildContext context) { final entries = ref.watch(cycleEntriesProvider); final user = ref.watch(userProfileProvider); final isIrregular = user?.isIrregularCycle ?? false; int cycleLength = user?.averageCycleLength ?? 28; if (isIrregular) { if (_predictionMode == PredictionMode.short) { cycleLength = user?.minCycleLength ?? 25; } else if (_predictionMode == PredictionMode.long) { cycleLength = user?.maxCycleLength ?? 35; } } final lastPeriodStart = user?.lastPeriodStartDate; return ProtectedContentWrapper( title: 'Calendar', isProtected: user?.isCalendarProtected ?? false, userProfile: user, child: SafeArea( child: SingleChildScrollView( child: Column( children: [ // Header Padding( padding: const EdgeInsets.all(20), child: Column( children: [ Row( children: [ Expanded( child: Text( 'Calendar', style: GoogleFonts.outfit( fontSize: 28, fontWeight: FontWeight.w600, color: AppColors.charcoal, ), ), ), _buildLegendButton(), ], ), if (isIrregular) ...[ const SizedBox(height: 16), _buildPredictionToggle(), ], ], ), ), // Calendar Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 15, offset: const Offset(0, 5), ), ], ), child: TableCalendar( firstDay: DateTime.now().subtract(const Duration(days: 365)), lastDay: DateTime.now().add(const Duration(days: 365)), focusedDay: _focusedDay, calendarFormat: _calendarFormat, selectedDayPredicate: (day) => isSameDay(_selectedDay, day), onDaySelected: (selectedDay, focusedDay) { setState(() { _selectedDay = selectedDay; _focusedDay = focusedDay; }); }, onFormatChanged: (format) { setState(() => _calendarFormat = format); }, onPageChanged: (focusedDay) { _focusedDay = focusedDay; }, calendarStyle: CalendarStyle( outsideDaysVisible: false, defaultTextStyle: GoogleFonts.outfit( fontSize: 14, color: Theme.of(context).textTheme.bodyMedium?.color ?? AppColors.charcoal, ), weekendTextStyle: GoogleFonts.outfit( fontSize: 14, color: Theme.of(context).textTheme.bodyMedium?.color ?? AppColors.charcoal, ), todayDecoration: BoxDecoration( color: AppColors.sageGreen.withOpacity(0.3), shape: BoxShape.circle, ), todayTextStyle: GoogleFonts.outfit( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.sageGreen, ), selectedDecoration: const BoxDecoration( color: AppColors.sageGreen, shape: BoxShape.circle, ), selectedTextStyle: GoogleFonts.outfit( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white, ), ), headerStyle: HeaderStyle( formatButtonVisible: false, titleCentered: true, titleTextStyle: GoogleFonts.outfit( fontSize: 18, fontWeight: FontWeight.w600, color: Theme.of(context).textTheme.titleLarge?.color ?? AppColors.charcoal, ), leftChevronIcon: Icon( Icons.chevron_left, color: Theme.of(context).iconTheme.color ?? AppColors.warmGray, ), rightChevronIcon: Icon( Icons.chevron_right, color: Theme.of(context).iconTheme.color ?? AppColors.warmGray, ), ), daysOfWeekStyle: DaysOfWeekStyle( weekdayStyle: GoogleFonts.outfit( fontSize: 12, fontWeight: FontWeight.w500, color: Theme.of(context).textTheme.bodySmall?.color ?? AppColors.warmGray, ), weekendStyle: GoogleFonts.outfit( fontSize: 12, fontWeight: FontWeight.w500, color: Theme.of(context).textTheme.bodySmall?.color ?? AppColors.warmGray, ), ), calendarBuilders: CalendarBuilders( defaultBuilder: (context, day, focusedDay) { return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isSelected: false, isToday: false); }, todayBuilder: (context, day, focusedDay) { return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isToday: true); }, selectedBuilder: (context, day, focusedDay) { return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isSelected: true); }, markerBuilder: (context, date, events) { final entry = _getEntryForDate(date, entries); if (entry == null) { final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength); if (phase != null) { return Positioned( bottom: 4, child: Container( width: 5, height: 5, decoration: BoxDecoration( color: _getPhaseColor(phase), shape: BoxShape.circle, ), ), ); } return null; } // If we have an entry, show icons/markers return Positioned( bottom: 4, child: Row( mainAxisSize: MainAxisSize.min, children: [ if (entry.isPeriodDay) Container( width: 6, height: 6, margin: const EdgeInsets.symmetric(horizontal: 1), decoration: const BoxDecoration( color: AppColors.menstrualPhase, shape: BoxShape.circle, ), ), if (entry.mood != null || entry.energyLevel != 3 || entry.hasSymptoms) Container( width: 6, height: 6, margin: const EdgeInsets.symmetric(horizontal: 1), decoration: const BoxDecoration( color: AppColors.softGold, shape: BoxShape.circle, ), ), ], ), ); }, ), ), ), const SizedBox(height: 24), // Divider / Header for Day Info if (_selectedDay != null) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( children: [ Text( 'Daily Log', style: GoogleFonts.outfit( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.warmGray, letterSpacing: 1, ), ), const SizedBox(width: 12), const Expanded(child: Divider(color: AppColors.lightGray)), ], ), ), const SizedBox(height: 12), // Day Info (No longer Expanded) _buildDayInfo( _selectedDay!, lastPeriodStart, cycleLength, entries, user), const SizedBox(height: 40), // Bottom padding ], ], ), ), )); } Widget _buildPredictionToggle() { return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: AppColors.lightGray.withOpacity(0.5), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ _buildToggleItem(PredictionMode.short, 'Short (-)', AppColors.menstrualPhase), _buildToggleItem(PredictionMode.regular, 'Regular', AppColors.sageGreen), _buildToggleItem(PredictionMode.long, 'Long (+)', AppColors.lutealPhase), ], ), ); } Widget _buildToggleItem(PredictionMode mode, String label, Color color) { final isSelected = _predictionMode == mode; return Expanded( child: GestureDetector( onTap: () => setState(() => _predictionMode = mode), child: Container( padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: isSelected ? Colors.white : Colors.transparent, borderRadius: BorderRadius.circular(8), boxShadow: isSelected ? [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ) ] : null, ), child: Text( label, textAlign: TextAlign.center, style: GoogleFonts.outfit( fontSize: 13, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, color: isSelected ? color : AppColors.warmGray, ), ), ), ), ); } Widget _buildLegendButton() { return GestureDetector( onTap: () => _showLegend(), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: AppColors.blushPink.withOpacity(0.5), borderRadius: BorderRadius.circular(20), ), child: Row( children: [ Icon(Icons.info_outline, size: 16, color: AppColors.rose), const SizedBox(width: 4), Text( 'Legend', style: GoogleFonts.outfit( fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.rose, ), ), ], ), ), ); } void _showLegend() { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) => Container( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Legend', style: GoogleFonts.outfit( fontSize: 20, fontWeight: FontWeight.w600, color: AppColors.charcoal, ), ), const SizedBox(height: 20), _buildLegendItem(AppColors.menstrualPhase, 'Period'), _buildLegendItem(AppColors.follicularPhase, 'Follicular Phase'), _buildLegendItem(AppColors.ovulationPhase, 'Ovulation Window'), _buildLegendItem(AppColors.lutealPhase, 'Luteal Phase'), const SizedBox(height: 20), ], ), ), ); } Widget _buildLegendItem(Color color, String label) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Container( width: 16, height: 16, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), const SizedBox(width: 12), Text( label, style: GoogleFonts.outfit( fontSize: 14, color: AppColors.charcoal, ), ), ], ), ); } Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart, int cycleLength, List entries, UserProfile? user) { final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength); final entry = _getEntryForDate(date, entries); return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${_getMonthName(date.month)} ${date.day}, ${date.year}', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w600, ), ), if (entry != null) const Icon(Icons.check_circle, color: AppColors.sageGreen, size: 20), ], ), const SizedBox(height: 16), if (phase != null) Padding( padding: const EdgeInsets.only(bottom: 16), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: _getPhaseColor(phase).withOpacity(0.15), borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(phase.emoji), const SizedBox(width: 6), Text( phase.label, style: GoogleFonts.outfit( fontSize: 14, fontWeight: FontWeight.w500, color: _getPhaseColor(phase), ), ), ], ), ), ), if (entry == null) ...[ Text( phase?.description ?? 'No data for this date', style: Theme.of(context) .textTheme .bodyMedium ?.copyWith(color: AppColors.warmGray), ), if (user?.isPadTrackingEnabled == true && phase != CyclePhase.menstrual && (user?.padSupplies?.any((s) => s.type == PadType.pantyLiner) ?? false)) ...[ const SizedBox(height: 16), _buildPantylinerPrompt(date, null), ], ] else ...[ // Period Detail if (entry.isPeriodDay) _buildDetailRow(Icons.water_drop, 'Period Day', AppColors.menstrualPhase, value: entry.flowIntensity?.label), // Mood Detail if (entry.mood != null) _buildDetailRow( Icons.emoji_emotions_outlined, 'Mood', AppColors.softGold, value: '${entry.mood!.emoji} ${entry.mood!.label}'), // Energy Detail _buildDetailRow( Icons.flash_on, 'Energy Level', AppColors.follicularPhase, value: _getEnergyLabel(entry.energyLevel)), // Symptoms if (entry.hasSymptoms) _buildDetailRow( Icons.healing_outlined, 'Symptoms', AppColors.lavender, value: _getSymptomsString(entry)), // Contextual Recommendation _buildRecommendation(entry), // Pad Tracking Specifics (Not shared with husband) if (user?.isPadTrackingEnabled == true) ...[ const SizedBox(height: 16), if (entry.usedPantyliner) _buildDetailRow(Icons.layers_outlined, 'Supplies Used', AppColors.menstrualPhase, value: '${entry.pantylinerCount}'), if (!entry.usedPantyliner && !entry.isPeriodDay) _buildPantylinerPrompt(date, entry), ], // Notes if (entry.notes?.isNotEmpty == true) Padding( padding: const EdgeInsets.only(top: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Notes', style: GoogleFonts.outfit( fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.warmGray)), const SizedBox(height: 4), Text(entry.notes!, style: GoogleFonts.outfit(fontSize: 14)), ], ), ), ], if (user?.isPadTrackingEnabled == true) ...[ const SizedBox(height: 16), _buildManualSupplyEntryButton(date), ], const SizedBox(height: 24), // Action Buttons if (!widget.readOnly) Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => Scaffold( appBar: AppBar( title: Text( 'Log for ${_getMonthName(date.month)} ${date.day}'), ), body: LogScreen(initialDate: date), ), ), ); }, icon: Icon(entry != null ? Icons.edit_note : Icons.add_circle_outline), label: Text(entry != null ? 'Edit Log' : 'Add Log'), style: ElevatedButton.styleFrom( backgroundColor: AppColors.sageGreen, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), ), ), ], ), ], ), ); } Widget _buildPantylinerPrompt(DateTime date, CycleEntry? entry) { return Container( margin: const EdgeInsets.only(top: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.menstrualPhase.withOpacity(0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.2)), ), child: Row( children: [ const Icon(Icons.help_outline, color: AppColors.menstrualPhase, size: 20), const SizedBox(width: 12), Expanded( child: Text( 'Did you use pantyliners today?', style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal), ), ), TextButton( onPressed: () { if (entry != null) { ref.read(cycleEntriesProvider.notifier).updateEntry( entry.copyWith(usedPantyliner: true, pantylinerCount: 1), ); } else { final newEntry = CycleEntry( id: const Uuid().v4(), date: date, usedPantyliner: true, pantylinerCount: 1, createdAt: DateTime.now(), updatedAt: DateTime.now(), ); ref.read(cycleEntriesProvider.notifier).addEntry(newEntry); } }, child: Text('Yes', style: GoogleFonts.outfit(color: AppColors.menstrualPhase, fontWeight: FontWeight.bold)), ), ], ), ); } Widget _buildManualSupplyEntryButton(DateTime date) { return SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () { // Open a simplified version of the supply management or just log a change ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Supply usage recorded manually.')), ); }, icon: const Icon(Icons.add_shopping_cart, size: 18), label: const Text('Manual Supply Entry'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.menstrualPhase, side: const BorderSide(color: AppColors.menstrualPhase), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ); } Widget _buildRecommendation(CycleEntry entry) { final scripture = ScriptureDatabase().getRecommendedScripture(entry); if (scripture == null) return const SizedBox.shrink(); final user = ref.read(userProfileProvider); final translation = user?.bibleTranslation ?? BibleTranslation.esv; final isDark = Theme.of(context).brightness == Brightness.dark; return Container( margin: const EdgeInsets.only(top: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.softGold.withOpacity(isDark ? 0.15 : 0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.softGold.withOpacity(0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.auto_awesome, color: AppColors.softGold, size: 18), const SizedBox(width: 8), Text( 'Daily Encouragement', style: GoogleFonts.outfit( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.softGold, ), ), ], ), const SizedBox(height: 12), Text( '"${scripture.getVerse(translation)}"', style: GoogleFonts.lora( fontSize: 14, fontStyle: FontStyle.italic, color: isDark ? Colors.white : AppColors.charcoal, height: 1.5, ), ), const SizedBox(height: 8), Text( '— ${scripture.reference}', style: GoogleFonts.outfit( fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.warmGray, ), ), ], ), ); } Widget _buildDetailRow(IconData icon, String label, Color color, {String? value}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: color, size: 18), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: GoogleFonts.outfit( fontSize: 12, color: AppColors.warmGray, ), ), if (value != null) Text( value, style: GoogleFonts.outfit( fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), ], ), ); } String _getSymptomsString(CycleEntry entry) { List s = []; if (entry.crampIntensity != null && entry.crampIntensity! > 0) s.add('Cramps (${entry.crampIntensity}/5)'); if (entry.hasHeadache) s.add('Headache'); if (entry.hasBloating) s.add('Bloating'); if (entry.hasBreastTenderness) s.add('Breast Tenderness'); if (entry.hasFatigue) s.add('Fatigue'); if (entry.hasAcne) s.add('Acne'); return s.join(', '); } String _getEnergyLabel(int? energyLevel) { if (energyLevel == null) return 'Not logged'; if (energyLevel <= 1) return 'Very Low'; if (energyLevel == 2) return 'Low'; if (energyLevel == 3) return 'Neutral'; if (energyLevel == 4) return 'High'; return 'Very High'; } CyclePhase? _getPhaseForDate( DateTime date, DateTime? lastPeriodStart, int cycleLength) { if (lastPeriodStart == null) return null; final daysSinceLastPeriod = date.difference(lastPeriodStart).inDays; if (daysSinceLastPeriod < 0) return null; final dayOfCycle = (daysSinceLastPeriod % cycleLength) + 1; if (dayOfCycle <= 5) return CyclePhase.menstrual; if (dayOfCycle <= 13) return CyclePhase.follicular; if (dayOfCycle <= 16) return CyclePhase.ovulation; return CyclePhase.luteal; } Color _getPhaseColor(CyclePhase phase) { switch (phase) { case CyclePhase.menstrual: return AppColors.menstrualPhase; case CyclePhase.follicular: return AppColors.follicularPhase; case CyclePhase.ovulation: return AppColors.ovulationPhase; case CyclePhase.luteal: return AppColors.lutealPhase; } } String _getMonthName(int month) { const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; return months[month - 1]; } bool _isLoggedPeriodDay(DateTime date, List entries) { final entry = _getEntryForDate(date, entries); return entry?.isPeriodDay ?? false; } Widget _buildCalendarDay( DateTime day, DateTime focusedDay, List entries, DateTime? lastPeriodStart, int cycleLength, {bool isSelected = false, bool isToday = false, bool isWeekend = false}) { final phase = _getPhaseForDate(day, lastPeriodStart, cycleLength); final isDark = Theme.of(context).brightness == Brightness.dark; // Determine the Day of Cycle int? doc; if (lastPeriodStart != null) { final diff = day.difference(lastPeriodStart).inDays; if (diff >= 0) { doc = (diff % cycleLength) + 1; } } final isOvulationDay = doc == 14; final isPeriodStart = doc == 1; // Background decoration based on phase BoxDecoration? decoration; if (isSelected) { decoration = const BoxDecoration( color: AppColors.sageGreen, shape: BoxShape.circle, ); } else if (isToday) { decoration = BoxDecoration( color: AppColors.sageGreen.withOpacity(0.2), shape: BoxShape.circle, ); } else if (phase != null) { decoration = BoxDecoration( color: _getPhaseColor(phase).withOpacity(isDark ? 0.2 : 0.15), shape: BoxShape.circle, ); } // Text style TextStyle textStyle = GoogleFonts.outfit( fontSize: (isOvulationDay || isPeriodStart) ? 18 : 14, fontWeight: (isOvulationDay || isPeriodStart) ? FontWeight.bold : FontWeight.normal, color: isSelected ? Colors.white : (isToday ? AppColors.sageGreen : (Theme.of(context).textTheme.bodyMedium?.color)), ); if (isOvulationDay) { textStyle = textStyle.copyWith(color: isSelected ? Colors.white : AppColors.ovulationPhase); } else if (isPeriodStart) { textStyle = textStyle.copyWith(color: isSelected ? Colors.white : AppColors.menstrualPhase); } return Container( margin: const EdgeInsets.all(4), alignment: Alignment.center, decoration: decoration, child: Text( '${day.day}', style: textStyle, ), ); } CycleEntry? _getEntryForDate(DateTime date, List entries) { try { return entries.firstWhere( (entry) => isSameDay(entry.date, date), ); } catch (_) { return null; } } }