fix(#129): peaufiner le wizard famille (co-parent, hauteurs, à naître).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -35,19 +35,23 @@ class ValidationFormMetrics {
|
||||
);
|
||||
|
||||
static double get sectionTitleBlockHeight =>
|
||||
sectionTitleFontSize + sectionTitleGapBelow;
|
||||
sectionTitleFontSize * 1.25 + sectionTitleGapBelow;
|
||||
|
||||
/// Libellé : marge au-dessus de [fieldLabelFontSize] (métriques police).
|
||||
static double get labeledRowHeight =>
|
||||
fieldLabelFontSize + fieldLabelGapBelow + fieldHeight + rowGapBelow;
|
||||
fieldLabelFontSize * 1.25 +
|
||||
fieldLabelGapBelow +
|
||||
fieldHeight +
|
||||
rowGapBelow;
|
||||
|
||||
/// Corps modale AM : padding wizard + titre + [rows] lignes + nav.
|
||||
/// Corps modale AM / famille : padding wizard + titre + [rows] lignes + nav.
|
||||
static double shellBodyHeightForRows(int rows) =>
|
||||
20 * 2 + // padding wizard
|
||||
4 + // espace haut
|
||||
sectionTitleBlockHeight +
|
||||
rows * labeledRowHeight +
|
||||
24 + // avant nav
|
||||
44; // boutons
|
||||
48; // boutons (+ marge anti-overflow)
|
||||
}
|
||||
|
||||
/// Bloc type formulaire (titre de section + champs read-only) pour les modales de validation.
|
||||
@@ -134,6 +138,7 @@ class ValidationFormGrid extends StatelessWidget {
|
||||
label: f.label,
|
||||
field: f.field,
|
||||
expand: expandVertically,
|
||||
labelTrailing: f.labelTrailing,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
@@ -262,27 +267,36 @@ class ValidationLabeledField extends StatelessWidget {
|
||||
final String label;
|
||||
final Widget field;
|
||||
final bool expand;
|
||||
/// Widget aligné à droite sur la ligne du libellé (ex. switch « Même adresse »).
|
||||
final Widget? labelTrailing;
|
||||
|
||||
const ValidationLabeledField({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.field,
|
||||
this.expand = false,
|
||||
this.labelTrailing,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final labelStyle = TextStyle(
|
||||
fontSize: ValidationFormMetrics.fieldLabelFontSize,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade700,
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: ValidationFormMetrics.fieldLabelFontSize,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
if (labelTrailing == null)
|
||||
Text(label, style: labelStyle)
|
||||
else
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Text(label, style: labelStyle)),
|
||||
labelTrailing!,
|
||||
],
|
||||
),
|
||||
SizedBox(height: ValidationFormMetrics.fieldLabelGapBelow),
|
||||
if (expand) Expanded(child: field) else field,
|
||||
@@ -299,6 +313,7 @@ class ValidationEditableField extends StatelessWidget {
|
||||
final String? hintText;
|
||||
final int maxLines;
|
||||
final bool compact;
|
||||
final bool enabled;
|
||||
|
||||
const ValidationEditableField({
|
||||
super.key,
|
||||
@@ -308,6 +323,7 @@ class ValidationEditableField extends StatelessWidget {
|
||||
this.hintText,
|
||||
this.maxLines = 1,
|
||||
this.compact = false,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
static const double _compactFieldHeight = 34;
|
||||
@@ -339,6 +355,7 @@ class ValidationEditableField extends StatelessWidget {
|
||||
if (maxLines > 1) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
enabled: enabled,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLines: maxLines,
|
||||
@@ -350,6 +367,7 @@ class ValidationEditableField extends StatelessWidget {
|
||||
return _validationFieldFillHeight(
|
||||
TextField(
|
||||
controller: controller,
|
||||
enabled: enabled,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLines: 1,
|
||||
@@ -365,6 +383,7 @@ class ValidationEditableField extends StatelessWidget {
|
||||
decoration: _compactDecoration(),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
enabled: enabled,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLines: 1,
|
||||
@@ -381,13 +400,15 @@ class ValidationEditableField extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Étire le champ si le parent impose une hauteur (grille [expandVertically]).
|
||||
/// Hauteur TF fixe ; en grille [expandVertically], étire jusqu’à la hauteur dispo.
|
||||
Widget _validationFieldFillHeight(Widget field) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, c) {
|
||||
if (!c.hasBoundedHeight || !c.maxHeight.isFinite) return field;
|
||||
final h = (c.hasBoundedHeight && c.maxHeight.isFinite)
|
||||
? c.maxHeight
|
||||
: ValidationFormMetrics.fieldHeight;
|
||||
return SizedBox(
|
||||
height: c.maxHeight,
|
||||
height: h,
|
||||
width: double.infinity,
|
||||
child: field,
|
||||
);
|
||||
@@ -481,12 +502,14 @@ class ValidationPostalCodeField extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final String? hintText;
|
||||
final bool allowEmpty;
|
||||
final bool enabled;
|
||||
|
||||
const ValidationPostalCodeField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.hintText,
|
||||
this.allowEmpty = false,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -536,6 +559,7 @@ class _ValidationPostalCodeFieldState extends State<ValidationPostalCodeField> {
|
||||
key: _fieldKey,
|
||||
controller: widget.controller,
|
||||
focusNode: _focusNode,
|
||||
enabled: widget.enabled,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
|
||||
@@ -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,59 +320,121 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Ajouter un co-parent'),
|
||||
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 Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(
|
||||
const 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,
|
||||
else
|
||||
ValidationFormGrid(
|
||||
compact: true,
|
||||
rowLayout: IdentityBlock.rowLayout,
|
||||
rowFlex: IdentityBlock.rowFlex,
|
||||
fields: [
|
||||
ValidationLabeledField(
|
||||
label: 'Nom',
|
||||
field: ValidationEditableField(
|
||||
controller: _p2NomCtrl,
|
||||
inputFormatters: const [PersonNameInputFormatter()],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Même adresse que le parent principal'),
|
||||
),
|
||||
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) {
|
||||
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) {
|
||||
@@ -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 'L’email du co-parent doit être différent de celui du parent principal.';
|
||||
}
|
||||
if (_sameAddress) {
|
||||
_copyP1AddressToP2();
|
||||
return null;
|
||||
}
|
||||
if (_p2AdresseCtrl.text.trim().isEmpty) {
|
||||
return 'L’adresse du co-parent est requise.';
|
||||
}
|
||||
@@ -1134,7 +1239,19 @@ 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) {
|
||||
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) {
|
||||
@@ -1144,6 +1261,7 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user