From ecf4a5cede0e7f0fa6602cd35305fca63d168d7f Mon Sep 17 00:00:00 2001 From: ElPoyo Date: Thu, 12 Mar 2026 21:14:44 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20mise=20=C3=A0=20jour=20de=20la=20versio?= =?UTF-8?q?n=20=C3=A0=201.1.18=20et=20am=C3=A9lioration=20de=20la=20page?= =?UTF-8?q?=20calendrier=20avec=20ajout=20de=20la=20fonctionnalit=C3=A9=20?= =?UTF-8?q?de=20rafra=C3=AEchissement=20des=20=C3=A9v=C3=A9nements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache | 32 +-- em2rp/CHANGELOG.md | 2 + em2rp/lib/config/app_version.dart | 2 +- em2rp/lib/config/env.dart | 7 +- em2rp/lib/providers/event_provider.dart | 9 + em2rp/lib/services/data_service.dart | 2 +- em2rp/lib/views/calendar_page.dart | 238 ++++++++++++++---- .../event_status_button.dart | 10 + .../widgets/calendar_widgets/month_view.dart | 185 ++++++++++---- .../event_assigned_equipment_section.dart | 162 +++++------- em2rp/web/version.json | 6 +- 11 files changed, 434 insertions(+), 221 deletions(-) diff --git a/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache b/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache index 0720f35..4795fc5 100644 --- a/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache +++ b/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache @@ -1,3 +1,4 @@ +test_audio_tts.js,1772996026925,be4d2d713c256578bc16646116e3e81fc2627a1d89e45b211318b51e3612f259 manifest.json,1766235870190,1fb17c7a1d11e0160d9ffe48e4e4f7fb5028d23477915a17ca496083050946e2 flutter.js,1759914809272,d9a92a27a30723981b176a08293dedbe86c080fcc08e0128e5f8a01ce1d3fcb4 favicon.png,1766235850956,3cf717d02cd8014f223307dee1bde538442eb9de23568e649fd8aae686dc9db0 @@ -22,8 +23,8 @@ assets/packages/flutter_dropzone_web/assets/flutter_dropzone.js,1748366257688,d6 assets/assets/Google__G__logo.svg,1741027482182,b365d560438f8f04caf08ffaa5d8995eff6c09b36b4483f44d6f5f82d8559d4f assets/assets/google.png,1741029771653,537ca60ffa74c28eca4e62d249237403a7d47d2bc90bb90431b8d5aa923a92ee assets/assets/EM2_NsurB.jpg,1741031103452,687267bbd4e1a663ffc1d2256c34969dd424cbaaf503b530db529a345570ddcd -assets/assets/sounds/ok.mp3,1771938119844,cb452794752fa5e7622b2bd9413e9245464788be3f88cc838a7c9716f87f82a3 -assets/assets/sounds/error.mp3,1771938125144,5e1974fa40050421304357c75e834ab5f7c8ba7a61acfbb5885ed913afc0fc0b +assets/assets/sounds/ok.mp3,1772996026461,cb452794752fa5e7622b2bd9413e9245464788be3f88cc838a7c9716f87f82a3 +assets/assets/sounds/error.mp3,1772996026458,5e1974fa40050421304357c75e834ab5f7c8ba7a61acfbb5885ed913afc0fc0b assets/assets/logos/SquareLogoWhite.png,1760462340000,786ce2571303bb96dfae1fba5faaab57a9142468fa29ad73ab6b3c1f75be3703 assets/assets/logos/SquareLogoBlack.png,1760462340000,b4425fae1dbd25ce7c218c602d530f75d85e0eb444746b48b09b5028ed88bbd1 assets/assets/logos/RectangleLogoWhite.png,1760462340000,1f6df22df6560a2dae2d42cf6e29f01e6df4002f1a9c20a8499923d74b02115c @@ -33,17 +34,16 @@ assets/assets/images/tshirt-incrust.webp,1737393735487,af7cb34adfca19c0b41c8eb63 assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d -version.json,1772532792027,2b3f91e827bc27a1901342a048b1bd81d0aabc50935717f9851e1a3ad6cb7411 -test_audio_tts.js,1772532705302,d7b70556456d3b5e7832506b2dafe31480d94db8d0027b89c1633cc9b5c5bdae -index.html,1772532797157,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10 -flutter_bootstrap.js,1772532797146,ca3df8691f4db5962ed165489bd051dfd31307628ab4f1ee68842dc747d39fd9 -flutter_service_worker.js,1772532894886,9ce6b8d9f09c957b763a8d3db3baf03c96d4f84e805f6d629294749d9966cfad -assets/FontManifest.json,1772532889954,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5 -assets/AssetManifest.json,1772532889954,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6 -assets/AssetManifest.bin.json,1772532889954,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53 -assets/AssetManifest.bin,1772532889954,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907 -assets/shaders/ink_sparkle.frag,1772532890224,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 -assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1772532893514,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb -assets/fonts/MaterialIcons-Regular.otf,1772532893530,71c7128cf890cf3e18fffca405a98480f174bb3fa79d20c575b473d36c8c3093 -assets/NOTICES,1772532889955,8479783d331c9ff6d2b2e2e0a4b1705eda46ab0000b7753779fb98526ae54d74 -main.dart.js,1772532888607,df89975075062e0983691b8997b9e4a1ae4b4d5dfe6c06ca5b42ffa5407fdd3f +version.json,1773324020831,d5cd7334d7c3a990dbff0821b9aaab39129803e306b0d96599b8adc6d4f433a6 +index.html,1773324025840,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10 +flutter_service_worker.js,1773324116910,8582e401e070055f59183c207cf7a7e6a9219a50f5089a24a77d91d3ff77dcbc +flutter_bootstrap.js,1773324025827,74eaa66055c715df232ee96fc4114d5473f67717278fb4effa38d8b1b362e303 +assets/FontManifest.json,1773324113335,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5 +assets/AssetManifest.json,1773324113335,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6 +assets/AssetManifest.bin.json,1773324113335,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53 +assets/AssetManifest.bin,1773324113335,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907 +assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1773324115847,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb +assets/shaders/ink_sparkle.frag,1773324113551,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 +assets/fonts/MaterialIcons-Regular.otf,1773324115852,d1409c3c8050990bdc63a413539d600245a27c9794a053c211299cc86d4f6a5c +assets/NOTICES,1773324113339,1d9a08da58db7959b9607f0f1f342f96243af76dc608ed659614d586ec58cd79 +main.dart.js,1773324112059,bfc66ab7e817db63dee4b996af3dea0629c4c4e87ba91070c15b133ab5104848 diff --git a/em2rp/CHANGELOG.md b/em2rp/CHANGELOG.md index ee11f5c..997754d 100644 --- a/em2rp/CHANGELOG.md +++ b/em2rp/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog - EM2RP Toutes les modifications notables de ce projet seront documentées dans ce fichier. +## 12/03/2026bis +Fix BUG : Ajout equipement à un evenement existant, boutons de modification de statut d'un evenement ne fonctionnaient pas. Refonte legere de la page calendrier. ## 12/03/2026 Ajout d'une page de statistiques détaillées pour les équipements et les événements. diff --git a/em2rp/lib/config/app_version.dart b/em2rp/lib/config/app_version.dart index 461375c..4c2febe 100644 --- a/em2rp/lib/config/app_version.dart +++ b/em2rp/lib/config/app_version.dart @@ -1,6 +1,6 @@ /// Configuration de la version de l'application class AppVersion { - static const String version = '1.1.17'; + static const String version = '1.1.18'; /// Retourne la version complète de l'application static String get fullVersion => 'v$version'; diff --git a/em2rp/lib/config/env.dart b/em2rp/lib/config/env.dart index 539a953..eff3b62 100644 --- a/em2rp/lib/config/env.dart +++ b/em2rp/lib/config/env.dart @@ -1,9 +1,9 @@ class Env { - static const bool isDevelopment = true; + static const bool isDevelopment = false; // Configuration de l'auto-login en développement - static const String devAdminEmail = 'paul.fournel@em2events.fr'; - static const String devAdminPassword = 'Pastis51!'; + static const String devAdminEmail = ''; + static const String devAdminPassword = ''; // URLs et endpoints static const String baseUrl = 'https://em2rp-951dc.firebaseapp.com'; @@ -14,4 +14,3 @@ class Env { // Autres configurations static const int apiTimeout = 30000; // 30 secondes } - diff --git a/em2rp/lib/providers/event_provider.dart b/em2rp/lib/providers/event_provider.dart index 887323c..6db1b36 100644 --- a/em2rp/lib/providers/event_provider.dart +++ b/em2rp/lib/providers/event_provider.dart @@ -220,6 +220,15 @@ class EventProvider with ChangeNotifier { }); } + /// Vide entièrement le cache (mois + métadonnées) pour forcer un rechargement complet + void clearAllCache() { + _eventsByMonth.clear(); + _lastLoadTime = null; + _lastUserId = null; + _currentMonth = null; + print('[EventProvider] Cache entièrement vidé'); + } + /// Recharger les événements (utilise le dernier userId) Future refreshEvents(String userId, {bool canViewAllEvents = false}) async { await loadUserEvents(userId, canViewAllEvents: canViewAllEvents, forceReload: true); diff --git a/em2rp/lib/services/data_service.dart b/em2rp/lib/services/data_service.dart index 4d4f509..7c62b13 100644 --- a/em2rp/lib/services/data_service.dart +++ b/em2rp/lib/services/data_service.dart @@ -211,7 +211,7 @@ class DataService { /// Met à jour une option Future updateOption(String optionId, Map data) async { try { - final requestData = {'optionId': optionId, ...data}; + final requestData = {'optionId': optionId, 'data': data}; await _apiService.call('updateOption', requestData); } catch (e) { throw Exception('Erreur lors de la mise à jour de l\'option: $e'); diff --git a/em2rp/lib/views/calendar_page.dart b/em2rp/lib/views/calendar_page.dart index 148840c..2773f88 100644 --- a/em2rp/lib/views/calendar_page.dart +++ b/em2rp/lib/views/calendar_page.dart @@ -1,3 +1,5 @@ +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'; @@ -24,13 +26,22 @@ class CalendarPage extends StatefulWidget { } 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) + String? + _selectedUserId; // Filtre par utilisateur (null = tous les événements) + bool _isRefreshing = false; + double _detailsPaneFraction = 0.35; @override void initState() { @@ -46,13 +57,15 @@ class _CalendarPageState extends State { Future _loadCurrentMonthEvents() async { PerformanceMonitor.start('CalendarPage.loadCurrentMonthEvents'); - final localAuthProvider = Provider.of(context, listen: false); + 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}'); + print( + '[CalendarPage] Loading events for ${_focusedDay.year}-${_focusedDay.month}'); await eventProvider.loadMonthEvents( userId, @@ -79,6 +92,19 @@ class _CalendarPageState extends State { 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); + } + } + /// Charge les événements de manière asynchrone et sélectionne l'événement approprié /// DEPRECATED: Utiliser _loadCurrentMonthEvents à la place Future _loadEventsAsync() async { @@ -107,9 +133,10 @@ class _CalendarPageState extends State { 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)); + start.month == now.month && + start.day == now.day; + }).toList() + ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); EventModel? selected; DateTime? selectedDay; @@ -121,7 +148,8 @@ class _CalendarPageState extends State { // Chercher le prochain événement à venir final futureEvents = events .where((e) => e.startDateTime.isAfter(now)) - .toList()..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); + .toList() + ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); if (futureEvents.isNotEmpty) { selected = futureEvents[0]; @@ -186,21 +214,98 @@ class _CalendarPageState extends State { }); } + 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) { + return EventDetails( + event: _selectedEvent!, + selectedDate: _selectedDay, + events: filteredEvents, + 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) { + setState(() { + _detailsPaneFraction = _clampDetailsPaneFraction( + _detailsPaneFraction - (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); final canCreateEvents = localUserProvider.hasPermission('create_events'); - final canViewAllUserEvents = localUserProvider.hasPermission('view_all_user_events'); + final canViewAllUserEvents = + localUserProvider.hasPermission('view_all_user_events'); final isMobile = MediaQuery.of(context).size.width < 600; // Appliquer le filtre utilisateur si actif final filteredEvents = _getFilteredEvents(eventProvider.events); // Debug logs - print('[CalendarPage.build] Total events: ${eventProvider.events.length}, Filtered: ${filteredEvents.length}'); + 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}'); + print( + '[CalendarPage.build] First event: ${eventProvider.events.first.name} at ${eventProvider.events.first.startDateTime}'); } if (eventProvider.isLoading) { @@ -214,6 +319,26 @@ class _CalendarPageState extends State { 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( @@ -247,7 +372,9 @@ class _CalendarPageState extends State { ), // Corps du calendrier Expanded( - child: isMobile ? _buildMobileLayout(filteredEvents) : _buildDesktopLayout(filteredEvents), + child: isMobile + ? _buildMobileLayout(filteredEvents) + : _buildDesktopLayout(filteredEvents), ), ], ), @@ -271,36 +398,30 @@ class _CalendarPageState extends State { } Widget _buildDesktopLayout(List filteredEvents) { - return Row( - children: [ - // Calendrier (65% de la largeur) - Expanded( - flex: 65, - child: _buildCalendar(filteredEvents), - ), - // Détails de l'événement (35% de la largeur) - Expanded( - flex: 35, - child: _selectedEvent != null - ? EventDetails( - event: _selectedEvent!, - selectedDate: _selectedDay, - events: filteredEvents, - onSelectEvent: (event, date) { - setState(() { - _selectedEvent = event; - _selectedDay = date; - }); - }, - ) - : Center( - child: _selectedDay != null - ? Text('Aucun événement ne démarre à cette date') - : const Text( - 'Sélectionnez un événement pour voir les détails'), - ), - ), - ], + return LayoutBuilder( + builder: (context, constraints) { + final totalWidth = constraints.maxWidth; + final detailsPaneFraction = + _clampDetailsPaneFraction(_detailsPaneFraction, 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), + ), + ], + ); + }, ); } @@ -341,19 +462,23 @@ class _CalendarPageState extends State { if (details.primaryVelocity != null) { if (details.primaryVelocity! < -200) { // Swipe gauche : mois suivant - final newMonth = DateTime(_focusedDay.year, _focusedDay.month + 1, 1); + final newMonth = + DateTime(_focusedDay.year, _focusedDay.month + 1, 1); setState(() { _focusedDay = newMonth; }); - print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); + 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); + final newMonth = + DateTime(_focusedDay.year, _focusedDay.month - 1, 1); setState(() { _focusedDay = newMonth; }); - print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); + print( + '[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); _loadCurrentMonthEvents(); } } @@ -385,7 +510,8 @@ class _CalendarPageState extends State { setState(() { _focusedDay = newMonth; }); - print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); + print( + '[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); _loadCurrentMonthEvents(); } else if (details.primaryVelocity! > 200) { // Swipe droite : mois précédent @@ -394,7 +520,8 @@ class _CalendarPageState extends State { setState(() { _focusedDay = newMonth; }); - print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); + print( + '[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); _loadCurrentMonthEvents(); } } @@ -557,11 +684,13 @@ class _CalendarPageState extends State { icon: const Icon(Icons.chevron_left, color: AppColors.rouge, size: 28), onPressed: () { - final newMonth = DateTime(_focusedDay.year, _focusedDay.month - 1, 1); + final newMonth = + DateTime(_focusedDay.year, _focusedDay.month - 1, 1); setState(() { _focusedDay = newMonth; }); - print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); + print( + '[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); _loadCurrentMonthEvents(); }, ), @@ -600,11 +729,13 @@ class _CalendarPageState extends State { icon: const Icon(Icons.chevron_right, color: AppColors.rouge, size: 28), onPressed: () { - final newMonth = DateTime(_focusedDay.year, _focusedDay.month + 1, 1); + final newMonth = + DateTime(_focusedDay.year, _focusedDay.month + 1, 1); setState(() { _focusedDay = newMonth; }); - print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); + print( + '[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}'); _loadCurrentMonthEvents(); }, ), @@ -721,7 +852,7 @@ class _CalendarPageState extends State { onPageChanged: (focusedDay) { // Détecter si on a changé de mois final monthChanged = focusedDay.year != _focusedDay.year || - focusedDay.month != _focusedDay.month; + focusedDay.month != _focusedDay.month; setState(() { _focusedDay = focusedDay; @@ -729,7 +860,8 @@ class _CalendarPageState extends State { // Charger les événements du nouveau mois si nécessaire if (monthChanged) { - print('[CalendarPage] Month changed to ${focusedDay.year}-${focusedDay.month}'); + print( + '[CalendarPage] Month changed to ${focusedDay.year}-${focusedDay.month}'); _loadCurrentMonthEvents(); } }, diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_status_button.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_status_button.dart index 75b96c1..da1aec2 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_status_button.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_status_button.dart @@ -26,6 +26,16 @@ class _EventStatusButtonState extends State { EventStatus? _optimisticStatus; final DataService _dataService = DataService(FirebaseFunctionsApiService()); + @override + void didUpdateWidget(EventStatusButton oldWidget) { + super.didUpdateWidget(oldWidget); + // Réinitialiser le statut optimiste si on affiche un nouvel événement + if (oldWidget.event.id != widget.event.id) { + _optimisticStatus = null; + _loading = false; + } + } + Future _changeStatus(EventStatus newStatus) async { if ((widget.event.status == newStatus) || _loading) return; setState(() { diff --git a/em2rp/lib/views/widgets/calendar_widgets/month_view.dart b/em2rp/lib/views/widgets/calendar_widgets/month_view.dart index cde069e..f0b352a 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/month_view.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/month_view.dart @@ -5,6 +5,11 @@ 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; @@ -30,11 +35,17 @@ class MonthView extends StatelessWidget { Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - final rowHeight = (constraints.maxHeight - 100) / 6; + 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(8), + padding: const EdgeInsets.all(_calendarPadding), child: TableCalendar( firstDay: DateTime.utc(2020, 1, 1), lastDay: DateTime.utc(2030, 12, 31), @@ -42,6 +53,7 @@ class MonthView extends StatelessWidget { calendarFormat: calendarFormat, startingDayOfWeek: StartingDayOfWeek.monday, locale: 'fr_FR', + daysOfWeekHeight: _daysOfWeekHeight, availableCalendarFormats: const { CalendarFormat.month: 'Mois', CalendarFormat.week: 'Semaine', @@ -132,10 +144,9 @@ class MonthView extends StatelessWidget { 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); - final badgeColor = isSelected ? Colors.white : AppColors.rouge; - final badgeTextColor = isSelected ? AppColors.rouge : Colors.white; BoxDecoration decoration; if (isSelected) { @@ -161,56 +172,125 @@ class MonthView extends StatelessWidget { return Container( margin: const EdgeInsets.all(4), decoration: decoration, - child: Stack( - children: [ - Positioned( - top: 4, - left: 4, - child: Text( - day.day.toString(), - style: TextStyle(color: textColor), - ), - ), - if (dayEvents.isNotEmpty) - Positioned( - top: 4, - right: 4, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: badgeColor, - borderRadius: BorderRadius.circular(10), + 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), ), - child: Text( - dayEvents.length.toString(), - style: TextStyle( - color: badgeTextColor, - fontSize: 12, - fontWeight: FontWeight.bold, + 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(), ), ), ), - ), - if (dayEvents.isNotEmpty) - Positioned( - bottom: 2, - left: 2, - right: 2, - top: 28, - 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; @@ -228,7 +308,6 @@ class MonthView extends StatelessWidget { icon = Icons.close; break; case EventStatus.waitingForApproval: - default: color = Colors.amber; textColor = Colors.black; icon = Icons.hourglass_empty; @@ -243,7 +322,8 @@ class MonthView extends StatelessWidget { margin: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( - color: isSelected ? color.withAlpha(220) : color.withOpacity(0.18), + color: + isSelected ? color.withAlpha(220) : color.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(4), ), child: Row( @@ -282,4 +362,13 @@ class MonthView extends StatelessWidget { ), ); } + + /// 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(); + } } diff --git a/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart b/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart index 8554bf7..c10eb82 100644 --- a/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart +++ b/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart @@ -6,11 +6,8 @@ import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/providers/equipment_provider.dart'; import 'package:em2rp/providers/container_provider.dart'; -import 'package:em2rp/services/data_service.dart'; -import 'package:em2rp/services/api_service.dart'; import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/views/widgets/event/equipment_selection_dialog.dart'; -import 'package:em2rp/services/event_availability_service.dart'; /// Section pour afficher et gérer le matériel assigné à un événement class EventAssignedEquipmentSection extends StatefulWidget { @@ -37,8 +34,6 @@ class EventAssignedEquipmentSection extends StatefulWidget { class _EventAssignedEquipmentSectionState extends State { bool get _canAddMaterial => widget.startDate != null && widget.endDate != null; - final EventAvailabilityService _availabilityService = EventAvailabilityService(); - final DataService _dataService = DataService(FirebaseFunctionsApiService()); final Map _equipmentCache = {}; final Map _containerCache = {}; bool _isLoading = true; @@ -66,97 +61,53 @@ class _EventAssignedEquipmentSectionState extends State(); final containerProvider = context.read(); - // 🔧 FIX: Si on a un eventId, utiliser getEventWithDetails pour charger les données complètes - if (widget.eventId != null && widget.eventId!.isNotEmpty) { - DebugLog.info('[EventAssignedEquipmentSection] Loading event with details: ${widget.eventId}'); + DebugLog.info('[EventAssignedEquipmentSection] Loading caches from assigned lists'); - final result = await _dataService.getEventWithDetails(widget.eventId!); - final equipmentsMap = result['equipments'] as Map; - final containersMap = result['containers'] as Map; + // Toujours partir des données locales du formulaire pour éviter les décalages visuels. + final equipmentIds = widget.assignedEquipment.map((eq) => eq.equipmentId).toList(); + final containers = await containerProvider.getContainersByIds(widget.assignedContainers); - DebugLog.info('[EventAssignedEquipmentSection] Loaded ${equipmentsMap.length} equipments and ${containersMap.length} containers with details'); + final childEquipmentIds = []; + for (final container in containers) { + childEquipmentIds.addAll(container.equipmentIds); + } - // Construire les caches à partir des données reçues - _equipmentCache.clear(); - _containerCache.clear(); + final allEquipmentIds = {...equipmentIds, ...childEquipmentIds}.toList(); + final equipment = await equipmentProvider.getEquipmentsByIds(allEquipmentIds); - // Remplir le cache d'équipements - equipmentsMap.forEach((id, data) { - try { - _equipmentCache[id] = EquipmentModel.fromMap(data as Map, id); - } catch (e) { - DebugLog.error('[EventAssignedEquipmentSection] Error parsing equipment $id', e); - } - }); + _equipmentCache.clear(); + _containerCache.clear(); - // Remplir le cache de containers - containersMap.forEach((id, data) { - try { - _containerCache[id] = ContainerModel.fromMap(data as Map, id); - } catch (e) { - DebugLog.error('[EventAssignedEquipmentSection] Error parsing container $id', e); - } - }); + for (final eq in widget.assignedEquipment) { + final equipmentItem = equipment.firstWhere( + (e) => e.id == eq.equipmentId, + orElse: () => EquipmentModel( + id: eq.equipmentId, + name: 'Équipement inconnu', + category: EquipmentCategory.other, + status: EquipmentStatus.available, + maintenanceIds: [], + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ), + ); + _equipmentCache[eq.equipmentId] = equipmentItem; + } - DebugLog.info('[EventAssignedEquipmentSection] Caches populated: ${_equipmentCache.length} equipments, ${_containerCache.length} containers'); - - } else { - // Mode création d'événement : charger via les providers - DebugLog.info('[EventAssignedEquipmentSection] Loading via providers (creation mode)'); - - // Extraire les IDs des équipements assignés - final equipmentIds = widget.assignedEquipment - .map((eq) => eq.equipmentId) - .toList(); - - // Charger les conteneurs - final containers = await containerProvider.getContainersByIds(widget.assignedContainers); - - // Extraire les IDs des équipements enfants des containers - final childEquipmentIds = []; - for (var container in containers) { - childEquipmentIds.addAll(container.equipmentIds); - } - - // Combiner les IDs des équipements assignés + enfants des containers - final allEquipmentIds = {...equipmentIds, ...childEquipmentIds}.toList(); - - // Charger TOUS les équipements nécessaires - final equipment = await equipmentProvider.getEquipmentsByIds(allEquipmentIds); - - // Créer le cache des équipements - for (var eq in widget.assignedEquipment) { - final equipmentItem = equipment.firstWhere( - (e) => e.id == eq.equipmentId, - orElse: () => EquipmentModel( - id: eq.equipmentId, - name: 'Équipement inconnu', - category: EquipmentCategory.other, - status: EquipmentStatus.available, - maintenanceIds: [], - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ), - ); - _equipmentCache[eq.equipmentId] = equipmentItem; - } - - // Créer le cache des conteneurs - for (var containerId in widget.assignedContainers) { - final container = containers.firstWhere( - (c) => c.id == containerId, - orElse: () => ContainerModel( - id: containerId, - name: 'Conteneur inconnu', - type: ContainerType.flightCase, - status: EquipmentStatus.available, - equipmentIds: [], - updatedAt: DateTime.now(), - createdAt: DateTime.now(), - ), - ); - _containerCache[containerId] = container; - } + for (final containerId in widget.assignedContainers) { + final container = containers.firstWhere( + (c) => c.id == containerId, + orElse: () => ContainerModel( + id: containerId, + name: 'Conteneur inconnu', + type: ContainerType.flightCase, + status: EquipmentStatus.available, + equipmentIds: [], + updatedAt: DateTime.now(), + createdAt: DateTime.now(), + ), + ); + _containerCache[containerId] = container; } } catch (e) { DebugLog.error('[EventAssignedEquipmentSection] Error loading equipment and containers', e); @@ -262,9 +213,6 @@ class _EventAssignedEquipmentSectionState extends State _removeEquipment(eventEq.equipmentId), + ), + ), + ); } final isConsumable = equipment.category == EquipmentCategory.consumable || diff --git a/em2rp/web/version.json b/em2rp/web/version.json index 066e2f3..f2119f0 100644 --- a/em2rp/web/version.json +++ b/em2rp/web/version.json @@ -1,7 +1,7 @@ { - "version": "1.1.17", + "version": "1.1.18", "updateUrl": "https://app.em2events.fr", "forceUpdate": true, - "releaseNotes": "Ajout d'une page de statistiques détaillées pour les équipements et les événements.", - "timestamp": "2026-03-12T14:00:20.817Z" + "releaseNotes": "Fix BUG : Ajout equipement à un evenement existant, boutons de modification de statut d'un evenement ne fonctionnaient pas. Refonte legere de la page calendrier.", + "timestamp": "2026-03-12T20:11:54.548Z" } \ No newline at end of file