fix(#156): superviser nom, e-mail et CP comme à l’inscription.
IdentityBlock editable : capitalisation live, e-mail/CP normalisés et validés à la perte de focus (aligné création de compte). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,8 +10,10 @@ import 'package:p_tits_pas/services/api/api_config.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/utils/date_display_utils.dart';
|
||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||
import 'package:p_tits_pas/utils/nir_utils.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/utils/postal_utils.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/admin_detail_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
||||
@@ -361,7 +363,8 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
if (_adresseCtrl.text.trim().isEmpty) {
|
||||
return 'L’adresse est requise.';
|
||||
}
|
||||
if (_cpCtrl.text.trim().isEmpty) return 'Le code postal est requis.';
|
||||
final cpErr = validateFrenchPostalCode(_cpCtrl.text);
|
||||
if (cpErr != null) return cpErr;
|
||||
if (_villeCtrl.text.trim().isEmpty) return 'La ville est requise.';
|
||||
return null;
|
||||
}
|
||||
@@ -466,21 +469,24 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
|
||||
return <String, dynamic>{
|
||||
'email': normalizeEmailText(_emailCtrl.text),
|
||||
'prenom': _prenomCtrl.text.trim(),
|
||||
'nom': _nomCtrl.text.trim(),
|
||||
'prenom': formatPersonNameCase(_prenomCtrl.text),
|
||||
'nom': formatPersonNameCase(_nomCtrl.text),
|
||||
'telephone': normalizePhone(_telCtrl.text),
|
||||
'adresse':
|
||||
_adresseCtrl.text.trim().isNotEmpty ? _adresseCtrl.text.trim() : null,
|
||||
'code_postal':
|
||||
_cpCtrl.text.trim().isNotEmpty ? _cpCtrl.text.trim() : null,
|
||||
'ville':
|
||||
_villeCtrl.text.trim().isNotEmpty ? _villeCtrl.text.trim() : null,
|
||||
'ville': _villeCtrl.text.trim().isNotEmpty
|
||||
? formatPersonNameCase(_villeCtrl.text)
|
||||
: null,
|
||||
'photo_base64': photoBase64,
|
||||
'photo_filename': fn.isNotEmpty ? fn : 'photo_am.jpg',
|
||||
'consentement_photo': true,
|
||||
'date_naissance': birthIso,
|
||||
'lieu_naissance_ville': _lieuNaissanceVilleCtrl.text.trim(),
|
||||
'lieu_naissance_pays': _lieuNaissancePaysCtrl.text.trim(),
|
||||
'lieu_naissance_ville':
|
||||
formatPersonNameCase(_lieuNaissanceVilleCtrl.text),
|
||||
'lieu_naissance_pays':
|
||||
formatPersonNameCase(_lieuNaissancePaysCtrl.text),
|
||||
'nir': normalizeNir(_nirCtrl.text),
|
||||
'numero_agrement': _agrementCtrl.text.trim(),
|
||||
'date_agrement': agrementIso,
|
||||
@@ -695,12 +701,14 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
||||
label: 'Ville de naissance',
|
||||
field: ValidationEditableField(
|
||||
controller: _lieuNaissanceVilleCtrl,
|
||||
inputFormatters: const [PersonNameInputFormatter()],
|
||||
),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Pays de naissance',
|
||||
field: ValidationEditableField(
|
||||
controller: _lieuNaissancePaysCtrl,
|
||||
inputFormatters: const [PersonNameInputFormatter()],
|
||||
),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||
import 'package:p_tits_pas/utils/postal_utils.dart';
|
||||
import 'admin_detail_modal.dart';
|
||||
|
||||
/// Bloc type formulaire (titre de section + champs read-only) pour les modales de validation.
|
||||
@@ -295,6 +297,158 @@ class ValidationEditableField extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// E-mail style validation — même supervision que login / création de compte :
|
||||
/// [EmailMaxLengthFormatter] + à la perte de focus : trim/minuscules + validation.
|
||||
class ValidationEmailField extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final String? hintText;
|
||||
final bool allowEmpty;
|
||||
|
||||
const ValidationEmailField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.hintText,
|
||||
this.allowEmpty = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ValidationEmailField> createState() => _ValidationEmailFieldState();
|
||||
}
|
||||
|
||||
class _ValidationEmailFieldState extends State<ValidationEmailField> {
|
||||
final GlobalKey<FormFieldState<String>> _fieldKey =
|
||||
GlobalKey<FormFieldState<String>>();
|
||||
late final FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = FocusNode();
|
||||
_focusNode.addListener(_onFocusChange);
|
||||
}
|
||||
|
||||
void _onFocusChange() {
|
||||
if (_focusNode.hasFocus) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _focusNode.hasFocus) return;
|
||||
final c = widget.controller;
|
||||
final normalized = normalizeEmailText(c.text);
|
||||
if (normalized != c.text) {
|
||||
c.value = TextEditingValue(
|
||||
text: normalized,
|
||||
selection: TextSelection.collapsed(offset: normalized.length),
|
||||
);
|
||||
}
|
||||
_fieldKey.currentState?.validate();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_onFocusChange);
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
key: _fieldKey,
|
||||
controller: widget.controller,
|
||||
focusNode: _focusNode,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
textInputAction: TextInputAction.next,
|
||||
inputFormatters: const [EmailMaxLengthFormatter()],
|
||||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
||||
decoration: ValidationFieldDecoration.input(hint: widget.hintText).copyWith(
|
||||
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
||||
errorMaxLines: 2,
|
||||
),
|
||||
validator: (value) =>
|
||||
validateEmail(value, allowEmpty: widget.allowEmpty),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Code postal FR — même supervision que création de compte :
|
||||
/// chiffres uniquement (max 5) + validation à la perte de focus.
|
||||
class ValidationPostalCodeField extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final String? hintText;
|
||||
final bool allowEmpty;
|
||||
|
||||
const ValidationPostalCodeField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.hintText,
|
||||
this.allowEmpty = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ValidationPostalCodeField> createState() =>
|
||||
_ValidationPostalCodeFieldState();
|
||||
}
|
||||
|
||||
class _ValidationPostalCodeFieldState extends State<ValidationPostalCodeField> {
|
||||
final GlobalKey<FormFieldState<String>> _fieldKey =
|
||||
GlobalKey<FormFieldState<String>>();
|
||||
late final FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = FocusNode();
|
||||
_focusNode.addListener(_onFocusChange);
|
||||
}
|
||||
|
||||
void _onFocusChange() {
|
||||
if (_focusNode.hasFocus) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _focusNode.hasFocus) return;
|
||||
final c = widget.controller;
|
||||
final trimmed = c.text.trim();
|
||||
if (trimmed != c.text) {
|
||||
c.value = TextEditingValue(
|
||||
text: trimmed,
|
||||
selection: TextSelection.collapsed(offset: trimmed.length),
|
||||
);
|
||||
}
|
||||
_fieldKey.currentState?.validate();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_onFocusChange);
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
key: _fieldKey,
|
||||
controller: widget.controller,
|
||||
focusNode: _focusNode,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
inputFormatters: kFrenchPostalCodeInputFormatters,
|
||||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
||||
decoration: ValidationFieldDecoration.input(
|
||||
hint: widget.hintText ?? '5 chiffres',
|
||||
).copyWith(
|
||||
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
||||
errorMaxLines: 2,
|
||||
),
|
||||
validator: (value) =>
|
||||
validateFrenchPostalCode(value, allowEmpty: widget.allowEmpty),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Grille label/champ éditable (délègue à [ValidationFormGrid]).
|
||||
class ValidationEditableSection extends StatelessWidget {
|
||||
final List<ValidationLabeledField> fields;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
||||
|
||||
@@ -199,11 +200,17 @@ class IdentityBlock extends StatelessWidget { final String? title;
|
||||
fields: [
|
||||
ValidationLabeledField(
|
||||
label: 'Nom',
|
||||
field: ValidationEditableField(controller: _nomCtrl!),
|
||||
field: ValidationEditableField(
|
||||
controller: _nomCtrl!,
|
||||
inputFormatters: const [PersonNameInputFormatter()],
|
||||
),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Prénom',
|
||||
field: ValidationEditableField(controller: _prenomCtrl!),
|
||||
field: ValidationEditableField(
|
||||
controller: _prenomCtrl!,
|
||||
inputFormatters: const [PersonNameInputFormatter()],
|
||||
),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Téléphone',
|
||||
@@ -215,10 +222,7 @@ class IdentityBlock extends StatelessWidget { final String? title;
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Email',
|
||||
field: ValidationEditableField(
|
||||
controller: _emailCtrl!,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
field: ValidationEmailField(controller: _emailCtrl!),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Adresse (N° et Rue)',
|
||||
@@ -226,14 +230,14 @@ class IdentityBlock extends StatelessWidget { final String? title;
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Code postal',
|
||||
field: ValidationEditableField(
|
||||
controller: _cpCtrl!,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
field: ValidationPostalCodeField(controller: _cpCtrl!),
|
||||
),
|
||||
ValidationLabeledField(
|
||||
label: 'Ville',
|
||||
field: ValidationEditableField(controller: _villeCtrl!),
|
||||
field: ValidationEditableField(
|
||||
controller: _villeCtrl!,
|
||||
inputFormatters: const [PersonNameInputFormatter()],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user