import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:p_tits_pas/models/relais_model.dart'; import 'package:p_tits_pas/models/user.dart'; import 'package:p_tits_pas/services/relais_service.dart'; import 'package:p_tits_pas/services/user_service.dart'; class AdminUserFormDialog extends StatefulWidget { final AppUser? initialUser; final bool withRelais; final bool adminMode; const AdminUserFormDialog({ super.key, this.initialUser, this.withRelais = true, this.adminMode = false, }); @override State createState() => _AdminUserFormDialogState(); } class _AdminUserFormDialogState extends State { final _formKey = GlobalKey(); final _nomController = TextEditingController(); final _prenomController = TextEditingController(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _telephoneController = TextEditingController(); final _passwordToggleFocusNode = FocusNode(skipTraversal: true, canRequestFocus: false); bool _isSubmitting = false; bool _obscurePassword = true; bool _isLoadingRelais = true; List _relais = []; String? _selectedRelaisId; bool get _isEditMode => widget.initialUser != null; @override void initState() { super.initState(); final user = widget.initialUser; if (user != null) { _nomController.text = user.nom ?? ''; _prenomController.text = user.prenom ?? ''; _emailController.text = user.email; _telephoneController.text = _formatPhoneForDisplay(user.telephone ?? ''); // En édition, on ne préremplit jamais le mot de passe. _passwordController.clear(); final initialRelaisId = user.relaisId?.trim(); _selectedRelaisId = (initialRelaisId == null || initialRelaisId.isEmpty) ? null : initialRelaisId; } if (widget.withRelais) { _loadRelais(); } else { _isLoadingRelais = false; } } @override void dispose() { _nomController.dispose(); _prenomController.dispose(); _emailController.dispose(); _passwordController.dispose(); _telephoneController.dispose(); _passwordToggleFocusNode.dispose(); super.dispose(); } Future _loadRelais() async { try { final list = await RelaisService.getRelais(); if (!mounted) return; final uniqueById = {}; for (final relais in list) { uniqueById[relais.id] = relais; } final filtered = uniqueById.values.where((r) => r.actif).toList(); if (_selectedRelaisId != null && !filtered.any((r) => r.id == _selectedRelaisId)) { final selected = uniqueById[_selectedRelaisId!]; if (selected != null) { filtered.add(selected); } else { _selectedRelaisId = null; } } setState(() { _relais = filtered; _isLoadingRelais = false; }); } catch (_) { if (!mounted) return; setState(() { _selectedRelaisId = null; _relais = []; _isLoadingRelais = false; }); } } String? _required(String? value, String field) { if (value == null || value.trim().isEmpty) { return '$field est requis'; } return null; } String? _validateEmail(String? value) { final base = _required(value, 'Email'); if (base != null) return base; final email = value!.trim(); final ok = RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email); if (!ok) return 'Format email invalide'; return null; } String? _validatePassword(String? value) { if (_isEditMode && (value == null || value.trim().isEmpty)) { return null; } final base = _required(value, 'Mot de passe'); if (base != null) return base; if (value!.trim().length < 6) return 'Minimum 6 caractères'; return null; } String? _validatePhone(String? value) { if (_isEditMode && (value == null || value.trim().isEmpty)) { return null; } final base = _required(value, 'Téléphone'); if (base != null) return base; final digits = _normalizePhone(value!); if (digits.length != 10) { return 'Le téléphone doit contenir 10 chiffres'; } if (!digits.startsWith('0')) { return 'Le téléphone doit commencer par 0'; } return null; } String _normalizePhone(String raw) { return raw.replaceAll(RegExp(r'\D'), ''); } String _formatPhoneForDisplay(String raw) { final normalized = _normalizePhone(raw); final digits = normalized.length > 10 ? normalized.substring(0, 10) : normalized; final buffer = StringBuffer(); for (var i = 0; i < digits.length; i++) { if (i > 0 && i.isEven) buffer.write(' '); buffer.write(digits[i]); } return buffer.toString(); } String _toTitleCase(String raw) { final trimmed = raw.trim(); if (trimmed.isEmpty) return trimmed; final words = trimmed.split(RegExp(r'\s+')); final normalizedWords = words.map(_capitalizeComposedWord).toList(); return normalizedWords.join(' '); } String _capitalizeComposedWord(String word) { if (word.isEmpty) return word; final lower = word.toLowerCase(); final separators = {"-", "'", "’"}; final buffer = StringBuffer(); var capitalizeNext = true; for (var i = 0; i < lower.length; i++) { final char = lower[i]; if (capitalizeNext && RegExp(r'[a-zà-öø-ÿ]').hasMatch(char)) { buffer.write(char.toUpperCase()); capitalizeNext = false; } else { buffer.write(char); capitalizeNext = separators.contains(char); } } return buffer.toString(); } Future _submit() async { if (_isSubmitting) return; if (!_formKey.currentState!.validate()) return; setState(() { _isSubmitting = true; }); try { final normalizedNom = _toTitleCase(_nomController.text); final normalizedPrenom = _toTitleCase(_prenomController.text); final normalizedPhone = _normalizePhone(_telephoneController.text); final passwordProvided = _passwordController.text.trim().isNotEmpty; if (_isEditMode) { if (widget.adminMode) { await UserService.updateAdministrateur( adminId: widget.initialUser!.id, nom: normalizedNom, prenom: normalizedPrenom, email: _emailController.text.trim(), telephone: normalizedPhone.isEmpty ? _normalizePhone(widget.initialUser!.telephone ?? '') : normalizedPhone, password: passwordProvided ? _passwordController.text : null, ); } else { final currentUser = widget.initialUser!; final initialNom = _toTitleCase(currentUser.nom ?? ''); final initialPrenom = _toTitleCase(currentUser.prenom ?? ''); final initialEmail = currentUser.email.trim(); final initialPhone = _normalizePhone(currentUser.telephone ?? ''); final onlyRelaisChanged = normalizedNom == initialNom && normalizedPrenom == initialPrenom && _emailController.text.trim() == initialEmail && normalizedPhone == initialPhone && !passwordProvided; if (onlyRelaisChanged) { await UserService.updateGestionnaireRelais( gestionnaireId: currentUser.id, relaisId: _selectedRelaisId, ); } else { await UserService.updateGestionnaire( gestionnaireId: currentUser.id, nom: normalizedNom, prenom: normalizedPrenom, email: _emailController.text.trim(), telephone: normalizedPhone.isEmpty ? initialPhone : normalizedPhone, relaisId: _selectedRelaisId, password: passwordProvided ? _passwordController.text : null, ); } } } else { if (widget.adminMode) { await UserService.createAdministrateur( nom: normalizedNom, prenom: normalizedPrenom, email: _emailController.text.trim(), password: _passwordController.text, telephone: _normalizePhone(_telephoneController.text), ); } else { await UserService.createGestionnaire( nom: normalizedNom, prenom: normalizedPrenom, email: _emailController.text.trim(), password: _passwordController.text, telephone: _normalizePhone(_telephoneController.text), relaisId: _selectedRelaisId, ); } } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( _isEditMode ? (widget.adminMode ? 'Administrateur modifié avec succès.' : 'Gestionnaire modifié avec succès.') : (widget.adminMode ? 'Administrateur créé avec succès.' : 'Gestionnaire créé avec succès.'), ), ), ); Navigator.of(context).pop(true); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( e.toString().replaceFirst('Exception: ', ''), ), backgroundColor: Colors.red.shade700, ), ); } finally { if (!mounted) return; setState(() { _isSubmitting = false; }); } } Future _delete() async { if (!_isEditMode || _isSubmitting) return; final confirmed = await showDialog( context: context, builder: (ctx) { return AlertDialog( title: const Text('Confirmer la suppression'), content: Text( 'Supprimer ${widget.initialUser!.fullName.isEmpty ? widget.initialUser!.email : widget.initialUser!.fullName} ?', ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Annuler'), ), FilledButton( onPressed: () => Navigator.of(ctx).pop(true), style: FilledButton.styleFrom(backgroundColor: Colors.red.shade700), child: const Text('Supprimer'), ), ], ); }, ); if (confirmed != true) return; setState(() { _isSubmitting = true; }); try { await UserService.deleteUser(widget.initialUser!.id); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Gestionnaire supprimé.')), ); Navigator.of(context).pop(true); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString().replaceFirst('Exception: ', '')), backgroundColor: Colors.red.shade700, ), ); setState(() { _isSubmitting = false; }); } } @override Widget build(BuildContext context) { return AlertDialog( title: Row( children: [ Expanded( child: Text( _isEditMode ? (widget.adminMode ? 'Modifier un administrateur' : 'Modifier un gestionnaire') : (widget.adminMode ? 'Créer un administrateur' : 'Créer un gestionnaire'), ), ), if (_isEditMode) IconButton( icon: const Icon(Icons.close), tooltip: 'Fermer', onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false), ), ], ), content: SizedBox( width: 620, child: Form( key: _formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Expanded(child: _buildPrenomField()), const SizedBox(width: 12), Expanded(child: _buildNomField()), ], ), const SizedBox(height: 12), _buildEmailField(), const SizedBox(height: 12), Row( children: [ Expanded(child: _buildPasswordField()), const SizedBox(width: 12), Expanded(child: _buildTelephoneField()), ], ), if (widget.withRelais) ...[ const SizedBox(height: 12), _buildRelaisField(), ], ], ), ), ), ), actions: [ if (_isEditMode) ...[ OutlinedButton( onPressed: _isSubmitting ? null : _delete, style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700), child: const Text('Supprimer'), ), FilledButton.icon( onPressed: _isSubmitting ? null : _submit, icon: _isSubmitting ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.edit), label: Text(_isSubmitting ? 'Modification...' : 'Modifier'), ), ] else ...[ OutlinedButton( onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), FilledButton.icon( onPressed: _isSubmitting ? null : _submit, icon: _isSubmitting ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.person_add_alt_1), label: Text(_isSubmitting ? 'Création...' : 'Créer'), ), ], ], ); } Widget _buildNomField() { return TextFormField( controller: _nomController, textCapitalization: TextCapitalization.words, decoration: const InputDecoration( labelText: 'Nom', border: OutlineInputBorder(), ), validator: (v) => _required(v, 'Nom'), ); } Widget _buildPrenomField() { return TextFormField( controller: _prenomController, textCapitalization: TextCapitalization.words, decoration: const InputDecoration( labelText: 'Prénom', border: OutlineInputBorder(), ), validator: (v) => _required(v, 'Prénom'), ); } Widget _buildEmailField() { return TextFormField( controller: _emailController, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( labelText: 'Email', border: OutlineInputBorder(), ), validator: _validateEmail, ); } Widget _buildPasswordField() { return TextFormField( controller: _passwordController, obscureText: _obscurePassword, enableSuggestions: false, autocorrect: false, autofillHints: _isEditMode ? const [] : const [AutofillHints.newPassword], decoration: InputDecoration( labelText: _isEditMode ? 'Nouveau mot de passe' : 'Mot de passe', border: const OutlineInputBorder(), suffixIcon: ExcludeFocus( child: IconButton( focusNode: _passwordToggleFocusNode, onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, icon: Icon( _obscurePassword ? Icons.visibility_off : Icons.visibility, ), ), ), ), validator: _validatePassword, ); } Widget _buildTelephoneField() { return TextFormField( controller: _telephoneController, keyboardType: TextInputType.phone, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), _FrenchPhoneNumberFormatter(), ], decoration: const InputDecoration( labelText: 'Téléphone (ex: 06 12 34 56 78)', border: OutlineInputBorder(), ), validator: _validatePhone, ); } Widget _buildRelaisField() { final selectedValue = _selectedRelaisId != null && _relais.any((relais) => relais.id == _selectedRelaisId) ? _selectedRelaisId : null; return Column( mainAxisSize: MainAxisSize.min, children: [ DropdownButtonFormField( isExpanded: true, value: selectedValue, decoration: const InputDecoration( labelText: 'Relais principal', border: OutlineInputBorder(), ), items: [ const DropdownMenuItem( value: null, child: Text('Aucun relais'), ), ..._relais.map( (relais) => DropdownMenuItem( value: relais.id, child: Text(relais.nom), ), ), ], onChanged: _isLoadingRelais ? null : (value) { setState(() { _selectedRelaisId = value; }); }, ), if (_isLoadingRelais) ...[ const SizedBox(height: 8), const LinearProgressIndicator(minHeight: 2), ], ], ); } } class _FrenchPhoneNumberFormatter extends TextInputFormatter { const _FrenchPhoneNumberFormatter(); @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { final digits = newValue.text.replaceAll(RegExp(r'\D'), ''); final normalized = digits.length > 10 ? digits.substring(0, 10) : digits; final buffer = StringBuffer(); for (var i = 0; i < normalized.length; i++) { if (i > 0 && i.isEven) buffer.write(' '); buffer.write(normalized[i]); } final formatted = buffer.toString(); return TextEditingValue( text: formatted, selection: TextSelection.collapsed(offset: formatted.length), ); } }