import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/models/evenement_model.dart'; import '../../../../shared/theme/app_theme.dart'; import '../../../../shared/widgets/buttons/buttons.dart'; import '../bloc/evenement_bloc.dart'; import '../bloc/evenement_event.dart'; import '../bloc/evenement_state.dart'; /// Page de création d'un nouvel événement class EvenementCreatePage extends StatefulWidget { const EvenementCreatePage({super.key}); @override State createState() => _EvenementCreatePageState(); } class _EvenementCreatePageState extends State { final _formKey = GlobalKey(); final _scrollController = ScrollController(); // Controllers pour les champs de texte final _titreController = TextEditingController(); final _descriptionController = TextEditingController(); final _lieuController = TextEditingController(); final _adresseController = TextEditingController(); final _capaciteMaxController = TextEditingController(); final _prixController = TextEditingController(); final _notesController = TextEditingController(); // Variables pour les sélections DateTime? _dateDebut; DateTime? _dateFin; TimeOfDay? _heureDebut; TimeOfDay? _heureFin; TypeEvenement _typeSelectionne = TypeEvenement.reunion; bool _visiblePublic = true; bool _inscriptionRequise = true; bool _inscriptionPayante = false; late EvenementBloc _evenementBloc; @override void initState() { super.initState(); _evenementBloc = getIt(); } @override void dispose() { _titreController.dispose(); _descriptionController.dispose(); _lieuController.dispose(); _adresseController.dispose(); _capaciteMaxController.dispose(); _prixController.dispose(); _notesController.dispose(); _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocProvider.value( value: _evenementBloc, child: Scaffold( backgroundColor: AppTheme.backgroundLight, appBar: AppBar( title: const Text('Nouvel Événement'), backgroundColor: AppTheme.primaryColor, foregroundColor: Colors.white, elevation: 0, actions: [ BlocBuilder( builder: (context, state) { return TextButton( onPressed: state is EvenementLoading ? null : _sauvegarder, child: state is EvenementLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : const Text( 'Créer', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ); }, ), ], ), body: BlocListener( listener: (context, state) { if (state is EvenementOperationSuccess) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Événement créé avec succès !'), backgroundColor: AppTheme.successColor, ), ); Navigator.of(context).pop(true); // Retourner true pour indiquer la création } else if (state is EvenementError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur : ${state.message}'), backgroundColor: AppTheme.errorColor, ), ); } }, child: Form( key: _formKey, child: SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInformationsGenerales(), const SizedBox(height: 24), _buildDateEtHeure(), const SizedBox(height: 24), _buildLieuEtAdresse(), const SizedBox(height: 24), _buildParametres(), const SizedBox(height: 24), _buildInformationsComplementaires(), const SizedBox(height: 32), ], ), ), ), ), ), ); } Widget _buildInformationsGenerales() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Informations générales', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.primaryColor, ), ), const SizedBox(height: 16), TextFormField( controller: _titreController, decoration: const InputDecoration( labelText: 'Titre de l\'événement *', hintText: 'Ex: Assemblée générale 2025', prefixIcon: Icon(Icons.title), ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Le titre est obligatoire'; } if (value.trim().length < 3) { return 'Le titre doit contenir au moins 3 caractères'; } return null; }, textCapitalization: TextCapitalization.words, ), const SizedBox(height: 16), DropdownButtonFormField( value: _typeSelectionne, decoration: const InputDecoration( labelText: 'Type d\'événement *', prefixIcon: Icon(Icons.category), ), items: TypeEvenement.values.map((type) { return DropdownMenuItem( value: type, child: Row( children: [ Text(type.icone, style: const TextStyle(fontSize: 20)), const SizedBox(width: 8), Text(type.libelle), ], ), ); }).toList(), onChanged: (value) { if (value != null) { setState(() { _typeSelectionne = value; }); } }, ), const SizedBox(height: 16), TextFormField( controller: _descriptionController, decoration: const InputDecoration( labelText: 'Description', hintText: 'Décrivez votre événement...', prefixIcon: Icon(Icons.description), ), maxLines: 4, textCapitalization: TextCapitalization.sentences, ), ], ), ), ); } Widget _buildDateEtHeure() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Date et heure', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.primaryColor, ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: InkWell( onTap: _selectionnerDateDebut, child: InputDecorator( decoration: const InputDecoration( labelText: 'Date de début *', prefixIcon: Icon(Icons.calendar_today), ), child: Text( _dateDebut != null ? DateFormat('dd/MM/yyyy').format(_dateDebut!) : 'Sélectionner', style: TextStyle( color: _dateDebut != null ? null : Colors.grey[600], ), ), ), ), ), const SizedBox(width: 16), Expanded( child: InkWell( onTap: _selectionnerHeureDebut, child: InputDecorator( decoration: const InputDecoration( labelText: 'Heure de début *', prefixIcon: Icon(Icons.access_time), ), child: Text( _heureDebut != null ? _heureDebut!.format(context) : 'Sélectionner', style: TextStyle( color: _heureDebut != null ? null : Colors.grey[600], ), ), ), ), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: InkWell( onTap: _selectionnerDateFin, child: InputDecorator( decoration: const InputDecoration( labelText: 'Date de fin', prefixIcon: Icon(Icons.calendar_today), ), child: Text( _dateFin != null ? DateFormat('dd/MM/yyyy').format(_dateFin!) : 'Optionnel', style: TextStyle( color: _dateFin != null ? null : Colors.grey[600], ), ), ), ), ), const SizedBox(width: 16), Expanded( child: InkWell( onTap: _selectionnerHeureFin, child: InputDecorator( decoration: const InputDecoration( labelText: 'Heure de fin', prefixIcon: Icon(Icons.access_time), ), child: Text( _heureFin != null ? _heureFin!.format(context) : 'Optionnel', style: TextStyle( color: _heureFin != null ? null : Colors.grey[600], ), ), ), ), ), ], ), ], ), ), ); } Widget _buildLieuEtAdresse() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Lieu et adresse', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.primaryColor, ), ), const SizedBox(height: 16), TextFormField( controller: _lieuController, decoration: const InputDecoration( labelText: 'Lieu *', hintText: 'Ex: Salle des fêtes', prefixIcon: Icon(Icons.place), ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Le lieu est obligatoire'; } return null; }, textCapitalization: TextCapitalization.words, ), const SizedBox(height: 16), TextFormField( controller: _adresseController, decoration: const InputDecoration( labelText: 'Adresse complète', hintText: 'Ex: 123 Rue de la République, 75001 Paris', prefixIcon: Icon(Icons.location_on), ), maxLines: 2, textCapitalization: TextCapitalization.words, ), ], ), ), ); } Widget _buildParametres() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Paramètres', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.primaryColor, ), ), const SizedBox(height: 16), SwitchListTile( title: const Text('Visible au public'), subtitle: const Text('L\'événement sera visible par tous'), value: _visiblePublic, onChanged: (value) { setState(() { _visiblePublic = value; }); }, activeColor: AppTheme.primaryColor, ), SwitchListTile( title: const Text('Inscription requise'), subtitle: const Text('Les participants doivent s\'inscrire'), value: _inscriptionRequise, onChanged: (value) { setState(() { _inscriptionRequise = value; if (!value) { _inscriptionPayante = false; } }); }, activeColor: AppTheme.primaryColor, ), if (_inscriptionRequise) SwitchListTile( title: const Text('Inscription payante'), subtitle: const Text('L\'inscription nécessite un paiement'), value: _inscriptionPayante, onChanged: (value) { setState(() { _inscriptionPayante = value; }); }, activeColor: AppTheme.primaryColor, ), const SizedBox(height: 16), TextFormField( controller: _capaciteMaxController, decoration: const InputDecoration( labelText: 'Capacité maximale', hintText: 'Nombre maximum de participants', prefixIcon: Icon(Icons.people), suffixText: 'personnes', ), keyboardType: TextInputType.number, validator: (value) { if (value != null && value.isNotEmpty) { final capacite = int.tryParse(value); if (capacite == null || capacite <= 0) { return 'La capacité doit être un nombre positif'; } } return null; }, ), if (_inscriptionPayante) ...[ const SizedBox(height: 16), TextFormField( controller: _prixController, decoration: const InputDecoration( labelText: 'Prix de l\'inscription *', hintText: '0.00', prefixIcon: Icon(Icons.euro), suffixText: '€', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: (value) { if (_inscriptionPayante) { if (value == null || value.trim().isEmpty) { return 'Le prix est obligatoire pour une inscription payante'; } final prix = double.tryParse(value.replaceAll(',', '.')); if (prix == null || prix < 0) { return 'Le prix doit être un nombre positif'; } } return null; }, ), ], ], ), ), ); } Widget _buildInformationsComplementaires() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Informations complémentaires', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.primaryColor, ), ), const SizedBox(height: 16), TextFormField( controller: _notesController, decoration: const InputDecoration( labelText: 'Notes internes', hintText: 'Notes visibles uniquement par les organisateurs...', prefixIcon: Icon(Icons.note), ), maxLines: 3, textCapitalization: TextCapitalization.sentences, ), ], ), ), ); } // Méthodes de sélection de date et heure Future _selectionnerDateDebut() async { final date = await showDatePicker( context: context, initialDate: _dateDebut ?? DateTime.now(), firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365 * 2)), ); if (date != null) { setState(() { _dateDebut = date; // Si la date de fin est antérieure, la réinitialiser if (_dateFin != null && _dateFin!.isBefore(date)) { _dateFin = null; } }); } } Future _selectionnerDateFin() async { final date = await showDatePicker( context: context, initialDate: _dateFin ?? _dateDebut ?? DateTime.now(), firstDate: _dateDebut ?? DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365 * 2)), ); if (date != null) { setState(() { _dateFin = date; }); } } Future _selectionnerHeureDebut() async { final heure = await showTimePicker( context: context, initialTime: _heureDebut ?? TimeOfDay.now(), ); if (heure != null) { setState(() { _heureDebut = heure; }); } } Future _selectionnerHeureFin() async { final heure = await showTimePicker( context: context, initialTime: _heureFin ?? _heureDebut ?? TimeOfDay.now(), ); if (heure != null) { setState(() { _heureFin = heure; }); } } // Méthode de sauvegarde void _sauvegarder() { if (!_formKey.currentState!.validate()) { // Faire défiler vers le premier champ en erreur _scrollController.animateTo( 0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); return; } // Validation des dates if (_dateDebut == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('La date de début est obligatoire'), backgroundColor: AppTheme.errorColor, ), ); return; } if (_heureDebut == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('L\'heure de début est obligatoire'), backgroundColor: AppTheme.errorColor, ), ); return; } // Construire les DateTime complets final dateTimeDebut = DateTime( _dateDebut!.year, _dateDebut!.month, _dateDebut!.day, _heureDebut!.hour, _heureDebut!.minute, ); DateTime? dateTimeFin; if (_dateFin != null && _heureFin != null) { dateTimeFin = DateTime( _dateFin!.year, _dateFin!.month, _dateFin!.day, _heureFin!.hour, _heureFin!.minute, ); // Vérifier que la date de fin est après le début if (dateTimeFin.isBefore(dateTimeDebut)) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('La date de fin doit être après la date de début'), backgroundColor: AppTheme.errorColor, ), ); return; } } // Créer l'objet événement final evenement = EvenementModel( id: null, titre: _titreController.text.trim(), description: _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(), typeEvenement: _typeSelectionne, dateDebut: dateTimeDebut, dateFin: dateTimeFin, lieu: _lieuController.text.trim(), adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(), capaciteMax: _capaciteMaxController.text.isEmpty ? null : int.tryParse(_capaciteMaxController.text), prix: _inscriptionPayante && _prixController.text.isNotEmpty ? double.tryParse(_prixController.text.replaceAll(',', '.')) : null, visiblePublic: _visiblePublic, inscriptionRequise: _inscriptionRequise, instructionsParticulieres: _notesController.text.trim().isEmpty ? null : _notesController.text.trim(), statut: StatutEvenement.planifie, actif: true, creePar: null, // Sera défini par le backend dateCreation: null, // Sera défini par le backend modifiePar: null, dateModification: null, organisationId: null, // Sera défini par le backend selon l'utilisateur connecté organisateurId: null, // Sera défini par le backend selon l'utilisateur connecté ); // Envoyer l'événement au BLoC _evenementBloc.add(CreateEvenement(evenement)); } }