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 createState() => _OptionsManagementState(); } class _OptionsManagementState extends State { String _searchQuery = ''; List _options = []; Map _eventTypeNames = {}; bool _loading = true; @override void initState() { super.initState(); _loadData(); } Future _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 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>> _getBlockingEvents(String optionId) async { final eventsSnapshot = await FirebaseFirestore.instance .collection('events') .get(); final now = DateTime.now(); List> futureEvents = []; List> pastEvents = []; for (final doc in eventsSnapshot.docs) { final eventData = doc.data(); final options = eventData['options'] as List? ?? []; // 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 _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> 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 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(); final _codeController = TextEditingController(); final _nameController = TextEditingController(); final _detailsController = TextEditingController(); final _minPriceController = TextEditingController(); final _maxPriceController = TextEditingController(); List _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 _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 _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'), ), ], ); } }