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:
@@ -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',
|
||||
|
||||
@@ -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}';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user