- 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>
327 lines
12 KiB
Dart
Executable File
327 lines
12 KiB
Dart
Executable File
import 'package:flutter/material.dart';
|
|
import 'package:geosector_app/presentation/widgets/custom_text_field.dart';
|
|
import 'package:geosector_app/presentation/widgets/form_section.dart';
|
|
|
|
/// Exemple de validation avec CustomTextField
|
|
/// Montre comment les erreurs sont gérées automatiquement
|
|
class ValidationExample extends StatefulWidget {
|
|
const ValidationExample({super.key});
|
|
|
|
@override
|
|
State<ValidationExample> createState() => _ValidationExampleState();
|
|
}
|
|
|
|
class _ValidationExampleState extends State<ValidationExample> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
// Controllers
|
|
final _numeroController = TextEditingController();
|
|
final _rueController = TextEditingController();
|
|
final _villeController = TextEditingController();
|
|
final _nameController = TextEditingController();
|
|
final _emailController = TextEditingController();
|
|
final _montantController = TextEditingController();
|
|
|
|
// FocusNodes pour contrôler le focus
|
|
final _numeroFocus = FocusNode();
|
|
final _rueFocus = FocusNode();
|
|
final _villeFocus = FocusNode();
|
|
final _nameFocus = FocusNode();
|
|
final _emailFocus = FocusNode();
|
|
final _montantFocus = FocusNode();
|
|
|
|
@override
|
|
void dispose() {
|
|
_numeroController.dispose();
|
|
_rueController.dispose();
|
|
_villeController.dispose();
|
|
_nameController.dispose();
|
|
_emailController.dispose();
|
|
_montantController.dispose();
|
|
|
|
_numeroFocus.dispose();
|
|
_rueFocus.dispose();
|
|
_villeFocus.dispose();
|
|
_nameFocus.dispose();
|
|
_emailFocus.dispose();
|
|
_montantFocus.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
/// Validation du formulaire avec focus automatique sur erreur
|
|
void _validateForm() {
|
|
// Form.validate() fait automatiquement :
|
|
// 1. Valide tous les champs
|
|
// 2. Affiche les erreurs (bordures rouges)
|
|
// 3. Met le focus sur le PREMIER champ en erreur
|
|
if (_formKey.currentState!.validate()) {
|
|
// Formulaire valide
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Formulaire valide !'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
} else {
|
|
// Des erreurs existent - le focus est déjà mis automatiquement
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Veuillez corriger les erreurs'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Validation personnalisée pour email
|
|
String? _validateEmail(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null; // Email optionnel
|
|
}
|
|
|
|
const emailRegex = r'^[^@]+@[^@]+\.[^@]+$';
|
|
if (!RegExp(emailRegex).hasMatch(value)) {
|
|
return 'Format email invalide';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Validation pour montant
|
|
String? _validateMontant(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Le montant est obligatoire';
|
|
}
|
|
|
|
final montant = double.tryParse(value.replaceAll(',', '.'));
|
|
if (montant == null) {
|
|
return 'Montant invalide';
|
|
}
|
|
|
|
if (montant <= 0) {
|
|
return 'Le montant doit être supérieur à 0';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Exemple de Validation'),
|
|
),
|
|
body: Form( // ← Important : wrapper Form
|
|
key: _formKey,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
// Section Adresse
|
|
FormSection(
|
|
title: 'Adresse',
|
|
icon: Icons.location_on,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 1,
|
|
child: CustomTextField(
|
|
controller: _numeroController,
|
|
focusNode: _numeroFocus,
|
|
label: "Numéro",
|
|
isRequired: true,
|
|
showLabel: false,
|
|
textAlign: TextAlign.right,
|
|
keyboardType: TextInputType.number,
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Numéro obligatoire';
|
|
}
|
|
final numero = int.tryParse(value);
|
|
if (numero == null || numero <= 0) {
|
|
return 'Numéro invalide';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
const Expanded(
|
|
flex: 1,
|
|
child: CustomTextField(
|
|
label: "Bis/Ter",
|
|
showLabel: false,
|
|
// Pas de validation - champ optionnel
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
CustomTextField(
|
|
controller: _rueController,
|
|
focusNode: _rueFocus,
|
|
label: "Rue",
|
|
isRequired: true,
|
|
showLabel: false,
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'La rue est obligatoire';
|
|
}
|
|
if (value.trim().length < 3) {
|
|
return 'La rue doit contenir au moins 3 caractères';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
CustomTextField(
|
|
controller: _villeController,
|
|
focusNode: _villeFocus,
|
|
label: "Ville",
|
|
isRequired: true,
|
|
showLabel: false,
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'La ville est obligatoire';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Section Occupant
|
|
FormSection(
|
|
title: 'Occupant',
|
|
icon: Icons.person,
|
|
children: [
|
|
CustomTextField(
|
|
controller: _nameController,
|
|
focusNode: _nameFocus,
|
|
label: "Nom de l'occupant",
|
|
isRequired: true,
|
|
showLabel: false,
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Le nom est obligatoire pour les passages effectués';
|
|
}
|
|
if (value.trim().length < 2) {
|
|
return 'Le nom doit contenir au moins 2 caractères';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
CustomTextField(
|
|
controller: _emailController,
|
|
focusNode: _emailFocus,
|
|
label: "Email",
|
|
showLabel: false,
|
|
keyboardType: TextInputType.emailAddress,
|
|
validator: _validateEmail,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Section Règlement
|
|
FormSection(
|
|
title: 'Règlement',
|
|
icon: Icons.euro,
|
|
children: [
|
|
CustomTextField(
|
|
controller: _montantController,
|
|
focusNode: _montantFocus,
|
|
label: "Montant (€)",
|
|
isRequired: true,
|
|
showLabel: false,
|
|
hintText: "0.00",
|
|
textAlign: TextAlign.right,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
validator: _validateMontant,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 32),
|
|
|
|
// Boutons de test
|
|
Column(
|
|
children: [
|
|
ElevatedButton.icon(
|
|
onPressed: _validateForm,
|
|
icon: const Icon(Icons.check),
|
|
label: const Text('Valider le formulaire'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
foregroundColor: Colors.white,
|
|
minimumSize: const Size(200, 48),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
OutlinedButton.icon(
|
|
onPressed: () {
|
|
// Effacer le formulaire
|
|
_formKey.currentState?.reset();
|
|
_numeroController.clear();
|
|
_rueController.clear();
|
|
_villeController.clear();
|
|
_nameController.clear();
|
|
_emailController.clear();
|
|
_montantController.clear();
|
|
},
|
|
icon: const Icon(Icons.clear),
|
|
label: const Text('Effacer'),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Info box
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.info_outline,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Test de validation',
|
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'• Laissez des champs obligatoires vides et cliquez "Valider"\n'
|
|
'• Les bordures deviennent rouges automatiquement\n'
|
|
'• Le focus se met sur le premier champ en erreur\n'
|
|
'• Les messages d\'erreur s\'affichent sous les champs',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |