From 57c59c911a8a44c395789c5317d29c81fb058025 Mon Sep 17 00:00:00 2001 From: "PC-PAUL\\paulf" Date: Tue, 3 Jun 2025 19:59:40 +0200 Subject: [PATCH] Equipe sur event details OK Modif evenement OK --- em2rp/lib/main.dart | 2 +- em2rp/lib/views/calendar_page.dart | 2 +- em2rp/lib/views/event_add_page.dart | 239 +++++++++++------- .../auth => views}/reset_password_page.dart | 0 .../calendar_widgets/event_details.dart | 98 +++++++ .../user_multi_select_widget.dart | 69 +++-- 6 files changed, 299 insertions(+), 111 deletions(-) rename em2rp/lib/{pages/auth => views}/reset_password_page.dart (100%) diff --git a/em2rp/lib/main.dart b/em2rp/lib/main.dart index d6042f9..203e4e3 100644 --- a/em2rp/lib/main.dart +++ b/em2rp/lib/main.dart @@ -13,7 +13,7 @@ import 'views/user_management_page.dart'; import 'package:provider/provider.dart'; import 'providers/local_user_provider.dart'; import 'services/user_service.dart'; -import 'pages/auth/reset_password_page.dart'; +import 'views/reset_password_page.dart'; import 'config/env.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; diff --git a/em2rp/lib/views/calendar_page.dart b/em2rp/lib/views/calendar_page.dart index 2e5b5a4..d090ae8 100644 --- a/em2rp/lib/views/calendar_page.dart +++ b/em2rp/lib/views/calendar_page.dart @@ -145,7 +145,7 @@ class _CalendarPageState extends State { onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => const EventAddPage(), + builder: (context) => const EventAddEditPage(), ), ); }, diff --git a/em2rp/lib/views/event_add_page.dart b/em2rp/lib/views/event_add_page.dart index 64e16aa..8e322dd 100644 --- a/em2rp/lib/views/event_add_page.dart +++ b/em2rp/lib/views/event_add_page.dart @@ -23,14 +23,15 @@ import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart'; // ignore: avoid_web_libraries_in_flutter import 'dart:html' as html; -class EventAddPage extends StatefulWidget { - const EventAddPage({super.key}); +class EventAddEditPage extends StatefulWidget { + final EventModel? event; + const EventAddEditPage({super.key, this.event}); @override - State createState() => _EventAddPageState(); + State createState() => _EventAddEditPageState(); } -class _EventAddPageState extends State { +class _EventAddEditPageState extends State { final _formKey = GlobalKey(); final TextEditingController _nameController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); @@ -61,6 +62,8 @@ class _EventAddPageState extends State { bool _formChanged = false; EventStatus _selectedStatus = EventStatus.waitingForApproval; + bool get isEditMode => widget.event != null; + @override void initState() { super.initState(); @@ -73,7 +76,24 @@ class _EventAddPageState extends State { _addressController.addListener(_onAnyFieldChanged); _descriptionController.addListener(_onAnyFieldChanged); _addBeforeUnloadListener(); - _selectedStatus = EventStatus.waitingForApproval; + if (isEditMode) { + final e = widget.event!; + _nameController.text = e.name; + _descriptionController.text = e.description; + _basePriceController.text = e.basePrice.toStringAsFixed(2); + _installationController.text = e.installationTime.toString(); + _disassemblyController.text = e.disassemblyTime.toString(); + _addressController.text = e.address; + _startDateTime = e.startDateTime; + _endDateTime = e.endDateTime; + _selectedEventType = e.eventTypeId.isNotEmpty ? e.eventTypeId : null; + _selectedUserIds = e.workforce.map((ref) => ref.id).toList(); + _uploadedFiles = List>.from(e.documents); + _selectedOptions = List>.from(e.options); + _selectedStatus = e.status; + } else { + _selectedStatus = EventStatus.waitingForApproval; + } } void _handleDescriptionChange() { @@ -284,78 +304,121 @@ class _EventAddPageState extends State { }); try { final eventProvider = Provider.of(context, listen: false); - final newEvent = EventModel( - id: '', - name: _nameController.text.trim(), - description: _descriptionController.text.trim(), - startDateTime: _startDateTime!, - endDateTime: _endDateTime!, - basePrice: double.tryParse(_basePriceController.text) ?? 0.0, - installationTime: int.tryParse(_installationController.text) ?? 0, - disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0, - eventTypeId: _selectedEventType!, - customerId: '', - address: _addressController.text.trim(), - workforce: _selectedUserIds - .map((id) => FirebaseFirestore.instance.collection('users').doc(id)) - .toList(), - latitude: 0.0, - longitude: 0.0, - documents: _uploadedFiles, - options: _selectedOptions - .map((opt) => { - 'name': opt['name'], - 'price': opt['price'], - }) - .toList(), - status: _selectedStatus, - ); - final docRef = await FirebaseFirestore.instance - .collection('events') - .add(newEvent.toMap()); - final eventId = docRef.id; - List> newFiles = []; - for (final file in _uploadedFiles) { - final fileName = file['name']!; - final oldUrl = file['url']!; - String sourcePath; - final tempPattern = RegExp(r'events/temp/[^?]+'); - final match = tempPattern.firstMatch(oldUrl); - if (match != null) { - sourcePath = match.group(0)!; - } else { - final tempFileName = - Uri.decodeComponent(oldUrl.split('/').last.split('?').first); - sourcePath = tempFileName; - } - final destinationPath = 'events/$eventId/$fileName'; - final newUrl = await moveEventFileHttp( - sourcePath: sourcePath, - destinationPath: destinationPath, + if (isEditMode) { + // Edition : on met à jour l'événement existant + final updatedEvent = EventModel( + id: widget.event!.id, + name: _nameController.text.trim(), + description: _descriptionController.text.trim(), + startDateTime: _startDateTime!, + endDateTime: _endDateTime!, + basePrice: double.tryParse(_basePriceController.text) ?? 0.0, + installationTime: int.tryParse(_installationController.text) ?? 0, + disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0, + eventTypeId: _selectedEventType!, + customerId: '', + address: _addressController.text.trim(), + workforce: _selectedUserIds + .map((id) => + FirebaseFirestore.instance.collection('users').doc(id)) + .toList(), + latitude: 0.0, + longitude: 0.0, + documents: _uploadedFiles, + options: _selectedOptions + .map((opt) => { + 'name': opt['name'], + 'price': opt['price'], + }) + .toList(), + status: _selectedStatus, ); - if (newUrl != null) { - newFiles.add({'name': fileName, 'url': newUrl}); - } else { - newFiles.add({'name': fileName, 'url': oldUrl}); + final docRef = FirebaseFirestore.instance + .collection('events') + .doc(widget.event!.id); + await docRef.update(updatedEvent.toMap()); + // Gestion des fichiers (si besoin, à adapter selon ta logique) + // ... + setState(() { + _success = "Événement modifié avec succès !"; + }); + if (context.mounted) Navigator.of(context).pop(); + } else { + // Création : logique existante + final newEvent = EventModel( + id: '', + name: _nameController.text.trim(), + description: _descriptionController.text.trim(), + startDateTime: _startDateTime!, + endDateTime: _endDateTime!, + basePrice: double.tryParse(_basePriceController.text) ?? 0.0, + installationTime: int.tryParse(_installationController.text) ?? 0, + disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0, + eventTypeId: _selectedEventType!, + customerId: '', + address: _addressController.text.trim(), + workforce: _selectedUserIds + .map((id) => + FirebaseFirestore.instance.collection('users').doc(id)) + .toList(), + latitude: 0.0, + longitude: 0.0, + documents: _uploadedFiles, + options: _selectedOptions + .map((opt) => { + 'name': opt['name'], + 'price': opt['price'], + }) + .toList(), + status: _selectedStatus, + ); + final docRef = await FirebaseFirestore.instance + .collection('events') + .add(newEvent.toMap()); + final eventId = docRef.id; + List> newFiles = []; + for (final file in _uploadedFiles) { + final fileName = file['name']!; + final oldUrl = file['url']!; + String sourcePath; + final tempPattern = RegExp(r'events/temp/[^?]+'); + final match = tempPattern.firstMatch(oldUrl); + if (match != null) { + sourcePath = match.group(0)!; + } else { + final tempFileName = + Uri.decodeComponent(oldUrl.split('/').last.split('?').first); + sourcePath = tempFileName; + } + final destinationPath = 'events/$eventId/$fileName'; + final newUrl = await moveEventFileHttp( + sourcePath: sourcePath, + destinationPath: destinationPath, + ); + if (newUrl != null) { + newFiles.add({'name': fileName, 'url': newUrl}); + } else { + newFiles.add({'name': fileName, 'url': oldUrl}); + } } + await docRef.update({'documents': newFiles}); + final localUserProvider = + Provider.of(context, listen: false); + final userId = localUserProvider.uid; + final canViewAllEvents = + localUserProvider.hasPermission('view_all_events'); + if (userId != null) { + await eventProvider.loadUserEvents(userId, + canViewAllEvents: canViewAllEvents); + } + setState(() { + _success = "Événement créé avec succès !"; + }); + if (context.mounted) Navigator.of(context).pop(); } - await docRef.update({'documents': newFiles}); - final localUserProvider = - Provider.of(context, listen: false); - final userId = localUserProvider.uid; - final canViewAllEvents = - localUserProvider.hasPermission('view_all_events'); - if (userId != null) { - await eventProvider.loadUserEvents(userId, - canViewAllEvents: canViewAllEvents); - } - setState(() { - _success = "Événement créé avec succès !"; - }); - if (context.mounted) Navigator.of(context).pop(); } catch (e) { setState(() { - _error = "Erreur lors de la création : $e"; + _error = "Erreur lors de la sauvegarde : $e"; }); } finally { setState(() { @@ -385,7 +448,8 @@ class _EventAddPageState extends State { onWillPop: _onWillPop, child: Scaffold( appBar: AppBar( - title: const Text('Créer un événement'), + title: + Text(isEditMode ? 'Modifier un événement' : 'Créer un événement'), ), body: Center( child: SingleChildScrollView( @@ -393,10 +457,7 @@ class _EventAddPageState extends State { ? Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12), - child: Container( - // Pas de Card sur mobile, juste un conteneur - child: _buildFormContent(isMobile), - ), + child: _buildFormContent(isMobile), ) : Card( elevation: 6, @@ -764,23 +825,23 @@ class _EventAddPageState extends State { height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Text('Créer'), + : Text(isEditMode ? 'Enregistrer' : 'Créer'), ), ], ), - const SizedBox(height: 16), - Center( - child: ElevatedButton.icon( - icon: const Icon(Icons.check_circle, color: Colors.white), - label: const Text('Définir cet événement comme confirmé'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - textStyle: const TextStyle(fontWeight: FontWeight.bold), + if (!isEditMode) + Center( + child: ElevatedButton.icon( + icon: const Icon(Icons.check_circle, color: Colors.white), + label: const Text('Définir cet événement comme confirmé'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + textStyle: const TextStyle(fontWeight: FontWeight.bold), + ), + onPressed: null, ), - onPressed: null, ), - ), ], ), ); diff --git a/em2rp/lib/pages/auth/reset_password_page.dart b/em2rp/lib/views/reset_password_page.dart similarity index 100% rename from em2rp/lib/pages/auth/reset_password_page.dart rename to em2rp/lib/views/reset_password_page.dart diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart index aa68a41..54a12ec 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart @@ -9,6 +9,10 @@ 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'; class EventDetails extends StatelessWidget { final EventModel event; @@ -93,6 +97,20 @@ class EventDetails extends StatelessWidget { ), const SizedBox(width: 12), _buildStatusIcon(event.status), + const SizedBox(width: 8), + 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), + ), + ); + }, + ), ], ), if (Provider.of(context, listen: false) @@ -337,6 +355,9 @@ class EventDetails extends StatelessWidget { ); }).toList(), ), + // --- EQUIPE SECTION --- + const SizedBox(height: 16), + EquipeSection(workforce: event.workforce), ], ], ), @@ -773,3 +794,80 @@ class _FirestoreStatusButtonState extends State<_FirestoreStatusButton> { ); } } + +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('Erreur lors du chargement de l\'équipe', + style: 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/user_management/user_multi_select_widget.dart b/em2rp/lib/views/widgets/user_management/user_multi_select_widget.dart index a0beba3..76be450 100644 --- a/em2rp/lib/views/widgets/user_management/user_multi_select_widget.dart +++ b/em2rp/lib/views/widgets/user_management/user_multi_select_widget.dart @@ -70,26 +70,15 @@ class _UserMultiSelectState extends State<_UserMultiSelect> { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Wrap( - spacing: 12, - runSpacing: 12, - children: selectedUsers - .map((user) => Chip( - avatar: ProfilePictureWidget(userId: user.uid, radius: 28), - label: Text('${user.firstName} ${user.lastName}', - style: const TextStyle(fontSize: 16)), - labelPadding: const EdgeInsets.symmetric(horizontal: 8), - deleteIcon: const Icon(Icons.close, size: 20), - onDeleted: () { - final newList = List.from(widget.selectedUserIds) - ..remove(user.uid); - widget.onChanged(newList); - }, - backgroundColor: Colors.grey[200], - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - )) - .toList(), + UserChipsList( + users: selectedUsers, + selectedUserIds: widget.selectedUserIds, + showRemove: true, + onRemove: (uid) { + final newList = List.from(widget.selectedUserIds) + ..remove(uid); + widget.onChanged(newList); + }, ), const SizedBox(height: 16), ElevatedButton.icon( @@ -188,3 +177,43 @@ class _UserPickerDialogState extends State<_UserPickerDialog> { ); } } + +class UserChipsList extends StatelessWidget { + final List users; + final List selectedUserIds; + final ValueChanged? onRemove; + final bool showRemove; + final double avatarRadius; + const UserChipsList({ + super.key, + required this.users, + this.selectedUserIds = const [], + this.onRemove, + this.showRemove = false, + this.avatarRadius = 28, + }); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 12, + runSpacing: 12, + children: users + .map((user) => Chip( + avatar: ProfilePictureWidget( + userId: user.uid, radius: avatarRadius), + label: Text('${user.firstName} ${user.lastName}', + style: const TextStyle(fontSize: 16)), + labelPadding: const EdgeInsets.symmetric(horizontal: 8), + deleteIcon: + showRemove ? const Icon(Icons.close, size: 20) : null, + onDeleted: showRemove && onRemove != null + ? () => onRemove!(user.uid) + : null, + backgroundColor: Colors.grey[200], + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + )) + .toList(), + ); + } +}