split et refacto de event_details.dart
This commit is contained in:
		| @@ -1,19 +1,16 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:em2rp/models/event_model.dart'; | ||||
| import 'package:em2rp/utils/colors.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:em2rp/providers/local_user_provider.dart'; | ||||
| import 'package:em2rp/providers/event_provider.dart'; | ||||
| import 'package:latlong2/latlong.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
| import 'package:path/path.dart' as p; | ||||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||||
| import 'package:em2rp/views/widgets/user_management/user_card.dart'; | ||||
| import 'package:em2rp/models/user_model.dart'; | ||||
| import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart'; | ||||
| import 'package:em2rp/views/event_add_page.dart'; | ||||
| import 'package:em2rp/views/widgets/event_form/event_options_display_widget.dart'; | ||||
| import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_navigation.dart'; | ||||
| import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_header.dart'; | ||||
| import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_status_button.dart'; | ||||
| import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_info.dart'; | ||||
| import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_description.dart'; | ||||
| import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_documents.dart'; | ||||
| import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_equipe.dart'; | ||||
|  | ||||
| class EventDetails extends StatelessWidget { | ||||
|   final EventModel event; | ||||
| @@ -31,15 +28,11 @@ class EventDetails extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final dateFormat = DateFormat('dd/MM/yyyy HH:mm'); | ||||
|     final currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: '€'); | ||||
|     final fullDateFormat = DateFormat('EEEE d MMMM y', 'fr_FR'); | ||||
|     // Trie les événements par date de début | ||||
|     final sortedEvents = List<EventModel>.from(events) | ||||
|       ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); | ||||
|     final currentIndex = sortedEvents.indexWhere((e) => e.id == event.id); | ||||
|     final localUserProvider = Provider.of<LocalUserProvider>(context); | ||||
|     final isAdmin = localUserProvider.hasPermission('view_all_users'); | ||||
|     final canViewPrices = localUserProvider.hasPermission('view_event_prices'); | ||||
|  | ||||
|     return Card( | ||||
| @@ -49,121 +42,22 @@ class EventDetails extends StatelessWidget { | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Row( | ||||
|               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|               children: [ | ||||
|                 IconButton( | ||||
|                   onPressed: currentIndex > 0 | ||||
|                       ? () { | ||||
|                           final prevEvent = sortedEvents[currentIndex - 1]; | ||||
|                           onSelectEvent(prevEvent, prevEvent.startDateTime); | ||||
|                         } | ||||
|                       : null, | ||||
|                   icon: const Icon(Icons.arrow_back), | ||||
|                   color: AppColors.rouge, | ||||
|                 ), | ||||
|                 if (selectedDate != null) | ||||
|                   Expanded( | ||||
|                     child: Center( | ||||
|                       child: Text( | ||||
|                         fullDateFormat.format(selectedDate!), | ||||
|                         style: Theme.of(context).textTheme.titleLarge?.copyWith( | ||||
|                               color: AppColors.rouge, | ||||
|                               fontWeight: FontWeight.bold, | ||||
|                             ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 IconButton( | ||||
|                   onPressed: currentIndex < sortedEvents.length - 1 | ||||
|                       ? () { | ||||
|                           final nextEvent = sortedEvents[currentIndex + 1]; | ||||
|                           onSelectEvent(nextEvent, nextEvent.startDateTime); | ||||
|                         } | ||||
|                       : null, | ||||
|                   icon: const Icon(Icons.arrow_forward), | ||||
|                   color: AppColors.rouge, | ||||
|                 ), | ||||
|               ], | ||||
|             EventDetailsNavigation( | ||||
|               sortedEvents: sortedEvents, | ||||
|               currentIndex: currentIndex, | ||||
|               selectedDate: selectedDate, | ||||
|               onSelectEvent: onSelectEvent, | ||||
|             ), | ||||
|             const SizedBox(height: 16), | ||||
|             Row( | ||||
|               //Titre de l'événement | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Expanded( | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                     Row( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                           children: [ | ||||
|                             SelectableText( | ||||
|                               event.name, | ||||
|                               style: Theme.of(context).textTheme.headlineMedium?.copyWith( | ||||
|                                 color: AppColors.noir, | ||||
|                                 fontWeight: FontWeight.bold, | ||||
|                               ), | ||||
|                             ), | ||||
|                             const SizedBox(width: 12), | ||||
|                             _buildStatusIcon(event.status), | ||||
|                           ], | ||||
|                         ), | ||||
|                       const SizedBox(height: 4), | ||||
|                       //Type d'événement | ||||
|                       Text( | ||||
|                         event.eventTypeId, | ||||
|                         style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|                           color: AppColors.rouge, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|                 // Statut de l'événement | ||||
|  | ||||
|                 const SizedBox(width: 8), | ||||
|                 Spacer(), | ||||
|                 if (Provider.of<LocalUserProvider>(context, listen: false) | ||||
|                     .hasPermission('edit_event')) | ||||
|                   IconButton( | ||||
|                     icon: const Icon(Icons.edit, color: AppColors.rouge), | ||||
|                     tooltip: 'Modifier', | ||||
|                     onPressed: () { | ||||
|                       Navigator.of(context).push( | ||||
|                         MaterialPageRoute( | ||||
|                           builder: (context) => EventAddEditPage(event: event), | ||||
|                         ), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|               ], | ||||
|             ), | ||||
|             EventDetailsHeader(event: event), | ||||
|             if (Provider.of<LocalUserProvider>(context, listen: false) | ||||
|                 .hasPermission('change_event_status')) | ||||
|               Padding( | ||||
|                 padding: const EdgeInsets.symmetric(vertical: 12.0), | ||||
|                 child: _FirestoreStatusButton( | ||||
|                     eventId: event.id, | ||||
|                   currentStatus: event.status, | ||||
|                   onStatusChanged: (newStatus) async { | ||||
|                     await FirebaseFirestore.instance | ||||
|                         .collection('events') | ||||
|                         .doc(event.id) | ||||
|                         .update({'status': eventStatusToString(newStatus)}); | ||||
|                     // Recharge l'événement depuis Firestore et notifie le parent | ||||
|                     final snap = await FirebaseFirestore.instance | ||||
|                         .collection('events') | ||||
|                         .doc(event.id) | ||||
|                         .get(); | ||||
|                     final updatedEvent = | ||||
|                         EventModel.fromMap(snap.data()!, event.id); | ||||
|                     onSelectEvent(updatedEvent, | ||||
|                         selectedDate ?? updatedEvent.startDateTime); | ||||
|                     // Met à jour uniquement l'événement dans le provider (rafraîchissement local et fluide) | ||||
|                     await Provider.of<EventProvider>(context, listen: false) | ||||
|                         .updateEvent(updatedEvent); | ||||
|                   }, | ||||
|                 child: EventStatusButton( | ||||
|                   event: event, | ||||
|                   selectedDate: selectedDate, | ||||
|                   onSelectEvent: onSelectEvent, | ||||
|                 ), | ||||
|               ), | ||||
|             const SizedBox(height: 16), | ||||
| @@ -172,214 +66,15 @@ class EventDetails extends StatelessWidget { | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     _buildInfoRow( | ||||
|                       context, | ||||
|                       Icons.calendar_today, | ||||
|                       'Horaire de début', | ||||
|                       dateFormat.format(event.startDateTime), | ||||
|                     ), | ||||
|                     _buildInfoRow( | ||||
|                       context, | ||||
|                       Icons.calendar_today, | ||||
|                       'Horaire de fin', | ||||
|                       dateFormat.format(event.endDateTime), | ||||
|                     ), | ||||
|                     if (canViewPrices) | ||||
|                       _buildInfoRow( | ||||
|                         context, | ||||
|                         Icons.euro, | ||||
|                         'Prix de base', | ||||
|                         currencyFormat.format(event.basePrice), | ||||
|                       ), | ||||
|                     if (event.options.isNotEmpty) ...[ | ||||
|                       EventOptionsDisplayWidget( | ||||
|                         optionsData: event.options, | ||||
|                         canViewPrices: canViewPrices, | ||||
|                         showPriceCalculation: false, // On affiche le total séparément | ||||
|                       ), | ||||
|                     ], | ||||
|                     if (canViewPrices) ...[ | ||||
|                       const SizedBox(height: 4), | ||||
|                       Builder( | ||||
|                         builder: (context) { | ||||
|                           final total = event.basePrice + | ||||
|                               event.options.fold<num>(0, | ||||
|                                   (sum, opt) => sum + (opt['price'] ?? 0.0)); | ||||
|                           return Padding( | ||||
|                             padding: | ||||
|                                 const EdgeInsets.only(top: 8.0, bottom: 8.0), | ||||
|                             child: Row( | ||||
|                               children: [ | ||||
|                                 const Icon(Icons.attach_money, | ||||
|                                     color: AppColors.rouge), | ||||
|                                 const SizedBox(width: 8), | ||||
|                                 Text('Prix total : ', | ||||
|                                     style: Theme.of(context) | ||||
|                                         .textTheme | ||||
|                                         .titleMedium | ||||
|                                         ?.copyWith( | ||||
|                                           color: AppColors.noir, | ||||
|                                           fontWeight: FontWeight.bold, | ||||
|                                         )), | ||||
|                                 Text( | ||||
|                                   currencyFormat.format(total), | ||||
|                                   style: Theme.of(context) | ||||
|                                       .textTheme | ||||
|                                       .titleMedium | ||||
|                                       ?.copyWith( | ||||
|                                         color: AppColors.rouge, | ||||
|                                         fontWeight: FontWeight.bold, | ||||
|                                       ), | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                           ); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ], | ||||
|                     _buildInfoRow( | ||||
|                       context, | ||||
|                       Icons.build, | ||||
|                       'Temps d\'installation', | ||||
|                       '${event.installationTime} heures', | ||||
|                     ), | ||||
|                     // Sous-titre: Horaire d'arrivée prévisionnelle (début - installation) | ||||
|                     Builder( | ||||
|                       builder: (context) { | ||||
|                         final arrival = event.startDateTime.subtract(Duration(hours: event.installationTime)); | ||||
|                         return Padding( | ||||
|                           padding: const EdgeInsets.only(left: 36.0, bottom: 4.0), | ||||
|                           child: Text( | ||||
|                             'Horaire d\'arrivée prévisionnel : ${DateFormat('dd/MM/yyyy HH:mm').format(arrival)}', | ||||
|                             style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[700]), | ||||
|                           ), | ||||
|                         ); | ||||
|                       }, | ||||
|                     ), | ||||
|  | ||||
|                     _buildInfoRow( | ||||
|                       context, | ||||
|                       Icons.construction, | ||||
|                       'Temps de démontage', | ||||
|                       '${event.disassemblyTime} heures', | ||||
|                     ), | ||||
|                     // Sous-titre: Horaire de départ prévu (fin + démontage) | ||||
|                     Builder( | ||||
|                       builder: (context) { | ||||
|                         final departure = event.endDateTime.add(Duration(hours: event.disassemblyTime)); | ||||
|                         return Padding( | ||||
|                           padding: const EdgeInsets.only(left: 36.0, bottom: 4.0), | ||||
|                           child: Text( | ||||
|                             'Horaire de départ prévu : ${DateFormat('dd/MM/yyyy HH:mm').format(departure)}', | ||||
|                             style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[700]), | ||||
|                           ), | ||||
|                         ); | ||||
|                       }, | ||||
|                     EventDetailsInfo( | ||||
|                       event: event, | ||||
|                       canViewPrices: canViewPrices, | ||||
|                     ), | ||||
|                     const SizedBox(height: 16), | ||||
|                     Text( | ||||
|                       'Description', | ||||
|                       style: Theme.of(context).textTheme.titleLarge?.copyWith( | ||||
|                             color: AppColors.noir, | ||||
|                             fontWeight: FontWeight.bold, | ||||
|                           ), | ||||
|                     ), | ||||
|                     const SizedBox(height: 8), | ||||
|                     SelectableText( | ||||
|                       event.description, | ||||
|                       style: Theme.of(context).textTheme.bodyLarge, | ||||
|                     ), | ||||
|                     EventDetailsDescription(event: event), | ||||
|                     EventDetailsDocuments(documents: event.documents), | ||||
|                     const SizedBox(height: 16), | ||||
|                     Text( | ||||
|                       'Adresse', | ||||
|                       style: Theme.of(context).textTheme.titleLarge?.copyWith( | ||||
|                             color: AppColors.noir, | ||||
|                             fontWeight: FontWeight.bold, | ||||
|                           ), | ||||
|                     ), | ||||
|                     const SizedBox(height: 8), | ||||
|                     SelectableText( | ||||
|                       event.address, | ||||
|                       style: Theme.of(context).textTheme.bodyLarge, | ||||
|                     ), | ||||
|                     if (event.latitude != 0.0 || event.longitude != 0.0) ...[ | ||||
|                       const SizedBox(height: 4), | ||||
|                       SelectableText( | ||||
|                         '${event.latitude}° N, ${event.longitude}° E', | ||||
|                         style: Theme.of(context).textTheme.bodySmall, | ||||
|                       ), | ||||
|                     ], | ||||
|                     if (event.documents.isNotEmpty) ...[ | ||||
|                       const SizedBox(height: 16), | ||||
|                       Text('Documents', | ||||
|                           style: Theme.of(context) | ||||
|                               .textTheme | ||||
|                               .titleLarge | ||||
|                               ?.copyWith( | ||||
|                                   color: AppColors.noir, | ||||
|                                   fontWeight: FontWeight.bold)), | ||||
|                       const SizedBox(height: 8), | ||||
|                       Column( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: event.documents.map((doc) { | ||||
|                           final fileName = doc['name'] ?? ''; | ||||
|                           final url = doc['url'] ?? ''; | ||||
|                           final ext = p.extension(fileName).toLowerCase(); | ||||
|                           IconData icon; | ||||
|                           if ([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"] | ||||
|                               .contains(ext)) { | ||||
|                             icon = Icons.image; | ||||
|                           } else if (ext == ".pdf") { | ||||
|                             icon = Icons.picture_as_pdf; | ||||
|                           } else if ([ | ||||
|                             ".txt", | ||||
|                             ".md", | ||||
|                             ".csv", | ||||
|                             ".json", | ||||
|                             ".xml", | ||||
|                             ".docx", | ||||
|                             ".doc", | ||||
|                             ".xls", | ||||
|                             ".xlsx", | ||||
|                             ".ppt", | ||||
|                             ".pptx" | ||||
|                           ].contains(ext)) { | ||||
|                             icon = Icons.description; | ||||
|                           } else { | ||||
|                             icon = Icons.attach_file; | ||||
|                           } | ||||
|                           return ListTile( | ||||
|                             leading: Icon(icon, color: Colors.blueGrey), | ||||
|                             title: SelectableText( | ||||
|                               fileName, | ||||
|                               maxLines: 1, | ||||
|                               textAlign: TextAlign.left, | ||||
|                               style: Theme.of(context).textTheme.bodyMedium, | ||||
|                             ), | ||||
|                             trailing: IconButton( | ||||
|                               icon: const Icon(Icons.download), | ||||
|                               onPressed: () async { | ||||
|                                 if (await canLaunchUrl(Uri.parse(url))) { | ||||
|                                   await launchUrl(Uri.parse(url), | ||||
|                                       mode: LaunchMode.externalApplication); | ||||
|                                 } | ||||
|                               }, | ||||
|                             ), | ||||
|                             onTap: () async { | ||||
|                               if (await canLaunchUrl(Uri.parse(url))) { | ||||
|                                 await launchUrl(Uri.parse(url), | ||||
|                                     mode: LaunchMode.externalApplication); | ||||
|                               } | ||||
|                             }, | ||||
|                             contentPadding: EdgeInsets.zero, | ||||
|                             dense: true, | ||||
|                           ); | ||||
|                         }).toList(), | ||||
|                       ), | ||||
|                       // --- EQUIPE SECTION --- | ||||
|                       const SizedBox(height: 16), | ||||
|                       EquipeSection(workforce: event.workforce), | ||||
|                     ], | ||||
|                     EventDetailsEquipe(workforce: event.workforce), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
| @@ -389,64 +84,9 @@ class EventDetails extends StatelessWidget { | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildInfoRow( | ||||
|     BuildContext context, | ||||
|     IconData icon, | ||||
|     String label, | ||||
|     String value, | ||||
|   ) { | ||||
|     return Padding( | ||||
|       padding: const EdgeInsets.symmetric(vertical: 8), | ||||
|       child: Row( | ||||
|         children: [ | ||||
|           Icon(icon, color: AppColors.rouge), | ||||
|           const SizedBox(width: 8), | ||||
|           Text( | ||||
|             '$label : ', | ||||
|             style: Theme.of(context).textTheme.titleMedium?.copyWith( | ||||
|                   color: AppColors.noir, | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                 ), | ||||
|           ), | ||||
|           Text( | ||||
|             value, | ||||
|             style: Theme.of(context).textTheme.titleMedium, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildStatusIcon(EventStatus status) { | ||||
|     Color color; | ||||
|     IconData icon; | ||||
|     String tooltip; | ||||
|     switch (status) { | ||||
|       case EventStatus.confirmed: | ||||
|         color = Colors.green; | ||||
|         icon = Icons.check_circle; | ||||
|         tooltip = 'Confirmé'; | ||||
|         break; | ||||
|       case EventStatus.canceled: | ||||
|         color = Colors.red; | ||||
|         icon = Icons.cancel; | ||||
|         tooltip = 'Annulé'; | ||||
|         break; | ||||
|       case EventStatus.waitingForApproval: | ||||
|       default: | ||||
|         color = Colors.amber; | ||||
|         icon = Icons.hourglass_empty; | ||||
|         tooltip = 'En attente de validation'; | ||||
|         break; | ||||
|     } | ||||
|     return Tooltip( | ||||
|       message: tooltip, | ||||
|       child: Icon(icon, color: color, size: 28), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // La classe EventAddDialog reste inchangée car elle n'est pas liée aux détails d'événement | ||||
| class EventAddDialog extends StatefulWidget { | ||||
|   const EventAddDialog({super.key}); | ||||
|  | ||||
| @@ -486,7 +126,9 @@ class _EventAddDialogState extends State<EventAddDialog> { | ||||
|   Future<void> _submit() async { | ||||
|     if (!_formKey.currentState!.validate() || | ||||
|         _startDateTime == null || | ||||
|         _endDateTime == null) return; | ||||
|         _endDateTime == null) { | ||||
|       return; | ||||
|     } | ||||
|     setState(() { | ||||
|       _isLoading = true; | ||||
|       _error = null; | ||||
| @@ -685,214 +327,3 @@ class _EventAddDialogState extends State<EventAddDialog> { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _FirestoreStatusButton extends StatefulWidget { | ||||
|   final String eventId; | ||||
|   final EventStatus currentStatus; | ||||
|   final Future<void> Function(EventStatus) onStatusChanged; | ||||
|   const _FirestoreStatusButton({ | ||||
|     required this.eventId, | ||||
|     required this.currentStatus, | ||||
|     required this.onStatusChanged, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<_FirestoreStatusButton> createState() => _FirestoreStatusButtonState(); | ||||
| } | ||||
|  | ||||
| class _FirestoreStatusButtonState extends State<_FirestoreStatusButton> { | ||||
|   bool _loading = false; | ||||
|  | ||||
|   Future<void> changerStatut(EventStatus nouveau) async { | ||||
|     if (widget.currentStatus == nouveau) return; | ||||
|     setState(() => _loading = true); | ||||
|     await widget.onStatusChanged(nouveau); | ||||
|     setState(() => _loading = false); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void didUpdateWidget(covariant _FirestoreStatusButton oldWidget) { | ||||
|     super.didUpdateWidget(oldWidget); | ||||
|     // Si l'événement change, on arrête le loading (sécurité UX) | ||||
|     if (oldWidget.eventId != widget.eventId || | ||||
|         oldWidget.currentStatus != widget.currentStatus) { | ||||
|       if (_loading) setState(() => _loading = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final status = widget.currentStatus; | ||||
|     String texte; | ||||
|     Color couleurFond; | ||||
|     List<Widget> enfants = []; | ||||
|     switch (status) { | ||||
|       case EventStatus.waitingForApproval: | ||||
|         texte = "En Attente"; | ||||
|         couleurFond = Colors.yellow.shade600; | ||||
|         enfants = [ | ||||
|           _buildIconButton(Icons.close, Colors.red, | ||||
|               () => changerStatut(EventStatus.canceled)), | ||||
|           _buildLabel(texte, couleurFond), | ||||
|           _buildIconButton(Icons.check, Colors.green, | ||||
|               () => changerStatut(EventStatus.confirmed)), | ||||
|         ]; | ||||
|         break; | ||||
|       case EventStatus.confirmed: | ||||
|         texte = "Confirmé"; | ||||
|         couleurFond = Colors.green; | ||||
|         enfants = [ | ||||
|           _buildIconButton(Icons.close, Colors.red, | ||||
|               () => changerStatut(EventStatus.canceled)), | ||||
|           _buildIconButton(Icons.hourglass_empty, Colors.yellow.shade700, | ||||
|               () => changerStatut(EventStatus.waitingForApproval)), | ||||
|           _buildLabel(texte, couleurFond), | ||||
|         ]; | ||||
|         break; | ||||
|       case EventStatus.canceled: | ||||
|         texte = "Annulé"; | ||||
|         couleurFond = Colors.red; | ||||
|         enfants = [ | ||||
|           _buildLabel(texte, couleurFond), | ||||
|           _buildIconButton(Icons.hourglass_empty, Colors.yellow.shade700, | ||||
|               () => changerStatut(EventStatus.waitingForApproval)), | ||||
|           _buildIconButton(Icons.check, Colors.green, | ||||
|               () => changerStatut(EventStatus.confirmed)), | ||||
|         ]; | ||||
|         break; | ||||
|     } | ||||
|     return AnimatedContainer( | ||||
|       duration: const Duration(milliseconds: 200), | ||||
|       padding: const EdgeInsets.all(2), | ||||
|       decoration: const BoxDecoration( | ||||
|         color: Colors.transparent, | ||||
|       ), | ||||
|       child: Row( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: enfants, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildLabel(String texte, Color couleur) { | ||||
|     return AnimatedContainer( | ||||
|       duration: const Duration(milliseconds: 200), | ||||
|       margin: const EdgeInsets.symmetric(horizontal: 2), | ||||
|       padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       decoration: BoxDecoration( | ||||
|         color: couleur, | ||||
|         borderRadius: BorderRadius.circular(6), | ||||
|       ), | ||||
|       child: AnimatedSwitcher( | ||||
|         duration: const Duration(milliseconds: 200), | ||||
|         child: Text( | ||||
|           texte, | ||||
|           key: ValueKey(texte), | ||||
|           style: const TextStyle( | ||||
|               fontWeight: FontWeight.bold, color: Colors.white, fontSize: 13), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildIconButton( | ||||
|       IconData icone, Color couleur, VoidCallback onPressed) { | ||||
|     return AnimatedContainer( | ||||
|       duration: const Duration(milliseconds: 200), | ||||
|       margin: const EdgeInsets.symmetric(horizontal: 2), | ||||
|       decoration: BoxDecoration( | ||||
|         border: Border.all(color: couleur, width: 1.5), | ||||
|         borderRadius: BorderRadius.circular(6), | ||||
|       ), | ||||
|       child: IconButton( | ||||
|         icon: Icon(icone, color: couleur, size: 16), | ||||
|         onPressed: _loading ? null : onPressed, | ||||
|         splashRadius: 16, | ||||
|         tooltip: 'Changer statut', | ||||
|         padding: const EdgeInsets.all(4), | ||||
|         constraints: const BoxConstraints(minWidth: 28, minHeight: 28), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class EquipeSection extends StatelessWidget { | ||||
|   final List workforce; | ||||
|   const EquipeSection({super.key, required this.workforce}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (workforce.isEmpty) { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Text('Equipe', | ||||
|               style: Theme.of(context).textTheme.titleLarge?.copyWith( | ||||
|                     color: Colors.black, | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                   )), | ||||
|           const SizedBox(height: 8), | ||||
|           Text('Aucun membre assigné.', | ||||
|               style: Theme.of(context).textTheme.bodyMedium), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|     return FutureBuilder<List<UserModel>>( | ||||
|       future: _fetchUsers(), | ||||
|       builder: (context, snapshot) { | ||||
|         if (snapshot.connectionState == ConnectionState.waiting) { | ||||
|           return const Padding( | ||||
|             padding: EdgeInsets.symmetric(vertical: 16), | ||||
|             child: Center(child: CircularProgressIndicator()), | ||||
|           ); | ||||
|         } | ||||
|         if (snapshot.hasError) { | ||||
|           return Padding( | ||||
|             padding: const EdgeInsets.symmetric(vertical: 16), | ||||
|             child: Text( | ||||
|               snapshot.error.toString().contains('permission-denied') | ||||
|                   ? "Vous n'avez pas la permission de voir tous les membres de l'équipe." | ||||
|                   : "Erreur lors du chargement de l'équipe : ${snapshot.error}", | ||||
|               style: const TextStyle(color: Colors.red), | ||||
|             ), | ||||
|           ); | ||||
|         } | ||||
|         final users = snapshot.data ?? []; | ||||
|         return Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Text('Equipe', | ||||
|                 style: Theme.of(context).textTheme.titleLarge?.copyWith( | ||||
|                       color: Colors.black, | ||||
|                       fontWeight: FontWeight.bold, | ||||
|                     )), | ||||
|             const SizedBox(height: 8), | ||||
|             if (users.isEmpty) | ||||
|               Text('Aucun membre assigné.', | ||||
|                   style: Theme.of(context).textTheme.bodyMedium), | ||||
|             if (users.isNotEmpty) | ||||
|               UserChipsList( | ||||
|                 users: users, | ||||
|                 showRemove: false, | ||||
|               ), | ||||
|           ], | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<List<UserModel>> _fetchUsers() async { | ||||
|     final firestore = FirebaseFirestore.instance; | ||||
|     List<UserModel> users = []; | ||||
|     for (final ref in workforce) { | ||||
|       try { | ||||
|         final doc = await firestore.doc(ref.path).get(); | ||||
|         if (doc.exists) { | ||||
|           users.add( | ||||
|               UserModel.fromMap(doc.data() as Map<String, dynamic>, doc.id)); | ||||
|         } | ||||
|       } catch (_) {} | ||||
|     } | ||||
|     return users; | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 ElPoyo
					ElPoyo