import 'package:em2rp/providers/local_user_provider.dart'; import 'package:em2rp/providers/event_provider.dart'; import 'package:flutter/material.dart'; import 'package:em2rp/widgets/custom_app_bar.dart'; import 'package:em2rp/views/widgets/nav/main_drawer.dart'; import 'package:provider/provider.dart'; import 'package:em2rp/utils/colors.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/widgets/event_details.dart'; import 'package:latlong2/latlong.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; class CalendarPage extends StatefulWidget { const CalendarPage({Key? key}) : super(key: key); @override State createState() => _CalendarPageState(); } class _CalendarPageState extends State { CalendarFormat _calendarFormat = CalendarFormat.month; DateTime _focusedDay = DateTime.now(); DateTime? _selectedDay; EventModel? _selectedEvent; @override void initState() { super.initState(); initializeDateFormatting('fr_FR', null); Future.microtask(() => _loadEvents()); } Future _loadEvents() async { final localAuthProvider = Provider.of(context, listen: false); final eventProvider = Provider.of(context, listen: false); final userId = localAuthProvider.uid; if (userId != null) { await eventProvider.loadUserEvents(userId); } } void _changeWeek(int delta) { setState(() { _focusedDay = _focusedDay.add(Duration(days: 7 * delta)); }); } @override Widget build(BuildContext context) { final localAuthProvider = Provider.of(context); final eventProvider = Provider.of(context); final isMobile = MediaQuery.of(context).size.width < 600; if (eventProvider.isLoading) { return const Scaffold( body: Center( child: CircularProgressIndicator(), ), ); } return Scaffold( appBar: const CustomAppBar( title: 'Calendrier', ), drawer: const MainDrawer(currentPage: '/calendar'), body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(), ); } Widget _buildDesktopLayout() { return Row( children: [ // Calendrier (65% de la largeur) Expanded( flex: 65, child: _buildCalendar(), ), // Détails de l'événement (35% de la largeur) Expanded( flex: 35, child: _selectedEvent != null ? EventDetails(event: _selectedEvent!) : const Center( child: Text('Sélectionnez un événement pour voir les détails'), ), ), ], ); } Widget _buildMobileLayout() { return Column( children: [ // Calendrier Expanded( child: _buildCalendar(), ), // Détails de l'événement if (_selectedEvent != null) Expanded( child: EventDetails(event: _selectedEvent!), ), ], ); } Widget _buildCalendar() { return LayoutBuilder( builder: (context, constraints) { if (_calendarFormat == CalendarFormat.week) { return _buildWeekView(constraints); } else { return _buildMonthView(constraints); } }, ); } Widget _buildMonthView(BoxConstraints constraints) { // Calculer la hauteur des lignes en fonction de la hauteur disponible // Ajustement pour les mois à 6 semaines final rowHeight = (constraints.maxHeight - 100) / 6; // Augmenté de 80 à 100 pour donner plus d'espace return Container( height: constraints.maxHeight, padding: const EdgeInsets.all(8), // Réduit de 16 à 8 pour gagner de l'espace child: TableCalendar( firstDay: DateTime.utc(2020, 1, 1), lastDay: DateTime.utc(2030, 12, 31), focusedDay: _focusedDay, calendarFormat: _calendarFormat, startingDayOfWeek: StartingDayOfWeek.monday, locale: 'fr_FR', availableCalendarFormats: const { CalendarFormat.month: 'Mois', CalendarFormat.week: 'Semaine', }, selectedDayPredicate: (day) { return isSameDay(_selectedDay, day); }, onDaySelected: (selectedDay, focusedDay) { setState(() { _selectedDay = selectedDay; _focusedDay = focusedDay; }); }, onFormatChanged: (format) { setState(() { _calendarFormat = format; }); }, onPageChanged: (focusedDay) { _focusedDay = focusedDay; }, calendarStyle: 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.withOpacity(0.1), 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, ), rowHeight: rowHeight, headerStyle: 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: CalendarBuilders( dowBuilder: (context, day) { return Center( child: Text( _getDayName(day.weekday), style: const TextStyle( fontWeight: FontWeight.bold, ), ), ); }, defaultBuilder: (context, day, focusedDay) { final events = _getEventsForDay(day); return Container( margin: const EdgeInsets.all(4), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), ), child: Stack( children: [ // Numéro du jour en haut à gauche Positioned( top: 4, left: 4, child: Text( day.day.toString(), style: TextStyle( color: isSameDay(day, _selectedDay) ? Colors.white : null, ), ), ), // Badge du nombre d'événements en haut à droite if (events.isNotEmpty) Positioned( top: 4, right: 4, child: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: isSameDay(day, _selectedDay) ? Colors.white : AppColors.rouge, borderRadius: BorderRadius.circular(10), ), child: Text( events.length.toString(), style: TextStyle( color: isSameDay(day, _selectedDay) ? AppColors.rouge : Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ), // Liste des événements en dessous if (events.isNotEmpty) Positioned( bottom: 2, left: 2, right: 2, top: 28, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: events .map((event) => GestureDetector( onTap: () { setState(() { _selectedEvent = event; _selectedDay = day; }); }, child: Container( margin: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2), decoration: BoxDecoration( color: AppColors.rouge.withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( event.name, style: const TextStyle( fontSize: 12, color: AppColors.rouge, fontWeight: FontWeight.bold, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (_isMultiDayEvent(event)) Text( 'Jour ${_calculateDayNumber(event.startDateTime, day)}/${_calculateTotalDays(event)}', style: const TextStyle( fontSize: 10, color: AppColors.rouge, ), maxLines: 1, ), ], ), ), )) .toList(), ), ), ), ], ), ); }, selectedBuilder: (context, day, focusedDay) { final events = _getEventsForDay(day); return Container( margin: const EdgeInsets.all(4), decoration: BoxDecoration( color: AppColors.rouge, border: Border.all(color: AppColors.rouge), borderRadius: BorderRadius.circular(4), ), child: Stack( children: [ // Numéro du jour en haut à gauche Positioned( top: 4, left: 4, child: Text( day.day.toString(), style: const TextStyle(color: Colors.white), ), ), // Badge du nombre d'événements en haut à droite if (events.isNotEmpty) Positioned( top: 4, right: 4, child: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), ), child: Text( events.length.toString(), style: const TextStyle( color: AppColors.rouge, fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ), // Liste des événements en dessous if (events.isNotEmpty) Positioned( bottom: 2, left: 2, right: 2, top: 28, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: events .map((event) => GestureDetector( onTap: () { setState(() { _selectedEvent = event; }); }, child: Container( margin: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(4), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( event.name, style: const TextStyle( fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (_isMultiDayEvent(event)) Text( 'Jour ${_calculateDayNumber(event.startDateTime, day)}/${_calculateTotalDays(event)}', style: const TextStyle( fontSize: 10, color: Colors.white, ), maxLines: 1, ), ], ), ), )) .toList(), ), ), ), ], ), ); }, ), ), ); } Widget _buildWeekView(BoxConstraints constraints) { final weekStart = _focusedDay.subtract(Duration(days: _focusedDay.weekday - 1)); final weekEnd = weekStart.add(const Duration(days: 6)); // Ajustement de la hauteur pour éviter l'overflow double availableHeight = constraints.maxHeight - 80; // Réserver de l'espace pour les en-têtes final hourHeight = availableHeight / 24; final dayWidth = (constraints.maxWidth - 50) / 7; // Préparer les événements par jour (en tenant compte des multi-jours) List> eventsByDay = List.generate(7, (i) => []); for (final event in Provider.of(context).events) { // Pour chaque jour de la semaine for (int i = 0; i < 7; i++) { final day = weekStart.add(Duration(days: i)); final dayStart = DateTime(day.year, day.month, day.day, 0, 0); final dayEnd = DateTime(day.year, day.month, day.day, 23, 59, 59); // Si l'événement recouvre ce jour if (!(event.endDateTime.isBefore(dayStart) || event.startDateTime.isAfter(dayEnd))) { // Tronquer les heures de début/fin si besoin final start = event.startDateTime.isBefore(dayStart) ? dayStart : event.startDateTime; final end = event.endDateTime.isAfter(dayEnd) ? dayEnd : event.endDateTime; eventsByDay[i].add(_PositionedEvent(event, start, end)); } } } // Pour chaque jour, calculer les "colonnes" d'événements qui se chevauchent List> eventsWithColumnsByDay = []; for (final dayEvents in eventsByDay) { final columns = _assignColumns(dayEvents); eventsWithColumnsByDay.add(columns); } return Column( children: [ // Barre d'en-tête semaine Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon(Icons.chevron_left), onPressed: () => _changeWeek(-1), ), Text( _getMonthYearString(weekStart, weekEnd), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), Row( children: [ TextButton( onPressed: () { setState(() { _calendarFormat = CalendarFormat.month; }); }, style: TextButton.styleFrom( backgroundColor: AppColors.rouge, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8), ), child: const Text('Semaine'), ), IconButton( icon: const Icon(Icons.chevron_right), onPressed: () => _changeWeek(1), ), ], ), ], ), ), // En-tête avec les jours SizedBox( height: 40, child: Row( children: [ Container( width: 50, color: Colors.transparent, ), ...List.generate(7, (index) { final day = weekStart.add(Duration(days: index)); return Container( width: dayWidth, decoration: BoxDecoration( border: Border( right: BorderSide( color: index < 6 ? Colors.grey.shade300 : Colors.transparent, width: 1, ), ), ), child: Stack( children: [ Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_getDayName(day.weekday), style: const TextStyle( fontWeight: FontWeight.bold)), Text('${day.day}', style: const TextStyle(fontSize: 13)), ], ), ), if (_getEventsForDay(day).isNotEmpty) Positioned( top: 4, right: 4, child: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.rouge, borderRadius: BorderRadius.circular(10), ), child: Text( _getEventsForDay(day).length.toString(), style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ), ], ), ); }), ], ), ), // Grille des heures + jours Expanded( child: SingleChildScrollView( child: SizedBox( height: 24 * hourHeight, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Colonne des heures Column( children: List.generate(24, (index) { return Container( width: 50, height: hourHeight, alignment: Alignment.topRight, padding: const EdgeInsets.only(right: 4), child: Text( '${index.toString().padLeft(2, '0')}:00', style: TextStyle( color: Colors.grey.shade600, fontSize: 12, ), ), ); }), ), // Grille des jours Expanded( child: Stack( children: [ // Lignes horizontales Column( children: List.generate(24, (index) { return Container( height: hourHeight, decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Colors.grey.shade300, width: 0.5, ), ), ), ); }), ), // Bordures verticales entre jours Positioned.fill( child: Row( children: List.generate(7, (i) { return Container( width: dayWidth, decoration: BoxDecoration( border: Border( right: BorderSide( color: i < 6 ? Colors.grey.shade300 : Colors.transparent, width: 1, ), ), ), ); }), ), ), // Événements (chevauchements et multi-jours) ...List.generate(7, (dayIdx) { final dayEvents = eventsWithColumnsByDay[dayIdx]; return Stack( children: dayEvents.map((e) { final startHour = e.start.hour + e.start.minute / 60; final endHour = e.end.hour + e.end.minute / 60; final duration = endHour - startHour; final width = dayWidth / e.totalColumns; return Positioned( left: dayIdx * dayWidth + e.column * width, top: startHour * hourHeight, width: width, height: duration * hourHeight, child: GestureDetector( onTap: () { setState(() { _selectedEvent = e.event; _selectedDay = weekStart.add(Duration(days: dayIdx)); }); }, child: Container( margin: const EdgeInsets.all(2), padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: AppColors.rouge.withOpacity(0.2), border: Border.all(color: AppColors.rouge), borderRadius: BorderRadius.circular(4), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( e.event.name, style: const TextStyle( color: AppColors.rouge, fontSize: 12, fontWeight: FontWeight.bold, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (_isMultiDayEvent(e.event)) Text( 'Jour ${_calculateDayNumber(e.event.startDateTime, weekStart.add(Duration(days: dayIdx)))}/${_calculateTotalDays(e.event)}', style: const TextStyle( color: AppColors.rouge, fontSize: 10, ), maxLines: 1, ), ], ), ), ), ); }).toList(), ); }), ], ), ), ], ), ), ), ), ], ); } // Méthode pour récupérer les événements pour un jour donné (inclut les multi-jours) List _getEventsForDay(DateTime day) { final dayStart = DateTime(day.year, day.month, day.day, 0, 0); final dayEnd = DateTime(day.year, day.month, day.day, 23, 59, 59); return Provider.of(context).events.where((event) { return !(event.endDateTime.isBefore(dayStart) || event.startDateTime.isAfter(dayEnd)); }).toList(); } // Méthodes pour gérer les événements multi-jours bool _isMultiDayEvent(EventModel event) { return event.startDateTime.day != event.endDateTime.day || event.startDateTime.month != event.endDateTime.month || event.startDateTime.year != event.endDateTime.year; } int _calculateTotalDays(EventModel event) { final startDate = DateTime(event.startDateTime.year, event.startDateTime.month, event.startDateTime.day); final endDate = DateTime( event.endDateTime.year, event.endDateTime.month, event.endDateTime.day); return endDate.difference(startDate).inDays + 1; } int _calculateDayNumber(DateTime startDate, DateTime currentDay) { final start = DateTime(startDate.year, startDate.month, startDate.day); final current = DateTime(currentDay.year, currentDay.month, currentDay.day); return current.difference(start).inDays + 1; } String _getDayName(int weekday) { switch (weekday) { case DateTime.monday: return 'Lun'; case DateTime.tuesday: return 'Mar'; case DateTime.wednesday: return 'Mer'; case DateTime.thursday: return 'Jeu'; case DateTime.friday: return 'Ven'; case DateTime.saturday: return 'Sam'; case DateTime.sunday: return 'Dim'; default: return ''; } } String _getMonthName(int month) { switch (month) { case 1: return 'Janvier'; case 2: return 'Février'; case 3: return 'Mars'; case 4: return 'Avril'; case 5: return 'Mai'; case 6: return 'Juin'; case 7: return 'Juillet'; case 8: return 'Août'; case 9: return 'Septembre'; case 10: return 'Octobre'; case 11: return 'Novembre'; case 12: return 'Décembre'; default: return ''; } } String _getMonthYearString(DateTime weekStart, DateTime weekEnd) { if (weekStart.month == weekEnd.month) { return '${_getMonthName(weekStart.month)} ${weekStart.year}'; } else { return '${_getMonthName(weekStart.month)} - ${_getMonthName(weekEnd.month)} ${weekEnd.year}'; } } } class _PositionedEvent { final EventModel event; final DateTime start; final DateTime end; _PositionedEvent(this.event, this.start, this.end); } class _PositionedEventWithColumn extends _PositionedEvent { final int column; final int totalColumns; _PositionedEventWithColumn(EventModel event, DateTime start, DateTime end, this.column, this.totalColumns) : super(event, start, end); } List<_PositionedEventWithColumn> _assignColumns(List<_PositionedEvent> events) { // Algorithme simple : // - Trier par heure de début // - Pour chaque événement, trouver la première colonne libre // - Attribuer le nombre total de colonnes pour le groupe de chevauchement events.sort((a, b) => a.start.compareTo(b.start)); List<_PositionedEventWithColumn> result = []; List> columns = []; for (final e in events) { bool placed = false; for (int col = 0; col < columns.length; col++) { if (columns[col].isEmpty || !(_overlap(columns[col].last, e))) { columns[col] .add(_PositionedEventWithColumn(e.event, e.start, e.end, col, 0)); placed = true; break; } } if (!placed) { columns.add([ _PositionedEventWithColumn(e.event, e.start, e.end, columns.length, 0) ]); } } // Mettre à jour le nombre total de colonnes pour chaque événement int totalCols = columns.length; for (final col in columns) { for (final e in col) { result.add(_PositionedEventWithColumn( e.event, e.start, e.end, e.column, totalCols)); } } return result; } bool _overlap(_PositionedEvent a, _PositionedEvent b) { return a.end.isAfter(b.start) && a.start.isBefore(b.end); }