Refactor event type handling and add data management page (options and event types)
This commit is contained in:
		| @@ -205,14 +205,17 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> { | ||||
|             final firestoreData = doc.data()!; | ||||
|             enrichedOptions.add({ | ||||
|               'id': optionData['id'], | ||||
|               'name': firestoreData['name'], | ||||
|               'code': firestoreData['code'] ?? optionData['id'], // Récupérer le code | ||||
|               'name': firestoreData['code'] != null && firestoreData['code'].toString().isNotEmpty | ||||
|                   ? '${firestoreData['code']} - ${firestoreData['name']}' | ||||
|                   : firestoreData['name'], // Affichage avec code | ||||
|               'details': firestoreData['details'] ?? '', | ||||
|               'price': optionData['price'], | ||||
|             }); | ||||
|           } else { | ||||
|             enrichedOptions.add({ | ||||
|               'id': optionData['id'], | ||||
|               'name': 'Option supprimée', | ||||
|               'name': 'Option supprimée (${optionData['id']})', | ||||
|               'details': 'Cette option n\'existe plus', | ||||
|               'price': optionData['price'], | ||||
|             }); | ||||
| @@ -274,7 +277,10 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> { | ||||
|       final matchesType = opt.eventTypes.contains(widget.eventType); | ||||
|       print('  -> matchesType: $matchesType'); | ||||
|  | ||||
|       final matchesSearch = opt.name.toLowerCase().contains(_search.toLowerCase()); | ||||
|       // Recherche dans le code ET le nom | ||||
|       final searchLower = _search.toLowerCase(); | ||||
|       final matchesSearch = opt.name.toLowerCase().contains(searchLower) || | ||||
|                           opt.code.toLowerCase().contains(searchLower); | ||||
|       print('  -> matchesSearch: $matchesSearch'); | ||||
|  | ||||
|       final result = matchesType && matchesSearch; | ||||
| @@ -296,7 +302,7 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> { | ||||
|               padding: const EdgeInsets.all(12.0), | ||||
|               child: TextField( | ||||
|                 decoration: const InputDecoration( | ||||
|                   labelText: 'Rechercher une option', | ||||
|                   labelText: 'Rechercher par code ou nom', | ||||
|                   prefixIcon: Icon(Icons.search), | ||||
|                 ), | ||||
|                 onChanged: (v) => setState(() => _search = v), | ||||
| @@ -312,7 +318,7 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> { | ||||
|                       itemBuilder: (context, i) { | ||||
|                         final opt = filtered[i]; | ||||
|                         return ListTile( | ||||
|                           title: Text(opt.name), | ||||
|                           title: Text('${opt.code} - ${opt.name}'), // Affichage avec code | ||||
|                           subtitle: Text('${opt.details}\nFourchette: ${opt.valMin}€ ~ ${opt.valMax}€'), | ||||
|                           onTap: () async { | ||||
|                             final min = opt.valMin; | ||||
| @@ -325,7 +331,7 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> { | ||||
|                                 final priceController = | ||||
|                                     TextEditingController(text: defaultPrice); | ||||
|                                 return AlertDialog( | ||||
|                                   title: Text('Prix pour ${opt.name}'), | ||||
|                                   title: Text('Prix pour ${opt.code} - ${opt.name}'), // Affichage avec code | ||||
|                                   content: TextField( | ||||
|                                     controller: priceController, | ||||
|                                     keyboardType: | ||||
| @@ -408,22 +414,24 @@ class _CreateOptionDialog extends StatefulWidget { | ||||
|  | ||||
| class _CreateOptionDialogState extends State<_CreateOptionDialog> { | ||||
|   final _formKey = GlobalKey<FormState>(); | ||||
|   final _codeController = TextEditingController(); // Nouveau champ code | ||||
|   final _nameController = TextEditingController(); | ||||
|   final _detailsController = TextEditingController(); | ||||
|   final _minPriceController = TextEditingController(); | ||||
|   final _maxPriceController = TextEditingController(); | ||||
|   final List<String> _selectedTypes = []; | ||||
|   String? _error; | ||||
|   bool _checkingName = false; | ||||
|   bool _checkingCode = false; | ||||
|   List<Map<String,dynamic>> _allEventTypes = []; | ||||
|   bool _loading = true; | ||||
|  | ||||
|   Future<bool> _isNameUnique(String name) async { | ||||
|     final snap = await FirebaseFirestore.instance | ||||
|   Future<bool> _isCodeUnique(String code) async { | ||||
|     // Vérifier si le document avec ce code existe déjà | ||||
|     final doc = await FirebaseFirestore.instance | ||||
|         .collection('options') | ||||
|         .where('name', isEqualTo: name) | ||||
|         .doc(code) | ||||
|         .get(); | ||||
|     return snap.docs.isEmpty; | ||||
|     return !doc.exists; | ||||
|   } | ||||
|  | ||||
|   Future<void> _fetchEventTypes() async { | ||||
| @@ -453,6 +461,25 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: [ | ||||
|               TextFormField( | ||||
|                 controller: _codeController, | ||||
|                 decoration: const InputDecoration( | ||||
|                   labelText: 'Code de l\'option', | ||||
|                   hintText: 'Ex: M2', | ||||
|                   helperText: 'Max 16 caractères, lettres, chiffres, _ et -', | ||||
|                 ), | ||||
|                 maxLength: 16, | ||||
|                 textCapitalization: TextCapitalization.characters, | ||||
|                 validator: (v) { | ||||
|                   if (v == null || v.isEmpty) return 'Champ requis'; | ||||
|                   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: 8), | ||||
|               TextFormField( | ||||
|                 controller: _nameController, | ||||
|                 decoration: | ||||
| @@ -535,7 +562,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { | ||||
|           child: const Text('Annuler'), | ||||
|         ), | ||||
|         ElevatedButton( | ||||
|           onPressed: _checkingName | ||||
|           onPressed: _checkingCode | ||||
|               ? null | ||||
|               : () async { | ||||
|                   if (!_formKey.currentState!.validate()) return; | ||||
| @@ -553,19 +580,21 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { | ||||
|                         () => _error = 'Prix min et max doivent être valides'); | ||||
|                     return; | ||||
|                   } | ||||
|                   final code = _codeController.text.trim().toUpperCase(); | ||||
|                   final name = _nameController.text.trim(); | ||||
|                   setState(() => _checkingName = true); | ||||
|                   final unique = await _isNameUnique(name); | ||||
|                   setState(() => _checkingName = false); | ||||
|  | ||||
|                   setState(() => _checkingCode = true); | ||||
|                   final unique = await _isCodeUnique(code); | ||||
|                   setState(() => _checkingCode = false); | ||||
|                   if (!unique) { | ||||
|                     setState( | ||||
|                         () => _error = 'Ce nom d\'option est déjà utilisé.'); | ||||
|                         () => _error = 'Ce code d\'option est déjà utilisé.'); | ||||
|                     return; | ||||
|                   } | ||||
|                   try { | ||||
|                     // Debug : afficher le contenu envoyé | ||||
|                     print('Enregistrement option avec eventTypes : $_selectedTypes\u001b'); | ||||
|                     await FirebaseFirestore.instance.collection('options').add({ | ||||
|                     // Utiliser le code comme identifiant du document | ||||
|                     await FirebaseFirestore.instance.collection('options').doc(code).set({ | ||||
|                       'code': code, | ||||
|                       'name': name, | ||||
|                       'details': _detailsController.text.trim(), | ||||
|                       'valMin': min, | ||||
| @@ -574,10 +603,10 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { | ||||
|                     }); | ||||
|                     Navigator.pop(context, true); | ||||
|                   } catch (e) { | ||||
|                     setState(() => _error = 'Erreur lors de la création : $e\nEventTypes=$_selectedTypes'); | ||||
|                     setState(() => _error = 'Erreur lors de la création : $e'); | ||||
|                   } | ||||
|                 }, | ||||
|           child: _checkingName | ||||
|           child: _checkingCode | ||||
|               ? const SizedBox( | ||||
|                   width: 18, | ||||
|                   height: 18, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 ElPoyo
					ElPoyo