import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/models/membre_model.dart'; import '../../../../core/error/error_handler.dart'; import '../../../../core/validation/form_validator.dart'; import '../../../../core/feedback/user_feedback.dart'; import '../../../../core/animations/loading_animations.dart'; import '../../../../core/animations/page_transitions.dart'; import '../../../../shared/theme/app_theme.dart'; import '../../../../shared/widgets/custom_text_field.dart'; import '../../../../shared/widgets/buttons/buttons.dart'; import '../bloc/membres_bloc.dart'; import '../bloc/membres_event.dart'; import '../bloc/membres_state.dart'; /// Page de création d'un nouveau membre class MembreCreatePage extends StatefulWidget { const MembreCreatePage({super.key}); @override State createState() => _MembreCreatePageState(); } class _MembreCreatePageState extends State with SingleTickerProviderStateMixin { late MembresBloc _membresBloc; late TabController _tabController; final _formKey = GlobalKey(); // Controllers pour les champs du formulaire final _nomController = TextEditingController(); final _prenomController = TextEditingController(); final _emailController = TextEditingController(); final _telephoneController = TextEditingController(); final _adresseController = TextEditingController(); final _villeController = TextEditingController(); final _codePostalController = TextEditingController(); final _paysController = TextEditingController(); final _professionController = TextEditingController(); final _numeroMembreController = TextEditingController(); // Variables d'état DateTime? _dateNaissance; DateTime _dateAdhesion = DateTime.now(); bool _actif = true; bool _isLoading = false; int _currentStep = 0; @override void initState() { super.initState(); _membresBloc = getIt(); _tabController = TabController(length: 3, vsync: this); // Générer un numéro de membre automatique _generateMemberNumber(); // Initialiser les valeurs par défaut _paysController.text = 'Côte d\'Ivoire'; } @override void dispose() { _tabController.dispose(); _nomController.dispose(); _prenomController.dispose(); _emailController.dispose(); _telephoneController.dispose(); _adresseController.dispose(); _villeController.dispose(); _codePostalController.dispose(); _paysController.dispose(); _professionController.dispose(); _numeroMembreController.dispose(); super.dispose(); } void _generateMemberNumber() { final now = DateTime.now(); final year = now.year.toString().substring(2); final month = now.month.toString().padLeft(2, '0'); final random = (DateTime.now().millisecondsSinceEpoch % 1000).toString().padLeft(3, '0'); _numeroMembreController.text = 'MBR$year$month$random'; } @override Widget build(BuildContext context) { return BlocProvider.value( value: _membresBloc, child: Scaffold( backgroundColor: AppTheme.backgroundLight, appBar: _buildAppBar(), body: BlocConsumer( listener: (context, state) { if (state is MembreCreated) { // Fermer l'indicateur de chargement UserFeedback.hideLoading(context); setState(() { _isLoading = false; }); // Afficher le message de succès avec feedback haptique UserFeedback.showSuccess( context, 'Membre créé avec succès !', onAction: () => Navigator.of(context).pop(true), actionLabel: 'Voir la liste', ); // Retourner à la liste après un délai Future.delayed(const Duration(seconds: 2), () { if (mounted) { Navigator.of(context).pop(true); } }); } else if (state is MembresError) { // Fermer l'indicateur de chargement UserFeedback.hideLoading(context); setState(() { _isLoading = false; }); // Gérer l'erreur avec le nouveau système ErrorHandler.handleError( context, state.failure, onRetry: () => _submitForm(), ); } }, builder: (context, state) { return Column( children: [ _buildProgressIndicator(), Expanded( child: _buildFormContent(), ), _buildBottomActions(), ], ); }, ), ), ); } PreferredSizeWidget _buildAppBar() { return AppBar( backgroundColor: AppTheme.primaryColor, foregroundColor: Colors.white, title: const Text( 'Nouveau membre', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 18, ), ), actions: [ IconButton( icon: const Icon(Icons.help_outline), onPressed: _showHelp, tooltip: 'Aide', ), ], ); } Widget _buildProgressIndicator() { return Container( padding: const EdgeInsets.all(16), color: Colors.white, child: Column( children: [ Row( children: [ _buildStepIndicator(0, 'Informations\npersonnelles', Icons.person), _buildStepConnector(0), _buildStepIndicator(1, 'Contact &\nAdresse', Icons.contact_mail), _buildStepConnector(1), _buildStepIndicator(2, 'Finalisation', Icons.check_circle), ], ), const SizedBox(height: 8), LinearProgressIndicator( value: (_currentStep + 1) / 3, backgroundColor: AppTheme.backgroundLight, valueColor: const AlwaysStoppedAnimation(AppTheme.primaryColor), ), ], ), ); } Widget _buildStepIndicator(int step, String label, IconData icon) { final isActive = step == _currentStep; final isCompleted = step < _currentStep; Color color; if (isCompleted) { color = AppTheme.successColor; } else if (isActive) { color = AppTheme.primaryColor; } else { color = AppTheme.textHint; } return Expanded( child: Column( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: isCompleted ? AppTheme.successColor : isActive ? AppTheme.primaryColor : AppTheme.backgroundLight, shape: BoxShape.circle, border: Border.all(color: color, width: 2), ), child: Icon( isCompleted ? Icons.check : icon, color: isCompleted || isActive ? Colors.white : color, size: 20, ), ), const SizedBox(height: 8), Text( label, textAlign: TextAlign.center, style: TextStyle( fontSize: 10, color: color, fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, ), ), ], ), ); } Widget _buildStepConnector(int step) { final isCompleted = step < _currentStep; return Expanded( child: Container( height: 2, margin: const EdgeInsets.only(bottom: 32), color: isCompleted ? AppTheme.successColor : AppTheme.backgroundLight, ), ); } Widget _buildFormContent() { return Form( key: _formKey, child: PageView( controller: PageController(initialPage: _currentStep), onPageChanged: (index) { setState(() { _currentStep = index; }); }, children: [ _buildPersonalInfoStep(), _buildContactStep(), _buildFinalizationStep(), ], ), ); } Widget _buildPersonalInfoStep() { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Informations personnelles', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), const SizedBox(height: 8), const Text( 'Renseignez les informations de base du nouveau membre', style: TextStyle( fontSize: 14, color: AppTheme.textSecondary, ), ), const SizedBox(height: 24), // Numéro de membre (généré automatiquement) CustomTextField( controller: _numeroMembreController, label: 'Numéro de membre', prefixIcon: Icons.badge, enabled: false, validator: (value) { if (value == null || value.isEmpty) { return 'Le numéro de membre est requis'; } return null; }, ), const SizedBox(height: 16), // Nom et Prénom Row( children: [ Expanded( child: CustomTextField( controller: _prenomController, label: 'Prénom *', hintText: 'Jean', prefixIcon: Icons.person_outline, textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Le prénom est requis'; } if (value.trim().length < 2) { return 'Le prénom doit contenir au moins 2 caractères'; } return null; }, ), ), const SizedBox(width: 16), Expanded( child: CustomTextField( controller: _nomController, label: 'Nom *', hintText: 'Dupont', prefixIcon: Icons.person_outline, textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Le nom est requis'; } if (value.trim().length < 2) { return 'Le nom doit contenir au moins 2 caractères'; } return null; }, ), ), ], ), const SizedBox(height: 16), // Date de naissance InkWell( onTap: _selectDateNaissance, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: BoxDecoration( border: Border.all(color: AppTheme.borderColor), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ const Icon(Icons.cake_outlined, color: AppTheme.textSecondary), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Date de naissance', style: TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), Text( _dateNaissance != null ? DateFormat('dd/MM/yyyy').format(_dateNaissance!) : 'Sélectionner une date', style: TextStyle( fontSize: 16, color: _dateNaissance != null ? AppTheme.textPrimary : AppTheme.textHint, ), ), ], ), ), const Icon(Icons.calendar_today, color: AppTheme.textSecondary), ], ), ), ), const SizedBox(height: 16), // Profession CustomTextField( controller: _professionController, label: 'Profession', hintText: 'Enseignant, Commerçant, etc.', prefixIcon: Icons.work_outline, textInputAction: TextInputAction.next, ), ], ), ); } Widget _buildContactStep() { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Contact & Adresse', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), const SizedBox(height: 8), const Text( 'Informations de contact et adresse du membre', style: TextStyle( fontSize: 14, color: AppTheme.textSecondary, ), ), const SizedBox(height: 24), // Email CustomTextField( controller: _emailController, label: 'Email *', hintText: 'exemple@email.com', prefixIcon: Icons.email_outlined, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return 'L\'email est requis'; } if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { return 'Format d\'email invalide'; } return null; }, ), const SizedBox(height: 16), // Téléphone CustomTextField( controller: _telephoneController, label: 'Téléphone *', hintText: '+225 XX XX XX XX XX', prefixIcon: Icons.phone_outlined, keyboardType: TextInputType.phone, textInputAction: TextInputAction.next, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[0-9+\-\s\(\)]')), ], validator: (value) { if (value == null || value.trim().isEmpty) { return 'Le téléphone est requis'; } if (value.trim().length < 8) { return 'Numéro de téléphone invalide'; } return null; }, ), const SizedBox(height: 24), // Section Adresse const Text( 'Adresse', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), const SizedBox(height: 16), // Adresse CustomTextField( controller: _adresseController, label: 'Adresse', hintText: 'Rue, quartier, etc.', prefixIcon: Icons.location_on_outlined, textInputAction: TextInputAction.next, maxLines: 2, ), const SizedBox(height: 16), // Ville et Code postal Row( children: [ Expanded( flex: 2, child: CustomTextField( controller: _villeController, label: 'Ville', hintText: 'Abidjan', prefixIcon: Icons.location_city_outlined, textInputAction: TextInputAction.next, ), ), const SizedBox(width: 16), Expanded( child: CustomTextField( controller: _codePostalController, label: 'Code postal', hintText: '00225', prefixIcon: Icons.markunread_mailbox_outlined, keyboardType: TextInputType.number, textInputAction: TextInputAction.next, ), ), ], ), const SizedBox(height: 16), // Pays CustomTextField( controller: _paysController, label: 'Pays', prefixIcon: Icons.flag_outlined, textInputAction: TextInputAction.done, ), ], ), ); } Widget _buildFinalizationStep() { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Finalisation', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), const SizedBox(height: 8), const Text( 'Vérifiez les informations et finalisez la création', style: TextStyle( fontSize: 14, color: AppTheme.textSecondary, ), ), const SizedBox(height: 24), // Résumé des informations _buildSummaryCard(), const SizedBox(height: 24), // Date d'adhésion InkWell( onTap: _selectDateAdhesion, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: BoxDecoration( border: Border.all(color: AppTheme.borderColor), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ const Icon(Icons.calendar_today_outlined, color: AppTheme.textSecondary), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Date d\'adhésion', style: TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), Text( DateFormat('dd/MM/yyyy').format(_dateAdhesion), style: const TextStyle( fontSize: 16, color: AppTheme.textPrimary, ), ), ], ), ), const Icon(Icons.edit, color: AppTheme.textSecondary), ], ), ), ), const SizedBox(height: 16), // Statut actif SwitchListTile( title: const Text('Membre actif'), subtitle: const Text('Le membre peut accéder aux services'), value: _actif, onChanged: (value) { setState(() { _actif = value; }); }, activeColor: AppTheme.primaryColor, ), ], ), ); } Widget _buildSummaryCard() { return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.summarize, color: AppTheme.primaryColor), const SizedBox(width: 8), const Text( 'Résumé des informations', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), ], ), const SizedBox(height: 16), _buildSummaryRow('Nom complet', '${_prenomController.text} ${_nomController.text}'), _buildSummaryRow('Email', _emailController.text), _buildSummaryRow('Téléphone', _telephoneController.text), if (_dateNaissance != null) _buildSummaryRow('Date de naissance', DateFormat('dd/MM/yyyy').format(_dateNaissance!)), if (_professionController.text.isNotEmpty) _buildSummaryRow('Profession', _professionController.text), if (_adresseController.text.isNotEmpty) _buildSummaryRow('Adresse', _adresseController.text), ], ), ), ); } Widget _buildSummaryRow(String label, String value) { if (value.trim().isEmpty) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 120, child: Text( label, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, fontWeight: FontWeight.w500, ), ), ), Expanded( child: Text( value, style: const TextStyle( fontSize: 14, color: AppTheme.textPrimary, ), ), ), ], ), ); } Widget _buildBottomActions() { return Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: Offset(0, -2), ), ], ), child: Row( children: [ if (_currentStep > 0) Expanded( child: OutlinedButton( onPressed: _previousStep, style: OutlinedButton.styleFrom( foregroundColor: AppTheme.primaryColor, side: const BorderSide(color: AppTheme.primaryColor), padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: const Text('Précédent'), ), ), if (_currentStep > 0) const SizedBox(width: 16), Expanded( flex: _currentStep == 0 ? 1 : 1, child: ElevatedButton( onPressed: _isLoading ? null : _handleNextOrSubmit, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: _isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Text(_currentStep == 2 ? 'Créer le membre' : 'Suivant'), ), ), ], ), ); } void _previousStep() { if (_currentStep > 0) { setState(() { _currentStep--; }); } } void _handleNextOrSubmit() { if (_currentStep < 2) { if (_validateCurrentStep()) { setState(() { _currentStep++; }); } } else { _submitForm(); } } bool _validateCurrentStep() { switch (_currentStep) { case 0: return _validatePersonalInfo(); case 1: return _validateContactInfo(); case 2: return true; // Pas de validation spécifique pour la finalisation default: return false; } } bool _validatePersonalInfo() { final errors = []; // Validation du prénom final prenomError = FormValidator.name(_prenomController.text, fieldName: 'Le prénom'); if (prenomError != null) errors.add(prenomError); // Validation du nom final nomError = FormValidator.name(_nomController.text, fieldName: 'Le nom'); if (nomError != null) errors.add(nomError); // Validation de la date de naissance if (_dateNaissance != null) { final dateError = FormValidator.birthDate(_dateNaissance!, minAge: 16); if (dateError != null) errors.add(dateError); } if (errors.isNotEmpty) { UserFeedback.showWarning(context, errors.first); return false; } return true; } bool _validateContactInfo() { final errors = []; // Validation de l'email final emailError = FormValidator.email(_emailController.text); if (emailError != null) errors.add(emailError); // Validation du téléphone final phoneError = FormValidator.phone(_telephoneController.text); if (phoneError != null) errors.add(phoneError); // Validation de l'adresse (optionnelle) final addressError = FormValidator.address(_adresseController.text); if (addressError != null) errors.add(addressError); // Validation de la profession (optionnelle) final professionError = FormValidator.profession(_professionController.text); if (professionError != null) errors.add(professionError); if (errors.isNotEmpty) { UserFeedback.showWarning(context, errors.first); return false; } return true; } void _submitForm() { // Validation finale complète if (!_validateAllSteps()) { return; } if (!_formKey.currentState!.validate()) { UserFeedback.showWarning(context, 'Veuillez corriger les erreurs dans le formulaire'); return; } // Afficher l'indicateur de chargement UserFeedback.showLoading(context, message: 'Création du membre en cours...'); setState(() { _isLoading = true; }); try { // Créer le modèle membre avec validation des données final membre = MembreModel( id: '', // Sera généré par le backend numeroMembre: _numeroMembreController.text.trim(), nom: _nomController.text.trim(), prenom: _prenomController.text.trim(), email: _emailController.text.trim(), telephone: _telephoneController.text.trim(), dateNaissance: _dateNaissance, adresse: _adresseController.text.trim().isNotEmpty ? _adresseController.text.trim() : null, ville: _villeController.text.trim().isNotEmpty ? _villeController.text.trim() : null, codePostal: _codePostalController.text.trim().isNotEmpty ? _codePostalController.text.trim() : null, pays: _paysController.text.trim().isNotEmpty ? _paysController.text.trim() : null, profession: _professionController.text.trim().isNotEmpty ? _professionController.text.trim() : null, dateAdhesion: _dateAdhesion, actif: _actif, statut: 'ACTIF', version: 1, dateCreation: DateTime.now(), ); // Envoyer l'événement de création _membresBloc.add(CreateMembre(membre)); } catch (e) { UserFeedback.hideLoading(context); ErrorHandler.handleError(context, e, customMessage: 'Erreur lors de la préparation des données'); setState(() { _isLoading = false; }); } } bool _validateAllSteps() { // Valider toutes les étapes if (!_validatePersonalInfo()) return false; if (!_validateContactInfo()) return false; // Validation supplémentaire pour les champs obligatoires if (_dateNaissance == null) { UserFeedback.showWarning(context, 'La date de naissance est requise'); return false; } return true; } Future _selectDateNaissance() async { final date = await showDatePicker( context: context, initialDate: _dateNaissance ?? DateTime.now().subtract(const Duration(days: 365 * 25)), firstDate: DateTime(1900), lastDate: DateTime.now(), locale: const Locale('fr', 'FR'), ); if (date != null) { setState(() { _dateNaissance = date; }); } } Future _selectDateAdhesion() async { final date = await showDatePicker( context: context, initialDate: _dateAdhesion, firstDate: DateTime(2000), lastDate: DateTime.now().add(const Duration(days: 365)), locale: const Locale('fr', 'FR'), ); if (date != null) { setState(() { _dateAdhesion = date; }); } } void _showHelp() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Aide - Création de membre'), content: const SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( 'Étapes de création :', style: TextStyle(fontWeight: FontWeight.bold), ), SizedBox(height: 8), Text('1. Informations personnelles : Nom, prénom, date de naissance'), Text('2. Contact & Adresse : Email, téléphone, adresse'), Text('3. Finalisation : Vérification et validation'), SizedBox(height: 16), Text( 'Champs obligatoires :', style: TextStyle(fontWeight: FontWeight.bold), ), SizedBox(height: 8), Text('• Nom et prénom'), Text('• Email (format valide)'), Text('• Téléphone'), SizedBox(height: 16), Text( 'Le numéro de membre est généré automatiquement selon le format : MBR + Année + Mois + Numéro séquentiel', style: TextStyle(fontSize: 12, color: Colors.grey), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Fermer'), ), ], ), ); } }