import 'package:flutter/material.dart'; import 'package:p_tits_pas/models/relais_model.dart'; import 'package:p_tits_pas/models/user.dart'; import 'package:p_tits_pas/services/auth_service.dart'; import 'package:p_tits_pas/services/relais_service.dart'; import 'package:p_tits_pas/services/user_service.dart'; import 'package:p_tits_pas/utils/email_utils.dart'; import 'package:p_tits_pas/utils/name_format_utils.dart'; import 'package:p_tits_pas/utils/phone_utils.dart'; import 'package:p_tits_pas/utils/staff_deletion_rights.dart'; import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart'; import 'package:p_tits_pas/widgets/dashboard/common/validation_detail_section.dart'; import 'package:p_tits_pas/widgets/dashboard/validation_modal_theme.dart'; /// Modale création / édition / consultation staff (gestionnaire / admin) — #164. class StaffUserFormModal extends StatefulWidget { final AppUser? initialUser; final bool withRelais; final bool adminMode; final bool readOnly; const StaffUserFormModal({ super.key, this.initialUser, this.withRelais = true, this.adminMode = false, this.readOnly = false, }); @override State createState() => _StaffUserFormModalState(); } class _StaffUserFormModalState extends State { static const double _modalWidth = 930; 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; bool _dirty = false; List _relais = []; String? _selectedRelaisId; String? _currentUserId; String? _currentUserRole; String _baselineNom = ''; String _baselinePrenom = ''; String _baselineEmail = ''; String _baselinePhone = ''; String? _baselineRelaisId; bool get _isEditMode => widget.initialUser != null; bool get _isSuperAdminTarget => (widget.initialUser?.role ?? '').toLowerCase() == 'super_admin'; bool get _isSelfTarget => _isEditMode && _currentUserId != null && widget.initialUser!.id == _currentUserId; bool get _canDeleteTarget { if (!_isEditMode || widget.readOnly) return false; if (_isSelfTarget || _isSuperAdminTarget) return false; return canDeleteGestionnaire(_currentUserRole); } bool get _isLockedAdminIdentity => _isEditMode && widget.adminMode && _isSuperAdminTarget; bool get _fieldsEnabled => !widget.readOnly && !_isSubmitting; String get _targetRoleKey { if (widget.initialUser != null) { return (widget.initialUser!.role).toLowerCase(); } return widget.adminMode ? 'administrateur' : 'gestionnaire'; } String get _targetRoleLabel { switch (_targetRoleKey) { case 'super_admin': return 'Super administrateur'; case 'administrateur': return 'Administrateur'; case 'gestionnaire': return 'Gestionnaire'; default: return 'Utilisateur'; } } IconData get _targetRoleIcon { switch (_targetRoleKey) { case 'super_admin': return Icons.verified_user_outlined; case 'administrateur': return Icons.admin_panel_settings_outlined; case 'gestionnaire': return Icons.assignment_ind_outlined; default: return Icons.person_outline; } } @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 ?? ''); _passwordController.clear(); final initialRelaisId = user.relaisId?.trim(); _selectedRelaisId = (initialRelaisId == null || initialRelaisId.isEmpty) ? null : initialRelaisId; _captureBaseline(); } for (final c in [ _nomController, _prenomController, _emailController, _passwordController, _telephoneController, ]) { c.addListener(_onFieldChanged); } if (widget.withRelais) { _loadRelais(); } else { _isLoadingRelais = false; } _loadCurrentUserId(); } void _captureBaseline() { _baselineNom = formatPersonNameCase(_nomController.text); _baselinePrenom = formatPersonNameCase(_prenomController.text); _baselineEmail = normalizeEmailText(_emailController.text); _baselinePhone = normalizePhone(_telephoneController.text); _baselineRelaisId = _selectedRelaisId; } void _onFieldChanged() { if (widget.readOnly || !_isEditMode) return; final dirty = _computeDirty(); if (dirty != _dirty) setState(() => _dirty = dirty); } bool _computeDirty() { if (!_isEditMode) return true; final nom = formatPersonNameCase(_nomController.text); final prenom = formatPersonNameCase(_prenomController.text); final email = normalizeEmailText(_emailController.text); final phone = normalizePhone(_telephoneController.text); final passwordProvided = _passwordController.text.trim().isNotEmpty; return nom != _baselineNom || prenom != _baselinePrenom || email != _baselineEmail || phone != _baselinePhone || passwordProvided || _selectedRelaisId != _baselineRelaisId; } Future _loadCurrentUserId() async { final cached = await AuthService.getCurrentUser(); if (!mounted) return; if (cached != null) { setState(() { _currentUserId = cached.id; _currentUserRole = cached.role; }); return; } final refreshed = await AuthService.refreshCurrentUser(); if (!mounted || refreshed == null) return; setState(() { _currentUserId = refreshed.id; _currentUserRole = refreshed.role; }); } @override void dispose() { for (final c in [ _nomController, _prenomController, _emailController, _passwordController, _telephoneController, ]) { c.removeListener(_onFieldChanged); c.dispose(); } _passwordToggleFocusNode.dispose(); super.dispose(); } List _fallbackRelaisFromUser() { final id = _selectedRelaisId?.trim(); if (id == null || id.isEmpty) return const []; final nom = (widget.initialUser?.relaisNom ?? '').trim(); return [ RelaisModel( id: id, nom: nom.isNotEmpty ? nom : 'Relais actuel', adresse: '', actif: true, ), ]; } 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 { filtered.addAll(_fallbackRelaisFromUser()); } } setState(() { _relais = filtered; _isLoadingRelais = false; }); } catch (_) { if (!mounted) return; setState(() { _relais = _fallbackRelaisFromUser(); _isLoadingRelais = false; }); } } String? _required(String? value, String field) { if (value == null || value.trim().isEmpty) { return '$field est requis'; } 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; } Future _submit() async { if (widget.readOnly) return; if (_isSubmitting) return; if (_isEditMode && !_dirty) return; if (!_formKey.currentState!.validate()) return; setState(() => _isSubmitting = true); try { final normalizedNom = formatPersonNameCase(_nomController.text); final normalizedPrenom = formatPersonNameCase(_prenomController.text); final normalizedPhone = normalizePhone(_telephoneController.text); final passwordProvided = _passwordController.text.trim().isNotEmpty; if (_isEditMode) { if (widget.adminMode) { final lockedNom = formatPersonNameCase(widget.initialUser!.nom ?? ''); final lockedPrenom = formatPersonNameCase(widget.initialUser!.prenom ?? ''); await UserService.updateAdministrateur( adminId: widget.initialUser!.id, nom: _isLockedAdminIdentity ? lockedNom : normalizedNom, prenom: _isLockedAdminIdentity ? lockedPrenom : normalizedPrenom, email: normalizeEmailText(_emailController.text), telephone: normalizedPhone.isEmpty ? normalizePhone(widget.initialUser!.telephone ?? '') : normalizedPhone, password: passwordProvided ? _passwordController.text : null, ); } else { final currentUser = widget.initialUser!; final initialNom = formatPersonNameCase(currentUser.nom ?? ''); final initialPrenom = formatPersonNameCase(currentUser.prenom ?? ''); final initialEmail = normalizeEmailText(currentUser.email); final initialPhone = normalizePhone(currentUser.telephone ?? ''); final onlyRelaisChanged = normalizedNom == initialNom && normalizedPrenom == initialPrenom && normalizeEmailText(_emailController.text) == 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: normalizeEmailText(_emailController.text), telephone: normalizedPhone.isEmpty ? initialPhone : normalizedPhone, relaisId: _selectedRelaisId, password: passwordProvided ? _passwordController.text : null, ); } } } else { if (widget.adminMode) { await UserService.createAdministrateur( nom: normalizedNom, prenom: normalizedPrenom, email: normalizeEmailText(_emailController.text), password: _passwordController.text, telephone: normalizePhone(_telephoneController.text), ); } else { await UserService.createGestionnaire( nom: normalizedNom, prenom: normalizedPrenom, email: normalizeEmailText(_emailController.text), 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) { setState(() => _isSubmitting = false); } } } Future _delete() async { if (widget.readOnly) return; if (!_canDeleteTarget) return; if (!_isEditMode || _isSubmitting) return; final name = widget.initialUser!.fullName.isEmpty ? widget.initialUser!.email : widget.initialUser!.fullName; final confirmed = await showSuppressionConfirmDialog( context, title: widget.adminMode ? 'Supprimer l\'administrateur' : 'Supprimer le gestionnaire', people: [ widget.adminMode ? SuppressionPersonLine.administrateur(name) : SuppressionPersonLine.gestionnaire(name), ], footnotes: const [ 'Le compte sera définitivement supprimé.', ], ); if (!confirmed) return; setState(() => _isSubmitting = true); try { await UserService.deleteUser(widget.initialUser!.id); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( widget.adminMode ? 'Administrateur supprimé.' : '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); } } String _headerTitle() { if (!_isEditMode) { return widget.adminMode ? 'Créer un administrateur' : 'Créer un gestionnaire'; } final prenom = (_prenomController.text.trim().isNotEmpty ? _prenomController.text : (widget.initialUser?.prenom ?? '')) .trim(); final nom = (_nomController.text.trim().isNotEmpty ? _nomController.text : (widget.initialUser?.nom ?? '')) .trim(); final full = '$prenom $nom'.trim(); if (full.isNotEmpty) return full; return widget.initialUser?.email ?? _targetRoleLabel; } Widget _buildFooter() { if (widget.readOnly) { return Row( children: [ TextButton( onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false), child: const Text('Fermer'), ), ], ); } if (!_isEditMode) { return Row( children: [ TextButton( onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), const Spacer(), ElevatedButton( style: ValidationModalTheme.primaryElevatedStyle, onPressed: _isSubmitting ? null : _submit, child: _isSubmitting ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text('Créer'), ), ], ); } return Row( children: [ if (_canDeleteTarget) OutlinedButton( onPressed: _isSubmitting ? null : _delete, style: OutlinedButton.styleFrom( foregroundColor: Colors.red.shade700, ), child: const Text('Supprimer'), ), if (_canDeleteTarget) const SizedBox(width: 8), TextButton( onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false), child: const Text('Fermer'), ), const Spacer(), ElevatedButton( style: ValidationModalTheme.primaryElevatedStyle, onPressed: !_dirty || _isSubmitting ? null : _submit, child: _isSubmitting ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text(_dirty ? 'Sauvegarder' : 'Aucune modification'), ), ], ); } Widget _namedField({ required String label, required TextEditingController controller, required String requiredLabel, bool enabled = true, }) { return ValidationLabeledField( label: label, field: SizedBox( height: ValidationFormMetrics.fieldHeight, child: TextFormField( controller: controller, enabled: enabled, textCapitalization: TextCapitalization.words, textAlignVertical: TextAlignVertical.center, inputFormatters: const [PersonNameInputFormatter()], style: ValidationFormMetrics.fieldTextStyle, decoration: ValidationFieldDecoration.input(), validator: (!enabled || widget.readOnly) ? null : (v) => _required(v, requiredLabel), ), ), ); } Widget _passwordField() { return ValidationLabeledField( label: _isEditMode ? 'Nouveau mot de passe' : 'Mot de passe', field: SizedBox( height: ValidationFormMetrics.fieldHeight, child: TextFormField( controller: _passwordController, enabled: _fieldsEnabled, obscureText: _obscurePassword, enableSuggestions: false, autocorrect: false, autofillHints: _isEditMode ? const [] : const [AutofillHints.newPassword], textAlignVertical: TextAlignVertical.center, style: ValidationFormMetrics.fieldTextStyle, decoration: ValidationFieldDecoration.input().copyWith( suffixIcon: widget.readOnly ? null : ExcludeFocus( child: IconButton( focusNode: _passwordToggleFocusNode, onPressed: !_fieldsEnabled ? null : () { setState(() { _obscurePassword = !_obscurePassword; }); }, icon: Icon( _obscurePassword ? Icons.visibility_off : Icons.visibility, ), ), ), errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11), errorMaxLines: 2, ), validator: widget.readOnly ? null : _validatePassword, ), ), ); } Widget _relaisField() { final selectedValue = _selectedRelaisId != null && _relais.any((relais) => relais.id == _selectedRelaisId) ? _selectedRelaisId : null; return ValidationLabeledField( label: 'Relais principal', field: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( height: ValidationFormMetrics.fieldHeight, child: DropdownButtonFormField( isExpanded: true, value: selectedValue, decoration: ValidationFieldDecoration.input(), style: ValidationFormMetrics.fieldTextStyle, items: [ const DropdownMenuItem( value: null, child: Text('Aucun relais'), ), ..._relais.map( (relais) => DropdownMenuItem( value: relais.id, child: Text(relais.nom), ), ), ], onChanged: (_isLoadingRelais || !_fieldsEnabled) ? null : (value) { setState(() { _selectedRelaisId = value; if (_isEditMode) { _dirty = _computeDirty(); } }); }, ), ), if (_isLoadingRelais) ...[ const SizedBox(height: 8), const LinearProgressIndicator(minHeight: 2), ], ], ), ); } @override Widget build(BuildContext context) { final nameEnabled = _fieldsEnabled && !_isLockedAdminIdentity; return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: _modalWidth), child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(18, 16, 4, 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 2, right: 10), child: Icon( _targetRoleIcon, size: 22, color: ValidationModalTheme.primaryActionBackground, ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _headerTitle(), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, ), ), if (_isEditMode) ...[ const SizedBox(height: 4), Text( _targetRoleLabel, style: TextStyle( fontSize: 13, color: Colors.grey.shade700, ), ), ], ], ), ), IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 40, minHeight: 40, ), icon: const Icon(Icons.close), onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false), tooltip: 'Fermer', ), ], ), ), const Divider(height: 1), Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _namedField( label: 'Prénom', controller: _prenomController, requiredLabel: 'Prénom', enabled: nameEnabled, ), ), const SizedBox(width: 12), Expanded( child: _namedField( label: 'Nom', controller: _nomController, requiredLabel: 'Nom', enabled: nameEnabled, ), ), ], ), SizedBox(height: ValidationFormMetrics.rowGapBelow), ValidationLabeledField( label: 'Email', field: IgnorePointer( ignoring: !_fieldsEnabled, child: ValidationEmailField( controller: _emailController, hintText: 'ex. nom@domaine.fr', ), ), ), SizedBox(height: ValidationFormMetrics.rowGapBelow), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: _passwordField()), const SizedBox(width: 12), Expanded( child: ValidationLabeledField( label: 'Téléphone', field: IgnorePointer( ignoring: !_fieldsEnabled, child: ValidationPhoneField( controller: _telephoneController, hintText: '06 12 34 56 78', allowEmpty: _isEditMode, ), ), ), ), ], ), if (widget.withRelais) ...[ SizedBox(height: ValidationFormMetrics.rowGapBelow), _relaisField(), ], ], ), ), const Divider(height: 1), Padding( padding: const EdgeInsets.fromLTRB(12, 10, 16, 14), child: _buildFooter(), ), ], ), ), ), ); } }