feat(#135): mode édition dossier (PATCH fiche, co-parent, enfants).
Wizard edit famille/AM depuis la liste Dossiers ; save parents + co-parent ; enfants existants en PATCH (photo multipart) sans POST doublon. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,10 +22,15 @@ import 'package:p_tits_pas/widgets/admin/validation_valider_confirm_dialog.dart'
|
||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
||||
|
||||
enum ParentDossierWizardMode { review, create }
|
||||
enum ParentDossierWizardMode { review, create, edit }
|
||||
|
||||
/// Enfant en cours de saisie (mode création). Contrôleurs propres, à disposer.
|
||||
/// Enfant en cours de saisie (création / édition). Contrôleurs propres, à disposer.
|
||||
class _CreateChild {
|
||||
/// Id existant en base (mode edit) — null = nouvel enfant → POST.
|
||||
String? existingChildId;
|
||||
String? existingPhotoUrl;
|
||||
/// Statut d’origine (hors à naître) pour ne pas écraser garde/scolarise.
|
||||
String? existingStatus;
|
||||
final TextEditingController prenomCtrl = TextEditingController();
|
||||
final TextEditingController nomCtrl = TextEditingController();
|
||||
final TextEditingController dateCtrl = TextEditingController();
|
||||
@@ -34,6 +39,8 @@ class _CreateChild {
|
||||
Uint8List? photoBytes;
|
||||
String? photoFilename;
|
||||
|
||||
bool get hasExistingId => (existingChildId ?? '').trim().isNotEmpty;
|
||||
|
||||
void dispose() {
|
||||
prenomCtrl.dispose();
|
||||
nomCtrl.dispose();
|
||||
@@ -41,8 +48,7 @@ class _CreateChild {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wizard dossier famille — modes [review] (validation dossier en attente, ticket #107)
|
||||
/// et [create] (création dossier famille actif par le staff, ticket #129).
|
||||
/// Wizard dossier famille — [review] (#107), [create] (#129), [edit] (#135).
|
||||
class ParentDossierWizard extends StatefulWidget {
|
||||
final ParentDossierWizardMode mode;
|
||||
final DossierFamille? dossier;
|
||||
@@ -91,7 +97,25 @@ class ParentDossierWizard extends StatefulWidget {
|
||||
);
|
||||
}
|
||||
|
||||
factory ParentDossierWizard.edit({
|
||||
Key? key,
|
||||
required DossierFamille dossier,
|
||||
required VoidCallback onClose,
|
||||
required VoidCallback onSuccess,
|
||||
void Function(int step, int total)? onStepChanged,
|
||||
}) {
|
||||
return ParentDossierWizard._(
|
||||
key: key,
|
||||
mode: ParentDossierWizardMode.edit,
|
||||
dossier: dossier,
|
||||
onClose: onClose,
|
||||
onSuccess: onSuccess,
|
||||
onStepChanged: onStepChanged,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isCreate => mode == ParentDossierWizardMode.create;
|
||||
bool get isEdit => mode == ParentDossierWizardMode.edit;
|
||||
|
||||
/// Hauteur corps modale famille — 4 lignes TF + marge pour le bandeau switch.
|
||||
static double get shellBodyHeight =>
|
||||
@@ -141,16 +165,27 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
final List<_CreateChild> _children = [];
|
||||
late final TextEditingController _presentationCtrl;
|
||||
|
||||
/// Nb de parents déjà en base au moment de l’ouverture (mode edit).
|
||||
int _initialParentCount = 0;
|
||||
|
||||
/// Enfants existants retirés en edit → DELETE au save.
|
||||
final List<String> _removedEnfantIds = [];
|
||||
|
||||
bool get _isCreate => widget.isCreate;
|
||||
bool get _isEdit => widget.isEdit;
|
||||
bool get _isEditable => _isCreate || _isEdit;
|
||||
DossierFamille get _dossier => widget.dossier!;
|
||||
|
||||
bool get _isEnAttente => !_isCreate && _dossier.isEnAttente;
|
||||
bool get _isEnAttente => !_isCreate && !_isEdit && _dossier.isEnAttente;
|
||||
|
||||
String? get _firstParentId {
|
||||
if (_isCreate) return null;
|
||||
return _dossier.parents.isNotEmpty ? _dossier.parents.first.id : null;
|
||||
}
|
||||
|
||||
/// Co-parent déjà présent au chargement (edit) — pas un ajout via POST.
|
||||
bool get _hadExistingCoParent => _isEdit && _initialParentCount >= 2;
|
||||
|
||||
static String _v(String? s) =>
|
||||
(s != null && s.trim().isNotEmpty) ? s.trim() : 'Non défini';
|
||||
|
||||
@@ -184,11 +219,81 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
_presentationCtrl = TextEditingController();
|
||||
if (_isCreate) {
|
||||
_children.add(_CreateChild());
|
||||
} else if (_isEdit) {
|
||||
_prefillFromDossier();
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _emitStep());
|
||||
}
|
||||
|
||||
void _prefillFromDossier() {
|
||||
final parents = _dossier.parents;
|
||||
_initialParentCount = parents.length;
|
||||
|
||||
if (parents.isNotEmpty) {
|
||||
final p1 = parents.first;
|
||||
_p1NomCtrl.text = (p1.nom ?? '').trim();
|
||||
_p1PrenomCtrl.text = (p1.prenom ?? '').trim();
|
||||
_p1TelCtrl.text = (p1.telephone ?? '').trim();
|
||||
_p1EmailCtrl.text = (p1.email).trim();
|
||||
_p1AdresseCtrl.text = (p1.adresse ?? '').trim();
|
||||
_p1CpCtrl.text = (p1.codePostal ?? '').trim();
|
||||
_p1VilleCtrl.text = (p1.ville ?? '').trim();
|
||||
|
||||
if (parents.length >= 2) {
|
||||
_hasCoParent = true;
|
||||
final p2 = parents[1];
|
||||
_p2NomCtrl.text = (p2.nom ?? '').trim();
|
||||
_p2PrenomCtrl.text = (p2.prenom ?? '').trim();
|
||||
_p2TelCtrl.text = (p2.telephone ?? '').trim();
|
||||
_p2EmailCtrl.text = (p2.email).trim();
|
||||
_p2AdresseCtrl.text = (p2.adresse ?? '').trim();
|
||||
_p2CpCtrl.text = (p2.codePostal ?? '').trim();
|
||||
_p2VilleCtrl.text = (p2.ville ?? '').trim();
|
||||
} else {
|
||||
_hasCoParent = false;
|
||||
}
|
||||
}
|
||||
|
||||
_prefillChildrenFromDossier();
|
||||
}
|
||||
|
||||
void _prefillChildrenFromDossier() {
|
||||
for (final c in _children) {
|
||||
c.dispose();
|
||||
}
|
||||
_children.clear();
|
||||
_removedEnfantIds.clear();
|
||||
|
||||
final enfants = _dossier.enfants;
|
||||
if (enfants.isEmpty) {
|
||||
_children.add(_CreateChild());
|
||||
return;
|
||||
}
|
||||
|
||||
for (final e in enfants) {
|
||||
final status = (e.status ?? '').trim().toLowerCase();
|
||||
final id = e.id.trim();
|
||||
final child = _CreateChild()
|
||||
..existingChildId = id.isEmpty ? null : id
|
||||
..existingPhotoUrl = e.photoUrl
|
||||
..existingStatus = status.isEmpty ? null : status
|
||||
..isUnborn = status == 'a_naitre';
|
||||
child.prenomCtrl.text = (e.firstName ?? '').trim();
|
||||
child.nomCtrl.text = (e.lastName ?? '').trim();
|
||||
final g = (e.gender ?? '').trim();
|
||||
final gUp = g.toUpperCase();
|
||||
if (gUp == 'H' || gUp == 'F') {
|
||||
child.genre = gUp;
|
||||
} else if (g == 'Autre' || child.isUnborn) {
|
||||
child.genre = child.isUnborn ? 'Autre' : g;
|
||||
}
|
||||
final dateSrc = child.isUnborn ? e.dueDate : e.birthDate;
|
||||
child.dateCtrl.text = formatIsoDateFrInput(dateSrc);
|
||||
_children.add(child);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_enfantsScrollController.removeListener(_syncEnfantsScrollFades);
|
||||
@@ -291,7 +396,7 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
}
|
||||
|
||||
Widget _buildStep0() {
|
||||
if (_isCreate) {
|
||||
if (_isEditable) {
|
||||
return IdentityBlock.editable(
|
||||
title: 'Parent principal',
|
||||
nomController: _p1NomCtrl,
|
||||
@@ -310,13 +415,15 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
}
|
||||
|
||||
Widget _buildStep1() {
|
||||
if (_isCreate) {
|
||||
return _buildCoParentStepCreate();
|
||||
if (_isEditable) {
|
||||
return _buildCoParentStepEditable(
|
||||
allowToggle: !_hadExistingCoParent,
|
||||
);
|
||||
}
|
||||
return _buildParent2Step();
|
||||
}
|
||||
|
||||
Widget _buildCoParentStepCreate() {
|
||||
Widget _buildCoParentStepEditable({required bool allowToggle}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@@ -330,19 +437,21 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
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),
|
||||
if (allowToggle) ...[
|
||||
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),
|
||||
@@ -539,7 +648,7 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
}
|
||||
|
||||
Widget _buildEnfantsStep() {
|
||||
if (_isCreate) {
|
||||
if (_isCreate || _isEdit) {
|
||||
return _buildEnfantsStepCreate();
|
||||
}
|
||||
final enfants = _dossier.enfants;
|
||||
@@ -618,6 +727,10 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
if (_children.length <= 1) return;
|
||||
setState(() {
|
||||
final removed = _children.removeAt(index);
|
||||
final existingId = (removed.existingChildId ?? '').trim();
|
||||
if (existingId.isNotEmpty) {
|
||||
_removedEnfantIds.add(existingId);
|
||||
}
|
||||
removed.dispose();
|
||||
});
|
||||
}
|
||||
@@ -706,7 +819,8 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 14, 8),
|
||||
// Réserve l’angle haut-droit pour la croix de suppression.
|
||||
padding: EdgeInsets.fromLTRB(12, 12, canRemove ? 36 : 14, 8),
|
||||
child: ValidationLabeledField(
|
||||
label: 'Prénom',
|
||||
field: ValidationEditableField(
|
||||
@@ -747,6 +861,9 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
width: pw,
|
||||
height: ph,
|
||||
child: AdminAmPhotoFrame(
|
||||
photoUrl: child.photoBytes == null
|
||||
? child.existingPhotoUrl
|
||||
: null,
|
||||
imageBytes: child.photoBytes,
|
||||
onTap: () => _pickChildPhoto(index),
|
||||
onClear: child.photoBytes != null
|
||||
@@ -833,19 +950,28 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
),
|
||||
if (canRemove)
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
top: 6,
|
||||
right: 6,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
tooltip: 'Retirer cet enfant',
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
icon: Icon(Icons.close,
|
||||
size: 18, color: Colors.grey.shade700),
|
||||
onPressed: () => _removeChild(index),
|
||||
color: Colors.white,
|
||||
elevation: 1,
|
||||
shape: const CircleBorder(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () => _removeChild(index),
|
||||
child: Tooltip(
|
||||
message: 'Retirer cet enfant',
|
||||
child: SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 14,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1264,6 +1390,18 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
}
|
||||
|
||||
String? _validateCurrentStep() {
|
||||
if (_isEdit) {
|
||||
switch (_step) {
|
||||
case 0:
|
||||
return _validateP1();
|
||||
case 1:
|
||||
return _validateP2();
|
||||
case 2:
|
||||
return _validateEnfants();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!_isCreate) return null;
|
||||
switch (_step) {
|
||||
case 0:
|
||||
@@ -1440,6 +1578,299 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _parentFicheBody({
|
||||
required TextEditingController nom,
|
||||
required TextEditingController prenom,
|
||||
required TextEditingController email,
|
||||
required TextEditingController tel,
|
||||
required TextEditingController adresse,
|
||||
required TextEditingController cp,
|
||||
required TextEditingController ville,
|
||||
}) {
|
||||
return {
|
||||
'nom': formatPersonNameCase(nom.text),
|
||||
'prenom': formatPersonNameCase(prenom.text),
|
||||
'email': normalizeEmailText(email.text),
|
||||
'telephone': normalizePhone(tel.text),
|
||||
'adresse': adresse.text.trim(),
|
||||
'ville': formatPersonNameCase(ville.text),
|
||||
'code_postal': cp.text.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _saveEdit() async {
|
||||
if (_submitting || !_isEdit) return;
|
||||
final err0 = _validateP1();
|
||||
if (err0 != null) {
|
||||
_showError(err0);
|
||||
return;
|
||||
}
|
||||
final err1 = _validateP2();
|
||||
if (err1 != null) {
|
||||
_showError(err1);
|
||||
return;
|
||||
}
|
||||
final err2 = _validateEnfants();
|
||||
if (err2 != null) {
|
||||
_showError(err2);
|
||||
return;
|
||||
}
|
||||
|
||||
final pivotId = (_firstParentId ?? '').trim();
|
||||
if (pivotId.isEmpty) {
|
||||
_showError('Identifiant du parent principal manquant.');
|
||||
return;
|
||||
}
|
||||
|
||||
final addingCoParent = _hasCoParent && !_hadExistingCoParent;
|
||||
final ok = await showValidationValiderConfirmDialog(
|
||||
context,
|
||||
body: addingCoParent
|
||||
? 'Enregistrer le dossier et ajouter le co-parent ? Un e-mail de création de mot de passe lui sera envoyé.'
|
||||
: 'Enregistrer les modifications du dossier famille ?',
|
||||
);
|
||||
if (!mounted || !ok) return;
|
||||
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
await UserService.updateParentFiche(
|
||||
parentUserId: pivotId,
|
||||
body: _parentFicheBody(
|
||||
nom: _p1NomCtrl,
|
||||
prenom: _p1PrenomCtrl,
|
||||
email: _p1EmailCtrl,
|
||||
tel: _p1TelCtrl,
|
||||
adresse: _p1AdresseCtrl,
|
||||
cp: _p1CpCtrl,
|
||||
ville: _p1VilleCtrl,
|
||||
),
|
||||
);
|
||||
|
||||
if (_hadExistingCoParent) {
|
||||
final coId = _dossier.parents[1].id.trim();
|
||||
if (coId.isEmpty) {
|
||||
throw Exception('Identifiant du co-parent manquant.');
|
||||
}
|
||||
await UserService.updateParentFiche(
|
||||
parentUserId: coId,
|
||||
body: _parentFicheBody(
|
||||
nom: _p2NomCtrl,
|
||||
prenom: _p2PrenomCtrl,
|
||||
email: _p2EmailCtrl,
|
||||
tel: _p2TelCtrl,
|
||||
adresse: _p2AdresseCtrl,
|
||||
cp: _p2CpCtrl,
|
||||
ville: _p2VilleCtrl,
|
||||
),
|
||||
);
|
||||
} else if (addingCoParent) {
|
||||
if (_sameAddress) _copyP1AddressToP2();
|
||||
final body = <String, dynamic>{
|
||||
'email': normalizeEmailText(_p2EmailCtrl.text),
|
||||
'prenom': formatPersonNameCase(_p2PrenomCtrl.text),
|
||||
'nom': formatPersonNameCase(_p2NomCtrl.text),
|
||||
'telephone': normalizePhone(_p2TelCtrl.text),
|
||||
'meme_adresse': _sameAddress,
|
||||
};
|
||||
if (!_sameAddress) {
|
||||
body['adresse'] = _p2AdresseCtrl.text.trim();
|
||||
body['code_postal'] = _p2CpCtrl.text.trim();
|
||||
body['ville'] = formatPersonNameCase(_p2VilleCtrl.text);
|
||||
}
|
||||
await UserService.addCoParent(pivotId, body: body);
|
||||
}
|
||||
|
||||
await _saveEnfantsEdit(pivotId);
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
addingCoParent
|
||||
? 'Dossier enregistré. Co-parent ajouté.'
|
||||
: 'Dossier enregistré.',
|
||||
),
|
||||
duration: const Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
widget.onSuccess();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_showError(
|
||||
e is Exception ? e.toString().replaceFirst('Exception: ', '') : 'Erreur',
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _enfantStaffBody(_CreateChild c) {
|
||||
final prenom = formatPersonNameCase(c.prenomCtrl.text);
|
||||
final nomRaw = c.nomCtrl.text.trim();
|
||||
final nom = nomRaw.isNotEmpty
|
||||
? formatPersonNameCase(nomRaw)
|
||||
: formatPersonNameCase(_p1NomCtrl.text);
|
||||
final dateIso = parseFrDateToIso(c.dateCtrl.text);
|
||||
final existingStatus = (c.existingStatus ?? '').trim().toLowerCase();
|
||||
final status = c.isUnborn
|
||||
? 'a_naitre'
|
||||
: (existingStatus.isNotEmpty && existingStatus != 'a_naitre'
|
||||
? existingStatus
|
||||
: 'sans_garde');
|
||||
final gender = c.genre ?? 'H';
|
||||
|
||||
final map = <String, dynamic>{
|
||||
'status': status,
|
||||
'gender': gender,
|
||||
'consent_photo': true,
|
||||
'is_multiple': false,
|
||||
};
|
||||
if (prenom.length >= 2) map['first_name'] = prenom;
|
||||
if (nom.length >= 2) map['last_name'] = nom;
|
||||
if (c.isUnborn) {
|
||||
if (dateIso != null) map['due_date'] = dateIso;
|
||||
} else if (dateIso != null) {
|
||||
map['birth_date'] = dateIso;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// Empreinte identité pour rattacher un brouillon sans id à un enfant du GET.
|
||||
String _enfantIdentityKey({
|
||||
required String prenom,
|
||||
required String nom,
|
||||
required String? dateIso,
|
||||
required bool isUnborn,
|
||||
}) {
|
||||
final pn = formatPersonNameCase(prenom).toLowerCase().trim();
|
||||
final nm = formatPersonNameCase(nom).toLowerCase().trim();
|
||||
final d = _normalizeDateKey(dateIso);
|
||||
final kind = isUnborn ? 'due' : 'birth';
|
||||
return '$pn|$nm|$kind|$d';
|
||||
}
|
||||
|
||||
String _normalizeDateKey(String? raw) {
|
||||
final s = (raw ?? '').trim();
|
||||
if (s.isEmpty) return '';
|
||||
final fromFr = parseFrDateToIso(s);
|
||||
if (fromFr != null) return fromFr;
|
||||
try {
|
||||
final dt = DateTime.parse(s);
|
||||
final y = dt.year.toString().padLeft(4, '0');
|
||||
final m = dt.month.toString().padLeft(2, '0');
|
||||
final d = dt.day.toString().padLeft(2, '0');
|
||||
return '$y-$m-$d';
|
||||
} catch (_) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
String _enfantKeyFromCreate(_CreateChild c) {
|
||||
final prenom = formatPersonNameCase(c.prenomCtrl.text);
|
||||
final nomRaw = c.nomCtrl.text.trim();
|
||||
final nom = nomRaw.isNotEmpty
|
||||
? formatPersonNameCase(nomRaw)
|
||||
: formatPersonNameCase(_p1NomCtrl.text);
|
||||
return _enfantIdentityKey(
|
||||
prenom: prenom,
|
||||
nom: nom,
|
||||
dateIso: parseFrDateToIso(c.dateCtrl.text),
|
||||
isUnborn: c.isUnborn,
|
||||
);
|
||||
}
|
||||
|
||||
String _enfantKeyFromDossier(EnfantDossier e) {
|
||||
final isUnborn = (e.status ?? '').trim().toLowerCase() == 'a_naitre';
|
||||
final nomRaw = (e.lastName ?? '').trim();
|
||||
final nom = nomRaw.isNotEmpty ? nomRaw : (_p1NomCtrl.text.trim());
|
||||
return _enfantIdentityKey(
|
||||
prenom: e.firstName ?? '',
|
||||
nom: nom,
|
||||
dateIso: isUnborn ? e.dueDate : e.birthDate,
|
||||
isUnborn: isUnborn,
|
||||
);
|
||||
}
|
||||
|
||||
bool _isBlankChildDraft(_CreateChild c) {
|
||||
if (c.hasExistingId) return false;
|
||||
final prenom = c.prenomCtrl.text.trim();
|
||||
final nom = c.nomCtrl.text.trim();
|
||||
final date = c.dateCtrl.text.trim();
|
||||
return prenom.isEmpty && nom.isEmpty && date.isEmpty && c.genre == null;
|
||||
}
|
||||
|
||||
/// Résout l’id enfant : tracker edit, sinon match sur le GET dossier.
|
||||
String? _resolveExistingChildId(
|
||||
_CreateChild c,
|
||||
Map<String, String> dossierIdByIdentity,
|
||||
Set<String> alreadyUsedIds,
|
||||
) {
|
||||
final tracked = (c.existingChildId ?? '').trim();
|
||||
if (tracked.isNotEmpty && !alreadyUsedIds.contains(tracked)) {
|
||||
return tracked;
|
||||
}
|
||||
final key = _enfantKeyFromCreate(c);
|
||||
final matched = (dossierIdByIdentity[key] ?? '').trim();
|
||||
if (matched.isNotEmpty && !alreadyUsedIds.contains(matched)) {
|
||||
return matched;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _saveEnfantsEdit(String pivotUserId) async {
|
||||
// Index id connus du GET (ne jamais POST pour ceux-là).
|
||||
final dossierIdByIdentity = <String, String>{};
|
||||
for (final e in _dossier.enfants) {
|
||||
final id = e.id.trim();
|
||||
if (id.isEmpty) continue;
|
||||
dossierIdByIdentity[_enfantKeyFromDossier(e)] = id;
|
||||
}
|
||||
|
||||
for (final id in _removedEnfantIds) {
|
||||
await UserService.deleteEnfant(id);
|
||||
}
|
||||
|
||||
final usedIds = <String>{..._removedEnfantIds};
|
||||
for (final c in _children) {
|
||||
if (_isBlankChildDraft(c)) continue;
|
||||
|
||||
final existingId = _resolveExistingChildId(
|
||||
c,
|
||||
dossierIdByIdentity,
|
||||
usedIds,
|
||||
);
|
||||
final body = _enfantStaffBody(c);
|
||||
|
||||
if (existingId != null && existingId.isNotEmpty) {
|
||||
// Ré-attache l’id au modèle (si match identité a récupéré un id perdu).
|
||||
c.existingChildId = existingId;
|
||||
final bytes = c.photoBytes;
|
||||
await UserService.updateEnfant(
|
||||
enfantId: existingId,
|
||||
body: body,
|
||||
photoBytes: (bytes != null && bytes.isNotEmpty) ? bytes : null,
|
||||
photoFilename: c.photoFilename,
|
||||
);
|
||||
usedIds.add(existingId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Uniquement les vrais nouveaux enfants (pas d’id dossier).
|
||||
final bytes = c.photoBytes;
|
||||
final created = await UserService.createEnfant(
|
||||
parentUserId: pivotUserId,
|
||||
body: body,
|
||||
photoBytes: (bytes != null && bytes.isNotEmpty) ? bytes : null,
|
||||
photoFilename: c.photoFilename,
|
||||
);
|
||||
final newId = created.id.trim();
|
||||
if (newId.isNotEmpty) {
|
||||
c.existingChildId = newId;
|
||||
usedIds.add(newId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildNavigation() {
|
||||
if (_step == 3) {
|
||||
return Row(
|
||||
@@ -1463,6 +1894,12 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
onPressed: _submitting ? null : _createAndValidate,
|
||||
child: Text(_submitting ? 'Envoi...' : 'Créer et valider'),
|
||||
),
|
||||
] else if (_isEdit) ...[
|
||||
ElevatedButton(
|
||||
style: ValidationModalTheme.primaryElevatedStyle,
|
||||
onPressed: _submitting ? null : _saveEdit,
|
||||
child: Text(_submitting ? 'Envoi...' : 'Enregistrer'),
|
||||
),
|
||||
] else if (_isEnAttente && _firstParentId != null) ...[
|
||||
OutlinedButton(
|
||||
onPressed: _submitting ? null : _refuser,
|
||||
|
||||
Reference in New Issue
Block a user