import 'dart:convert'; import 'dart:typed_data'; import '../models/user_registration_data.dart'; import 'email_utils.dart'; /// Construction du body `POST /auth/register/parent` à partir du state d'inscription. /// Aligné sur [RegisterParentCompletDto] (backend). class ParentRegistrationPayload { ParentRegistrationPayload._(); static final RegExp _phoneFr = RegExp(r'^(\+33|0)[1-9](\d{2}){4}$'); /// Aligné sur `GenreType` (backend) : JSON exact `H`, `F`, `Autre`. static const Set apiGenres = {'H', 'F', 'Autre'}; /// Retourne un message d'erreur utilisateur, ou `null` si le formulaire est cohérent. static String? validateForApi(UserRegistrationData d) { final p1 = d.parent1; final p1Email = normalizeEmailText(p1.email); if (p1Email.isEmpty) { return 'L’email du parent principal est requis.'; } if (!isValidEmailFormat(p1Email)) { return 'L’email du parent principal n’est pas au bon format.'; } if (p1.firstName.trim().length < 2) { return 'Le prénom du parent principal doit contenir au moins 2 caractères.'; } if (p1.lastName.trim().length < 2) { return 'Le nom du parent principal doit contenir au moins 2 caractères.'; } final tel = _normalizePhone(p1.phone); if (!_phoneFr.hasMatch(tel)) { return 'Le numéro de téléphone du parent principal n’est pas valide (ex. 0612345678).'; } if (!d.cguAccepted) { return 'Vous devez accepter les CGU et la politique de confidentialité.'; } if (d.children.isEmpty) { return 'Au moins un enfant est requis.'; } final p2 = d.parent2; if (p2 != null) { final any = p2.email.trim().isNotEmpty || p2.firstName.trim().isNotEmpty || p2.lastName.trim().isNotEmpty || p2.phone.trim().isNotEmpty; if (any) { final p2Email = normalizeEmailText(p2.email); if (p2Email.isEmpty || p2.firstName.trim().length < 2 || p2.lastName.trim().length < 2) { return 'Les informations du co-parent sont incomplètes (email, prénom et nom requis).'; } if (!isValidEmailFormat(p2Email)) { return 'L’email du co-parent n’est pas au bon format.'; } if (p2Email == p1Email) { return 'L’email du co-parent doit être différent de celui du parent principal.'; } final tel2 = _normalizePhone(p2.phone); if (tel2.isNotEmpty && !_phoneFr.hasMatch(tel2)) { return 'Le numéro de téléphone du co-parent n’est pas valide.'; } } } for (var i = 0; i < d.children.length; i++) { final c = d.children[i]; final label = 'Enfant ${i + 1}'; if (!c.photoConsent) { return '$label : le consentement photo est obligatoire.'; } if (c.isUnbornChild) { final due = _ddMmYyyyToIso(c.dob); if (due == null) { return '$label : indiquez une date de naissance prévisionnelle.'; } if (!apiGenres.contains(c.genre)) { return '$label : indiquez Fille, Garçon ou Inconnu.'; } } else { if (c.firstName.trim().length < 2) { return '$label : le prénom doit contenir au moins 2 caractères.'; } final birth = _ddMmYyyyToIso(c.dob); if (birth == null) { return '$label : indiquez une date de naissance valide.'; } if (c.genre != 'H' && c.genre != 'F') { return '$label : indiquez Fille ou Garçon.'; } } } return null; } static Map toJson(UserRegistrationData d) { final p1 = d.parent1; final tel = _normalizePhone(p1.phone); final body = { 'email': normalizeEmailText(p1.email), 'prenom': p1.firstName.trim(), 'nom': p1.lastName.trim(), 'telephone': tel, 'acceptation_cgu': d.cguAccepted, 'acceptation_privacy': d.cguAccepted, }; _putIfNonEmpty(body, 'adresse', p1.address.trim()); _putIfNonEmpty(body, 'code_postal', p1.postalCode.trim()); _putIfNonEmpty(body, 'ville', p1.city.trim()); final p2 = d.parent2; if (p2 != null && p2.email.trim().isNotEmpty && p2.firstName.trim().length >= 2 && p2.lastName.trim().length >= 2) { final tel2 = _normalizePhone(p2.phone); final sameAddr = p2.address.trim() == p1.address.trim() && p2.postalCode.trim() == p1.postalCode.trim() && p2.city.trim() == p1.city.trim(); body['co_parent_email'] = normalizeEmailText(p2.email); body['co_parent_prenom'] = p2.firstName.trim(); body['co_parent_nom'] = p2.lastName.trim(); body['co_parent_meme_adresse'] = sameAddr; if (tel2.isNotEmpty) { body['co_parent_telephone'] = tel2; } if (!sameAddr) { _putIfNonEmpty(body, 'co_parent_adresse', p2.address.trim()); _putIfNonEmpty(body, 'co_parent_code_postal', p2.postalCode.trim()); _putIfNonEmpty(body, 'co_parent_ville', p2.city.trim()); } } if (d.motivationText.trim().isNotEmpty) { body['presentation_dossier'] = d.motivationText.trim(); } body['enfants'] = d.children.asMap().entries.map((e) { return _childToJson(e.value, e.key, d.parent1.lastName.trim()); }).toList(); 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', 'grossesse_multiple': c.multipleBirth, 'consent_photo': c.photoConsent, }; final prenom = c.firstName.trim(); if (prenom.length >= 2) { map['prenom'] = prenom; } final nom = c.lastName.trim(); if (nom.length >= 2) { map['nom'] = nom; } else if (parentNom.length >= 2) { map['nom'] = parentNom; } if (c.isUnbornChild) { final due = _ddMmYyyyToIso(c.dob); if (due != null) { map['date_previsionnelle_naissance'] = due; } } else { final birth = _ddMmYyyyToIso(c.dob); if (birth != null) { map['date_naissance'] = birth; } } final photo = _childPhotoBase64(c, index, prenom); if (photo != null) { map['photo_base64'] = photo.$1; map['photo_filename'] = photo.$2; } return map; } /// Sous-type `image/…` pour data-URL et extension côté API (`jpeg`, `png`, `gif`, `heic`, …). static String _imageMimeFromMagicBytes(Uint8List bytes) { if (bytes.length >= 8) { if (bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47) { return 'png'; } if (bytes[0] == 0xFF && bytes[1] == 0xD8) { return 'jpeg'; } if (bytes.length >= 12 && bytes[0] == 0x52 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x46) { return 'webp'; } if (bytes.length >= 6 && bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x38 && (bytes[4] == 0x37 || bytes[4] == 0x39) && bytes[5] == 0x61) { return 'gif'; } if (bytes.length >= 12 && bytes[4] == 0x66 && bytes[5] == 0x74 && bytes[6] == 0x79 && bytes[7] == 0x70) { final a = bytes[8], b = bytes[9], c = bytes[10], d = bytes[11]; if ((a == 0x68 && b == 0x65 && c == 0x69 && d == 0x63) || (a == 0x68 && b == 0x65 && c == 0x69 && d == 0x78) || (a == 0x6d && b == 0x69 && c == 0x66 && d == 0x31) || (a == 0x6d && b == 0x73 && c == 0x66 && d == 0x31)) { return 'heic'; } } } return 'jpeg'; } /// (`dataUrl`, `filename`) ou `null` si pas de fichier lisible. static (String, String)? _childPhotoBase64(ChildData c, int index, String prenom) { Uint8List? bytes = c.imageBytes; if (bytes == null || bytes.isEmpty) { final file = c.imageFile; if (file == null) return null; try { if (!file.existsSync()) return null; bytes = file.readAsBytesSync(); } catch (_) { return null; } } if (bytes.isEmpty) return null; final b64 = base64Encode(bytes); final mime = _imageMimeFromMagicBytes(bytes); final safeName = prenom.isNotEmpty ? '${prenom.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '_')}.$mime' : 'enfant_${index + 1}.$mime'; return ('data:image/$mime;base64,$b64', safeName); } static void _putIfNonEmpty(Map m, String key, String value) { if (value.isNotEmpty) { m[key] = value; } } static String _normalizePhone(String raw) { var s = raw.replaceAll(RegExp(r'\s'), ''); if (s.startsWith('0033')) { s = '+33${s.substring(4)}'; } return s; } /// `jj/mm/aaaa` → `aaaa-mm-jj`, ou `null` si invalide. static String? _ddMmYyyyToIso(String ddMmYyyy) { if (ddMmYyyy.isEmpty) return null; final parts = ddMmYyyy.split('/'); if (parts.length != 3) return null; final day = int.tryParse(parts[0]); final month = int.tryParse(parts[1]); final year = int.tryParse(parts[2]); if (day == null || month == null || year == null) return null; if (month < 1 || month > 12 || day < 1 || day > 31) return null; return '${year.toString().padLeft(4, '0')}-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}'; } }