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>
This commit is contained in:
327
app/lib/presentation/widgets/validation_example.dart
Executable file
327
app/lib/presentation/widgets/validation_example.dart
Executable file
@@ -0,0 +1,327 @@
|
||||
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',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user