From 004d442e677d03ca2d4a776eeaf2a5f06c5c9f01 Mon Sep 17 00:00:00 2001 From: "PC-PAUL\\paulf" Date: Thu, 29 May 2025 12:41:43 +0200 Subject: [PATCH] Passage en vue mobile du calendrier --- em2rp/lib/views/calendar_page.dart | 277 ++++++++++++++++-- .../calendar_widgets/event_details.dart | 13 +- .../mobile_calendar_view.dart | 139 +++++++++ .../widgets/calendar_widgets/week_view.dart | 4 - 4 files changed, 395 insertions(+), 38 deletions(-) create mode 100644 em2rp/lib/views/widgets/calendar_widgets/mobile_calendar_view.dart diff --git a/em2rp/lib/views/calendar_page.dart b/em2rp/lib/views/calendar_page.dart index 7c6a0fb..0f7ccc5 100644 --- a/em2rp/lib/views/calendar_page.dart +++ b/em2rp/lib/views/calendar_page.dart @@ -11,6 +11,8 @@ 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/pages/event_add_page.dart'; +import 'package:em2rp/views/widgets/calendar_widgets/mobile_calendar_view.dart'; +import 'package:em2rp/utils/colors.dart'; class CalendarPage extends StatefulWidget { const CalendarPage({super.key}); @@ -24,12 +26,40 @@ class _CalendarPageState extends State { DateTime _focusedDay = DateTime.now(); DateTime? _selectedDay; EventModel? _selectedEvent; + bool _calendarCollapsed = false; + int _selectedEventIndex = 0; @override void initState() { super.initState(); initializeDateFormatting('fr_FR', null); Future.microtask(() => _loadEvents()); + // Sélection automatique de l'événement le plus proche de maintenant + WidgetsBinding.instance.addPostFrameCallback((_) { + final eventProvider = Provider.of(context, listen: false); + final events = eventProvider.events; + if (events.isNotEmpty) { + final now = DateTime.now(); + events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); + int closestIdx = 0; + Duration minDiff = (events[0].startDateTime.difference(now)).abs(); + for (int i = 1; i < events.length; i++) { + final diff = (events[i].startDateTime.difference(now)).abs(); + if (diff < minDiff) { + minDiff = diff; + closestIdx = i; + } + } + final closestEvent = events[closestIdx]; + setState(() { + _selectedDay = DateTime(closestEvent.startDateTime.year, + closestEvent.startDateTime.month, closestEvent.startDateTime.day); + _focusedDay = _selectedDay!; + _selectedEventIndex = 0; + _selectedEvent = closestEvent; + }); + } + }); } Future _loadEvents() async { @@ -69,8 +99,23 @@ class _CalendarPageState extends State { } return Scaffold( - appBar: const CustomAppBar( - title: 'Calendrier', + appBar: CustomAppBar( + title: _getMonthName(_focusedDay.month), + actions: [ + IconButton( + icon: Icon( + _calendarCollapsed + ? Icons.keyboard_arrow_down + : Icons.keyboard_arrow_up, + color: AppColors.blanc, + ), + onPressed: () { + setState(() { + _calendarCollapsed = !_calendarCollapsed; + }); + }, + ), + ], ), drawer: const MainDrawer(currentPage: '/calendar'), body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(), @@ -128,37 +173,221 @@ class _CalendarPageState extends State { Widget _buildMobileLayout() { final eventProvider = Provider.of(context); - return Column( + final eventsForSelectedDay = _selectedDay == null + ? [] + : eventProvider.events + .where((e) => + e.startDateTime.year == _selectedDay!.year && + e.startDateTime.month == _selectedDay!.month && + e.startDateTime.day == _selectedDay!.day) + .toList() + ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); + final hasEvents = eventsForSelectedDay.isNotEmpty; + final currentEvent = + hasEvents && _selectedEventIndex < eventsForSelectedDay.length + ? eventsForSelectedDay[_selectedEventIndex] + : null; + + return Stack( children: [ - // Calendrier - Expanded( - child: _buildCalendar(), - ), - // Détails de l'événement - if (_selectedEvent != null) - Expanded( - child: EventDetails( - event: _selectedEvent!, - selectedDate: _selectedDay, - events: eventProvider.events, - onSelectEvent: (event, date) { - setState(() { - _selectedEvent = event; - _selectedDay = date; - }); - }, + // 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: Container( + height: MediaQuery.of(context).size.height, + child: Column( + children: [ + _buildMonthHeader(context), + if (!_calendarCollapsed) + MobileCalendarView( + focusedDay: _focusedDay, + selectedDay: _selectedDay, + events: eventProvider.events, + onDaySelected: (day) { + final eventsForDay = eventProvider.events + .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 + ? EventDetails( + event: eventsForSelectedDay[_selectedEventIndex], + selectedDate: _selectedDay, + events: eventsForSelectedDay.cast(), + 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')), + ), + ], ), ), - if (_selectedEvent == null && _selectedDay != null) - Expanded( - child: Center( - child: Text('Aucun événement ne démarre à cette date'), + ), + // Vue détail (prend tout l'espace quand calendrier caché) + if (_calendarCollapsed && _selectedDay != null) + AnimatedPositioned( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + top: _calendarCollapsed ? 0 : 600, + left: 0, + right: 0, + bottom: 0, + child: Container( + height: MediaQuery.of(context).size.height, + child: Column( + children: [ + _buildMonthHeader(context), + Expanded( + child: Stack( + children: [ + if (currentEvent != null) + EventDetails( + event: currentEvent, + selectedDate: _selectedDay, + events: eventsForSelectedDay.cast(), + onSelectEvent: (event, date) { + final idx = eventsForSelectedDay + .indexWhere((e) => e.id == event.id); + setState(() { + _selectedEventIndex = idx >= 0 ? idx : 0; + _selectedEvent = event; + }); + }, + ), + if (!hasEvents) + 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: () { + setState(() { + _focusedDay = + DateTime(_focusedDay.year, _focusedDay.month - 1, 1); + }); + }, + ), + 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: () { + setState(() { + _focusedDay = + DateTime(_focusedDay.year, _focusedDay.month + 1, 1); + }); + }, + ), + ], + ), + ); + } + + 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() { final eventProvider = Provider.of(context); diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart index c852f9f..45d0eff 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart @@ -115,16 +115,9 @@ class EventDetails extends StatelessWidget { EventModel.fromMap(snap.data()!, event.id); onSelectEvent(updatedEvent, selectedDate ?? updatedEvent.startDateTime); - // Recharge la liste des événements pour mettre à jour la vue calendrier - final localUserProvider = - Provider.of(context, listen: false); - final userId = localUserProvider.currentUser?.uid; - final canViewAll = - localUserProvider.hasPermission('view_all_users'); - if (userId != null) { - await Provider.of(context, listen: false) - .loadUserEvents(userId, canViewAllEvents: canViewAll); - } + // Met à jour uniquement l'événement dans le provider (rafraîchissement local et fluide) + await Provider.of(context, listen: false) + .updateEvent(updatedEvent); }, ), ), diff --git a/em2rp/lib/views/widgets/calendar_widgets/mobile_calendar_view.dart b/em2rp/lib/views/widgets/calendar_widgets/mobile_calendar_view.dart new file mode 100644 index 0000000..9140912 --- /dev/null +++ b/em2rp/lib/views/widgets/calendar_widgets/mobile_calendar_view.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/event_model.dart'; + +class MobileCalendarView extends StatelessWidget { + final DateTime focusedDay; + final DateTime? selectedDay; + final List events; + final void Function(DateTime) onDaySelected; + + const MobileCalendarView({ + super.key, + required this.focusedDay, + required this.selectedDay, + required this.events, + required this.onDaySelected, + }); + + @override + Widget build(BuildContext context) { + final daysInMonth = + DateUtils.getDaysInMonth(focusedDay.year, focusedDay.month); + final firstDayOfMonth = DateTime(focusedDay.year, focusedDay.month, 1); + final firstWeekday = firstDayOfMonth.weekday; + final days = []; + // Ajoute les jours vides avant le 1er du mois (pour aligner sur le bon jour de la semaine) + for (int i = 1; i < firstWeekday; i++) { + days.add(DateTime(0)); // jour vide + } + // Ajoute les jours du mois + for (int i = 0; i < daysInMonth; i++) { + days.add(DateTime(focusedDay.year, focusedDay.month, i + 1)); + } + // Complète la dernière semaine si besoin + while (days.length % 7 != 0) { + days.add(DateTime(0)); + } + + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(7, (i) { + const daysShort = ['L', 'M', 'M', 'J', 'V', 'S', 'D']; + return Expanded( + child: Center( + child: Text(daysShort[i], + style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ); + }), + ), + ), + ...List.generate(days.length ~/ 7, (week) { + return Row( + children: List.generate(7, (i) { + final day = days[week * 7 + i]; + if (day.year == 0) { + return const Expanded(child: SizedBox.shrink()); + } + final isSelected = + selectedDay != null && _isSameDay(day, selectedDay!); + final eventsForDay = events + .where((e) => _isSameDay(e.startDateTime, day)) + .toList(); + return Expanded( + child: GestureDetector( + onTap: () => onDaySelected(day), + child: Container( + color: Colors.transparent, + padding: const EdgeInsets.symmetric(vertical: 6), + child: Column( + children: [ + Container( + decoration: isSelected + ? BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary + .withOpacity(0.15), + shape: BoxShape.circle, + ) + : null, + padding: const EdgeInsets.all(4), + child: Text( + '${day.day}', + style: TextStyle( + fontWeight: FontWeight.w600, + color: isSelected + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + ), + const SizedBox(height: 2), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: eventsForDay + .map((event) => Container( + margin: const EdgeInsets.symmetric( + horizontal: 1), + width: 6, + height: 6, + decoration: BoxDecoration( + color: _getColorForStatus(event.status), + shape: BoxShape.circle, + ), + )) + .toList(), + ), + ], + ), + ), + ), + ); + }), + ); + }), + ], + ); + } + + bool _isSameDay(DateTime a, DateTime b) { + return a.year == b.year && a.month == b.month && a.day == b.day; + } + + Color _getColorForStatus(EventStatus status) { + switch (status) { + case EventStatus.confirmed: + return Colors.green; + case EventStatus.canceled: + return Colors.red; + case EventStatus.waitingForApproval: + default: + return Colors.amber; + } + } +} diff --git a/em2rp/lib/views/widgets/calendar_widgets/week_view.dart b/em2rp/lib/views/widgets/calendar_widgets/week_view.dart index 5fb93cd..17f6015 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/week_view.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/week_view.dart @@ -283,10 +283,6 @@ class WeekView extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(_getStatusIcon(e.event.status), - color: _getStatusTextColor(e.event.status), - size: 16), - const SizedBox(width: 4), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start,