fix(#129): peaufiner le wizard famille (co-parent, hauteurs, à naître).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-24 13:01:54 +02:00
co-authored by Cursor
parent cb5c1a5518
commit b4abb7d6de
2 changed files with 239 additions and 88 deletions
@@ -30,6 +30,7 @@ class _CreateChild {
final TextEditingController nomCtrl = TextEditingController();
final TextEditingController dateCtrl = TextEditingController();
String? genre;
bool isUnborn = false;
Uint8List? photoBytes;
String? photoFilename;
@@ -92,9 +93,9 @@ class ParentDossierWizard extends StatefulWidget {
bool get isCreate => mode == ParentDossierWizardMode.create;
/// Hauteur corps modale famille — dérivée de [ValidationFormMetrics] (4 lignes).
/// Hauteur corps modale famille — 4 lignes TF + marge pour le bandeau switch.
static double get shellBodyHeight =>
ValidationFormMetrics.shellBodyHeightForRows(4);
ValidationFormMetrics.shellBodyHeightForRows(4) + 20;
@override
State<ParentDossierWizard> createState() => _ParentDossierWizardState();
@@ -319,60 +320,122 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Ajouter un co-parent'),
value: _hasCoParent,
onChanged: (v) => setState(() => _hasCoParent = v),
),
if (!_hasCoParent)
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Text(
'Un seul parent pour ce dossier.',
style: TextStyle(color: Colors.black87),
),
)
else ...[
const SizedBox(height: 8),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
IdentityBlock.editable(
title: 'Deuxième parent',
nomController: _p2NomCtrl,
prenomController: _p2PrenomCtrl,
telephoneController: _p2TelCtrl,
emailController: _p2EmailCtrl,
adresseController: _p2AdresseCtrl,
codePostalController: _p2CpCtrl,
villeController: _p2VilleCtrl,
),
const SizedBox(height: 12),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Même adresse que le parent principal'),
value: _sameAddress,
onChanged: (v) => setState(() {
_sameAddress = v;
if (v) {
_p2AdresseCtrl.text = _p1AdresseCtrl.text;
_p2CpCtrl.text = _p1CpCtrl.text;
_p2VilleCtrl.text = _p1VilleCtrl.text;
}
}),
),
],
Row(
children: [
const Text(
'Deuxième parent',
style: TextStyle(
fontSize: ValidationFormMetrics.sectionTitleFontSize,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const Spacer(),
const Text(
'Ajouter un co-parent',
style: TextStyle(fontSize: 14, color: Colors.black87),
),
Transform.scale(
scale: 0.75,
child: Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: _hasCoParent,
onChanged: (v) => setState(() => _hasCoParent = v),
),
),
],
),
const SizedBox(height: 8),
if (!_hasCoParent)
const Text(
'Un seul parent pour ce dossier.',
style: TextStyle(color: Colors.black87),
)
else
ValidationFormGrid(
compact: true,
rowLayout: IdentityBlock.rowLayout,
rowFlex: IdentityBlock.rowFlex,
fields: [
ValidationLabeledField(
label: 'Nom',
field: ValidationEditableField(
controller: _p2NomCtrl,
inputFormatters: const [PersonNameInputFormatter()],
),
),
ValidationLabeledField(
label: 'Prénom',
field: ValidationEditableField(
controller: _p2PrenomCtrl,
inputFormatters: const [PersonNameInputFormatter()],
),
),
ValidationLabeledField(
label: 'Téléphone',
field: ValidationPhoneField(controller: _p2TelCtrl),
),
ValidationLabeledField(
label: 'Email',
field: ValidationEmailField(controller: _p2EmailCtrl),
),
ValidationLabeledField(
label: 'Adresse (N° et Rue)',
labelTrailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Même adresse',
style: TextStyle(
fontSize: ValidationFormMetrics.fieldLabelFontSize,
color: Colors.black87,
),
),
Transform.scale(
scale: 0.75,
child: Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: _sameAddress,
onChanged: (v) => setState(() {
_sameAddress = v;
if (v) _copyP1AddressToP2();
}),
),
),
],
),
field: ValidationEditableField(
controller: _p2AdresseCtrl,
enabled: !_sameAddress,
),
),
ValidationLabeledField(
label: 'Code postal',
field: ValidationPostalCodeField(
controller: _p2CpCtrl,
enabled: !_sameAddress,
),
),
ValidationLabeledField(
label: 'Ville',
field: ValidationEditableField(
controller: _p2VilleCtrl,
enabled: !_sameAddress,
inputFormatters: const [PersonNameInputFormatter()],
),
),
],
),
],
],
);
}
void _copyP1AddressToP2() {
_p2AdresseCtrl.text = _p1AdresseCtrl.text;
_p2CpCtrl.text = _p1CpCtrl.text;
_p2VilleCtrl.text = _p1VilleCtrl.text;
}
Widget _buildParent2Step() {
if (_dossier.parents.length < 2) {
return const Padding(
@@ -643,11 +706,12 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 40, 8),
padding: const EdgeInsets.fromLTRB(12, 12, 14, 8),
child: ValidationLabeledField(
label: 'Prénom',
field: ValidationEditableField(
controller: child.prenomCtrl,
hintText: child.isUnborn ? 'Facultatif' : null,
inputFormatters: const [PersonNameInputFormatter()],
),
),
@@ -715,7 +779,36 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
),
const SizedBox(height: 12),
ValidationLabeledField(
label: 'Date de naissance',
label: child.isUnborn
? 'Date prévisionnelle'
: 'Date de naissance',
labelTrailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'À naître',
style: TextStyle(
fontSize: ValidationFormMetrics
.fieldLabelFontSize,
color: Colors.black87,
),
),
Transform.scale(
scale: 0.75,
child: Switch(
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
value: child.isUnborn,
onChanged: (v) => setState(() {
child.isUnborn = v;
if (!v && child.genre == 'Autre') {
child.genre = null;
}
}),
),
),
],
),
field: ValidationEditableField(
controller: child.dateCtrl,
hintText: 'jj / mm / aaaa',
@@ -766,20 +859,28 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
Widget _genreDropdown(int index) {
final child = _children[index];
final items = <DropdownMenuItem<String>>[
const DropdownMenuItem(value: 'H', child: Text('Garçon')),
const DropdownMenuItem(value: 'F', child: Text('Fille')),
if (child.isUnborn)
const DropdownMenuItem(value: 'Autre', child: Text('Inconnu')),
];
final value = child.genre != null &&
items.any((e) => e.value == child.genre)
? child.genre
: null;
return SizedBox(
height: ValidationFormMetrics.fieldHeight,
child: DropdownButtonFormField<String>(
value: child.genre,
key: ValueKey('genre-$index-${child.isUnborn}'),
value: value,
isExpanded: true,
hint: Text(
'Sélectionner',
style: TextStyle(color: Colors.grey.shade600, fontSize: 14),
),
decoration: ValidationFieldDecoration.input(),
items: const [
DropdownMenuItem(value: 'H', child: Text('Garçon')),
DropdownMenuItem(value: 'F', child: Text('Fille')),
],
items: items,
onChanged: (v) => setState(() => child.genre = v),
),
);
@@ -1118,6 +1219,10 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
normalizeEmailText(_p1EmailCtrl.text)) {
return 'Lemail du co-parent doit être différent de celui du parent principal.';
}
if (_sameAddress) {
_copyP1AddressToP2();
return null;
}
if (_p2AdresseCtrl.text.trim().isEmpty) {
return 'Ladresse du co-parent est requise.';
}
@@ -1134,14 +1239,27 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
for (var i = 0; i < _children.length; i++) {
final c = _children[i];
final label = 'Enfant ${i + 1}';
if (c.prenomCtrl.text.trim().length < 2) {
return '$label : le prénom doit contenir au moins 2 caractères.';
}
if (parseFrDateToIso(c.dateCtrl.text) == null) {
return '$label : indiquez une date de naissance valide (jj/mm/aaaa).';
}
if (c.genre != 'H' && c.genre != 'F') {
return '$label : indiquez le genre (Garçon ou Fille).';
final prenom = c.prenomCtrl.text.trim();
if (c.isUnborn) {
if (prenom.isNotEmpty && prenom.length < 2) {
return '$label : le prénom doit contenir au moins 2 caractères.';
}
if (parseFrDateToIso(c.dateCtrl.text) == null) {
return '$label : indiquez une date prévisionnelle valide (jj/mm/aaaa).';
}
if (c.genre != 'H' && c.genre != 'F' && c.genre != 'Autre') {
return '$label : indiquez le genre (Garçon, Fille ou Inconnu).';
}
} else {
if (prenom.length < 2) {
return '$label : le prénom doit contenir au moins 2 caractères.';
}
if (parseFrDateToIso(c.dateCtrl.text) == null) {
return '$label : indiquez une date de naissance valide (jj/mm/aaaa).';
}
if (c.genre != 'H' && c.genre != 'F') {
return '$label : indiquez le genre (Garçon ou Fille).';
}
}
}
return null;
@@ -1207,17 +1325,29 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
final nom = nomRaw.isNotEmpty
? formatPersonNameCase(nomRaw)
: formatPersonNameCase(_p1NomCtrl.text);
final birthIso = parseFrDateToIso(c.dateCtrl.text);
final dateIso = parseFrDateToIso(c.dateCtrl.text);
final map = <String, dynamic>{
'prenom': prenom,
'nom': nom,
'date_naissance': birthIso,
'genre': c.genre,
'consent_photo': true,
'grossesse_multiple': false,
};
if (prenom.length >= 2) {
map['prenom'] = prenom;
}
if (nom.length >= 2) {
map['nom'] = nom;
}
if (c.isUnborn) {
if (dateIso != null) {
map['date_previsionnelle_naissance'] = dateIso;
}
} else if (dateIso != null) {
map['date_naissance'] = dateIso;
}
final bytes = c.photoBytes;
if (bytes != null && bytes.isNotEmpty) {
final mime = _imageMimeForBytes(bytes);
@@ -1243,16 +1373,13 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
};
if (_hasCoParent) {
final sameAddr =
_p2AdresseCtrl.text.trim() == _p1AdresseCtrl.text.trim() &&
_p2CpCtrl.text.trim() == _p1CpCtrl.text.trim() &&
_p2VilleCtrl.text.trim() == _p1VilleCtrl.text.trim();
if (_sameAddress) _copyP1AddressToP2();
body['co_parent_email'] = normalizeEmailText(_p2EmailCtrl.text);
body['co_parent_prenom'] = formatPersonNameCase(_p2PrenomCtrl.text);
body['co_parent_nom'] = formatPersonNameCase(_p2NomCtrl.text);
body['co_parent_telephone'] = normalizePhone(_p2TelCtrl.text);
body['co_parent_meme_adresse'] = sameAddr;
if (!sameAddr) {
body['co_parent_meme_adresse'] = _sameAddress;
if (!_sameAddress) {
body['co_parent_adresse'] = _p2AdresseCtrl.text.trim();
body['co_parent_code_postal'] = _p2CpCtrl.text.trim();
body['co_parent_ville'] = formatPersonNameCase(_p2VilleCtrl.text);