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'; class EventDetails extends StatelessWidget { final EventModel event; final DateTime? selectedDate; final List events; final void Function(EventModel, DateTime) onSelectEvent; const EventDetails({ super.key, required this.event, required this.selectedDate, required this.events, required this.onSelectEvent, }); @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( margin: const EdgeInsets.all(16), child: Padding( padding: const EdgeInsets.all(16), 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, ), ], ), const SizedBox(height: 16), Row( 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(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), ), ); }, ), ], ), 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); }, ), ), const SizedBox(height: 16), Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoRow( context, Icons.calendar_today, 'Date de début', dateFormat.format(event.startDateTime), ), _buildInfoRow( context, Icons.calendar_today, 'Date de fin', dateFormat.format(event.endDateTime), ), if (canViewPrices) _buildInfoRow( context, Icons.euro, 'Prix de base', currencyFormat.format(event.basePrice), ), if (event.options.isNotEmpty) ...[ const SizedBox(height: 8), Text('Options sélectionnées', style: Theme.of(context).textTheme.titleLarge?.copyWith( color: AppColors.noir, fontWeight: FontWeight.bold, )), const SizedBox(height: 4), Column( crossAxisAlignment: CrossAxisAlignment.start, children: event.options.map((opt) { final price = (opt['price'] ?? 0.0) as num; final isNegative = price < 0; return ListTile( leading: Icon(Icons.tune, color: isNegative ? Colors.red : AppColors.rouge), title: Text(opt['name'] ?? '', style: TextStyle(fontWeight: FontWeight.bold)), subtitle: Text(opt['details'] ?? ''), trailing: canViewPrices ? Text( (isNegative ? '- ' : '+ ') + currencyFormat.format(price.abs()), style: TextStyle( color: isNegative ? Colors.red : AppColors.noir, fontWeight: FontWeight.bold, ), ) : null, contentPadding: EdgeInsets.zero, dense: true, ); }).toList(), ), 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', ), _buildInfoRow( context, Icons.construction, 'Temps de démontage', '${event.disassemblyTime} heures', ), 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, ), 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), ], ], ), ), ), ], ), ), ); } 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), ); } } class EventAddDialog extends StatefulWidget { const EventAddDialog({super.key}); @override State createState() => _EventAddDialogState(); } class _EventAddDialogState extends State { final _formKey = GlobalKey(); final TextEditingController _nameController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); final TextEditingController _priceController = TextEditingController(); final TextEditingController _installationController = TextEditingController(); final TextEditingController _disassemblyController = TextEditingController(); final TextEditingController _latitudeController = TextEditingController(); final TextEditingController _longitudeController = TextEditingController(); final TextEditingController _addressController = TextEditingController(); DateTime? _startDateTime; DateTime? _endDateTime; bool _isLoading = false; String? _error; String? _success; @override void dispose() { _nameController.dispose(); _descriptionController.dispose(); _priceController.dispose(); _installationController.dispose(); _disassemblyController.dispose(); _latitudeController.dispose(); _longitudeController.dispose(); _addressController.dispose(); super.dispose(); } Future _submit() async { if (!_formKey.currentState!.validate() || _startDateTime == null || _endDateTime == null) return; setState(() { _isLoading = true; _error = null; _success = null; }); 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(_priceController.text) ?? 0.0, installationTime: int.tryParse(_installationController.text) ?? 0, disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0, eventTypeId: '', // à adapter si tu veux gérer les types customerId: '', // à adapter si tu veux gérer les clients address: _addressController.text.trim(), latitude: double.tryParse(_latitudeController.text) ?? 0.0, longitude: double.tryParse(_longitudeController.text) ?? 0.0, workforce: [], documents: [], ); await eventProvider.addEvent(newEvent); setState(() { _success = "Événement créé avec succès !"; }); Navigator.of(context).pop(); } catch (e) { setState(() { _error = "Erreur lors de la création : $e"; }); } finally { setState(() { _isLoading = false; }); } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Créer un événement'), content: SingleChildScrollView( child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: _nameController, decoration: const InputDecoration(labelText: 'Nom'), validator: (v) => v == null || v.isEmpty ? 'Champ requis' : null, ), TextFormField( controller: _descriptionController, decoration: const InputDecoration(labelText: 'Description'), maxLines: 2, ), TextFormField( controller: _priceController, decoration: const InputDecoration(labelText: 'Prix (€)'), keyboardType: TextInputType.number, ), TextFormField( controller: _installationController, decoration: const InputDecoration(labelText: 'Installation (h)'), keyboardType: TextInputType.number, ), TextFormField( controller: _disassemblyController, decoration: const InputDecoration(labelText: 'Démontage (h)'), keyboardType: TextInputType.number, ), TextFormField( controller: _latitudeController, decoration: const InputDecoration(labelText: 'Latitude'), keyboardType: TextInputType.number, ), TextFormField( controller: _longitudeController, decoration: const InputDecoration(labelText: 'Longitude'), keyboardType: TextInputType.number, ), TextFormField( controller: _addressController, decoration: const InputDecoration(labelText: 'Adresse'), ), const SizedBox(height: 8), Row( children: [ Expanded( child: OutlinedButton( onPressed: () async { final picked = await showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime(2020), lastDate: DateTime(2030), ); if (picked != null) { final time = await showTimePicker( context: context, initialTime: TimeOfDay.now(), ); if (time != null) { setState(() { _startDateTime = DateTime( picked.year, picked.month, picked.day, time.hour, time.minute, ); }); } } }, child: Text(_startDateTime == null ? 'Début' : DateFormat('dd/MM/yyyy HH:mm') .format(_startDateTime!)), ), ), const SizedBox(width: 8), Expanded( child: OutlinedButton( onPressed: () async { final picked = await showDatePicker( context: context, initialDate: _startDateTime ?? DateTime.now(), firstDate: DateTime(2020), lastDate: DateTime(2030), ); if (picked != null) { final time = await showTimePicker( context: context, initialTime: TimeOfDay.now(), ); if (time != null) { setState(() { _endDateTime = DateTime( picked.year, picked.month, picked.day, time.hour, time.minute, ); }); } } }, child: Text(_endDateTime == null ? 'Fin' : DateFormat('dd/MM/yyyy HH:mm') .format(_endDateTime!)), ), ), ], ), if (_error != null) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text(_error!, style: const TextStyle(color: Colors.red)), ), if (_success != null) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text(_success!, style: const TextStyle(color: Colors.green)), ), ], ), ), ), actions: [ TextButton( onPressed: _isLoading ? null : () => Navigator.of(context).pop(), child: const Text('Annuler'), ), ElevatedButton( onPressed: _isLoading ? null : _submit, child: _isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Créer'), ), ], ); } } 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; } }