[#101] [Frontend] Inscription parent — API, soumission et validation
Squash merge de develop vers master. Livrables principaux (ticket #101 et mise au point associée) : - Branchement du formulaire d'inscription parent sur POST /api/v1/auth/register/parent - Payload DTO (parents, enfants, photos base64, CGU) et services Auth - Parcours gestionnaire : cartes dossiers, wizard validation famille, images authentifiées - Scripts d'inscription test (Martin, Durand/Rousseau, Lecomte) ; .gitignore .cursor/ Inclut également les ajustements develop fusionnés dans ce lot (inscription AM, champs relais, etc.). Closes #101 Made-with: Cursor
This commit is contained in:
@@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||
import 'package:p_tits_pas/utils/postal_utils.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
@@ -76,6 +79,19 @@ class PersonalInfoFormScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
/// Ordre de tabulation explicite (étape 1 / parent 2, etc.)
|
||||
static const double _focusSecondPersonToggle = 1;
|
||||
static const double _focusSameAddressToggle = 2;
|
||||
static const double _focusLastName = 10;
|
||||
static const double _focusFirstName = 11;
|
||||
static const double _focusPhone = 12;
|
||||
static const double _focusEmail = 13;
|
||||
static const double _focusAddress = 14;
|
||||
static const double _focusPostal = 15;
|
||||
static const double _focusCity = 16;
|
||||
static const double _focusNavPrevious = 100;
|
||||
static const double _focusNavNext = 101;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _lastNameController;
|
||||
late TextEditingController _firstNameController;
|
||||
@@ -89,13 +105,22 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
bool _sameAddress = false;
|
||||
bool _fieldsEnabled = true;
|
||||
|
||||
FocusNode? _lastNameFocus;
|
||||
FocusNode? _firstNameFocus;
|
||||
FocusNode? _cityFocus;
|
||||
FocusNode? _emailFocus;
|
||||
final GlobalKey<FormFieldState<String>> _emailFormKey =
|
||||
GlobalKey<FormFieldState<String>>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_lastNameController = TextEditingController(text: widget.initialData.lastName);
|
||||
_firstNameController = TextEditingController(text: widget.initialData.firstName);
|
||||
_phoneController = TextEditingController(text: formatPhoneForDisplay(widget.initialData.phone));
|
||||
_emailController = TextEditingController(text: widget.initialData.email);
|
||||
_emailController = TextEditingController(
|
||||
text: normalizeEmailText(widget.initialData.email),
|
||||
);
|
||||
_addressController = TextEditingController(text: widget.initialData.address);
|
||||
_postalCodeController = TextEditingController(text: widget.initialData.postalCode);
|
||||
_cityController = TextEditingController(text: widget.initialData.city);
|
||||
@@ -109,10 +134,147 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
_sameAddress = widget.initialSameAddress ?? false;
|
||||
_updateAddressFields();
|
||||
}
|
||||
|
||||
if (widget.mode == DisplayMode.editable) {
|
||||
_lastNameFocus = FocusNode();
|
||||
_firstNameFocus = FocusNode();
|
||||
_cityFocus = FocusNode();
|
||||
_emailFocus = FocusNode();
|
||||
_lastNameFocus!.addListener(_onLastNameFocusChange);
|
||||
_firstNameFocus!.addListener(_onFirstNameFocusChange);
|
||||
_cityFocus!.addListener(_onCityFocusChange);
|
||||
_emailFocus!.addListener(_onEmailFocusChange);
|
||||
}
|
||||
}
|
||||
|
||||
void _onLastNameFocusChange() {
|
||||
if (_lastNameFocus == null || _lastNameFocus!.hasFocus) {
|
||||
return;
|
||||
}
|
||||
_applyPersonNameFormat(_lastNameController);
|
||||
}
|
||||
|
||||
void _onFirstNameFocusChange() {
|
||||
if (_firstNameFocus == null || _firstNameFocus!.hasFocus) {
|
||||
return;
|
||||
}
|
||||
_applyPersonNameFormat(_firstNameController);
|
||||
}
|
||||
|
||||
void _onCityFocusChange() {
|
||||
if (_cityFocus == null || _cityFocus!.hasFocus) {
|
||||
return;
|
||||
}
|
||||
_applyPersonNameFormat(_cityController);
|
||||
}
|
||||
|
||||
void _onEmailFocusChange() {
|
||||
if (_emailFocus == null || _emailFocus!.hasFocus) {
|
||||
return;
|
||||
}
|
||||
if (widget.showSecondPersonToggle && !_fieldsEnabled) {
|
||||
return;
|
||||
}
|
||||
// Reporter normalisation + validate au frame suivant pour ne pas casser Tab.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _emailFocus == null || _emailFocus!.hasFocus) {
|
||||
return;
|
||||
}
|
||||
if (widget.showSecondPersonToggle && !_fieldsEnabled) {
|
||||
return;
|
||||
}
|
||||
final normalized = normalizeEmailText(_emailController.text);
|
||||
if (normalized != _emailController.text) {
|
||||
_emailController.value = TextEditingValue(
|
||||
text: normalized,
|
||||
selection: TextSelection.collapsed(offset: normalized.length),
|
||||
);
|
||||
}
|
||||
// Pas d’erreur « obligatoire » au blur si le champ est encore vide :
|
||||
// évite un message dès l’activation du parent 2 (focus / rebuild) et
|
||||
// la soumission du formulaire valide toujours.
|
||||
if (normalizeEmailText(_emailController.text).isEmpty) {
|
||||
return;
|
||||
}
|
||||
_emailFormKey.currentState?.validate();
|
||||
});
|
||||
}
|
||||
|
||||
String? _validateRequiredFrenchPhone(String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Ce champ est obligatoire';
|
||||
}
|
||||
return validateFrenchNationalPhone(value, allowEmpty: false);
|
||||
}
|
||||
|
||||
String? _validateRequiredEmail(String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Ce champ est obligatoire.';
|
||||
}
|
||||
return validateEmail(value, allowEmpty: true);
|
||||
}
|
||||
|
||||
/// Téléphone / e-mail du parent 2 : pas de validation si « Ajouter Parent 2 » est désactivé.
|
||||
String? _validateFrenchPhoneIfSecondParentNeeded(String? value) {
|
||||
if (widget.showSecondPersonToggle && !_fieldsEnabled) {
|
||||
return null;
|
||||
}
|
||||
return _validateRequiredFrenchPhone(value);
|
||||
}
|
||||
|
||||
String? _validateEmailIfSecondParentNeeded(String? value) {
|
||||
if (widget.showSecondPersonToggle && !_fieldsEnabled) {
|
||||
return null;
|
||||
}
|
||||
return _validateRequiredEmail(value);
|
||||
}
|
||||
|
||||
/// Adresse / code postal / ville : pas de contrôle si « Même adresse » (valeurs prises du parent 1).
|
||||
String? _validateAddressLineIfManualEntry(String? value) {
|
||||
if (!_fieldsEnabled) {
|
||||
return null;
|
||||
}
|
||||
if (widget.showSameAddressCheckbox && _sameAddress) {
|
||||
return null;
|
||||
}
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Ce champ est obligatoire';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Code postal : 5 chiffres ; ignoré si parent 2 désactivé ou « Même adresse ».
|
||||
String? _validatePostalCodeField(String? value) {
|
||||
if (!_fieldsEnabled) {
|
||||
return null;
|
||||
}
|
||||
if (widget.showSameAddressCheckbox && _sameAddress) {
|
||||
return null;
|
||||
}
|
||||
return validateFrenchPostalCode(value, allowEmpty: false);
|
||||
}
|
||||
|
||||
void _applyPersonNameFormat(TextEditingController controller) {
|
||||
final formatted = formatPersonNameCase(controller.text);
|
||||
if (formatted == controller.text) {
|
||||
return;
|
||||
}
|
||||
controller.value = TextEditingValue(
|
||||
text: formatted,
|
||||
selection: TextSelection.collapsed(offset: formatted.length),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_lastNameFocus?.removeListener(_onLastNameFocusChange);
|
||||
_firstNameFocus?.removeListener(_onFirstNameFocusChange);
|
||||
_cityFocus?.removeListener(_onCityFocusChange);
|
||||
_emailFocus?.removeListener(_onEmailFocusChange);
|
||||
_lastNameFocus?.dispose();
|
||||
_firstNameFocus?.dispose();
|
||||
_cityFocus?.dispose();
|
||||
_emailFocus?.dispose();
|
||||
_lastNameController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
@@ -131,13 +293,50 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _wrapScrollBodyIfEditable({
|
||||
required DisplayConfig config,
|
||||
required Widget child,
|
||||
}) {
|
||||
if (!config.isEditable) return child;
|
||||
return FocusTraversalGroup(
|
||||
policy: OrderedTraversalPolicy(),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _orderedFocus({
|
||||
required bool enabled,
|
||||
required double order,
|
||||
required Widget child,
|
||||
}) {
|
||||
if (!enabled) return child;
|
||||
return FocusTraversalOrder(
|
||||
order: NumericFocusOrder(order),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSubmit() {
|
||||
if (widget.mode == DisplayMode.editable) {
|
||||
_applyPersonNameFormat(_lastNameController);
|
||||
_applyPersonNameFormat(_firstNameController);
|
||||
_applyPersonNameFormat(_cityController);
|
||||
}
|
||||
// Parent 2 désactivé : pas de contrôle sur les champs (désactivés à l’écran).
|
||||
if (widget.showSecondPersonToggle && !_hasSecondPerson) {
|
||||
widget.onSubmit(
|
||||
PersonalInfoData(),
|
||||
hasSecondPerson: false,
|
||||
sameAddress: widget.showSameAddressCheckbox ? _sameAddress : null,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (widget.mode == DisplayMode.readonly || _formKey.currentState!.validate()) {
|
||||
final data = PersonalInfoData(
|
||||
firstName: _firstNameController.text,
|
||||
lastName: _lastNameController.text,
|
||||
phone: normalizePhone(_phoneController.text),
|
||||
email: _emailController.text,
|
||||
email: normalizeEmailText(_emailController.text),
|
||||
address: _addressController.text,
|
||||
postalCode: _postalCodeController.text,
|
||||
city: _cityController.text,
|
||||
@@ -169,75 +368,84 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.stepText,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: config.isMobile ? 13 : 16,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
SizedBox(height: config.isMobile ? 6 : 10),
|
||||
Text(
|
||||
widget.title,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: config.isMobile ? 18 : 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: config.isMobile ? 16 : 30),
|
||||
_buildCard(context, config, screenSize),
|
||||
|
||||
// Boutons mobile sous la carte (dans le scroll)
|
||||
if (config.isMobile) ...[
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: screenSize.width * 0.05, // Même marge que la carte (0.9 = 0.05 de chaque côté)
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: HoverReliefWidget(
|
||||
child: CustomNavigationButton(
|
||||
text: 'Précédent',
|
||||
style: NavigationButtonStyle.purple,
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
context.pop();
|
||||
} else {
|
||||
context.go(widget.previousRoute);
|
||||
}
|
||||
},
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16), // Écart entre les boutons
|
||||
Expanded(
|
||||
child: HoverReliefWidget(
|
||||
child: CustomNavigationButton(
|
||||
text: 'Suivant',
|
||||
style: NavigationButtonStyle.green,
|
||||
onPressed: _handleSubmit,
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: _wrapScrollBodyIfEditable(
|
||||
config: config,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.stepText,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: config.isMobile ? 13 : 16,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(height: config.isMobile ? 6 : 10),
|
||||
Text(
|
||||
widget.title,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: config.isMobile ? 18 : 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: config.isMobile ? 16 : 30),
|
||||
_buildCard(context, config, screenSize),
|
||||
if (config.isMobile) ...[
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: screenSize.width * 0.05,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _orderedFocus(
|
||||
enabled: config.isEditable,
|
||||
order: _focusNavPrevious,
|
||||
child: HoverReliefWidget(
|
||||
child: CustomNavigationButton(
|
||||
text: 'Précédent',
|
||||
style: NavigationButtonStyle.purple,
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
context.pop();
|
||||
} else {
|
||||
context.go(widget.previousRoute);
|
||||
}
|
||||
},
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _orderedFocus(
|
||||
enabled: config.isEditable,
|
||||
order: _focusNavNext,
|
||||
child: HoverReliefWidget(
|
||||
child: CustomNavigationButton(
|
||||
text: 'Suivant',
|
||||
style: NavigationButtonStyle.green,
|
||||
onPressed: _handleSubmit,
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -522,17 +730,34 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Transform.scale(
|
||||
scale: config.isMobile ? 0.85 : 1.0,
|
||||
child: Switch(
|
||||
value: _hasSecondPerson,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_hasSecondPerson = value;
|
||||
_fieldsEnabled = value;
|
||||
});
|
||||
},
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
_orderedFocus(
|
||||
enabled: config.isEditable,
|
||||
order: _focusSecondPersonToggle,
|
||||
child: Transform.scale(
|
||||
scale: config.isMobile ? 0.85 : 1.0,
|
||||
child: Switch(
|
||||
value: _hasSecondPerson,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_hasSecondPerson = value;
|
||||
_fieldsEnabled = value;
|
||||
if (value && _sameAddress) {
|
||||
_updateAddressFields();
|
||||
}
|
||||
});
|
||||
if (!value) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Si l’utilisateur a déjà recoché Parent 2, ne pas valider :
|
||||
// sinon des champs vides affichent une erreur tout de suite.
|
||||
if (!mounted || _hasSecondPerson) {
|
||||
return;
|
||||
}
|
||||
_formKey.currentState?.validate();
|
||||
});
|
||||
}
|
||||
},
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -558,17 +783,21 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Transform.scale(
|
||||
scale: config.isMobile ? 0.85 : 1.0,
|
||||
child: Switch(
|
||||
value: _sameAddress,
|
||||
onChanged: _fieldsEnabled ? (value) {
|
||||
setState(() {
|
||||
_sameAddress = value;
|
||||
_updateAddressFields();
|
||||
});
|
||||
} : null,
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
_orderedFocus(
|
||||
enabled: config.isEditable,
|
||||
order: _focusSameAddressToggle,
|
||||
child: Transform.scale(
|
||||
scale: config.isMobile ? 0.85 : 1.0,
|
||||
child: Switch(
|
||||
value: _sameAddress,
|
||||
onChanged: _fieldsEnabled ? (value) {
|
||||
setState(() {
|
||||
_sameAddress = value;
|
||||
_updateAddressFields();
|
||||
});
|
||||
} : null,
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -606,6 +835,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
controller: _lastNameController,
|
||||
hint: 'Votre nom de famille',
|
||||
enabled: _fieldsEnabled,
|
||||
focusOrder: _focusLastName,
|
||||
focusNode: _lastNameFocus,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
@@ -616,6 +847,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
controller: _firstNameController,
|
||||
hint: 'Votre prénom',
|
||||
enabled: _fieldsEnabled,
|
||||
focusOrder: _focusFirstName,
|
||||
focusNode: _firstNameFocus,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -633,7 +866,9 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
hint: 'Votre numéro de téléphone',
|
||||
keyboardType: TextInputType.phone,
|
||||
enabled: _fieldsEnabled,
|
||||
inputFormatters: _phoneInputFormatters,
|
||||
inputFormatters: frenchPhoneInputFormatters,
|
||||
focusOrder: _focusPhone,
|
||||
fieldValidator: _validateFrenchPhoneIfSecondParentNeeded,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
@@ -645,6 +880,11 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
hint: 'Votre adresse e-mail',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
enabled: _fieldsEnabled,
|
||||
focusOrder: _focusEmail,
|
||||
fieldValidator: _validateEmailIfSecondParentNeeded,
|
||||
inputFormatters: const [EmailMaxLengthFormatter()],
|
||||
focusNode: _emailFocus,
|
||||
formFieldKey: _emailFormKey,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -658,6 +898,10 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
controller: _addressController,
|
||||
hint: 'Numéro et nom de votre rue',
|
||||
enabled: _fieldsEnabled && !_sameAddress,
|
||||
focusOrder: _focusAddress,
|
||||
fieldValidator: widget.showSameAddressCheckbox
|
||||
? _validateAddressLineIfManualEntry
|
||||
: null,
|
||||
),
|
||||
SizedBox(height: verticalSpacing),
|
||||
|
||||
@@ -670,9 +914,12 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
config: config,
|
||||
label: 'Code Postal',
|
||||
controller: _postalCodeController,
|
||||
hint: 'Code postal',
|
||||
hint: '5 chiffres',
|
||||
keyboardType: TextInputType.number,
|
||||
enabled: _fieldsEnabled && !_sameAddress,
|
||||
focusOrder: _focusPostal,
|
||||
fieldValidator: _validatePostalCodeField,
|
||||
inputFormatters: kFrenchPostalCodeInputFormatters,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
@@ -684,6 +931,11 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
controller: _cityController,
|
||||
hint: 'Votre ville',
|
||||
enabled: _fieldsEnabled && !_sameAddress,
|
||||
focusOrder: _focusCity,
|
||||
focusNode: _cityFocus,
|
||||
fieldValidator: widget.showSameAddressCheckbox
|
||||
? _validateAddressLineIfManualEntry
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -852,6 +1104,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
controller: _lastNameController,
|
||||
hint: 'Votre nom de famille',
|
||||
enabled: _fieldsEnabled,
|
||||
focusOrder: _focusLastName,
|
||||
focusNode: _lastNameFocus,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -862,6 +1116,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
controller: _firstNameController,
|
||||
hint: 'Votre prénom',
|
||||
enabled: _fieldsEnabled,
|
||||
focusOrder: _focusFirstName,
|
||||
focusNode: _firstNameFocus,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -873,7 +1129,9 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
hint: 'Votre numéro de téléphone',
|
||||
keyboardType: TextInputType.phone,
|
||||
enabled: _fieldsEnabled,
|
||||
inputFormatters: _phoneInputFormatters,
|
||||
inputFormatters: frenchPhoneInputFormatters,
|
||||
focusOrder: _focusPhone,
|
||||
fieldValidator: _validateFrenchPhoneIfSecondParentNeeded,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -885,6 +1143,11 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
hint: 'Votre adresse e-mail',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
enabled: _fieldsEnabled,
|
||||
focusOrder: _focusEmail,
|
||||
fieldValidator: _validateEmailIfSecondParentNeeded,
|
||||
inputFormatters: const [EmailMaxLengthFormatter()],
|
||||
focusNode: _emailFocus,
|
||||
formFieldKey: _emailFormKey,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -895,6 +1158,10 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
controller: _addressController,
|
||||
hint: 'Numéro et nom de votre rue',
|
||||
enabled: _fieldsEnabled && !_sameAddress,
|
||||
focusOrder: _focusAddress,
|
||||
fieldValidator: widget.showSameAddressCheckbox
|
||||
? _validateAddressLineIfManualEntry
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -903,9 +1170,12 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
config: config,
|
||||
label: 'Code Postal',
|
||||
controller: _postalCodeController,
|
||||
hint: 'Code postal',
|
||||
hint: '5 chiffres',
|
||||
keyboardType: TextInputType.number,
|
||||
enabled: _fieldsEnabled && !_sameAddress,
|
||||
focusOrder: _focusPostal,
|
||||
fieldValidator: _validatePostalCodeField,
|
||||
inputFormatters: kFrenchPostalCodeInputFormatters,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -916,6 +1186,11 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
controller: _cityController,
|
||||
hint: 'Votre ville',
|
||||
enabled: _fieldsEnabled && !_sameAddress,
|
||||
focusOrder: _focusCity,
|
||||
focusNode: _cityFocus,
|
||||
fieldValidator: widget.showSameAddressCheckbox
|
||||
? _validateAddressLineIfManualEntry
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -930,6 +1205,10 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
TextInputType? keyboardType,
|
||||
bool enabled = true,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
double? focusOrder,
|
||||
FocusNode? focusNode,
|
||||
GlobalKey<FormFieldState<String>>? formFieldKey,
|
||||
String? Function(String?)? fieldValidator,
|
||||
}) {
|
||||
if (config.isReadonly) {
|
||||
// Mode readonly : utiliser FormFieldWrapper (téléphone formaté pour affichage)
|
||||
@@ -942,9 +1221,12 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
value: displayValue,
|
||||
);
|
||||
} else {
|
||||
// Mode éditable : style adapté mobile/desktop
|
||||
return CustomAppTextField(
|
||||
final effectiveKeyboardType = keyboardType ?? TextInputType.text;
|
||||
final isEmail = effectiveKeyboardType == TextInputType.emailAddress;
|
||||
Widget field = CustomAppTextField(
|
||||
formFieldKey: formFieldKey,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
labelText: label,
|
||||
hintText: hint ?? label,
|
||||
style: CustomAppTextFieldStyle.beige,
|
||||
@@ -952,10 +1234,23 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||
fieldHeight: config.isMobile ? 45.0 : 53.0,
|
||||
labelFontSize: config.isMobile ? 15.0 : 22.0,
|
||||
inputFontSize: config.isMobile ? 14.0 : 20.0,
|
||||
keyboardType: keyboardType ?? TextInputType.text,
|
||||
keyboardType: effectiveKeyboardType,
|
||||
autocorrect: !isEmail,
|
||||
enableSuggestions: !isEmail,
|
||||
autofillHints: isEmail ? const [AutofillHints.email] : null,
|
||||
textInputAction: isEmail ? TextInputAction.next : null,
|
||||
enabled: enabled,
|
||||
inputFormatters: inputFormatters,
|
||||
validator: fieldValidator,
|
||||
isRequired: fieldValidator == null,
|
||||
);
|
||||
if (focusOrder != null) {
|
||||
field = FocusTraversalOrder(
|
||||
order: NumericFocusOrder(focusOrder),
|
||||
child: field,
|
||||
);
|
||||
}
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user