From f10a6088010d76a0227b8ce5ce42663f124691ab Mon Sep 17 00:00:00 2001 From: ElPoyo Date: Wed, 15 Oct 2025 14:09:44 +0200 Subject: [PATCH] split et refacto de event_details.dart --- em2rp/devtools_options.yaml | 1 + em2rp/lib/models/option_model.dart | 1 - em2rp/lib/providers/event_provider.dart | 2 +- em2rp/lib/views/event_add_page.dart | 1 - .../calendar_widgets/event_details.dart | 623 +----------------- .../event_details_description.dart | 54 ++ .../event_details_documents.dart | 99 +++ .../event_details_equipe.dart | 137 ++++ .../event_details_header.dart | 92 +++ .../event_details_info.dart | 162 +++++ .../event_details_navigation.dart | 63 ++ .../event_status_button.dart | 182 +++++ .../event_options_display_widget.dart | 1 - .../event_staff_and_documents_section.dart | 2 - .../inputs/option_selector_widget.dart | 4 +- 15 files changed, 820 insertions(+), 604 deletions(-) create mode 100644 em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_description.dart create mode 100644 em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_documents.dart create mode 100644 em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_equipe.dart create mode 100644 em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart create mode 100644 em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_info.dart create mode 100644 em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_navigation.dart create mode 100644 em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_status_button.dart diff --git a/em2rp/devtools_options.yaml b/em2rp/devtools_options.yaml index fa0b357..2bc8e05 100644 --- a/em2rp/devtools_options.yaml +++ b/em2rp/devtools_options.yaml @@ -1,3 +1,4 @@ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: + - provider: true \ No newline at end of file diff --git a/em2rp/lib/models/option_model.dart b/em2rp/lib/models/option_model.dart index ca38f8d..595f18d 100644 --- a/em2rp/lib/models/option_model.dart +++ b/em2rp/lib/models/option_model.dart @@ -1,4 +1,3 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; class EventOption { final String id; diff --git a/em2rp/lib/providers/event_provider.dart b/em2rp/lib/providers/event_provider.dart index 8df344a..7bc4df6 100644 --- a/em2rp/lib/providers/event_provider.dart +++ b/em2rp/lib/providers/event_provider.dart @@ -58,7 +58,7 @@ class EventProvider with ChangeNotifier { }); if (isInWorkforce) { - print('Event ${event.name} includes user ${userId}'); + print('Event ${event.name} includes user $userId'); } return isInWorkforce; diff --git a/em2rp/lib/views/event_add_page.dart b/em2rp/lib/views/event_add_page.dart index 07b2183..25ec0f4 100644 --- a/em2rp/lib/views/event_add_page.dart +++ b/em2rp/lib/views/event_add_page.dart @@ -7,7 +7,6 @@ import 'package:em2rp/views/widgets/event_form/event_details_section.dart'; import 'package:em2rp/views/widgets/event_form/event_staff_and_documents_section.dart'; import 'package:em2rp/views/widgets/event_form/event_form_actions.dart'; import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart'; -import 'package:flutter/foundation.dart'; class EventAddEditPage extends StatefulWidget { final EventModel? event; diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart index 8dfdc38..48dc2e2 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart @@ -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.from(events) ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); final currentIndex = sortedEvents.indexWhere((e) => e.id == event.id); final localUserProvider = Provider.of(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(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(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(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(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 { Future _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 { ); } } - -class _FirestoreStatusButton extends StatefulWidget { - final String eventId; - final EventStatus currentStatus; - final Future 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 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 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>( - 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> _fetchUsers() async { - final firestore = FirebaseFirestore.instance; - List 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, doc.id)); - } - } catch (_) {} - } - return users; - } -} diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_description.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_description.dart new file mode 100644 index 0000000..61d04a2 --- /dev/null +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_description.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/event_model.dart'; +import 'package:em2rp/utils/colors.dart'; + +class EventDetailsDescription extends StatelessWidget { + final EventModel event; + + const EventDetailsDescription({ + super.key, + required this.event, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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, + ), + 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, + ), + ], + ], + ); + } +} + diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_documents.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_documents.dart new file mode 100644 index 0000000..a4c1a35 --- /dev/null +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_documents.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/utils/colors.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:path/path.dart' as p; + +class EventDetailsDocuments extends StatelessWidget { + final List> documents; + + const EventDetailsDocuments({ + super.key, + required this.documents, + }); + + @override + Widget build(BuildContext context) { + if (documents.isEmpty) { + return const SizedBox.shrink(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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: 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(), + ), + ], + ); + } +} + diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_equipe.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_equipe.dart new file mode 100644 index 0000000..4b557b3 --- /dev/null +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_equipe.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:em2rp/models/user_model.dart'; +import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart'; + +class EventDetailsEquipe extends StatelessWidget { + final List workforce; + + const EventDetailsEquipe({ + 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>( + future: _fetchUsers(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + 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), + const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center(child: CircularProgressIndicator()), + ), + ], + ); + } + + if (snapshot.hasError) { + 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), + 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é ou erreur de chargement.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.orange[700], + ), + ), + if (users.isNotEmpty) + UserChipsList( + users: users, + showRemove: false, + ), + ], + ); + }, + ); + } + + Future> _fetchUsers() async { + final firestore = FirebaseFirestore.instance; + List users = []; + + for (int i = 0; i < workforce.length; i++) { + final ref = workforce[i]; + try { + if (ref is DocumentReference) { + final doc = await firestore.doc(ref.path).get(); + if (doc.exists) { + final userData = doc.data() as Map; + users.add(UserModel.fromMap(userData, doc.id)); + } + } + } catch (e) { + // Log silencieux des erreurs individuelles + debugPrint('Error fetching user $i: $e'); + } + } + + return users; + } +} + diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart new file mode 100644 index 0000000..e55ada5 --- /dev/null +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/event_model.dart'; +import 'package:em2rp/utils/colors.dart'; +import 'package:provider/provider.dart'; +import 'package:em2rp/providers/local_user_provider.dart'; +import 'package:em2rp/views/event_add_page.dart'; + +class EventDetailsHeader extends StatelessWidget { + final EventModel event; + + const EventDetailsHeader({ + super.key, + required this.event, + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + constraints: const BoxConstraints(maxHeight: 80), + child: SelectableText( + event.name, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: AppColors.noir, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 4), + Text( + event.eventTypeId, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: AppColors.rouge, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + _buildStatusIcon(event.status), + if (Provider.of(context, listen: false) + .hasPermission('edit_event')) ...[ + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.edit, color: AppColors.rouge), + tooltip: 'Modifier', + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => EventAddEditPage(event: event), + ), + ); + }, + ), + ], + ], + ); + } + + 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: + color = Colors.amber; + icon = Icons.hourglass_empty; + tooltip = 'En attente de validation'; + break; + } + return Tooltip( + message: tooltip, + child: Icon(icon, color: color, size: 28), + ); + } +} diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_info.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_info.dart new file mode 100644 index 0000000..247d256 --- /dev/null +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_info.dart @@ -0,0 +1,162 @@ +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:em2rp/views/widgets/event_form/event_options_display_widget.dart'; + +class EventDetailsInfo extends StatelessWidget { + final EventModel event; + final bool canViewPrices; + + const EventDetailsInfo({ + super.key, + required this.event, + required this.canViewPrices, + }); + + @override + Widget build(BuildContext context) { + final dateFormat = DateFormat('dd/MM/yyyy HH:mm'); + final currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: '€'); + + return 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, + ), + ], + if (canViewPrices) ...[ + const SizedBox(height: 4), + Builder( + builder: (context) { + final total = event.basePrice + + event.options.fold( + 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', + ), + 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.format(arrival)}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.grey[700], + ), + ), + ); + }, + ), + _buildInfoRow( + context, + Icons.construction, + 'Temps de démontage', + '${event.disassemblyTime} heures', + ), + 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.format(departure)}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.grey[700], + ), + ), + ); + }, + ), + ], + ); + } + + 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, + ), + ], + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_navigation.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_navigation.dart new file mode 100644 index 0000000..859fc42 --- /dev/null +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_navigation.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/event_model.dart'; +import 'package:em2rp/utils/colors.dart'; +import 'package:intl/intl.dart'; + +class EventDetailsNavigation extends StatelessWidget { + final List sortedEvents; + final int currentIndex; + final DateTime? selectedDate; + final void Function(EventModel, DateTime) onSelectEvent; + + const EventDetailsNavigation({ + super.key, + required this.sortedEvents, + required this.currentIndex, + required this.selectedDate, + required this.onSelectEvent, + }); + + @override + Widget build(BuildContext context) { + final fullDateFormat = DateFormat('EEEE d MMMM y', 'fr_FR'); + + return 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, + ), + ], + ); + } +} + 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 new file mode 100644 index 0000000..ec1b968 --- /dev/null +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_status_button.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/event_model.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:provider/provider.dart'; +import 'package:em2rp/providers/event_provider.dart'; + +class EventStatusButton extends StatefulWidget { + final EventModel event; + final DateTime? selectedDate; + final void Function(EventModel, DateTime) onSelectEvent; + + const EventStatusButton({ + super.key, + required this.event, + required this.selectedDate, + required this.onSelectEvent, + }); + + @override + State createState() => _EventStatusButtonState(); +} + +class _EventStatusButtonState extends State { + bool _loading = false; + + Future _changeStatus(EventStatus newStatus) async { + if (widget.event.status == newStatus) return; + setState(() => _loading = true); + + try { + await FirebaseFirestore.instance + .collection('events') + .doc(widget.event.id) + .update({'status': eventStatusToString(newStatus)}); + + final snap = await FirebaseFirestore.instance + .collection('events') + .doc(widget.event.id) + .get(); + final updatedEvent = EventModel.fromMap(snap.data()!, widget.event.id); + + widget.onSelectEvent( + updatedEvent, + widget.selectedDate ?? updatedEvent.startDateTime, + ); + + await Provider.of(context, listen: false) + .updateEvent(updatedEvent); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erreur lors du changement de statut: $e')), + ); + } + } finally { + if (mounted) { + setState(() => _loading = false); + } + } + } + + @override + Widget build(BuildContext context) { + final status = widget.event.status; + String texte; + Color couleurFond; + List enfants = []; + + switch (status) { + case EventStatus.waitingForApproval: + texte = "En Attente"; + couleurFond = Colors.yellow.shade600; + enfants = [ + _buildIconButton( + Icons.close, + Colors.red, + () => _changeStatus(EventStatus.canceled), + ), + _buildLabel(texte, couleurFond), + _buildIconButton( + Icons.check, + Colors.green, + () => _changeStatus(EventStatus.confirmed), + ), + ]; + break; + case EventStatus.confirmed: + texte = "Confirmé"; + couleurFond = Colors.green; + enfants = [ + _buildIconButton( + Icons.close, + Colors.red, + () => _changeStatus(EventStatus.canceled), + ), + _buildIconButton( + Icons.hourglass_empty, + Colors.yellow.shade700, + () => _changeStatus(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, + () => _changeStatus(EventStatus.waitingForApproval), + ), + _buildIconButton( + Icons.check, + Colors.green, + () => _changeStatus(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), + ), + ); + } +} diff --git a/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart b/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart index 1ac05bc..c885603 100644 --- a/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart +++ b/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:em2rp/models/option_model.dart'; import 'package:intl/intl.dart'; import 'package:em2rp/utils/colors.dart'; diff --git a/em2rp/lib/views/widgets/event_form/event_staff_and_documents_section.dart b/em2rp/lib/views/widgets/event_form/event_staff_and_documents_section.dart index e3680b9..852d329 100644 --- a/em2rp/lib/views/widgets/event_form/event_staff_and_documents_section.dart +++ b/em2rp/lib/views/widgets/event_form/event_staff_and_documents_section.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:em2rp/models/user_model.dart'; import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart'; import 'package:em2rp/views/widgets/inputs/dropzone_upload_widget.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:firebase_storage/firebase_storage.dart'; class EventStaffAndDocumentsSection extends StatelessWidget { final List allUsers; diff --git a/em2rp/lib/views/widgets/inputs/option_selector_widget.dart b/em2rp/lib/views/widgets/inputs/option_selector_widget.dart index de6a4cf..cef2b3b 100644 --- a/em2rp/lib/views/widgets/inputs/option_selector_widget.dart +++ b/em2rp/lib/views/widgets/inputs/option_selector_widget.dart @@ -564,7 +564,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { } try { // Debug : afficher le contenu envoyé - print('Enregistrement option avec eventTypes : ' + _selectedTypes.toString() + '\u001b[0m'); + print('Enregistrement option avec eventTypes : $_selectedTypes\u001b'); await FirebaseFirestore.instance.collection('options').add({ 'name': name, 'details': _detailsController.text.trim(), @@ -574,7 +574,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { }); Navigator.pop(context, true); } catch (e) { - setState(() => _error = 'Erreur lors de la création : ' + e.toString() + '\nEventTypes=' + _selectedTypes.toString()); + setState(() => _error = 'Erreur lors de la création : $e\nEventTypes=$_selectedTypes'); } }, child: _checkingName