import 'dart:async'; import 'dart:math' as math; import 'package:em2rp/providers/local_user_provider.dart'; import 'package:em2rp/providers/event_provider.dart'; import 'package:em2rp/utils/performance_monitor.dart'; import 'package:flutter/material.dart'; import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; import 'package:em2rp/views/widgets/nav/main_drawer.dart'; import 'package:provider/provider.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/views/widgets/calendar_widgets/event_details.dart'; import 'package:intl/intl.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:em2rp/views/widgets/calendar_widgets/month_view.dart'; import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart'; import 'package:em2rp/views/event_add_page.dart'; import 'package:em2rp/views/widgets/calendar_widgets/mobile_calendar_view.dart'; import 'package:em2rp/views/widgets/calendar_widgets/user_filter_dropdown.dart'; import 'package:em2rp/utils/colors.dart'; class CalendarPage extends StatefulWidget { const CalendarPage({super.key}); @override State createState() => _CalendarPageState(); } class _CalendarPageState extends State { static const double _minDetailsPaneFraction = 0.25; static const double _maxDetailsPaneFraction = 0.5; static const double _desktopResizeHandleWidth = 12; static const double _minCalendarPaneWidth = 480; static const double _minDetailsPaneWidth = 320; CalendarFormat _calendarFormat = CalendarFormat.month; DateTime _focusedDay = DateTime.now(); DateTime? _selectedDay; EventModel? _selectedEvent; bool _calendarCollapsed = false; int _selectedEventIndex = 0; String? _selectedUserId; // Filtre par utilisateur (null = tous les événements) final TextEditingController _searchController = TextEditingController(); Timer? _searchDebounce; List _searchResults = []; String _searchQuery = ''; String? _searchError; bool _isSearching = false; int _searchRequestId = 0; bool _isMobileSearchVisible = false; bool _isRefreshing = false; final ValueNotifier _detailsPaneFraction = ValueNotifier(0.35); String? _lastLoadedUserId; bool _initialLoadScheduled = false; @override void initState() { super.initState(); initializeDateFormatting('fr_FR', null); // Charger les événements du mois courant après le premier build WidgetsBinding.instance.addPostFrameCallback((_) { _loadCurrentMonthEvents(); }); } /// Charge les événements du mois courant avec lazy loading Future _loadCurrentMonthEvents() async { PerformanceMonitor.start('CalendarPage.loadCurrentMonthEvents'); final localAuthProvider = Provider.of(context, listen: false); final eventProvider = Provider.of(context, listen: false); final userId = localAuthProvider.uid; final canViewAllEvents = localAuthProvider.hasPermission('view_all_events'); if (userId != null) { print( '[CalendarPage] Loading events for ${_focusedDay.year}-${_focusedDay.month}'); await eventProvider.loadMonthEvents( userId, _focusedDay.year, _focusedDay.month, canViewAllEvents: canViewAllEvents, ); // Précharger les mois adjacents en arrière-plan eventProvider.preloadAdjacentMonths( userId, _focusedDay.year, _focusedDay.month, canViewAllEvents: canViewAllEvents, ); if (mounted) { PerformanceMonitor.start('CalendarPage.selectDefaultEvent'); _selectDefaultEvent(); PerformanceMonitor.end('CalendarPage.selectDefaultEvent'); } } PerformanceMonitor.end('CalendarPage.loadCurrentMonthEvents'); } /// Vide le cache et recharge les événements du mois courant Future _refreshEvents() async { if (_isRefreshing) return; setState(() => _isRefreshing = true); try { final eventProvider = Provider.of(context, listen: false); eventProvider.clearAllCache(); await _loadCurrentMonthEvents(); } finally { if (mounted) setState(() => _isRefreshing = false); } } void _scheduleInitialEventsLoad(String? userId) { if (userId == null || userId == _lastLoadedUserId || _initialLoadScheduled) { return; } _initialLoadScheduled = true; WidgetsBinding.instance.addPostFrameCallback((_) async { try { if (!mounted) return; if (_lastLoadedUserId == userId) return; await _loadCurrentMonthEvents(); _lastLoadedUserId = userId; } finally { _initialLoadScheduled = false; } }); } /// Sélectionne automatiquement l'événement le plus proche de maintenant void _selectDefaultEvent() { final eventProvider = Provider.of(context, listen: false); final events = eventProvider.events; if (events.isEmpty) return; final now = DateTime.now(); // Trouver les événements d'aujourd'hui final todayEvents = events.where((e) { final start = e.startDateTime; return start.year == now.year && start.month == now.month && start.day == now.day; }).toList() ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); EventModel? selected; DateTime? selectedDay; if (todayEvents.isNotEmpty) { selected = todayEvents[0]; selectedDay = DateTime(now.year, now.month, now.day); } else { // Chercher le prochain événement à venir final futureEvents = events .where((e) => e.startDateTime.isAfter(now)) .toList() ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); if (futureEvents.isNotEmpty) { selected = futureEvents[0]; final start = selected.startDateTime; selectedDay = DateTime(start.year, start.month, start.day); } else { // Aucun événement à venir, prendre le plus récent final sortedEvents = events.toList() ..sort((a, b) => b.startDateTime.compareTo(a.startDateTime)); selected = sortedEvents.first; final start = selected.startDateTime; selectedDay = DateTime(start.year, start.month, start.day); } } if (mounted) { setState(() { _selectedDay = selectedDay; _focusedDay = selectedDay!; _selectedEventIndex = 0; _selectedEvent = selected; }); } } Future _loadEvents() async { final localAuthProvider = Provider.of(context, listen: false); final eventProvider = Provider.of(context, listen: false); final userId = localAuthProvider.uid; final canViewAllEvents = localAuthProvider.hasPermission('view_all_events'); if (userId != null) { await eventProvider.loadUserEvents(userId, canViewAllEvents: canViewAllEvents); } } @override void dispose() { _searchDebounce?.cancel(); _searchController.dispose(); _detailsPaneFraction.dispose(); super.dispose(); } /// Filtre les événements selon l'utilisateur sélectionné (si filtre actif). List _filterEventsByUser(List allEvents) { if (_selectedUserId == null) { return allEvents; // Pas de filtre, retourner tous les événements } // Filtrer les événements où l'utilisateur sélectionné fait partie de la workforce return allEvents.where((event) { return event.workforce.any((worker) { if (worker is String) { return worker == _selectedUserId; } // Si c'est une DocumentReference, on ne peut pas facilement comparer // On suppose que les données sont chargées correctement en String return false; }); }).toList(); } bool _isSameDay(DateTime left, DateTime right) { return left.year == right.year && left.month == right.month && left.day == right.day; } List _getEventsForDay( List events, DateTime? day, { EventModel? selectedEvent, }) { if (day == null) { return []; } final dayEvents = events .where((event) => _isSameDay(event.startDateTime, day)) .toList() ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); if (selectedEvent != null && _isSameDay(selectedEvent.startDateTime, day) && !dayEvents.any((event) => event.id == selectedEvent.id)) { dayEvents.add(selectedEvent); dayEvents.sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); } return dayEvents; } List _getDetailsEvents(List events) { final mergedEvents = [...events]; if (_selectedEvent != null && !mergedEvents.any((event) => event.id == _selectedEvent!.id)) { mergedEvents.add(_selectedEvent!); } mergedEvents.sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); return mergedEvents; } String _formatSearchResultDate(DateTime dateTime) { return DateFormat('EEE d MMM yyyy • HH:mm', 'fr_FR').format(dateTime); } Color _getStatusColor(EventStatus status) { switch (status) { case EventStatus.confirmed: return Colors.green; case EventStatus.canceled: return Colors.red; case EventStatus.waitingForApproval: default: return Colors.amber; } } /// Combine uniquement le filtre utilisateur avec la vue calendrier. List _getFilteredEvents(List allEvents) { return _filterEventsByUser(allEvents); } void _cancelPendingSearch() { _searchDebounce?.cancel(); _searchDebounce = null; } void _scheduleSearch(String value) { _cancelPendingSearch(); _searchDebounce = Timer(const Duration(milliseconds: 300), () { _runSearch(value); }); } void _onSearchChanged(String value) { final isMobile = MediaQuery.of(context).size.width < 600; if (isMobile && value.isNotEmpty && !_isMobileSearchVisible) { setState(() { _isMobileSearchVisible = true; }); } setState(() { _searchQuery = value; }); if (value.trim().isEmpty) { _cancelPendingSearch(); setState(() { _searchResults = []; _searchError = null; _isSearching = false; }); return; } _scheduleSearch(value); } void _clearSearch() { _cancelPendingSearch(); if (_searchController.text.isEmpty) { return; } _searchController.clear(); setState(() { _searchQuery = ''; _searchResults = []; _searchError = null; _isSearching = false; }); } Future _runSearch(String value) async { final query = value.trim(); if (query.isEmpty) { return; } final localUserProvider = context.read(); final userId = localUserProvider.uid; if (userId == null) { return; } final searchId = ++_searchRequestId; setState(() { _isSearching = true; _searchError = null; _searchResults = []; }); try { final eventProvider = context.read(); final results = await eventProvider.searchEvents( userId: userId, query: query, ); if (!mounted) { return; } if (_searchQuery.trim() != query) { return; } if (searchId != _searchRequestId) { return; } setState(() { _searchResults = results; _searchError = null; _isSearching = false; }); } catch (e) { if (!mounted || _searchQuery.trim() != query) { return; } setState(() { _searchResults = []; _searchError = 'Erreur lors de la recherche : $e'; _isSearching = false; }); } } Widget _buildDesktopFiltersBar({required bool canViewAllUserEvents}) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), color: Colors.grey[100], child: Row( children: [ Expanded( child: TextField( controller: _searchController, onChanged: _onSearchChanged, decoration: InputDecoration( hintText: 'Rechercher (titre, description, lieu)', prefixIcon: const Icon(Icons.search, color: AppColors.rouge), suffixIcon: _searchQuery.isNotEmpty ? IconButton( tooltip: 'Effacer la recherche', icon: const Icon(Icons.close), onPressed: _clearSearch, ) : null, isDense: true, filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), ), ), ), if (canViewAllUserEvents) ...[ const SizedBox(width: 12), _buildCompactUserFilter(), ], ], ), ); } Widget _buildCompactUserFilter() { return ConstrainedBox( constraints: const BoxConstraints(maxWidth: 300), child: Row( children: [ Expanded( child: UserFilterDropdown( selectedUserId: _selectedUserId, onUserSelected: (userId) { setState(() { _selectedUserId = userId; }); }, ), ), ], ), ); } Widget _buildMobileSearchBar() { return Container( color: Colors.grey[100], padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Column( children: [ Row( children: [ IconButton( icon: Icon( _isMobileSearchVisible ? Icons.search_off : Icons.search, color: AppColors.rouge, ), tooltip: _isMobileSearchVisible ? 'Masquer la recherche' : 'Afficher la recherche', onPressed: () { setState(() { _isMobileSearchVisible = !_isMobileSearchVisible; }); }, ), Expanded( child: Text( _searchQuery.isEmpty ? 'Rechercher un événement' : 'Recherche active', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ), if (_searchQuery.isNotEmpty) IconButton( icon: const Icon(Icons.close), tooltip: 'Effacer la recherche', onPressed: _clearSearch, ), ], ), AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: _isMobileSearchVisible ? Padding( key: const ValueKey('mobile-search-visible'), padding: const EdgeInsets.only(top: 4, left: 8, right: 8), child: TextField( controller: _searchController, onChanged: _onSearchChanged, decoration: InputDecoration( hintText: 'Titre, description ou lieu', prefixIcon: const Icon(Icons.search, color: AppColors.rouge), isDense: true, filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), ), ), ) : const SizedBox.shrink( key: ValueKey('mobile-search-hidden'), ), ), ], ), ); } Widget _buildSearchResultsPanel({required bool isMobile}) { final hasQuery = _searchQuery.trim().isNotEmpty; if (!hasQuery && !_isSearching && _searchError == null) { return const SizedBox.shrink(); } final panelPadding = EdgeInsets.symmetric( horizontal: isMobile ? 8 : 16, vertical: 8, ); return Container( width: double.infinity, padding: panelPadding, color: Colors.grey[50], child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Icon(Icons.manage_search, color: AppColors.rouge, size: 20), const SizedBox(width: 8), Expanded( child: Text( hasQuery ? 'Résultats pour "$_searchQuery"' : 'Recherche d’événements', style: const TextStyle( fontWeight: FontWeight.w600, ), ), ), if (_isSearching) const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), ], ), if (_searchError != null) ...[ const SizedBox(height: 8), Text( _searchError!, style: const TextStyle(color: Colors.red), ), ] else if (!hasQuery) ...[ const SizedBox(height: 8), Text( 'Saisissez un titre, une description ou un lieu pour lancer la recherche.', style: TextStyle(color: Colors.grey.shade700), ), ] else if (!_isSearching) ...[ const SizedBox(height: 8), if (_searchResults.isEmpty) Text( 'Aucun résultat trouvé.', style: TextStyle(color: Colors.grey.shade700), ) else ConstrainedBox( constraints: BoxConstraints( maxHeight: isMobile ? 240 : 280, ), child: ListView.builder( shrinkWrap: true, itemCount: _searchResults.length, physics: const ClampingScrollPhysics(), // ✅ prototypeItem : les résultats ont une hauteur variable // selon la présence du champ adresse (~56px sans, ~70px avec). // prototypeItem à 72px (cas avec adresse + padding) pour // que Flutter estime correctement la hauteur scrollable. // ListView.separated ne supporte pas itemExtent/prototypeItem, // d'où la conversion en ListView.builder avec séparateur intégré. prototypeItem: const SizedBox(height: 72), itemBuilder: (context, index) { final event = _searchResults[index]; final isSelected = _selectedEvent?.id == event.id; final isLast = index == _searchResults.length - 1; return Column( mainAxisSize: MainAxisSize.min, children: [ Material( color: isSelected ? AppColors.rouge.withOpacity(0.08) : Colors.white, borderRadius: BorderRadius.circular(12), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () => _onSearchResultSelected(event), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Container( width: 10, height: 10, decoration: BoxDecoration( color: _getStatusColor(event.status), shape: BoxShape.circle, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( event.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Text( _formatSearchResultDate( event.startDateTime), style: TextStyle( color: Colors.grey.shade700, fontSize: 12, ), ), if (event.address.isNotEmpty) ...[ const SizedBox(height: 2), Text( event.address, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.grey.shade600, fontSize: 12, ), ), ], ], ), ), const SizedBox(width: 8), const Icon(Icons.chevron_right, color: Colors.grey), ], ), ), ), ), if (!isLast) const SizedBox(height: 8), ], ); }, ), ), ], ], ), ); } Future _onSearchResultSelected(EventModel event) async { final localUserProvider = context.read(); final eventProvider = context.read(); final userId = localUserProvider.uid; if (userId == null) { return; } final canViewAllEvents = localUserProvider.hasPermission('view_all_events'); final selectedDay = DateTime( event.startDateTime.year, event.startDateTime.month, event.startDateTime.day, ); final shouldLoadMonth = _focusedDay.year != event.startDateTime.year || _focusedDay.month != event.startDateTime.month || eventProvider.events.isEmpty; if (shouldLoadMonth) { await eventProvider.loadMonthEvents( userId, event.startDateTime.year, event.startDateTime.month, canViewAllEvents: canViewAllEvents, ); eventProvider.preloadAdjacentMonths( userId, event.startDateTime.year, event.startDateTime.month, canViewAllEvents: canViewAllEvents, ); } if (!mounted) { return; } final eventsForSelectedDay = _getEventsForDay( eventProvider.events, selectedDay, selectedEvent: event, ); final isMobile = MediaQuery.of(context).size.width < 600; setState(() { _focusedDay = selectedDay; _selectedDay = selectedDay; _selectedEvent = event; _selectedEventIndex = eventsForSelectedDay.indexWhere((e) => e.id == event.id); if (_selectedEventIndex < 0) { _selectedEventIndex = 0; } _calendarCollapsed = false; if (isMobile) { _isMobileSearchVisible = true; } }); } void _changeWeek(int delta) { setState(() { _focusedDay = _focusedDay.add(Duration(days: 7 * delta)); }); } double _clampDetailsPaneFraction(double fraction, double totalWidth) { if (totalWidth <= 0) { return fraction.clamp(_minDetailsPaneFraction, _maxDetailsPaneFraction); } final minFractionFromPixels = _minDetailsPaneWidth / totalWidth; final maxFractionFromPixels = (totalWidth - _desktopResizeHandleWidth - _minCalendarPaneWidth) / totalWidth; final minFraction = math.max(_minDetailsPaneFraction, minFractionFromPixels); final maxFraction = math.min(_maxDetailsPaneFraction, maxFractionFromPixels); if (maxFraction < minFraction) { return fraction.clamp(_minDetailsPaneFraction, _maxDetailsPaneFraction); } return fraction.clamp(minFraction, maxFraction); } Widget _buildDesktopDetailsPane(List filteredEvents) { if (_selectedEvent != null) { final detailsEvents = _getDetailsEvents(filteredEvents); return EventDetails( event: _selectedEvent!, selectedDate: _selectedDay, events: detailsEvents, onSelectEvent: (event, date) { setState(() { _selectedEvent = event; _selectedDay = date; }); }, ); } return Center( child: _selectedDay != null ? const Text('Aucun événement ne démarre à cette date') : const Text('Sélectionnez un événement pour voir les détails'), ); } Widget _buildDesktopResizeHandle(double totalWidth) { return MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, child: GestureDetector( behavior: HitTestBehavior.opaque, onHorizontalDragUpdate: (details) { _detailsPaneFraction.value = _clampDetailsPaneFraction( _detailsPaneFraction.value - (details.delta.dx / totalWidth), totalWidth, ); }, child: SizedBox( width: _desktopResizeHandleWidth, child: Center( child: Container( width: 4, margin: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(999), ), ), ), ), ), ); } @override Widget build(BuildContext context) { final eventProvider = Provider.of(context); final localUserProvider = Provider.of(context); _scheduleInitialEventsLoad(localUserProvider.uid); final canCreateEvents = localUserProvider.hasPermission('create_events'); final canViewAllUserEvents = localUserProvider.hasPermission('view_all_user_events'); final isMobile = MediaQuery.of(context).size.width < 600; final showSearchResults = _searchQuery.trim().isNotEmpty || _isSearching || _searchError != null; // Appliquer le filtre utilisateur si actif final filteredEvents = _getFilteredEvents(eventProvider.events); // Debug logs print( '[CalendarPage.build] Total events: ${eventProvider.events.length}, Filtered: ${filteredEvents.length}'); if (eventProvider.events.isNotEmpty) { print( '[CalendarPage.build] First event: ${eventProvider.events.first.name} at ${eventProvider.events.first.startDateTime}'); } if (eventProvider.isLoading) { return const Scaffold( body: Center( child: CircularProgressIndicator(), ), ); } return Scaffold( appBar: CustomAppBar( title: "Calendrier", actions: [ if (_isRefreshing) const Padding( padding: EdgeInsets.symmetric(horizontal: 12), child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ), ) else IconButton( icon: const Icon(Icons.refresh, color: Colors.white), tooltip: 'Mettre à jour les événements', onPressed: _refreshEvents, ), ], ), drawer: const MainDrawer(currentPage: '/calendar'), body: Column( children: [ if (isMobile) _buildMobileSearchBar() else _buildDesktopFiltersBar(canViewAllUserEvents: canViewAllUserEvents), if (showSearchResults) _buildSearchResultsPanel(isMobile: isMobile), // Corps du calendrier Expanded( child: isMobile ? _buildMobileLayout(filteredEvents) : _buildDesktopLayout(filteredEvents), ), ], ), floatingActionButton: canCreateEvents ? FloatingActionButton( backgroundColor: Colors.white, onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => EventAddEditPage( selectedDate: _selectedDay ?? DateTime.now(), ), ), ); }, tooltip: 'Ajouter un événement', child: const Icon(Icons.add, color: Colors.red), ) : null, ); } Widget _buildDesktopLayout(List filteredEvents) { return LayoutBuilder( builder: (context, constraints) { final totalWidth = constraints.maxWidth; return ValueListenableBuilder( valueListenable: _detailsPaneFraction, builder: (context, fraction, child) { final detailsPaneFraction = _clampDetailsPaneFraction(fraction, totalWidth); final detailsWidth = totalWidth * detailsPaneFraction; final calendarWidth = totalWidth - _desktopResizeHandleWidth - detailsWidth; return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( width: calendarWidth, child: _buildCalendar(filteredEvents), ), _buildDesktopResizeHandle(totalWidth), SizedBox( width: detailsWidth, child: _buildDesktopDetailsPane(filteredEvents), ), ], ); }, ); }, ); } Widget _buildMobileLayout(List filteredEvents) { final eventsForSelectedDay = _getEventsForDay( filteredEvents, _selectedDay, selectedEvent: _selectedEvent, ); final hasEvents = eventsForSelectedDay.isNotEmpty; final selectedEventIndex = _selectedEvent == null ? -1 : eventsForSelectedDay .indexWhere((event) => event.id == _selectedEvent!.id); final currentEvent = hasEvents && selectedEventIndex >= 0 ? eventsForSelectedDay[selectedEventIndex] : hasEvents && _selectedEventIndex < eventsForSelectedDay.length ? eventsForSelectedDay[_selectedEventIndex] : null; // GESTURE DETECTOR pour swipe vertical (plier/déplier) et horizontal (mois) return GestureDetector( onVerticalDragEnd: (details) { if (details.primaryVelocity != null) { if (details.primaryVelocity! < -200) { // Swipe vers le haut : plier setState(() { _calendarCollapsed = true; }); } else if (details.primaryVelocity! > 200) { // Swipe vers le bas : déplier setState(() { _calendarCollapsed = false; }); } } }, onHorizontalDragEnd: (details) { if (details.primaryVelocity != null) { if (details.primaryVelocity! < -200) { // Swipe gauche : mois suivant final newMonth = DateTime(_focusedDay.year, _focusedDay.month + 1, 1); setState(() { _focusedDay = newMonth; }); print( '[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); _loadCurrentMonthEvents(); } else if (details.primaryVelocity! > 200) { // Swipe droite : mois précédent final newMonth = DateTime(_focusedDay.year, _focusedDay.month - 1, 1); setState(() { _focusedDay = newMonth; }); print( '[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); _loadCurrentMonthEvents(); } } }, child: Stack( children: [ // Calendrier + détails en dessous AnimatedPositioned( duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, top: _calendarCollapsed ? -600 : 0, // cache le calendrier en haut left: 0, right: 0, height: _calendarCollapsed ? 0 : null, child: SizedBox( height: MediaQuery.of(context).size.height, child: Column( children: [ _buildMonthHeader(context), if (!_calendarCollapsed) // Ajout d'un GestureDetector pour swipe horizontal sur le calendrier GestureDetector( onHorizontalDragEnd: (details) { if (details.primaryVelocity != null) { if (details.primaryVelocity! < -200) { // Swipe gauche : mois suivant final newMonth = DateTime( _focusedDay.year, _focusedDay.month + 1, 1); setState(() { _focusedDay = newMonth; }); print( '[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); _loadCurrentMonthEvents(); } else if (details.primaryVelocity! > 200) { // Swipe droite : mois précédent final newMonth = DateTime( _focusedDay.year, _focusedDay.month - 1, 1); setState(() { _focusedDay = newMonth; }); print( '[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); _loadCurrentMonthEvents(); } } }, child: MobileCalendarView( focusedDay: _focusedDay, selectedDay: _selectedDay, events: filteredEvents, onDaySelected: (day) { final eventsForDay = filteredEvents .where((e) => e.startDateTime.year == day.year && e.startDateTime.month == day.month && e.startDateTime.day == day.day) .toList() ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); setState(() { _selectedDay = day; _calendarCollapsed = false; _selectedEventIndex = 0; _selectedEvent = eventsForDay.isNotEmpty ? eventsForDay[0] : null; }); }, ), ), Expanded( child: hasEvents // Ajout d'un GestureDetector pour swipe horizontal sur le détail événement ? GestureDetector( onHorizontalDragEnd: (details) { if (details.primaryVelocity != null) { if (details.primaryVelocity! < -200) { // Swipe gauche : événement suivant if (_selectedEventIndex < eventsForSelectedDay.length - 1) { setState(() { _selectedEventIndex++; _selectedEvent = eventsForSelectedDay[ _selectedEventIndex]; }); } } else if (details.primaryVelocity! > 200) { // Swipe droite : événement précédent if (_selectedEventIndex > 0) { setState(() { _selectedEventIndex--; _selectedEvent = eventsForSelectedDay[ _selectedEventIndex]; }); } } } }, child: EventDetails( event: eventsForSelectedDay[_selectedEventIndex], selectedDate: _selectedDay, events: eventsForSelectedDay, onSelectEvent: (event, date) { final idx = eventsForSelectedDay .indexWhere((e) => e.id == event.id); setState(() { _selectedEventIndex = idx >= 0 ? idx : 0; _selectedEvent = event; }); }, ), ) : Center( child: Text( 'Aucun événement ne démarre à cette date')), ), ], ), ), ), // Vue détail (prend tout l'espace quand calendrier cache) if (_calendarCollapsed && _selectedDay != null) AnimatedPositioned( duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, top: _calendarCollapsed ? 0 : 600, left: 0, right: 0, bottom: 0, child: SizedBox( height: MediaQuery.of(context).size.height, child: Column( children: [ _buildMonthHeader(context), Expanded( child: Stack( children: [ if (currentEvent != null) // Ajout d'un GestureDetector pour swipe horizontal sur le détail événement GestureDetector( onHorizontalDragEnd: (details) { if (details.primaryVelocity != null) { if (details.primaryVelocity! < -200) { // Swipe gauche : événement suivant if (_selectedEventIndex < eventsForSelectedDay.length - 1) { setState(() { _selectedEventIndex++; _selectedEvent = eventsForSelectedDay[ _selectedEventIndex]; }); } } else if (details.primaryVelocity! > 200) { // Swipe droite : événement précédent if (_selectedEventIndex > 0) { setState(() { _selectedEventIndex--; _selectedEvent = eventsForSelectedDay[ _selectedEventIndex]; }); } } } }, child: EventDetails( event: currentEvent, selectedDate: _selectedDay, events: eventsForSelectedDay, onSelectEvent: (event, date) { final idx = eventsForSelectedDay .indexWhere((e) => e.id == event.id); setState(() { _selectedEventIndex = idx >= 0 ? idx : 0; _selectedEvent = event; }); }, ), ), if (!hasEvents) const Center( child: Text( 'Aucun événement ne démarre à cette date'), ), ], ), ), ], ), ), ), ], ), ); } Widget _buildMonthHeader(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 8, bottom: 8), child: Row( children: [ IconButton( icon: const Icon(Icons.chevron_left, color: AppColors.rouge, size: 28), onPressed: () { final newMonth = DateTime(_focusedDay.year, _focusedDay.month - 1, 1); setState(() { _focusedDay = newMonth; }); print( '[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); _loadCurrentMonthEvents(); }, ), Expanded( child: GestureDetector( onTap: () { setState(() { _calendarCollapsed = !_calendarCollapsed; }); }, child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Text( _getMonthName(_focusedDay.month), style: const TextStyle( color: AppColors.rouge, fontWeight: FontWeight.bold, fontSize: 20, ), ), const SizedBox(width: 6), Icon( _calendarCollapsed ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_up, color: AppColors.rouge, size: 26, ), ], ), ), ), IconButton( icon: const Icon(Icons.chevron_right, color: AppColors.rouge, size: 28), onPressed: () { final newMonth = DateTime(_focusedDay.year, _focusedDay.month + 1, 1); setState(() { _focusedDay = newMonth; }); print( '[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); _loadCurrentMonthEvents(); }, ), ], ), ); } 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 ''; } } Widget _buildCalendar(List filteredEvents) { if (_calendarFormat == CalendarFormat.week) { return WeekView( focusedDay: _focusedDay, events: filteredEvents, onWeekChange: _changeWeek, onEventSelected: (event) { setState(() { _selectedEvent = event; _selectedDay = event.startDateTime; }); }, onSwitchToMonth: () { setState(() { _calendarFormat = CalendarFormat.month; }); }, onDaySelected: (selectedDay) { final eventsForDay = filteredEvents .where((e) => e.startDateTime.year == selectedDay.year && e.startDateTime.month == selectedDay.month && e.startDateTime.day == selectedDay.day) .toList(); eventsForDay .sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); setState(() { _selectedDay = selectedDay; if (eventsForDay.isNotEmpty) { _selectedEvent = eventsForDay.first; } else { _selectedEvent = null; } }); if (eventsForDay.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Aucun événement ne démarre à cette date."), duration: Duration(seconds: 2), ), ); } }, selectedEvent: _selectedEvent, ); } else { return MonthView( focusedDay: _focusedDay, selectedDay: _selectedDay, calendarFormat: _calendarFormat, events: filteredEvents, onDaySelected: (selectedDay, focusedDay) { final eventsForDay = filteredEvents .where((event) => event.startDateTime.year == selectedDay.year && event.startDateTime.month == selectedDay.month && event.startDateTime.day == selectedDay.day) .toList() ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); setState(() { _selectedDay = selectedDay; _focusedDay = focusedDay; if (eventsForDay.isNotEmpty) { _selectedEvent = eventsForDay.first; } else { _selectedEvent = null; } }); }, onFormatChanged: (format) { setState(() { _calendarFormat = format; }); }, onPageChanged: (focusedDay) { // Détecter si on a changé de mois final monthChanged = focusedDay.year != _focusedDay.year || focusedDay.month != _focusedDay.month; setState(() { _focusedDay = focusedDay; }); // Charger les événements du nouveau mois si nécessaire if (monthChanged) { print( '[CalendarPage] Month changed to ${focusedDay.year}-${focusedDay.month}'); _loadCurrentMonthEvents(); } }, onEventSelected: (event) { setState(() { _selectedEvent = event; }); }, ); } } }