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 String _cleanString(String input) { return input.toLowerCase() .replaceAll(RegExp(r'[^a-z0-9]'), ''); // Garder seulement lettres et chiffres } // 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 l'algorithme spécifié 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 (uniquement ceux autorisés: ., _, -) 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 10 caractères while (username.length < 10) { username += _random.nextInt(10).toString(); } // S'assurer que l'username ne contient que des caractères autorisés (a-z, 0-9, ., -, _) // Normalement déjà le cas avec notre algorithme, mais au cas où username = username.toLowerCase().replaceAll(RegExp(r'[^a-z0-9._-]'), ''); 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 règles 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 } // Faire un trim pour retirer les espaces en début/fin final trimmedValue = value.trim(); if (trimmedValue.isEmpty) { return "Le mot de passe ne peut pas être vide"; } // Vérifier qu'il n'y a pas d'espaces dans le mot de passe if (trimmedValue.contains(' ')) { return "Le mot de passe ne doit pas contenir d'espaces"; } // Vérifier la longueur if (trimmedValue.length < 12) { return "Le mot de passe doit contenir au moins 12 caractères"; } if (trimmedValue.length > 16) { return "Le mot de passe ne doit pas dépasser 16 caractères"; } // Vérifier qu'il n'est pas égal au username (après trim des deux) if (trimmedValue == _usernameController.text.trim()) { return "Le mot de passe ne doit pas être identique au nom d'utilisateur"; } // Vérifier la présence d'au moins une minuscule if (!trimmedValue.contains(RegExp(r'[a-z]'))) { return "Le mot de passe doit contenir au moins une lettre minuscule"; } // Vérifier la présence d'au moins une majuscule if (!trimmedValue.contains(RegExp(r'[A-Z]'))) { return "Le mot de passe doit contenir au moins une lettre majuscule"; } // Vérifier la présence d'au moins un chiffre if (!trimmedValue.contains(RegExp(r'[0-9]'))) { return "Le mot de passe doit contenir au moins un chiffre"; } // Vérifier la présence d'au moins un caractère spécial if (!trimmedValue.contains(RegExp(r'[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]'))) { return "Le mot de passe doit contenir au moins un caractère spécial (!@#\$%^&*()_+-=[]{}|;:,.<>?)"; } return null; } // Générer un mot de passe aléatoire respectant les règles String _generatePassword() { const String lowercase = 'abcdefghijklmnopqrstuvwxyz'; const String uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const String digits = '0123456789'; const String special = '!@#\$%^&*()_+-=[]{}|;:,.<>?'; // Longueur aléatoire entre 12 et 16 final length = 12 + _random.nextInt(5); // S'assurer d'avoir au moins un caractère de chaque type List password = []; password.add(lowercase[_random.nextInt(lowercase.length)]); password.add(uppercase[_random.nextInt(uppercase.length)]); password.add(digits[_random.nextInt(digits.length)]); password.add(special[_random.nextInt(special.length)]); // Compléter avec des caractères aléatoires const String allChars = lowercase + uppercase + digits + special; for (int i = password.length; i < length; i++) { password.add(allChars[_random.nextInt(allChars.length)]); } // Mélanger les caractères password.shuffle(_random); return password.join(''); } // Méthode publique pour récupérer le mot de passe si défini String? getPassword() { final password = _passwordController.text.trim(); 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.trim(), // Appliquer trim 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.trim(), // Appliquer trim 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 ? "Min. 10 caractères (a-z, 0-9, . - _)" : null, validator: canEditUsername ? (value) { if (value == null || value.isEmpty) { return "Veuillez entrer le nom d'utilisateur"; } // Faire un trim pour retirer les espaces en début/fin final trimmedValue = value.trim(); if (trimmedValue.isEmpty) { return "Le nom d'utilisateur ne peut pas être vide"; } // Vérifier qu'il n'y a pas d'espaces dans l'username if (trimmedValue.contains(' ')) { return "Le nom d'utilisateur ne doit pas contenir d'espaces"; } // Vérifier la longueur minimale if (trimmedValue.length < 10) { return "Le nom d'utilisateur doit contenir au moins 10 caractères"; } // Vérifier les caractères autorisés (a-z, 0-9, ., -, _) if (!RegExp(r'^[a-z0-9._-]+$').hasMatch(trimmedValue)) { return "Caractères autorisés : lettres minuscules, chiffres, . - _"; } 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" : "12-16 car. avec min/maj, chiffres et spéciaux (!@#\$%^&*()_+-=[]{}|;:,.<>?)", 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 ? "Min. 10 caractères (a-z, 0-9, . - _)" : null, validator: canEditUsername ? (value) { if (value == null || value.isEmpty) { return "Veuillez entrer le nom d'utilisateur"; } // Faire un trim pour retirer les espaces en début/fin final trimmedValue = value.trim(); if (trimmedValue.isEmpty) { return "Le nom d'utilisateur ne peut pas être vide"; } // Vérifier qu'il n'y a pas d'espaces dans l'username if (trimmedValue.contains(' ')) { return "Le nom d'utilisateur ne doit pas contenir d'espaces"; } // Vérifier la longueur minimale if (trimmedValue.length < 10) { return "Le nom d'utilisateur doit contenir au moins 10 caractères"; } // Vérifier les caractères autorisés (a-z, 0-9, ., -, _) if (!RegExp(r'^[a-z0-9._-]+$').hasMatch(trimmedValue)) { return "Caractères autorisés : lettres minuscules, chiffres, . - _"; } 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" : "12-16 car. avec min/maj, chiffres et spéciaux (!@#\$%^&*()_+-=[]{}|;:,.<>?)", 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;