feat(#112): aligner reprise front sur dossier complet GET/PATCH

Préremplit parents, enfants, motivation et fiche AM depuis reprise-dossier
et envoie le body PATCH complet (co-parent, enfants par id, champs pro AM).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 16:46:51 +02:00
co-authored by Cursor
parent 25c10c885a
commit f300505225
11 changed files with 439 additions and 92 deletions
@@ -149,6 +149,20 @@ class ParentRegistrationPayload {
return body;
}
/// Enfant pour PATCH reprise (inclut `id` si connu).
static Map<String, dynamic> childToRepriseJson(
ChildData c,
int index,
String parentNom,
) {
final map = _childToJson(c, index, parentNom);
final id = c.repriseChildId?.trim();
if (id != null && id.isNotEmpty) {
map['id'] = id;
}
return map;
}
static Map<String, dynamic> _childToJson(ChildData c, int index, String parentNom) {
final map = <String, dynamic>{
'genre': apiGenres.contains(c.genre) ? c.genre : 'Autre',
+120
View File
@@ -0,0 +1,120 @@
import 'package:p_tits_pas/models/card_assets.dart';
import 'package:p_tits_pas/models/dossier_unifie.dart';
import 'package:p_tits_pas/models/reprise_dossier.dart';
import 'package:p_tits_pas/models/user_registration_data.dart';
import 'package:p_tits_pas/services/api/api_config.dart';
/// Mapping GET reprise-dossier → modèles wizard. Ticket #112.
class RepriseMapper {
RepriseMapper._();
static const List<CardColorVertical> _childCardColors = [
CardColorVertical.lavender,
CardColorVertical.pink,
CardColorVertical.peach,
CardColorVertical.lime,
CardColorVertical.red,
CardColorVertical.green,
CardColorVertical.blue,
];
static ParentData parentFromDossier(ParentDossier p) {
return ParentData(
firstName: p.prenom ?? '',
lastName: p.nom ?? '',
phone: p.telephone ?? '',
email: p.email,
address: p.adresse ?? '',
postalCode: p.codePostal ?? '',
city: p.ville ?? '',
password: '',
);
}
static ChildData childFromEnfant(EnfantDossier e, int index) {
final isUnborn = e.status == 'a_naitre';
final dob = isUnborn
? isoToDdMmYyyy(e.dueDate)
: isoToDdMmYyyy(e.birthDate);
final photo = e.photoUrl?.trim();
return ChildData(
firstName: e.firstName ?? '',
lastName: e.lastName ?? '',
dob: dob,
genre: e.gender ?? '',
photoConsent: e.consentPhoto,
multipleBirth: e.estMultiple,
isUnbornChild: isUnborn,
cardColor: _childCardColors[index % _childCardColors.length],
repriseChildId: e.id,
existingPhotoUrl: photo != null && photo.isNotEmpty
? ApiConfig.absoluteMediaUrl(photo)
: null,
);
}
static void applyParentDossier(UserRegistrationData data, RepriseDossier dossier) {
ParentDossier? titulaire;
ParentDossier? coParent;
if (dossier.parents.isNotEmpty) {
for (final p in dossier.parents) {
if (p.id == dossier.id) {
titulaire = p;
} else {
coParent = p;
}
}
titulaire ??= dossier.parents.first;
if (coParent == null && dossier.parents.length > 1) {
coParent = dossier.parents.firstWhere(
(p) => p.id != titulaire!.id,
orElse: () => dossier.parents.last,
);
}
}
final p1 = titulaire != null
? parentFromDossier(titulaire)
: ParentData(
firstName: dossier.prenom ?? '',
lastName: dossier.nom ?? '',
phone: dossier.telephone ?? '',
email: dossier.email,
address: dossier.adresse ?? '',
postalCode: dossier.codePostal ?? '',
city: dossier.ville ?? '',
password: '',
);
final children = dossier.enfants
.asMap()
.entries
.map((e) => childFromEnfant(e.value, e.key))
.toList();
data.resetForReprise(
parent1Data: p1,
parent2Data: coParent != null ? parentFromDossier(coParent) : null,
childrenData: children,
motivation: dossier.texteMotivation ?? '',
);
}
static DateTime? parseIsoDate(String? raw) {
if (raw == null || raw.trim().isEmpty) return null;
try {
return DateTime.parse(raw.trim());
} catch (_) {
return null;
}
}
static String isoToDdMmYyyy(String? iso) {
final dt = parseIsoDate(iso);
if (dt == null) return '';
return '${dt.day.toString().padLeft(2, '0')}/'
'${dt.month.toString().padLeft(2, '0')}/'
'${dt.year}';
}
}
+150
View File
@@ -0,0 +1,150 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import '../models/am_registration_data.dart';
import '../models/user_registration_data.dart';
import 'nir_utils.dart';
import 'parent_registration_payload.dart';
/// Body PATCH /auth/reprise-resoumettre. Ticket #112.
class ReprisePayload {
ReprisePayload._();
static Map<String, dynamic> parentPatch(
UserRegistrationData data,
String token,
) {
final base = ParentRegistrationPayload.toJson(data);
base.remove('email');
base.remove('acceptation_cgu');
base.remove('acceptation_privacy');
base['token'] = token.trim();
final enfants = data.children.asMap().entries.map((e) {
final childMap = ParentRegistrationPayload.childToRepriseJson(
e.value,
e.key,
data.parent1.lastName.trim(),
);
return childMap;
}).toList();
base['enfants'] = enfants;
return base;
}
static Future<Map<String, dynamic>> amPatch(
AmRegistrationData data,
String token, {
String? existingPhotoUrl,
}) async {
final body = <String, dynamic>{'token': token.trim()};
_put(body, 'prenom', data.firstName.trim());
_put(body, 'nom', data.lastName.trim());
_put(body, 'telephone', data.phone.trim());
_put(body, 'adresse', data.streetAddress.trim());
_put(body, 'code_postal', data.postalCode.trim());
_put(body, 'ville', data.city.trim());
body['consentement_photo'] = data.photoConsent;
if (data.dateOfBirth != null) {
final d = data.dateOfBirth!;
body['date_naissance'] =
'${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
}
_put(body, 'lieu_naissance_ville', data.birthCity.trim());
_put(body, 'lieu_naissance_pays', data.birthCountry.trim());
_put(body, 'numero_agrement', data.agrementNumber.trim());
if (data.nir.trim().isNotEmpty) {
body['nir'] = normalizeNir(data.nir);
}
if (data.agreementDate != null) {
final d = data.agreementDate!;
body['date_agrement'] =
'${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
}
if (data.capacity != null) {
body['capacite_accueil'] = data.capacity;
}
if (data.placesAvailable != null) {
body['places_disponibles'] = data.placesAvailable;
}
if (data.presentationText.trim().isNotEmpty) {
body['biographie'] = data.presentationText.trim();
}
final photo = await _amPhotoPayload(data, existingPhotoUrl);
if (photo != null) {
body.addAll(photo);
}
return body;
}
static Future<Map<String, String>?> _amPhotoPayload(
AmRegistrationData data,
String? existingPhotoUrl,
) async {
if (data.photoBytes != null && data.photoBytes!.isNotEmpty) {
final mime = _imageMimeForBytes(data.photoBytes!);
final fn = (data.photoFilename ?? '').trim();
return {
'photo_base64':
'data:$mime;base64,${base64Encode(data.photoBytes!)}',
'photo_filename': fn.isNotEmpty ? fn : 'photo_am.jpg',
};
}
if (!kIsWeb &&
data.photoPath != null &&
data.photoPath!.isNotEmpty &&
!data.photoPath!.startsWith('assets/') &&
!data.photoPath!.startsWith('http')) {
try {
final file = File(data.photoPath!);
if (await file.exists()) {
final bytes = await file.readAsBytes();
final mime = _imageMimeForBytes(bytes);
return {
'photo_base64': 'data:$mime;base64,${base64Encode(bytes)}',
'photo_filename':
_basenameFromPath(data.photoPath!) ?? 'photo_am.jpg',
};
}
} catch (_) {}
}
final url = (existingPhotoUrl ?? data.repriseExistingPhotoUrl ?? '').trim();
if (url.isNotEmpty) {
return {'photo_url': url};
}
return null;
}
static String _imageMimeForBytes(Uint8List bytes) {
if (bytes.length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xD8) {
return 'image/jpeg';
}
if (bytes.length >= 8 &&
bytes[0] == 0x89 &&
bytes[1] == 0x50 &&
bytes[2] == 0x4E &&
bytes[3] == 0x47) {
return 'image/png';
}
return 'image/jpeg';
}
static String? _basenameFromPath(String path) {
final parts = path.replaceAll('\\', '/').split('/');
return parts.isEmpty ? null : parts.last;
}
static void _put(Map<String, dynamic> m, String key, String value) {
if (value.isNotEmpty) m[key] = value;
}
}