import 'package:flutter/material.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/utils/calendar_utils.dart'; class MonthView extends StatelessWidget { static const double _calendarPadding = 8.0; static const double _headerHeight = 52.0; static const double _headerVerticalPadding = 16.0; static const double _daysOfWeekHeight = 16.0; final DateTime focusedDay; final DateTime? selectedDay; final CalendarFormat calendarFormat; final Function(DateTime, DateTime) onDaySelected; final Function(CalendarFormat) onFormatChanged; final Function(DateTime) onPageChanged; final List events; final Function(EventModel) onEventSelected; const MonthView({ super.key, required this.focusedDay, required this.selectedDay, required this.calendarFormat, required this.onDaySelected, required this.onFormatChanged, required this.onPageChanged, required this.events, required this.onEventSelected, }); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final rowCount = _computeRowCount(focusedDay); final availableHeight = constraints.maxHeight - (_calendarPadding * 2) - _headerHeight - _headerVerticalPadding - _daysOfWeekHeight; final rowHeight = availableHeight / rowCount; return Container( height: constraints.maxHeight, padding: const EdgeInsets.all(_calendarPadding), child: TableCalendar( firstDay: DateTime.utc(2020, 1, 1), lastDay: DateTime.utc(2030, 12, 31), focusedDay: focusedDay, calendarFormat: calendarFormat, startingDayOfWeek: StartingDayOfWeek.monday, locale: 'fr_FR', daysOfWeekHeight: _daysOfWeekHeight, availableCalendarFormats: const { CalendarFormat.month: 'Mois', CalendarFormat.week: 'Semaine', }, selectedDayPredicate: (day) => isSameDay(selectedDay, day), onDaySelected: onDaySelected, onFormatChanged: onFormatChanged, onPageChanged: onPageChanged, calendarStyle: _buildCalendarStyle(), rowHeight: rowHeight, headerStyle: _buildHeaderStyle(), calendarBuilders: _buildCalendarBuilders(), ), ); }, ); } CalendarStyle _buildCalendarStyle() { return CalendarStyle( defaultDecoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), ), selectedDecoration: BoxDecoration( color: AppColors.rouge, border: Border.all(color: AppColors.rouge), borderRadius: BorderRadius.circular(4), ), todayDecoration: BoxDecoration( color: AppColors.rouge.withAlpha(26), border: Border.all(color: AppColors.rouge), borderRadius: BorderRadius.circular(4), ), outsideDecoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), ), outsideDaysVisible: false, cellMargin: EdgeInsets.zero, cellPadding: EdgeInsets.zero, ); } HeaderStyle _buildHeaderStyle() { return HeaderStyle( formatButtonVisible: true, titleCentered: true, formatButtonShowsNext: false, formatButtonDecoration: BoxDecoration( color: AppColors.rouge, borderRadius: BorderRadius.circular(16), ), formatButtonTextStyle: const TextStyle(color: Colors.white), leftChevronIcon: const Icon(Icons.chevron_left, color: AppColors.rouge), rightChevronIcon: const Icon(Icons.chevron_right, color: AppColors.rouge), headerPadding: const EdgeInsets.symmetric(vertical: 8), titleTextStyle: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ); } CalendarBuilders _buildCalendarBuilders() { return CalendarBuilders( dowBuilder: (context, day) { return Center( child: Text( CalendarUtils.getShortDayName(day.weekday), style: const TextStyle( fontWeight: FontWeight.bold, ), ), ); }, defaultBuilder: (context, day, focusedDay) { return _buildDayCell(day, false); }, selectedBuilder: (context, day, focusedDay) { return _buildDayCell(day, true); }, todayBuilder: (context, day, focusedDay) { return _buildDayCell(day, false, isToday: true); }, ); } Widget _buildDayCell(DateTime day, bool isSelected, {bool isToday = false}) { final dayEvents = CalendarUtils.getEventsForDay(day, events); final statusCounts = _getStatusCounts(dayEvents); final textColor = isSelected ? Colors.white : (isToday ? AppColors.rouge : null); BoxDecoration decoration; if (isSelected) { decoration = BoxDecoration( color: AppColors.rouge, border: Border.all(color: AppColors.rouge), borderRadius: BorderRadius.circular(4), ); } else if (isToday) { decoration = BoxDecoration( color: AppColors.rouge.withAlpha(26), border: Border.all(color: AppColors.rouge), borderRadius: BorderRadius.circular(4), ); } else { decoration = BoxDecoration( color: null, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), ); } return Container( margin: const EdgeInsets.all(4), decoration: decoration, child: Padding( padding: const EdgeInsets.all(4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( day.day.toString(), style: TextStyle(color: textColor), ), const SizedBox(width: 4), Expanded( child: Align( alignment: Alignment.topRight, child: Wrap( spacing: 4, runSpacing: 2, alignment: WrapAlignment.end, children: _buildStatusBadges(statusCounts), ), ), ), ], ), if (dayEvents.isNotEmpty) ...[ const SizedBox(height: 4), Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: dayEvents .map((event) => _buildEventItem(event, isSelected, day)) .toList(), ), ), ), ], ], ), ), ); } Map _getStatusCounts(List dayEvents) { final counts = { EventStatus.confirmed: 0, EventStatus.waitingForApproval: 0, EventStatus.canceled: 0, }; for (final event in dayEvents) { counts[event.status] = (counts[event.status] ?? 0) + 1; } return counts; } List _buildStatusBadges(Map statusCounts) { final badges = []; void addBadge({ required EventStatus status, required Color backgroundColor, required Color textColor, required String tooltipLabel, }) { final count = statusCounts[status] ?? 0; if (count <= 0) { return; } badges.add( Tooltip( message: '$count $tooltipLabel', child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(999), ), child: Text( count.toString(), style: TextStyle( color: textColor, fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ), ); } addBadge( status: EventStatus.confirmed, backgroundColor: Colors.green, textColor: Colors.white, tooltipLabel: 'validé${(statusCounts[EventStatus.confirmed] ?? 0) > 1 ? 's' : ''}', ); addBadge( status: EventStatus.waitingForApproval, backgroundColor: Colors.amber, textColor: Colors.black, tooltipLabel: 'en attente', ); addBadge( status: EventStatus.canceled, backgroundColor: Colors.red, textColor: Colors.white, tooltipLabel: 'annulé${(statusCounts[EventStatus.canceled] ?? 0) > 1 ? 's' : ''}', ); return badges; } Widget _buildEventItem( EventModel event, bool isSelected, DateTime currentDay) { Color color; Color textColor; IconData icon; switch (event.status) { case EventStatus.confirmed: color = Colors.green; textColor = Colors.white; icon = Icons.check; break; case EventStatus.canceled: color = Colors.red; textColor = Colors.white; icon = Icons.close; break; case EventStatus.waitingForApproval: color = Colors.amber; textColor = Colors.black; icon = Icons.hourglass_empty; break; } return GestureDetector( onTap: () { onDaySelected(currentDay, currentDay); onEventSelected(event); }, child: Container( margin: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: isSelected ? color.withAlpha(220) : color.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(4), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(icon, color: textColor, size: 16), const SizedBox(width: 4), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( event.name, style: TextStyle( fontSize: 12, color: textColor, fontWeight: FontWeight.bold, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (CalendarUtils.isMultiDayEvent(event)) Text( 'Jour ${CalendarUtils.calculateDayNumber(event.startDateTime, currentDay)}/${CalendarUtils.calculateTotalDays(event)}', style: TextStyle( fontSize: 10, color: textColor, ), maxLines: 1, ), ], ), ), ], ), ), ); } /// Calcule le nombre de rangées affichées pour le mois de [focusedDay] /// (calendrier commençant le lundi : offset = weekday - 1) int _computeRowCount(DateTime focusedDay) { final firstOfMonth = DateTime(focusedDay.year, focusedDay.month, 1); final daysInMonth = DateTime(focusedDay.year, focusedDay.month + 1, 0).day; final offset = (firstOfMonth.weekday - 1) % 7; // 0 = lundi, 6 = dimanche return ((daysInMonth + offset) / 7).ceil(); } }