import 'package:em2rp/providers/local_user_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 Provider 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'; 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; // Événements de test final List _testEvents = [ EventModel( id: '1', name: 'Bal a Grammond', description: 'Lorem Ipsum', startDateTime: DateTime(2025, 3, 28, 12, 30), endDateTime: DateTime(2025, 3, 28, 23, 30), price: 950.34, installationTime: 3, disassemblyTime: 2, eventTypeId: '/eventTypes/Bal', customerId: '/customers/DnjJ1HOPBLqEeExs0nDl', address: const LatLng(45.566521035268224, 4.439601075086365), ), EventModel( id: '2', name: 'Mariage à Lyon', description: 'Cérémonie et réception', startDateTime: DateTime(2025, 3, 28, 15, 0), endDateTime: DateTime(2025, 3, 29, 4, 0), price: 2500.00, installationTime: 4, disassemblyTime: 3, eventTypeId: '/eventTypes/Mariage', customerId: '/customers/Test123', address: const LatLng(45.7578137, 4.8320114), ), ]; void _changeWeek(int delta) { setState(() { _focusedDay = _focusedDay.add(Duration(days: 7 * delta)); }); } @override Widget build(BuildContext context) { final localAuthProvider = Provider.of(context); final isMobile = MediaQuery.of(context).size.width < 600; 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, 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), ), 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), ), calendarBuilders: CalendarBuilders( 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: [ Center( child: Text( day.day.toString(), style: TextStyle( color: isSameDay(day, _selectedDay) ? Colors.white : null, ), ), ), if (events.isNotEmpty) Positioned( bottom: 2, left: 2, right: 2, top: 24, // Espace pour le numéro du jour 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: [ Center( child: Text( day.day.toString(), style: const TextStyle(color: Colors.white), ), ), if (events.isNotEmpty) Positioned( bottom: 2, left: 2, right: 2, top: 24, // Espace pour le numéro du jour 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 _testEvents) { // 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('Mois'), ), 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: 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)), ], ), ), ); }), ], ), ), // 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 _testEvents.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 _getMonthYearString(DateTime weekStart, DateTime weekEnd) { final months = [ '', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre' ]; if (weekStart.month == weekEnd.month) { return '${months[weekStart.month]} ${weekStart.year}'; } else { return '${months[weekStart.month]} - ${months[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); }