feat: Livraison version 3.0.6
- Amélioration de la gestion des entités et des utilisateurs - Mise à jour des modèles Amicale et Client avec champs supplémentaires - Ajout du service de logging et amélioration du chargement UI - Refactoring des formulaires utilisateur et amicale - Intégration de file_picker et image_picker pour la gestion des fichiers - Amélioration de la gestion des membres et de leur suppression - Optimisation des performances de l'API - Mise à jour de la documentation technique 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
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 {
|
||||
@@ -10,6 +13,8 @@ class UserForm extends StatefulWidget {
|
||||
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,
|
||||
@@ -18,6 +23,8 @@ class UserForm extends StatefulWidget {
|
||||
this.readOnly = false,
|
||||
this.allowUsernameEdit = false,
|
||||
this.allowSectNameEdit = false,
|
||||
this.amicale,
|
||||
this.isAdmin = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -37,11 +44,19 @@ class _UserFormState extends State<UserForm> {
|
||||
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() {
|
||||
@@ -64,11 +79,34 @@ class _UserFormState extends State<UserForm> {
|
||||
|
||||
_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();
|
||||
@@ -78,6 +116,7 @@ class _UserFormState extends State<UserForm> {
|
||||
_emailController.dispose();
|
||||
_dateNaissanceController.dispose();
|
||||
_dateEmbaucheController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -98,13 +137,49 @@ class _UserFormState extends State<UserForm> {
|
||||
void _selectDate(BuildContext context, bool isDateNaissance) {
|
||||
// Utiliser un bloc try-catch pour capturer toutes les erreurs possibles
|
||||
try {
|
||||
// Afficher le sélecteur de date sans spécifier de locale
|
||||
// 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: DateTime.now(), // Toujours utiliser la date actuelle
|
||||
initialDate: initialDate,
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime.now(),
|
||||
// Ne pas spécifier de locale pour éviter les problèmes
|
||||
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) {
|
||||
@@ -141,30 +216,247 @@ class _UserFormState extends State<UserForm> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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<Map<String, dynamic>> _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<void> _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<String> 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,
|
||||
firstName: _firstNameController.text,
|
||||
name: _nameController.text,
|
||||
sectName: _sectNameController.text,
|
||||
phone: _phoneController.text,
|
||||
mobile: _mobileController.text,
|
||||
email: _emailController.text,
|
||||
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,
|
||||
firstName: _firstNameController.text,
|
||||
name: _nameController.text,
|
||||
sectName: _sectNameController.text,
|
||||
phone: _phoneController.text,
|
||||
mobile: _mobileController.text,
|
||||
email: _emailController.text,
|
||||
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,
|
||||
@@ -180,90 +472,36 @@ class _UserFormState extends State<UserForm> {
|
||||
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: [
|
||||
// Ligne 1: Username et Email (si écran large)
|
||||
if (isWideScreen)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _usernameController,
|
||||
label: "Nom d'utilisateur",
|
||||
readOnly: !widget.allowUsernameEdit, // Utiliser le paramètre
|
||||
prefixIcon: Icons.account_circle,
|
||||
isRequired: widget.allowUsernameEdit,
|
||||
validator: widget.allowUsernameEdit
|
||||
? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer le nom d'utilisateur";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _emailController,
|
||||
label: "Email",
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
readOnly: widget.readOnly,
|
||||
isRequired: true, // Email toujours obligatoire
|
||||
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;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
// Version mobile: Username seul
|
||||
CustomTextField(
|
||||
controller: _usernameController,
|
||||
label: "Nom d'utilisateur",
|
||||
readOnly: !widget.allowUsernameEdit, // Utiliser le paramètre
|
||||
prefixIcon: Icons.account_circle,
|
||||
isRequired: widget.allowUsernameEdit, // Obligatoire si éditable
|
||||
validator: widget.allowUsernameEdit
|
||||
? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer le nom d'utilisateur";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Email seul en mobile
|
||||
CustomTextField(
|
||||
controller: _emailController,
|
||||
label: "Email",
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
readOnly: widget.readOnly,
|
||||
isRequired: true, // Email toujours obligatoire
|
||||
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;
|
||||
},
|
||||
),
|
||||
],
|
||||
// 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)
|
||||
@@ -378,7 +616,7 @@ class _UserFormState extends State<UserForm> {
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Ligne 3: Téléphones (fixe et mobile)
|
||||
// Ligne 2: Téléphones (fixe et mobile)
|
||||
if (isWideScreen)
|
||||
Row(
|
||||
children: [
|
||||
@@ -458,6 +696,224 @@ class _UserFormState extends State<UserForm> {
|
||||
),
|
||||
],
|
||||
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<Color>(
|
||||
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<Color>(
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user