Files
geo/app/lib/presentation/widgets/user_form.dart
pierre 1018b86537 feat: Gestion des secteurs et migration v3.0.4+304
- Ajout système complet de gestion des secteurs avec contours géographiques
- Import des contours départementaux depuis GeoJSON
- API REST pour la gestion des secteurs (/api/sectors)
- Service de géolocalisation pour déterminer les secteurs
- Migration base de données avec tables x_departements_contours et sectors_adresses
- Interface Flutter pour visualisation et gestion des secteurs
- Ajout thème sombre dans l'application
- Corrections diverses et optimisations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 11:01:45 +02:00

555 lines
20 KiB
Dart
Executable File

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:geosector_app/core/data/models/user_model.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;
const UserForm({
super.key,
this.user,
this.onSubmit,
this.readOnly = false,
this.allowUsernameEdit = false,
this.allowSectNameEdit = false,
});
@override
State<UserForm> createState() => _UserFormState();
}
class _UserFormState extends State<UserForm> {
final _formKey = GlobalKey<FormState>();
// 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;
// Form values
int _fkTitre = 1; // 1 = M., 2 = Mme
DateTime? _dateNaissance;
DateTime? _dateEmbauche;
@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!) : '');
_fkTitre = user?.fkTitre ?? 1;
}
@override
void dispose() {
_usernameController.dispose();
_firstNameController.dispose();
_nameController.dispose();
_sectNameController.dispose();
_phoneController.dispose();
_mobileController.dispose();
_emailController.dispose();
_dateNaissanceController.dispose();
_dateEmbaucheController.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 {
// Afficher le sélecteur de date sans spécifier de locale
showDatePicker(
context: context,
initialDate: DateTime.now(), // Toujours utiliser la date actuelle
firstDate: DateTime(1900),
lastDate: DateTime.now(),
// Ne pas spécifier de locale pour éviter les problèmes
).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,
),
);
}
}
// 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,
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,
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;
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;
},
),
],
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 3: 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 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<int>(
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;