650 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			650 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:flutter/material.dart';
 | |
| import 'package:cloud_firestore/cloud_firestore.dart';
 | |
| import 'package:em2rp/models/option_model.dart';
 | |
| import 'package:em2rp/utils/colors.dart';
 | |
| import 'package:intl/intl.dart';
 | |
| 
 | |
| class OptionsManagement extends StatefulWidget {
 | |
|   const OptionsManagement({super.key});
 | |
| 
 | |
|   @override
 | |
|   State<OptionsManagement> createState() => _OptionsManagementState();
 | |
| }
 | |
| 
 | |
| class _OptionsManagementState extends State<OptionsManagement> {
 | |
|   String _searchQuery = '';
 | |
|   List<EventOption> _options = [];
 | |
|   Map<String, String> _eventTypeNames = {};
 | |
|   bool _loading = true;
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     _loadData();
 | |
|   }
 | |
| 
 | |
|   Future<void> _loadData() async {
 | |
|     setState(() => _loading = true);
 | |
|     try {
 | |
|       // Charger les types d'événements pour les noms
 | |
|       final eventTypesSnapshot = await FirebaseFirestore.instance
 | |
|           .collection('eventTypes')
 | |
|           .get();
 | |
| 
 | |
|       _eventTypeNames = {
 | |
|         for (var doc in eventTypesSnapshot.docs)
 | |
|           doc.id: doc.data()['name'] as String
 | |
|       };
 | |
| 
 | |
|       // Charger les options
 | |
|       final optionsSnapshot = await FirebaseFirestore.instance
 | |
|           .collection('options')
 | |
|           .orderBy('code')
 | |
|           .get();
 | |
| 
 | |
|       setState(() {
 | |
|         _options = optionsSnapshot.docs
 | |
|             .map((doc) => EventOption.fromMap(doc.data(), doc.id))
 | |
|             .toList();
 | |
|         _loading = false;
 | |
|       });
 | |
|     } catch (e) {
 | |
|       setState(() => _loading = false);
 | |
|       ScaffoldMessenger.of(context).showSnackBar(
 | |
|         SnackBar(content: Text('Erreur lors du chargement : $e')),
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   List<EventOption> get _filteredOptions {
 | |
|     if (_searchQuery.isEmpty) return _options;
 | |
|     return _options.where((option) =>
 | |
|         option.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
 | |
|         option.code.toLowerCase().contains(_searchQuery.toLowerCase()) ||
 | |
|         option.details.toLowerCase().contains(_searchQuery.toLowerCase())
 | |
|     ).toList();
 | |
|   }
 | |
| 
 | |
|   Future<List<Map<String, dynamic>>> _getBlockingEvents(String optionId) async {
 | |
|     final eventsSnapshot = await FirebaseFirestore.instance
 | |
|         .collection('events')
 | |
|         .get();
 | |
| 
 | |
|     final now = DateTime.now();
 | |
|     List<Map<String, dynamic>> futureEvents = [];
 | |
|     List<Map<String, dynamic>> pastEvents = [];
 | |
| 
 | |
|     for (final doc in eventsSnapshot.docs) {
 | |
|       final eventData = doc.data();
 | |
|       final options = eventData['options'] as List<dynamic>? ?? [];
 | |
| 
 | |
|       // Vérifier si cette option est utilisée dans cet événement
 | |
|       bool optionUsed = options.any((opt) => opt['id'] == optionId);
 | |
| 
 | |
|       if (optionUsed) {
 | |
|         final eventDate = eventData['StartDateTime']?.toDate() ?? DateTime.now();
 | |
|         // Corriger la récupération du nom - utiliser 'Name' au lieu de 'name'
 | |
|         final eventName = eventData['Name'] as String? ?? 'Événement sans nom';
 | |
| 
 | |
|         if (eventDate.isAfter(now)) {
 | |
|           futureEvents.add({
 | |
|             'id': doc.id,
 | |
|             'name': eventName,
 | |
|             'startDateTime': eventDate,
 | |
|           });
 | |
|         } else {
 | |
|           pastEvents.add({
 | |
|             'id': doc.id,
 | |
|             'name': eventName,
 | |
|             'startDateTime': eventDate,
 | |
|           });
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return [...futureEvents, ...pastEvents];
 | |
|   }
 | |
| 
 | |
|   Future<void> _deleteOption(EventOption option) async {
 | |
|     final events = await _getBlockingEvents(option.id);
 | |
|     final futureEvents = events.where((e) =>
 | |
|         (e['startDateTime'] as DateTime).isAfter(DateTime.now())).toList();
 | |
| 
 | |
|     if (futureEvents.isNotEmpty) {
 | |
|       // Il y a des événements futurs, empêcher la suppression
 | |
|       _showBlockingEventsDialog(option, events, canDelete: false);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (events.isNotEmpty) {
 | |
|       // Il n'y a que des événements passés, afficher un avertissement
 | |
|       _showBlockingEventsDialog(option, events, canDelete: true);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Aucun événement, suppression directe
 | |
|     _confirmAndDelete(option);
 | |
|   }
 | |
| 
 | |
|   void _showBlockingEventsDialog(EventOption option, List<Map<String, dynamic>> events, {required bool canDelete}) {
 | |
|     final futureEvents = events.where((e) =>
 | |
|         (e['startDateTime'] as DateTime).isAfter(DateTime.now())).toList();
 | |
|     final pastEvents = events.where((e) =>
 | |
|         (e['startDateTime'] as DateTime).isBefore(DateTime.now())).toList();
 | |
| 
 | |
|     showDialog(
 | |
|       context: context,
 | |
|       builder: (context) => AlertDialog(
 | |
|         title: Text(canDelete ? 'Avertissement' : 'Suppression impossible'),
 | |
|         content: SizedBox(
 | |
|           width: double.maxFinite,
 | |
|           child: Column(
 | |
|             mainAxisSize: MainAxisSize.min,
 | |
|             crossAxisAlignment: CrossAxisAlignment.start,
 | |
|             children: [
 | |
|               if (!canDelete) ...[
 | |
|                 const Text(
 | |
|                   'Impossible de supprimer cette option car elle est utilisée par des événements futurs :',
 | |
|                   style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
 | |
|                 ),
 | |
|                 const SizedBox(height: 12),
 | |
|                 ...futureEvents.map((event) => Padding(
 | |
|                   padding: const EdgeInsets.only(bottom: 4),
 | |
|                   child: Text(
 | |
|                     '• ${event['name']} - ${DateFormat('dd/MM/yyyy').format(event['startDateTime'])}',
 | |
|                     style: const TextStyle(color: Colors.red),
 | |
|                   ),
 | |
|                 )),
 | |
|               ],
 | |
|               if (canDelete && pastEvents.isNotEmpty) ...[
 | |
|                 const Text(
 | |
|                   'Cette option est utilisée par des événements passés :',
 | |
|                   style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
 | |
|                 ),
 | |
|                 const SizedBox(height: 12),
 | |
|                 ...pastEvents.take(5).map((event) => Padding(
 | |
|                   padding: const EdgeInsets.only(bottom: 4),
 | |
|                   child: Text(
 | |
|                     '• ${event['name']} - ${DateFormat('dd/MM/yyyy').format(event['startDateTime'])}',
 | |
|                     style: const TextStyle(color: Colors.orange),
 | |
|                   ),
 | |
|                 )),
 | |
|                 if (pastEvents.length > 5)
 | |
|                   Text('... et ${pastEvents.length - 5} autres événements'),
 | |
|                 const SizedBox(height: 12),
 | |
|                 const Text('Voulez-vous vraiment continuer la suppression ?'),
 | |
|               ],
 | |
|             ],
 | |
|           ),
 | |
|         ),
 | |
|         actions: [
 | |
|           TextButton(
 | |
|             onPressed: () => Navigator.pop(context),
 | |
|             child: const Text('Annuler'),
 | |
|           ),
 | |
|           if (canDelete)
 | |
|             ElevatedButton(
 | |
|               onPressed: () {
 | |
|                 Navigator.pop(context);
 | |
|                 _confirmAndDelete(option);
 | |
|               },
 | |
|               style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
 | |
|               child: const Text('Supprimer quand même', style: TextStyle(color: Colors.white)),
 | |
|             ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   void _confirmAndDelete(EventOption option) {
 | |
|     showDialog(
 | |
|       context: context,
 | |
|       builder: (context) => AlertDialog(
 | |
|         title: const Text('Confirmer la suppression'),
 | |
|         content: Text('Êtes-vous sûr de vouloir supprimer l\'option "${option.code} - ${option.name}" ?'),
 | |
|         actions: [
 | |
|           TextButton(
 | |
|             onPressed: () => Navigator.pop(context),
 | |
|             child: const Text('Annuler'),
 | |
|           ),
 | |
|           ElevatedButton(
 | |
|             onPressed: () async {
 | |
|               Navigator.pop(context);
 | |
|               try {
 | |
|                 await FirebaseFirestore.instance
 | |
|                     .collection('options')
 | |
|                     .doc(option.id)
 | |
|                     .delete();
 | |
| 
 | |
|                 ScaffoldMessenger.of(context).showSnackBar(
 | |
|                   const SnackBar(content: Text('Option supprimée avec succès')),
 | |
|                 );
 | |
|                 _loadData();
 | |
|               } catch (e) {
 | |
|                 ScaffoldMessenger.of(context).showSnackBar(
 | |
|                   SnackBar(content: Text('Erreur lors de la suppression : $e')),
 | |
|                 );
 | |
|               }
 | |
|             },
 | |
|             style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
 | |
|             child: const Text('Supprimer', style: TextStyle(color: Colors.white)),
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   void _showCreateEditDialog({EventOption? option}) {
 | |
|     showDialog(
 | |
|       context: context,
 | |
|       builder: (context) => _OptionFormDialog(
 | |
|         option: option,
 | |
|         eventTypeNames: _eventTypeNames,
 | |
|         onSaved: _loadData,
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Padding(
 | |
|       padding: const EdgeInsets.all(16.0),
 | |
|       child: Column(
 | |
|         children: [
 | |
|           // Header avec recherche et bouton ajouter
 | |
|           Row(
 | |
|             children: [
 | |
|               Expanded(
 | |
|                 child: TextField(
 | |
|                   decoration: const InputDecoration(
 | |
|                     labelText: 'Rechercher une option (code, nom, détails)',
 | |
|                     prefixIcon: Icon(Icons.search),
 | |
|                     border: OutlineInputBorder(),
 | |
|                   ),
 | |
|                   onChanged: (value) => setState(() => _searchQuery = value),
 | |
|                 ),
 | |
|               ),
 | |
|               const SizedBox(width: 16),
 | |
|               ElevatedButton.icon(
 | |
|                 onPressed: () => _showCreateEditDialog(),
 | |
|                 icon: const Icon(Icons.add),
 | |
|                 label: const Text('Nouvelle option'),
 | |
|                 style: ElevatedButton.styleFrom(
 | |
|                   backgroundColor: AppColors.rouge,
 | |
|                   foregroundColor: Colors.white,
 | |
|                 ),
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|           const SizedBox(height: 16),
 | |
| 
 | |
|           // Liste des options
 | |
|           Expanded(
 | |
|             child: _loading
 | |
|                 ? const Center(child: CircularProgressIndicator())
 | |
|                 : _filteredOptions.isEmpty
 | |
|                     ? const Center(
 | |
|                         child: Text(
 | |
|                           'Aucune option trouvée',
 | |
|                           style: TextStyle(fontSize: 16, color: Colors.grey),
 | |
|                         ),
 | |
|                       )
 | |
|                     : ListView.builder(
 | |
|                         itemCount: _filteredOptions.length,
 | |
|                         itemBuilder: (context, index) {
 | |
|                           final option = _filteredOptions[index];
 | |
|                           final currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: '€');
 | |
| 
 | |
|                           return Card(
 | |
|                             margin: const EdgeInsets.only(bottom: 8),
 | |
|                             child: ListTile(
 | |
|                               leading: CircleAvatar(
 | |
|                                 backgroundColor: AppColors.rouge,
 | |
|                                 child: Text(
 | |
|                                   option.code.substring(0, 2).toUpperCase(),
 | |
|                                   style: const TextStyle(
 | |
|                                     color: Colors.white,
 | |
|                                     fontWeight: FontWeight.bold,
 | |
|                                     fontSize: 12,
 | |
|                                   ),
 | |
|                                 ),
 | |
|                               ),
 | |
|                               title: Text(
 | |
|                                 '${option.code} - ${option.name}',
 | |
|                                 style: const TextStyle(fontWeight: FontWeight.bold),
 | |
|                               ),
 | |
|                               subtitle: Column(
 | |
|                                 crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                                 children: [
 | |
|                                   if (option.details.isNotEmpty)
 | |
|                                     Text(option.details),
 | |
|                                   const SizedBox(height: 4),
 | |
|                                   Text(
 | |
|                                     'Prix : ${currencyFormat.format(option.valMin)} - ${currencyFormat.format(option.valMax)}',
 | |
|                                     style: const TextStyle(
 | |
|                                       fontWeight: FontWeight.w500,
 | |
|                                       color: Colors.green,
 | |
|                                     ),
 | |
|                                   ),
 | |
|                                   const SizedBox(height: 4),
 | |
|                                   Wrap(
 | |
|                                     spacing: 4,
 | |
|                                     children: option.eventTypes.map((typeId) {
 | |
|                                       final typeName = _eventTypeNames[typeId] ?? typeId;
 | |
|                                       return Chip(
 | |
|                                         label: Text(
 | |
|                                           typeName,
 | |
|                                           style: const TextStyle(fontSize: 10),
 | |
|                                         ),
 | |
|                                         materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
 | |
|                                         backgroundColor: AppColors.rouge.withOpacity(0.1),
 | |
|                                       );
 | |
|                                     }).toList(),
 | |
|                                   ),
 | |
|                                 ],
 | |
|                               ),
 | |
|                               trailing: Row(
 | |
|                                 mainAxisSize: MainAxisSize.min,
 | |
|                                 children: [
 | |
|                                   IconButton(
 | |
|                                     icon: const Icon(Icons.edit, color: Colors.blue),
 | |
|                                     onPressed: () => _showCreateEditDialog(option: option),
 | |
|                                     tooltip: 'Modifier',
 | |
|                                   ),
 | |
|                                   IconButton(
 | |
|                                     icon: const Icon(Icons.delete, color: Colors.red),
 | |
|                                     onPressed: () => _deleteOption(option),
 | |
|                                     tooltip: 'Supprimer',
 | |
|                                   ),
 | |
|                                 ],
 | |
|                               ),
 | |
|                             ),
 | |
|                           );
 | |
|                         },
 | |
|                       ),
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _OptionFormDialog extends StatefulWidget {
 | |
|   final EventOption? option;
 | |
|   final Map<String, String> eventTypeNames;
 | |
|   final VoidCallback onSaved;
 | |
| 
 | |
|   const _OptionFormDialog({
 | |
|     this.option,
 | |
|     required this.eventTypeNames,
 | |
|     required this.onSaved,
 | |
|   });
 | |
| 
 | |
|   @override
 | |
|   State<_OptionFormDialog> createState() => _OptionFormDialogState();
 | |
| }
 | |
| 
 | |
| class _OptionFormDialogState extends State<_OptionFormDialog> {
 | |
|   final _formKey = GlobalKey<FormState>();
 | |
|   final _codeController = TextEditingController();
 | |
|   final _nameController = TextEditingController();
 | |
|   final _detailsController = TextEditingController();
 | |
|   final _minPriceController = TextEditingController();
 | |
|   final _maxPriceController = TextEditingController();
 | |
|   List<String> _selectedTypes = [];
 | |
|   bool _loading = false;
 | |
|   String? _error;
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     if (widget.option != null) {
 | |
|       _codeController.text = widget.option!.code;
 | |
|       _nameController.text = widget.option!.name;
 | |
|       _detailsController.text = widget.option!.details;
 | |
|       _minPriceController.text = widget.option!.valMin.toString();
 | |
|       _maxPriceController.text = widget.option!.valMax.toString();
 | |
|       _selectedTypes = List.from(widget.option!.eventTypes);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     _codeController.dispose();
 | |
|     _nameController.dispose();
 | |
|     _detailsController.dispose();
 | |
|     _minPriceController.dispose();
 | |
|     _maxPriceController.dispose();
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   Future<bool> _isCodeUnique(String code) async {
 | |
|     final doc = await FirebaseFirestore.instance
 | |
|         .collection('options')
 | |
|         .doc(code)
 | |
|         .get();
 | |
| 
 | |
|     // Si on modifie et que c'est le même document, c'est OK
 | |
|     if (widget.option != null && widget.option!.id == code) {
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     return !doc.exists;
 | |
|   }
 | |
| 
 | |
|   Future<void> _submit() async {
 | |
|     if (!_formKey.currentState!.validate()) return;
 | |
|     if (_selectedTypes.isEmpty) {
 | |
|       setState(() => _error = 'Sélectionnez au moins un type d\'événement');
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     final code = _codeController.text.trim().toUpperCase();
 | |
|     final name = _nameController.text.trim();
 | |
|     final min = double.tryParse(_minPriceController.text.replaceAll(',', '.'));
 | |
|     final max = double.tryParse(_maxPriceController.text.replaceAll(',', '.'));
 | |
| 
 | |
|     if (min == null || max == null) {
 | |
|       setState(() => _error = 'Les prix doivent être des nombres valides');
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (min > max) {
 | |
|       setState(() => _error = 'Le prix minimum ne peut pas être supérieur au prix maximum');
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     setState(() => _loading = true);
 | |
| 
 | |
|     try {
 | |
|       // Vérifier l'unicité du code seulement pour les nouvelles options
 | |
|       if (widget.option == null) {
 | |
|         final isUnique = await _isCodeUnique(code);
 | |
|         if (!isUnique) {
 | |
|           setState(() {
 | |
|             _error = 'Ce code d\'option existe déjà';
 | |
|             _loading = false;
 | |
|           });
 | |
|           return;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       final data = {
 | |
|         'code': code,
 | |
|         'name': name,
 | |
|         'details': _detailsController.text.trim(),
 | |
|         'valMin': min,
 | |
|         'valMax': max,
 | |
|         'eventTypes': _selectedTypes,
 | |
|       };
 | |
| 
 | |
|       if (widget.option == null) {
 | |
|         // Création - utiliser le code comme ID
 | |
|         await FirebaseFirestore.instance.collection('options').doc(code).set(data);
 | |
|         ScaffoldMessenger.of(context).showSnackBar(
 | |
|           const SnackBar(content: Text('Option créée avec succès')),
 | |
|         );
 | |
|       } else {
 | |
|         // Modification
 | |
|         await FirebaseFirestore.instance
 | |
|             .collection('options')
 | |
|             .doc(widget.option!.id)
 | |
|             .update(data);
 | |
|         ScaffoldMessenger.of(context).showSnackBar(
 | |
|           const SnackBar(content: Text('Option modifiée avec succès')),
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       widget.onSaved();
 | |
|       Navigator.pop(context);
 | |
|     } catch (e) {
 | |
|       setState(() {
 | |
|         _error = 'Erreur : $e';
 | |
|         _loading = false;
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return AlertDialog(
 | |
|       title: Text(widget.option == null ? 'Nouvelle option' : 'Modifier l\'option'),
 | |
|       content: SizedBox(
 | |
|         width: 500,
 | |
|         child: SingleChildScrollView(
 | |
|           child: Form(
 | |
|             key: _formKey,
 | |
|             child: Column(
 | |
|               mainAxisSize: MainAxisSize.min,
 | |
|               children: [
 | |
|                 TextFormField(
 | |
|                   controller: _codeController,
 | |
|                   decoration: const InputDecoration(
 | |
|                     labelText: 'Code de l\'option *',
 | |
|                     hintText: 'Ex: TENT_50M2',
 | |
|                     helperText: 'Max 16 caractères, lettres, chiffres, _ et -',
 | |
|                     border: OutlineInputBorder(),
 | |
|                   ),
 | |
|                   enabled: widget.option == null, // Code non modifiable en édition
 | |
|                   maxLength: 16,
 | |
|                   textCapitalization: TextCapitalization.characters,
 | |
|                   validator: (v) {
 | |
|                     if (v == null || v.isEmpty) return 'Le code est obligatoire';
 | |
|                     if (v.length > 16) return 'Maximum 16 caractères';
 | |
|                     if (!RegExp(r'^[A-Z0-9_-]+$').hasMatch(v)) {
 | |
|                       return 'Seuls les lettres, chiffres, _ et - sont autorisés';
 | |
|                     }
 | |
|                     return null;
 | |
|                   },
 | |
|                 ),
 | |
|                 const SizedBox(height: 16),
 | |
|                 TextFormField(
 | |
|                   controller: _nameController,
 | |
|                   decoration: const InputDecoration(
 | |
|                     labelText: 'Nom de l\'option *',
 | |
|                     border: OutlineInputBorder(),
 | |
|                   ),
 | |
|                   validator: (v) => v == null || v.trim().isEmpty ? 'Le nom est obligatoire' : null,
 | |
|                 ),
 | |
|                 const SizedBox(height: 16),
 | |
|                 TextFormField(
 | |
|                   controller: _detailsController,
 | |
|                   decoration: const InputDecoration(
 | |
|                     labelText: 'Détails',
 | |
|                     border: OutlineInputBorder(),
 | |
|                   ),
 | |
|                   maxLines: 3,
 | |
|                 ),
 | |
|                 const SizedBox(height: 16),
 | |
|                 Row(
 | |
|                   children: [
 | |
|                     Expanded(
 | |
|                       child: TextFormField(
 | |
|                         controller: _minPriceController,
 | |
|                         decoration: const InputDecoration(
 | |
|                           labelText: 'Prix min (€) *',
 | |
|                           border: OutlineInputBorder(),
 | |
|                         ),
 | |
|                         keyboardType: const TextInputType.numberWithOptions(decimal: true),
 | |
|                         validator: (v) => v == null || v.isEmpty ? 'Obligatoire' : null,
 | |
|                       ),
 | |
|                     ),
 | |
|                     const SizedBox(width: 8),
 | |
|                     Expanded(
 | |
|                       child: TextFormField(
 | |
|                         controller: _maxPriceController,
 | |
|                         decoration: const InputDecoration(
 | |
|                           labelText: 'Prix max (€) *',
 | |
|                           border: OutlineInputBorder(),
 | |
|                         ),
 | |
|                         keyboardType: const TextInputType.numberWithOptions(decimal: true),
 | |
|                         validator: (v) => v == null || v.isEmpty ? 'Obligatoire' : null,
 | |
|                       ),
 | |
|                     ),
 | |
|                   ],
 | |
|                 ),
 | |
|                 const SizedBox(height: 16),
 | |
|                 Column(
 | |
|                   crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                   children: [
 | |
|                     const Text('Types d\'événement associés *',
 | |
|                         style: TextStyle(fontWeight: FontWeight.w500)),
 | |
|                     const SizedBox(height: 8),
 | |
|                     Wrap(
 | |
|                       spacing: 8,
 | |
|                       children: widget.eventTypeNames.entries.map((entry) {
 | |
|                         return FilterChip(
 | |
|                           label: Text(entry.value),
 | |
|                           selected: _selectedTypes.contains(entry.key),
 | |
|                           onSelected: (selected) {
 | |
|                             setState(() {
 | |
|                               if (selected) {
 | |
|                                 _selectedTypes.add(entry.key);
 | |
|                               } else {
 | |
|                                 _selectedTypes.remove(entry.key);
 | |
|                               }
 | |
|                               _error = null; // Effacer l'erreur lors de la sélection
 | |
|                             });
 | |
|                           },
 | |
|                           selectedColor: AppColors.rouge.withOpacity(0.3),
 | |
|                         );
 | |
|                       }).toList(),
 | |
|                     ),
 | |
|                   ],
 | |
|                 ),
 | |
|                 if (_error != null) ...[
 | |
|                   const SizedBox(height: 16),
 | |
|                   Text(
 | |
|                     _error!,
 | |
|                     style: const TextStyle(color: Colors.red),
 | |
|                   ),
 | |
|                 ],
 | |
|               ],
 | |
|             ),
 | |
|           ),
 | |
|         ),
 | |
|       ),
 | |
|       actions: [
 | |
|         TextButton(
 | |
|           onPressed: _loading ? null : () => Navigator.pop(context),
 | |
|           child: const Text('Annuler'),
 | |
|         ),
 | |
|         ElevatedButton(
 | |
|           onPressed: _loading ? null : _submit,
 | |
|           style: ElevatedButton.styleFrom(
 | |
|             backgroundColor: AppColors.rouge,
 | |
|             foregroundColor: Colors.white,
 | |
|           ),
 | |
|           child: _loading
 | |
|               ? const SizedBox(
 | |
|                   width: 20,
 | |
|                   height: 20,
 | |
|                   child: CircularProgressIndicator(strokeWidth: 2),
 | |
|                 )
 | |
|               : Text(widget.option == null ? 'Créer' : 'Modifier'),
 | |
|         ),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| }
 | 
