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/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/date_display_utils.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/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/nir_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/phone_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_am_photo_frame.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_detail_modal.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';
|
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) {
|
if (_adresseCtrl.text.trim().isEmpty) {
|
||||||
return 'L’adresse est requise.';
|
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.';
|
if (_villeCtrl.text.trim().isEmpty) return 'La ville est requise.';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -466,21 +469,24 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
|||||||
|
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'email': normalizeEmailText(_emailCtrl.text),
|
'email': normalizeEmailText(_emailCtrl.text),
|
||||||
'prenom': _prenomCtrl.text.trim(),
|
'prenom': formatPersonNameCase(_prenomCtrl.text),
|
||||||
'nom': _nomCtrl.text.trim(),
|
'nom': formatPersonNameCase(_nomCtrl.text),
|
||||||
'telephone': normalizePhone(_telCtrl.text),
|
'telephone': normalizePhone(_telCtrl.text),
|
||||||
'adresse':
|
'adresse':
|
||||||
_adresseCtrl.text.trim().isNotEmpty ? _adresseCtrl.text.trim() : null,
|
_adresseCtrl.text.trim().isNotEmpty ? _adresseCtrl.text.trim() : null,
|
||||||
'code_postal':
|
'code_postal':
|
||||||
_cpCtrl.text.trim().isNotEmpty ? _cpCtrl.text.trim() : null,
|
_cpCtrl.text.trim().isNotEmpty ? _cpCtrl.text.trim() : null,
|
||||||
'ville':
|
'ville': _villeCtrl.text.trim().isNotEmpty
|
||||||
_villeCtrl.text.trim().isNotEmpty ? _villeCtrl.text.trim() : null,
|
? formatPersonNameCase(_villeCtrl.text)
|
||||||
|
: null,
|
||||||
'photo_base64': photoBase64,
|
'photo_base64': photoBase64,
|
||||||
'photo_filename': fn.isNotEmpty ? fn : 'photo_am.jpg',
|
'photo_filename': fn.isNotEmpty ? fn : 'photo_am.jpg',
|
||||||
'consentement_photo': true,
|
'consentement_photo': true,
|
||||||
'date_naissance': birthIso,
|
'date_naissance': birthIso,
|
||||||
'lieu_naissance_ville': _lieuNaissanceVilleCtrl.text.trim(),
|
'lieu_naissance_ville':
|
||||||
'lieu_naissance_pays': _lieuNaissancePaysCtrl.text.trim(),
|
formatPersonNameCase(_lieuNaissanceVilleCtrl.text),
|
||||||
|
'lieu_naissance_pays':
|
||||||
|
formatPersonNameCase(_lieuNaissancePaysCtrl.text),
|
||||||
'nir': normalizeNir(_nirCtrl.text),
|
'nir': normalizeNir(_nirCtrl.text),
|
||||||
'numero_agrement': _agrementCtrl.text.trim(),
|
'numero_agrement': _agrementCtrl.text.trim(),
|
||||||
'date_agrement': agrementIso,
|
'date_agrement': agrementIso,
|
||||||
@@ -695,12 +701,14 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
|||||||
label: 'Ville de naissance',
|
label: 'Ville de naissance',
|
||||||
field: ValidationEditableField(
|
field: ValidationEditableField(
|
||||||
controller: _lieuNaissanceVilleCtrl,
|
controller: _lieuNaissanceVilleCtrl,
|
||||||
|
inputFormatters: const [PersonNameInputFormatter()],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Pays de naissance',
|
label: 'Pays de naissance',
|
||||||
field: ValidationEditableField(
|
field: ValidationEditableField(
|
||||||
controller: _lieuNaissancePaysCtrl,
|
controller: _lieuNaissancePaysCtrl,
|
||||||
|
inputFormatters: const [PersonNameInputFormatter()],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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';
|
import 'admin_detail_modal.dart';
|
||||||
|
|
||||||
/// Bloc type formulaire (titre de section + champs read-only) pour les modales de validation.
|
/// 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]).
|
/// Grille label/champ éditable (délègue à [ValidationFormGrid]).
|
||||||
class ValidationEditableSection extends StatelessWidget {
|
class ValidationEditableSection extends StatelessWidget {
|
||||||
final List<ValidationLabeledField> fields;
|
final List<ValidationLabeledField> fields;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||||
import 'package:p_tits_pas/models/user.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/utils/phone_utils.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.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: [
|
fields: [
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Nom',
|
label: 'Nom',
|
||||||
field: ValidationEditableField(controller: _nomCtrl!),
|
field: ValidationEditableField(
|
||||||
|
controller: _nomCtrl!,
|
||||||
|
inputFormatters: const [PersonNameInputFormatter()],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Prénom',
|
label: 'Prénom',
|
||||||
field: ValidationEditableField(controller: _prenomCtrl!),
|
field: ValidationEditableField(
|
||||||
|
controller: _prenomCtrl!,
|
||||||
|
inputFormatters: const [PersonNameInputFormatter()],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Téléphone',
|
label: 'Téléphone',
|
||||||
@@ -215,10 +222,7 @@ class IdentityBlock extends StatelessWidget { final String? title;
|
|||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
field: ValidationEditableField(
|
field: ValidationEmailField(controller: _emailCtrl!),
|
||||||
controller: _emailCtrl!,
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Adresse (N° et Rue)',
|
label: 'Adresse (N° et Rue)',
|
||||||
@@ -226,14 +230,14 @@ class IdentityBlock extends StatelessWidget { final String? title;
|
|||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Code postal',
|
label: 'Code postal',
|
||||||
field: ValidationEditableField(
|
field: ValidationPostalCodeField(controller: _cpCtrl!),
|
||||||
controller: _cpCtrl!,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Ville',
|
label: 'Ville',
|
||||||
field: ValidationEditableField(controller: _villeCtrl!),
|
field: ValidationEditableField(
|
||||||
|
controller: _villeCtrl!,
|
||||||
|
inputFormatters: const [PersonNameInputFormatter()],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user