import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'dart:math'; import 'package:geosector_app/core/data/models/user_model.dart'; import 'package:geosector_app/core/data/models/amicale_model.dart'; import 'package:geosector_app/core/services/api_service.dart'; import 'custom_text_field.dart'; class UserForm extends StatefulWidget { final UserModel? user; final Function(UserModel)? onSubmit; final bool readOnly; final bool allowUsernameEdit; final bool allowSectNameEdit; final AmicaleModel? amicale; // Nouveau paramètre pour l'amicale final bool isAdmin; // Nouveau paramètre pour savoir si c'est un admin const UserForm({ super.key, this.user, this.onSubmit, this.readOnly = false, this.allowUsernameEdit = false, this.allowSectNameEdit = false, this.amicale, this.isAdmin = false, }); @override State createState() => _UserFormState(); } class _UserFormState extends State { final _formKey = GlobalKey(); // Controllers late final TextEditingController _usernameController; late final TextEditingController _firstNameController; late final TextEditingController _nameController; late final TextEditingController _sectNameController; late final TextEditingController _phoneController; late final TextEditingController _mobileController; late final TextEditingController _emailController; late final TextEditingController _dateNaissanceController; late final TextEditingController _dateEmbaucheController; late final TextEditingController _passwordController; // Nouveau controller pour le mot de passe // Form values int _fkTitre = 1; // 1 = M., 2 = Mme DateTime? _dateNaissance; DateTime? _dateEmbauche; // Pour la génération automatique d'username bool _isGeneratingUsername = false; final Random _random = Random(); // Pour afficher/masquer le mot de passe bool _obscurePassword = true; @override void initState() { super.initState(); // Initialize controllers with user data if available final user = widget.user; _usernameController = TextEditingController(text: user?.username ?? ''); _firstNameController = TextEditingController(text: user?.firstName ?? ''); _nameController = TextEditingController(text: user?.name ?? ''); _sectNameController = TextEditingController(text: user?.sectName ?? ''); _phoneController = TextEditingController(text: user?.phone ?? ''); _mobileController = TextEditingController(text: user?.mobile ?? ''); _emailController = TextEditingController(text: user?.email ?? ''); _dateNaissance = user?.dateNaissance; _dateEmbauche = user?.dateEmbauche; _dateNaissanceController = TextEditingController(text: _dateNaissance != null ? DateFormat('dd/MM/yyyy').format(_dateNaissance!) : ''); _dateEmbaucheController = TextEditingController(text: _dateEmbauche != null ? DateFormat('dd/MM/yyyy').format(_dateEmbauche!) : ''); _passwordController = TextEditingController(); // Initialiser le controller du mot de passe _fkTitre = user?.fkTitre ?? 1; // Ajouter des listeners pour auto-générer l'username en création if (widget.user?.id == 0 && widget.isAdmin && widget.amicale?.chkUsernameManuel == true) { _nameController.addListener(_onNameOrSectNameChanged); _sectNameController.addListener(_onNameOrSectNameChanged); } } void _onNameOrSectNameChanged() { // Auto-générer username seulement en création et si le champ username est vide if (widget.user?.id == 0 && _usernameController.text.isEmpty && (_nameController.text.isNotEmpty || _sectNameController.text.isNotEmpty)) { _generateAndCheckUsername(); } } @override void dispose() { // Retirer les listeners si ajoutés if (widget.user?.id == 0 && widget.isAdmin && widget.amicale?.chkUsernameManuel == true) { _nameController.removeListener(_onNameOrSectNameChanged); _sectNameController.removeListener(_onNameOrSectNameChanged); } _usernameController.dispose(); _firstNameController.dispose(); _nameController.dispose(); _sectNameController.dispose(); _phoneController.dispose(); _mobileController.dispose(); _emailController.dispose(); _dateNaissanceController.dispose(); _dateEmbaucheController.dispose(); _passwordController.dispose(); super.dispose(); } // Validation conditionnelle pour name/sectName String? _validateNameOrSectName(String? value, bool isNameField) { final nameValue = _nameController.text.trim(); final sectNameValue = _sectNameController.text.trim(); // Si les deux sont vides if (nameValue.isEmpty && sectNameValue.isEmpty) { return isNameField ? "Veuillez renseigner soit le nom soit le nom de tournée" : "Veuillez renseigner soit le nom de tournée soit le nom"; } return null; // Validation OK si au moins un des deux est rempli } // Méthode simplifiée pour sélectionner une date void _selectDate(BuildContext context, bool isDateNaissance) { // Utiliser un bloc try-catch pour capturer toutes les erreurs possibles try { // Déterminer la date initiale DateTime initialDate; if (isDateNaissance) { initialDate = _dateNaissance ?? DateTime.now().subtract(const Duration(days: 365 * 30)); // 30 ans par défaut } else { initialDate = _dateEmbauche ?? DateTime.now(); } // S'assurer que la date initiale est dans la plage autorisée if (initialDate.isAfter(DateTime.now())) { initialDate = DateTime.now(); } if (initialDate.isBefore(DateTime(1900))) { initialDate = DateTime(1950); } // Afficher le sélecteur de date avec locale française showDatePicker( context: context, initialDate: initialDate, firstDate: DateTime(1900), lastDate: DateTime.now(), locale: const Locale('fr', 'FR'), // Forcer la locale française builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: Theme.of(context).colorScheme.copyWith( primary: Theme.of(context).colorScheme.primary, onPrimary: Colors.white, surface: Colors.white, onSurface: Colors.black, ), ), child: child!, ); }, helpText: isDateNaissance ? 'SÉLECTIONNER LA DATE DE NAISSANCE' : 'SÉLECTIONNER LA DATE D\'EMBAUCHE', cancelText: 'ANNULER', confirmText: 'VALIDER', fieldLabelText: 'Entrer une date', fieldHintText: 'jj/mm/aaaa', errorFormatText: 'Format de date invalide', errorInvalidText: 'Date invalide', ).then((DateTime? picked) { // Vérifier si une date a été sélectionnée if (picked != null) { setState(() { // Mettre à jour la date et le texte du contrôleur if (isDateNaissance) { _dateNaissance = picked; _dateNaissanceController.text = DateFormat('dd/MM/yyyy').format(picked); } else { _dateEmbauche = picked; _dateEmbaucheController.text = DateFormat('dd/MM/yyyy').format(picked); } }); } }).catchError((error) { // Gérer les erreurs spécifiques au sélecteur de date debugPrint('Erreur lors de la sélection de la date: $error'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Erreur lors de la sélection de la date'), backgroundColor: Colors.red, ), ); }); } catch (e) { // Gérer toutes les autres erreurs debugPrint('Exception lors de l\'affichage du sélecteur de date: $e'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Impossible d\'afficher le sélecteur de date'), backgroundColor: Colors.red, ), ); } } // Nettoyer une chaîne pour l'username (NIST: garder plus de caractères) String _cleanString(String input) { // Avec NIST, on peut garder plus de caractères, mais pour la génération automatique // on reste sur des caractères simples pour éviter les problèmes return input.toLowerCase() .replaceAll(RegExp(r'[^a-z0-9\s]'), ''); // Garder lettres, chiffres et espaces } // Extraire une partie aléatoire d'une chaîne String _extractRandomPart(String input, int minLength, int maxLength) { if (input.isEmpty) return ''; final cleaned = _cleanString(input); if (cleaned.isEmpty) return ''; final length = minLength + _random.nextInt(maxLength - minLength + 1); if (cleaned.length <= length) return cleaned; // Prendre les premiers caractères jusqu'à la longueur désirée return cleaned.substring(0, length); } // Générer un username selon les nouvelles normes NIST String _generateUsername() { // Récupérer les données nécessaires final nom = _nameController.text.isNotEmpty ? _nameController.text : _sectNameController.text; final codePostal = widget.amicale?.codePostal ?? ''; final ville = widget.amicale?.ville ?? ''; // Nettoyer et extraire les parties final nomPart = _extractRandomPart(nom, 2, 5); final cpPart = _extractRandomPart(codePostal, 2, 3); final villePart = _extractRandomPart(ville, 2, 4); final nombreAleatoire = 10 + _random.nextInt(990); // 10 à 999 // Choisir des séparateurs aléatoires (on peut maintenant utiliser des espaces aussi) final separateurs = ['', '.', '_', '-', ' ']; final sep1 = separateurs[_random.nextInt(separateurs.length)]; final sep2 = separateurs[_random.nextInt(separateurs.length)]; // Assembler l'username String username = '$nomPart$sep1$cpPart$sep2$villePart$nombreAleatoire'; // Si trop court, ajouter des chiffres pour atteindre minimum 8 caractères (NIST) while (username.length < 8) { username += _random.nextInt(10).toString(); } // Avec NIST, on n'a plus besoin de nettoyer les caractères spéciaux // Tous les caractères sont acceptés return username; } // Vérifier la disponibilité d'un username via l'API Future> _checkUsernameAvailability(String username) async { try { final response = await ApiService.instance.post( '/users/check-username', data: {'username': username}, ); if (response.statusCode == 200) { return response.data; } return {'available': false}; } catch (e) { debugPrint('Erreur lors de la vérification de l\'username: $e'); return {'available': false}; } } // Générer et vérifier un username jusqu'à en trouver un disponible Future _generateAndCheckUsername() async { if (_isGeneratingUsername) return; // Éviter les appels multiples setState(() { _isGeneratingUsername = true; }); try { int attempts = 0; const maxAttempts = 10; while (attempts < maxAttempts) { final username = _generateUsername(); debugPrint('Tentative ${attempts + 1}: Vérification de $username'); final result = await _checkUsernameAvailability(username); if (result['available'] == true) { // Username disponible, l'utiliser setState(() { _usernameController.text = username; }); debugPrint('✅ Username disponible trouvé: $username'); break; } else { // Si l'API propose des suggestions, essayer la première if (result['suggestions'] != null && result['suggestions'].isNotEmpty) { final suggestion = result['suggestions'][0]; debugPrint('Vérification de la suggestion: $suggestion'); final suggestionResult = await _checkUsernameAvailability(suggestion); if (suggestionResult['available'] == true) { setState(() { _usernameController.text = suggestion; }); debugPrint('✅ Suggestion disponible utilisée: $suggestion'); break; } } } attempts++; } if (attempts >= maxAttempts) { debugPrint('⚠️ Impossible de trouver un username disponible après $maxAttempts tentatives'); } } finally { setState(() { _isGeneratingUsername = false; }); } } // Valider le mot de passe selon les normes NIST SP 800-63B String? _validatePassword(String? value) { if (value == null || value.isEmpty) { // Pour un nouveau membre, le mot de passe est obligatoire si le champ est affiché if (widget.user?.id == 0) { return "Veuillez entrer un mot de passe"; } return null; // En modification, vide = garder l'ancien } // Vérifier la longueur minimale (NIST: 8 caractères) if (value.length < 8) { return "Le mot de passe doit contenir au moins 8 caractères"; } // Vérifier la longueur maximale (NIST: 64 caractères) if (value.length > 64) { return "Le mot de passe ne doit pas dépasser 64 caractères"; } // NIST: Pas de vérification de composition obligatoire // Les espaces sont autorisés, tous les caractères sont acceptés // L'API vérifiera contre Have I Been Pwned // Pas de vérification password == username (demande client) return null; } // Générer un mot de passe selon les normes NIST (phrases de passe recommandées) String _generatePassword() { // Listes de mots pour créer des phrases de passe mémorables final sujets = [ 'Mon chat', 'Le chien', 'Ma voiture', 'Mon vélo', 'La maison', 'Mon jardin', 'Le soleil', 'La lune', 'Mon café', 'Le train', 'Ma pizza', 'Le gâteau', 'Mon livre', 'La musique', 'Mon film' ]; final noms = [ 'Félix', 'Max', 'Luna', 'Bella', 'Charlie', 'Rocky', 'Maya', 'Oscar', 'Ruby', 'Leo', 'Emma', 'Jack', 'Sophie', 'Milo', 'Zoé' ]; final verbes = [ 'aime', 'mange', 'court', 'saute', 'danse', 'chante', 'joue', 'dort', 'rêve', 'vole', 'nage', 'lit', 'écrit', 'peint', 'cuisine' ]; final complements = [ 'dans le jardin', 'sous la pluie', 'avec joie', 'très vite', 'tout le temps', 'en été', 'le matin', 'la nuit', 'au soleil', 'dans la neige', 'sur la plage', 'à Paris', 'en vacances', 'avec passion', 'doucement' ]; // Choisir un type de phrase aléatoirement final typePhrase = _random.nextInt(3); String phrase; switch (typePhrase) { case 0: // Type: Sujet + nom propre + verbe + complément final sujet = sujets[_random.nextInt(sujets.length)]; final nom = noms[_random.nextInt(noms.length)]; final verbe = verbes[_random.nextInt(verbes.length)]; final complement = complements[_random.nextInt(complements.length)]; phrase = '$sujet $nom $verbe $complement'; break; case 1: // Type: Nom propre + a + nombre + ans + point d'exclamation final nom = noms[_random.nextInt(noms.length)]; final age = 1 + _random.nextInt(20); phrase = '$nom a $age ans!'; break; default: // Type: Sujet + verbe + nombre + complément final sujet = sujets[_random.nextInt(sujets.length)]; final verbe = verbes[_random.nextInt(verbes.length)]; final nombre = 1 + _random.nextInt(100); final complement = complements[_random.nextInt(complements.length)]; phrase = '$sujet $verbe $nombre fois $complement'; } // Ajouter éventuellement un caractère spécial à la fin if (_random.nextBool()) { final speciaux = ['!', '?', '.', '...', '♥', '☀', '★', '♪']; phrase += speciaux[_random.nextInt(speciaux.length)]; } // S'assurer que la phrase fait au moins 8 caractères (elle le sera presque toujours) if (phrase.length < 8) { phrase += ' ${1000 + _random.nextInt(9000)}'; } // Tronquer si trop long (max 64 caractères selon NIST) if (phrase.length > 64) { phrase = phrase.substring(0, 64); } return phrase; } // Méthode publique pour récupérer le mot de passe si défini String? getPassword() { final password = _passwordController.text; // NIST: ne pas faire de trim, les espaces sont autorisés return password.isNotEmpty ? password : null; } // Méthode publique pour valider et récupérer l'utilisateur UserModel? validateAndGetUser() { if (_formKey.currentState!.validate()) { return widget.user?.copyWith( username: _usernameController.text, // NIST: ne pas faire de trim sur username firstName: _firstNameController.text.trim(), name: _nameController.text.trim(), sectName: _sectNameController.text.trim(), phone: _phoneController.text.trim(), mobile: _mobileController.text.trim(), email: _emailController.text.trim(), fkTitre: _fkTitre, dateNaissance: _dateNaissance, dateEmbauche: _dateEmbauche, ) ?? UserModel( id: 0, username: _usernameController.text, // NIST: ne pas faire de trim sur username firstName: _firstNameController.text.trim(), name: _nameController.text.trim(), sectName: _sectNameController.text.trim(), phone: _phoneController.text.trim(), mobile: _mobileController.text.trim(), email: _emailController.text.trim(), fkTitre: _fkTitre, dateNaissance: _dateNaissance, dateEmbauche: _dateEmbauche, role: 1, createdAt: DateTime.now(), lastSyncedAt: DateTime.now(), ); } return null; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isWideScreen = MediaQuery.of(context).size.width > 900; // Déterminer si on doit afficher le champ username selon les règles final bool shouldShowUsernameField = widget.isAdmin && widget.amicale?.chkUsernameManuel == true; // Déterminer si le username est éditable (seulement en création, jamais en modification) final bool canEditUsername = shouldShowUsernameField && widget.allowUsernameEdit && widget.user?.id == 0; // Déterminer si on doit afficher le champ mot de passe final bool shouldShowPasswordField = widget.isAdmin && widget.amicale?.chkMdpManuel == true; return Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Email seul sur la première ligne CustomTextField( controller: _emailController, label: "Email", keyboardType: TextInputType.emailAddress, readOnly: widget.readOnly, isRequired: true, validator: (value) { if (value == null || value.isEmpty) { return "Veuillez entrer l'adresse email"; } if (!value.contains('@') || !value.contains('.')) { return "Veuillez entrer une adresse email valide"; } return null; }, ), const SizedBox(height: 16), // Titre (M. ou Mme) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Titre", style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 8), Row( children: [ _buildRadioOption( value: 1, label: 'M.', groupValue: _fkTitre, onChanged: widget.readOnly ? null : (value) { setState(() { _fkTitre = value!; }); }, ), const SizedBox(width: 40), _buildRadioOption( value: 2, label: 'Mme', groupValue: _fkTitre, onChanged: widget.readOnly ? null : (value) { setState(() { _fkTitre = value!; }); }, ), ], ), ], ), const SizedBox(height: 16), // Ligne 2: Prénom et Nom if (isWideScreen) Row( children: [ Expanded( child: CustomTextField( controller: _firstNameController, label: "Prénom", readOnly: widget.readOnly, ), ), const SizedBox(width: 16), Expanded( child: CustomTextField( controller: _nameController, label: "Nom", readOnly: widget.readOnly, validator: (value) => _validateNameOrSectName(value, true), onChanged: (value) { // Revalider sectName quand name change if (widget.allowSectNameEdit) { _formKey.currentState?.validate(); } }, ), ), ], ) else ...[ // Version mobile: Prénom et nom séparés CustomTextField( controller: _firstNameController, label: "Prénom", readOnly: widget.readOnly, ), const SizedBox(height: 16), CustomTextField( controller: _nameController, label: "Nom", readOnly: widget.readOnly, validator: (value) => _validateNameOrSectName(value, true), onChanged: (value) { // Revalider sectName quand name change if (widget.allowSectNameEdit) { _formKey.currentState?.validate(); } }, ), ], const SizedBox(height: 16), // Ligne 2.5: Nom de tournée (sectName) - uniquement si éditable if (widget.allowSectNameEdit) ...[ CustomTextField( controller: _sectNameController, label: "Nom de tournée", readOnly: widget.readOnly, validator: (value) => _validateNameOrSectName(value, false), onChanged: (value) { // Revalider name quand sectName change _formKey.currentState?.validate(); }, hintText: "Nom utilisé pour identifier la tournée", ), const SizedBox(height: 16), ], // Ligne 2: Téléphones (fixe et mobile) if (isWideScreen) Row( children: [ Expanded( child: CustomTextField( controller: _phoneController, label: "Téléphone fixe", keyboardType: TextInputType.phone, readOnly: widget.readOnly, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), ], validator: (value) { if (value != null && value.isNotEmpty && value.length < 10) { return "Le numéro doit contenir 10 chiffres"; } return null; }, ), ), const SizedBox(width: 16), Expanded( child: CustomTextField( controller: _mobileController, label: "Téléphone mobile", keyboardType: TextInputType.phone, readOnly: widget.readOnly, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), ], validator: (value) { if (value != null && value.isNotEmpty && value.length < 10) { return "Le numéro doit contenir 10 chiffres"; } return null; }, ), ), ], ) else ...[ // Version mobile: Téléphones séparés CustomTextField( controller: _phoneController, label: "Téléphone fixe", keyboardType: TextInputType.phone, readOnly: widget.readOnly, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), ], validator: (value) { if (value != null && value.isNotEmpty && value.length < 10) { return "Le numéro doit contenir 10 chiffres"; } return null; }, ), const SizedBox(height: 16), CustomTextField( controller: _mobileController, label: "Téléphone mobile", keyboardType: TextInputType.phone, readOnly: widget.readOnly, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), ], validator: (value) { if (value != null && value.isNotEmpty && value.length < 10) { return "Le numéro doit contenir 10 chiffres"; } return null; }, ), ], const SizedBox(height: 16), // Ligne 3: Username et Password (si applicable) if (shouldShowUsernameField || shouldShowPasswordField) ...[ if (isWideScreen) Row( children: [ if (shouldShowUsernameField) Expanded( child: CustomTextField( controller: _usernameController, label: "Nom d'utilisateur", readOnly: !canEditUsername, prefixIcon: Icons.account_circle, isRequired: canEditUsername, suffixIcon: (widget.user?.id == 0 && canEditUsername) ? _isGeneratingUsername ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), ), ) : IconButton( icon: Icon(Icons.refresh), onPressed: _generateAndCheckUsername, tooltip: "Générer un nom d'utilisateur", ) : null, helperText: canEditUsername ? "8 à 64 caractères. Tous les caractères sont acceptés, y compris les espaces et accents." : null, helperMaxLines: 2, validator: canEditUsername ? (value) { if (value == null || value.isEmpty) { return "Veuillez entrer le nom d'utilisateur"; } // Vérifier la longueur minimale (NIST: 8 caractères) if (value.length < 8) { return "Le nom d'utilisateur doit contenir au moins 8 caractères"; } // Vérifier la longueur maximale (NIST: 64 caractères) if (value.length > 64) { return "Le nom d'utilisateur ne doit pas dépasser 64 caractères"; } // Pas de vérification sur le type de caractères (NIST: tous acceptés) return null; } : null, ), ), if (shouldShowUsernameField && shouldShowPasswordField) const SizedBox(width: 16), if (shouldShowPasswordField) Expanded( child: CustomTextField( controller: _passwordController, label: "Mot de passe", obscureText: _obscurePassword, readOnly: widget.readOnly, prefixIcon: Icons.lock, suffixIcon: Row( mainAxisSize: MainAxisSize.min, children: [ // Bouton pour afficher/masquer le mot de passe IconButton( icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off), onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, tooltip: _obscurePassword ? "Afficher le mot de passe" : "Masquer le mot de passe", ), // Bouton pour générer un mot de passe (seulement si éditable) if (!widget.readOnly) IconButton( icon: Icon(Icons.auto_awesome), onPressed: () { final newPassword = _generatePassword(); setState(() { _passwordController.text = newPassword; _obscurePassword = false; // Afficher le mot de passe généré }); // Revalider le formulaire _formKey.currentState?.validate(); }, tooltip: "Générer un mot de passe sécurisé", ), ], ), helperText: widget.user?.id != 0 ? "Laissez vide pour conserver le mot de passe actuel" : "8 à 64 caractères. Phrases de passe recommandées (ex: Mon chat Félix a 3 ans!)", helperMaxLines: 3, validator: _validatePassword, ), ), // Si seulement un des deux est affiché, ajouter un Expanded vide pour garder l'alignement if ((shouldShowUsernameField && !shouldShowPasswordField) || (!shouldShowUsernameField && shouldShowPasswordField)) const Expanded(child: SizedBox()), ], ) else ...[ // Version mobile: Username et Password séparés if (shouldShowUsernameField) ...[ CustomTextField( controller: _usernameController, label: "Nom d'utilisateur", readOnly: !canEditUsername, prefixIcon: Icons.account_circle, isRequired: canEditUsername, suffixIcon: (widget.user?.id == 0 && canEditUsername) ? _isGeneratingUsername ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), ), ) : IconButton( icon: Icon(Icons.refresh), onPressed: _generateAndCheckUsername, tooltip: "Générer un nom d'utilisateur", ) : null, helperText: canEditUsername ? "8 à 64 caractères. Tous les caractères sont acceptés, y compris les espaces et accents." : null, helperMaxLines: 2, validator: canEditUsername ? (value) { if (value == null || value.isEmpty) { return "Veuillez entrer le nom d'utilisateur"; } // Vérifier la longueur minimale (NIST: 8 caractères) if (value.length < 8) { return "Le nom d'utilisateur doit contenir au moins 8 caractères"; } // Vérifier la longueur maximale (NIST: 64 caractères) if (value.length > 64) { return "Le nom d'utilisateur ne doit pas dépasser 64 caractères"; } // Pas de vérification sur le type de caractères (NIST: tous acceptés) return null; } : null, ), const SizedBox(height: 16), ], if (shouldShowPasswordField) ...[ CustomTextField( controller: _passwordController, label: "Mot de passe", obscureText: _obscurePassword, readOnly: widget.readOnly, prefixIcon: Icons.lock, suffixIcon: Row( mainAxisSize: MainAxisSize.min, children: [ // Bouton pour afficher/masquer le mot de passe IconButton( icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off), onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, tooltip: _obscurePassword ? "Afficher le mot de passe" : "Masquer le mot de passe", ), // Bouton pour générer un mot de passe (seulement si éditable) if (!widget.readOnly) IconButton( icon: Icon(Icons.auto_awesome), onPressed: () { final newPassword = _generatePassword(); setState(() { _passwordController.text = newPassword; _obscurePassword = false; // Afficher le mot de passe généré }); // Revalider le formulaire _formKey.currentState?.validate(); }, tooltip: "Générer un mot de passe sécurisé", ), ], ), helperText: widget.user?.id != 0 ? "Laissez vide pour conserver le mot de passe actuel" : "8 à 64 caractères. Phrases de passe recommandées (ex: Mon chat Félix a 3 ans!)", helperMaxLines: 3, validator: _validatePassword, ), const SizedBox(height: 16), ], ], const SizedBox(height: 16), ], // Ligne 4: Dates (naissance et embauche) if (isWideScreen) Row( children: [ Expanded( child: CustomTextField( controller: _dateNaissanceController, label: "Date de naissance", readOnly: true, onTap: widget.readOnly ? null : () => _selectDate(context, true), suffixIcon: Icon( Icons.calendar_today, color: theme.colorScheme.primary, ), ), ), const SizedBox(width: 16), Expanded( child: CustomTextField( controller: _dateEmbaucheController, label: "Date d'embauche", readOnly: true, onTap: widget.readOnly ? null : () => _selectDate(context, false), suffixIcon: Icon( Icons.calendar_today, color: theme.colorScheme.primary, ), ), ), ], ) else ...[ // Version mobile: Dates séparées CustomTextField( controller: _dateNaissanceController, label: "Date de naissance", readOnly: true, onTap: widget.readOnly ? null : () => _selectDate(context, true), suffixIcon: Icon( Icons.calendar_today, color: theme.colorScheme.primary, ), ), const SizedBox(height: 16), CustomTextField( controller: _dateEmbaucheController, label: "Date d'embauche", readOnly: true, onTap: widget.readOnly ? null : () => _selectDate(context, false), suffixIcon: Icon( Icons.calendar_today, color: theme.colorScheme.primary, ), ), ], const SizedBox(height: 16), ], ), ); } Widget _buildRadioOption({ required int value, required String label, required int groupValue, required Function(int?)? onChanged, }) { final theme = Theme.of(context); final isSelected = value == groupValue; return Row( children: [ Radio( value: value, groupValue: groupValue, onChanged: onChanged, activeColor: const Color(0xFF20335E), ), Text( label, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), ], ); } } // Exporter la classe State pour pouvoir l'utiliser avec GlobalKey typedef UserFormState = _UserFormState;