From f300505225bac6257a8dd68dccea91892a1ad813 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Tue, 16 Jun 2026 16:46:51 +0200 Subject: [PATCH] feat(#112): aligner reprise front sur dossier complet GET/PATCH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/lib/models/am_registration_data.dart | 34 ++-- frontend/lib/models/dossier_unifie.dart | 4 + frontend/lib/models/reprise_dossier.dart | 66 +++++++- .../lib/models/user_registration_data.dart | 47 +++--- .../auth/am_register_step4_screen.dart | 14 +- .../auth/parent_register_step5_screen.dart | 12 +- frontend/lib/services/auth_service.dart | 33 +--- frontend/lib/services/reprise_session.dart | 37 +++-- .../utils/parent_registration_payload.dart | 14 ++ frontend/lib/utils/reprise_mapper.dart | 120 ++++++++++++++ frontend/lib/utils/reprise_payload.dart | 150 ++++++++++++++++++ 11 files changed, 439 insertions(+), 92 deletions(-) create mode 100644 frontend/lib/utils/reprise_mapper.dart create mode 100644 frontend/lib/utils/reprise_payload.dart diff --git a/frontend/lib/models/am_registration_data.dart b/frontend/lib/models/am_registration_data.dart index d61cbdd..08ecc60 100644 --- a/frontend/lib/models/am_registration_data.dart +++ b/frontend/lib/models/am_registration_data.dart @@ -117,6 +117,16 @@ class AmRegistrationData extends ChangeNotifier { required String postalCode, required String city, String? existingPhotoUrl, + bool consentementPhoto = false, + DateTime? dateOfBirth, + String birthCity = '', + String birthCountry = '', + String nir = '', + String agrementNumber = '', + DateTime? agreementDate, + int? capacity, + int? placesAvailable, + String presentationText = '', }) { this.firstName = firstName; this.lastName = lastName; @@ -130,16 +140,16 @@ class AmRegistrationData extends ChangeNotifier { photoPath = existingPhotoUrl; photoBytes = null; photoFilename = null; - photoConsent = false; - dateOfBirth = null; - birthCity = ''; - birthCountry = ''; - nir = ''; - agrementNumber = ''; - agreementDate = null; - capacity = null; - placesAvailable = null; - presentationText = ''; + photoConsent = consentementPhoto; + dateOfBirth = dateOfBirth; + this.birthCity = birthCity; + this.birthCountry = birthCountry; + this.nir = nir; + this.agrementNumber = agrementNumber; + agreementDate = agreementDate; + this.capacity = capacity; + placesAvailable = placesAvailable; + this.presentationText = presentationText; cguAccepted = false; notifyListeners(); } @@ -187,8 +197,8 @@ class AmRegistrationData extends ChangeNotifier { bool get isRegistrationComplete => isStep1Complete && isStep2Complete && isStep3Complete; - /// Reprise (#112) : champs renvoyés par PATCH reprise-resoumettre. - bool get isRepriseSubmitReady => isStep1Complete && cguAccepted; + /// Reprise (#112) : dossier AM complet renvoyé par PATCH reprise-resoumettre. + bool get isRepriseSubmitReady => isRegistrationComplete; @override String toString() { diff --git a/frontend/lib/models/dossier_unifie.dart b/frontend/lib/models/dossier_unifie.dart index 98cde26..07b2d56 100644 --- a/frontend/lib/models/dossier_unifie.dart +++ b/frontend/lib/models/dossier_unifie.dart @@ -184,6 +184,7 @@ class EnfantDossier { final String? dueDate; final String? photoUrl; final bool consentPhoto; + final bool estMultiple; EnfantDossier({ required this.id, @@ -195,6 +196,7 @@ class EnfantDossier { this.dueDate, this.photoUrl, this.consentPhoto = false, + this.estMultiple = false, }); String get fullName => '${firstName ?? ''} ${lastName ?? ''}'.trim(); @@ -223,6 +225,8 @@ class EnfantDossier { photoUrl: resolvedPhoto, consentPhoto: json['consent_photo'] == true || json['consentPhoto'] == true, + estMultiple: + json['est_multiple'] == true || json['estMultiple'] == true, ); } } diff --git a/frontend/lib/models/reprise_dossier.dart b/frontend/lib/models/reprise_dossier.dart index e0cc685..0275354 100644 --- a/frontend/lib/models/reprise_dossier.dart +++ b/frontend/lib/models/reprise_dossier.dart @@ -1,4 +1,6 @@ -/// Réponse GET /auth/reprise-dossier. Ticket #111, #112. +import 'package:p_tits_pas/models/dossier_unifie.dart'; + +/// Réponse GET /auth/reprise-dossier. Tickets #111, #112. class RepriseDossier { final String id; final String email; @@ -14,6 +16,21 @@ class RepriseDossier { final String? genre; final String? situationFamiliale; + final List parents; + final List enfants; + final String? texteMotivation; + + final bool? consentementPhoto; + final String? dateNaissance; + final String? lieuNaissanceVille; + final String? lieuNaissancePays; + final String? numeroAgrement; + final String? nir; + final String? dateAgrement; + final int? nbMaxEnfants; + final int? placeDisponible; + final String? biographie; + const RepriseDossier({ required this.id, required this.email, @@ -28,12 +45,44 @@ class RepriseDossier { this.photoUrl, this.genre, this.situationFamiliale, + this.parents = const [], + this.enfants = const [], + this.texteMotivation, + this.consentementPhoto, + this.dateNaissance, + this.lieuNaissanceVille, + this.lieuNaissancePays, + this.numeroAgrement, + this.nir, + this.dateAgrement, + this.nbMaxEnfants, + this.placeDisponible, + this.biographie, }); bool get isParent => role == 'parent'; bool get isAm => role == 'assistante_maternelle'; factory RepriseDossier.fromJson(Map json) { + final parentsRaw = json['parents']; + final parentsList = parentsRaw is List + ? parentsRaw + .where((e) => e is Map) + .map((e) => ParentDossier.fromJson(Map.from(e as Map))) + .toList() + : []; + + final enfantsRaw = json['enfants']; + final enfantsList = enfantsRaw is List + ? enfantsRaw + .where((e) => e is Map) + .map((e) => EnfantDossier.fromJson(Map.from(e as Map))) + .toList() + : []; + + final nbMax = json['nb_max_enfants']; + final places = json['place_disponible']; + return RepriseDossier( id: json['id']?.toString() ?? '', email: json['email']?.toString() ?? '', @@ -48,6 +97,21 @@ class RepriseDossier { photoUrl: json['photo_url']?.toString(), genre: json['genre']?.toString(), situationFamiliale: json['situation_familiale']?.toString(), + parents: parentsList, + enfants: enfantsList, + texteMotivation: (json['texte_motivation'] ?? json['presentation_dossier']) + ?.toString(), + consentementPhoto: json['consentement_photo'] == true, + dateNaissance: json['date_naissance']?.toString(), + lieuNaissanceVille: json['lieu_naissance_ville']?.toString(), + lieuNaissancePays: json['lieu_naissance_pays']?.toString(), + numeroAgrement: json['numero_agrement']?.toString(), + nir: json['nir']?.toString(), + dateAgrement: json['date_agrement']?.toString(), + nbMaxEnfants: nbMax is int ? nbMax : (nbMax is num ? nbMax.toInt() : null), + placeDisponible: + places is int ? places : (places is num ? places.toInt() : null), + biographie: json['biographie']?.toString(), ); } } diff --git a/frontend/lib/models/user_registration_data.dart b/frontend/lib/models/user_registration_data.dart index 5f29ce5..e418e7d 100644 --- a/frontend/lib/models/user_registration_data.dart +++ b/frontend/lib/models/user_registration_data.dart @@ -43,6 +43,10 @@ class ChildData { /// Octets de la photo (fiable à l’envoi API ; [imageFile] peut être absent sur le web). Uint8List? imageBytes; CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte + /// UUID enfant en base (reprise #112) — requis pour PATCH reprise-resoumettre. + String? repriseChildId; + /// Photo déjà stockée (affichage reprise sans re-upload). + String? existingPhotoUrl; ChildData({ this.firstName = '', @@ -55,6 +59,8 @@ class ChildData { this.imageFile, this.imageBytes, required this.cardColor, // Rendre requis dans le constructeur + this.repriseChildId, + this.existingPhotoUrl, }); ChildData copyWith({ @@ -68,6 +74,8 @@ class ChildData { Object? imageFile = _unsetImage, Object? imageBytes = _unsetImageBytes, CardColorVertical? cardColor, + String? repriseChildId, + String? existingPhotoUrl, }) { return ChildData( firstName: firstName ?? this.firstName, @@ -81,6 +89,8 @@ class ChildData { imageBytes: identical(imageBytes, _unsetImageBytes) ? this.imageBytes : imageBytes as Uint8List?, cardColor: cardColor ?? this.cardColor, + repriseChildId: repriseChildId ?? this.repriseChildId, + existingPhotoUrl: existingPhotoUrl ?? this.existingPhotoUrl, ); } } @@ -180,27 +190,17 @@ class UserRegistrationData extends ChangeNotifier { /// Reprise après refus (#112) : réinitialise le flux parent avec les données dossier. void resetForReprise({ - required String firstName, - required String lastName, - required String phone, - required String email, - required String address, - required String postalCode, - required String city, + required ParentData parent1Data, + ParentData? parent2Data, + List? childrenData, + String motivation = '', }) { - parent1 = ParentData( - firstName: firstName, - lastName: lastName, - phone: phone, - email: email, - address: address, - postalCode: postalCode, - city: city, - password: '', - ); - parent2 = null; - children.clear(); - motivationText = ''; + parent1 = parent1Data; + parent2 = parent2Data; + children + ..clear() + ..addAll(childrenData ?? const []); + motivationText = motivation; cguAccepted = false; bankDetails = null; attestationCafNumber = ''; @@ -208,11 +208,16 @@ class UserRegistrationData extends ChangeNotifier { notifyListeners(); } - /// Reprise (#112) : coordonnées parent principal + CGU. + /// Reprise (#112) : coordonnées + enfants connus + CGU. bool get isRepriseSubmitReady => parent1.firstName.isNotEmpty && parent1.lastName.isNotEmpty && parent1.email.isNotEmpty && + children.isNotEmpty && + children.every( + (c) => + c.repriseChildId != null && c.repriseChildId!.trim().isNotEmpty, + ) && cguAccepted; // Méthode pour vérifier si toutes les données requises sont là (simplifié) diff --git a/frontend/lib/screens/auth/am_register_step4_screen.dart b/frontend/lib/screens/auth/am_register_step4_screen.dart index 3a93079..83520b9 100644 --- a/frontend/lib/screens/auth/am_register_step4_screen.dart +++ b/frontend/lib/screens/auth/am_register_step4_screen.dart @@ -9,6 +9,7 @@ import '../../models/card_assets.dart'; import '../../config/display_config.dart'; import '../../services/auth_service.dart'; import '../../services/reprise_session.dart'; +import '../../utils/reprise_payload.dart'; import '../../widgets/hover_relief_widget.dart'; import '../../widgets/image_button.dart'; import '../../widgets/custom_navigation_button.dart'; @@ -59,14 +60,11 @@ class _AmRegisterStep4ScreenState extends State { try { if (RepriseSession.isActive) { await AuthService.resoumettreReprise( - token: RepriseSession.token!, - prenom: registrationData.firstName, - nom: registrationData.lastName, - telephone: registrationData.phone, - adresse: registrationData.streetAddress, - ville: registrationData.city, - codePostal: registrationData.postalCode, - photoUrl: RepriseSession.photoUrl, + await ReprisePayload.amPatch( + registrationData, + RepriseSession.token!, + existingPhotoUrl: RepriseSession.photoUrlForApi, + ), ); RepriseSession.clear(); if (!mounted) return; diff --git a/frontend/lib/screens/auth/parent_register_step5_screen.dart b/frontend/lib/screens/auth/parent_register_step5_screen.dart index ac0512f..87ef640 100644 --- a/frontend/lib/screens/auth/parent_register_step5_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step5_screen.dart @@ -15,6 +15,7 @@ import '../../widgets/child_card_widget.dart'; import '../../widgets/presentation_form_screen.dart'; import '../../services/auth_service.dart'; import '../../services/reprise_session.dart'; +import '../../utils/reprise_payload.dart'; class ParentRegisterStep5Screen extends StatefulWidget { const ParentRegisterStep5Screen({super.key}); @@ -33,7 +34,7 @@ class _ParentRegisterStep5ScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Vérifiez vos coordonnées et acceptez les conditions.', + 'Vérifiez vos coordonnées, les enfants et acceptez les conditions.', style: GoogleFonts.merienda(fontSize: 14), ), backgroundColor: Colors.red.shade700, @@ -46,15 +47,8 @@ class _ParentRegisterStep5ScreenState extends State { try { if (RepriseSession.isActive) { final token = RepriseSession.token!; - final p = data.parent1; await AuthService.resoumettreReprise( - token: token, - prenom: p.firstName, - nom: p.lastName, - telephone: p.phone, - adresse: p.address, - ville: p.city, - codePostal: p.postalCode, + ReprisePayload.parentPatch(data, token), ); RepriseSession.clear(); if (!context.mounted) return; diff --git a/frontend/lib/services/auth_service.dart b/frontend/lib/services/auth_service.dart index e89ca5e..956339a 100644 --- a/frontend/lib/services/auth_service.dart +++ b/frontend/lib/services/auth_service.dart @@ -291,43 +291,20 @@ class AuthService { } /// Resoumission après refus. PATCH /auth/reprise-resoumettre. Ticket #112. - static Future resoumettreReprise({ - required String token, - String? prenom, - String? nom, - String? telephone, - String? adresse, - String? ville, - String? codePostal, - String? photoUrl, - }) async { - final cleaned = token.trim(); + static Future resoumettreReprise(Map body) async { + final cleaned = body['token']?.toString().trim() ?? ''; if (cleaned.isEmpty) { throw Exception('Lien invalide ou expiré.'); } - final body = {'token': cleaned}; - if (prenom != null && prenom.trim().isNotEmpty) body['prenom'] = prenom.trim(); - if (nom != null && nom.trim().isNotEmpty) body['nom'] = nom.trim(); - if (telephone != null && telephone.trim().isNotEmpty) { - body['telephone'] = telephone.trim(); - } - if (adresse != null && adresse.trim().isNotEmpty) { - body['adresse'] = adresse.trim(); - } - if (ville != null && ville.trim().isNotEmpty) body['ville'] = ville.trim(); - if (codePostal != null && codePostal.trim().isNotEmpty) { - body['code_postal'] = codePostal.trim(); - } - if (photoUrl != null && photoUrl.trim().isNotEmpty) { - body['photo_url'] = photoUrl.trim(); - } + final payload = Map.from(body); + payload['token'] = cleaned; late final http.Response response; try { response = await http.patch( Uri.parse('${ApiConfig.baseUrl}${ApiConfig.repriseResoumettre}'), headers: ApiConfig.headers, - body: jsonEncode(body), + body: jsonEncode(payload), ); } on http.ClientException { throw Exception( diff --git a/frontend/lib/services/reprise_session.dart b/frontend/lib/services/reprise_session.dart index 637d158..8309371 100644 --- a/frontend/lib/services/reprise_session.dart +++ b/frontend/lib/services/reprise_session.dart @@ -2,6 +2,7 @@ import 'package:p_tits_pas/models/am_registration_data.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'; +import 'package:p_tits_pas/utils/reprise_mapper.dart'; /// Contexte reprise après refus (token e-mail ou identify). Ticket #112. class RepriseSession { @@ -10,6 +11,7 @@ class RepriseSession { static String? _token; static String? _role; static String? _photoUrl; + static String? _photoUrlForApi; static bool get isActive => _token != null && _token!.trim().isNotEmpty; @@ -20,16 +22,22 @@ class RepriseSession { static bool get isAm => _role == 'assistante_maternelle'; + /// URL absolue pour l'affichage. static String? get photoUrl => _photoUrl; + /// Chemin relatif API (`/uploads/…`) pour PATCH sans re-upload. + static String? get photoUrlForApi => _photoUrlForApi; + static void start({ required String token, required RepriseDossier dossier, }) { _token = token.trim(); _role = dossier.role; - _photoUrl = dossier.photoUrl?.trim().isNotEmpty == true - ? ApiConfig.absoluteMediaUrl(dossier.photoUrl) + final raw = dossier.photoUrl?.trim(); + _photoUrlForApi = raw != null && raw.isNotEmpty ? raw : null; + _photoUrl = _photoUrlForApi != null + ? ApiConfig.absoluteMediaUrl(_photoUrlForApi) : null; } @@ -37,22 +45,15 @@ class RepriseSession { _token = null; _role = null; _photoUrl = null; + _photoUrlForApi = null; } static void applyToParent(UserRegistrationData data, RepriseDossier dossier) { - data.resetForReprise( - firstName: dossier.prenom ?? '', - lastName: dossier.nom ?? '', - phone: dossier.telephone ?? '', - email: dossier.email, - address: dossier.adresse ?? '', - postalCode: dossier.codePostal ?? '', - city: dossier.ville ?? '', - ); + RepriseMapper.applyParentDossier(data, dossier); } static void applyToAm(AmRegistrationData data, RepriseDossier dossier) { - final photo = _photoUrl; + final displayPhoto = _photoUrl; data.resetForReprise( firstName: dossier.prenom ?? '', lastName: dossier.nom ?? '', @@ -61,7 +62,17 @@ class RepriseSession { streetAddress: dossier.adresse ?? '', postalCode: dossier.codePostal ?? '', city: dossier.ville ?? '', - existingPhotoUrl: photo, + existingPhotoUrl: displayPhoto, + consentementPhoto: dossier.consentementPhoto ?? false, + dateOfBirth: RepriseMapper.parseIsoDate(dossier.dateNaissance), + birthCity: dossier.lieuNaissanceVille ?? '', + birthCountry: dossier.lieuNaissancePays ?? '', + nir: dossier.nir ?? '', + agrementNumber: dossier.numeroAgrement ?? '', + agreementDate: RepriseMapper.parseIsoDate(dossier.dateAgrement), + capacity: dossier.nbMaxEnfants, + placesAvailable: dossier.placeDisponible, + presentationText: dossier.biographie ?? '', ); } } diff --git a/frontend/lib/utils/parent_registration_payload.dart b/frontend/lib/utils/parent_registration_payload.dart index 05f8d79..70a9459 100644 --- a/frontend/lib/utils/parent_registration_payload.dart +++ b/frontend/lib/utils/parent_registration_payload.dart @@ -149,6 +149,20 @@ class ParentRegistrationPayload { return body; } + /// Enfant pour PATCH reprise (inclut `id` si connu). + static Map 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 _childToJson(ChildData c, int index, String parentNom) { final map = { 'genre': apiGenres.contains(c.genre) ? c.genre : 'Autre', diff --git a/frontend/lib/utils/reprise_mapper.dart b/frontend/lib/utils/reprise_mapper.dart new file mode 100644 index 0000000..a44947a --- /dev/null +++ b/frontend/lib/utils/reprise_mapper.dart @@ -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 _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}'; + } +} diff --git a/frontend/lib/utils/reprise_payload.dart b/frontend/lib/utils/reprise_payload.dart new file mode 100644 index 0000000..6ce887a --- /dev/null +++ b/frontend/lib/utils/reprise_payload.dart @@ -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 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> amPatch( + AmRegistrationData data, + String token, { + String? existingPhotoUrl, + }) async { + final body = {'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?> _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 m, String key, String value) { + if (value.isNotEmpty) m[key] = value; + } +}