petitspas/frontend/lib/utils/parent_registration_payload.dart
Julien Martin 110240b682 feat(frontend): inscription — UI cartes enfant, formatage noms, focus Tab
- Formulaire infos perso : ordre de tabulation explicite, Checkbox Material
  pour le consentement photo, formatage nom/prénom/ville à la perte de focus
  et à la soumission (name_format_utils).
- Carte enfant : cadre photo, croix, ombres, consentement ; formatage
  prénom/nom enfant ; normalisation avant passage étape 4.
- Asset photo_frame.png ; HoverReliefWidget clipBehavior.
- Ajustements payload / modèle / étapes inscription liés au parcours.

Made-with: Cursor
2026-03-28 20:40:27 +01:00

226 lines
7.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import '../models/user_registration_data.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<String> 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;
if (p1.email.trim().isEmpty) {
return 'Lemail du parent principal est requis.';
}
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 nest 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) {
if (p2.email.trim().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 (p2.email.trim().toLowerCase() == p1.email.trim().toLowerCase()) {
return 'Lemail 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 nest 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<String, dynamic> toJson(UserRegistrationData d) {
final p1 = d.parent1;
final tel = _normalizePhone(p1.phone);
final body = <String, dynamic>{
'email': p1.email.trim(),
'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'] = p2.email.trim();
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;
}
static Map<String, dynamic> _childToJson(ChildData c, int index, String parentNom) {
final map = <String, dynamic>{
'genre': apiGenres.contains(c.genre) ? c.genre : 'Autre',
'grossesse_multiple': false,
};
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;
}
/// (`dataUrl`, `filename`) ou `null` si pas de fichier lisible.
static (String, String)? _childPhotoBase64(ChildData c, int index, String prenom) {
final file = c.imageFile;
if (file == null) return null;
try {
if (!file.existsSync()) return null;
final bytes = file.readAsBytesSync();
if (bytes.isEmpty) return null;
final b64 = base64Encode(bytes);
final safeName = prenom.isNotEmpty
? '${prenom.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '_')}.jpg'
: 'enfant_${index + 1}.jpg';
return ('data:image/jpeg;base64,$b64', safeName);
} catch (_) {
return null;
}
}
static void _putIfNonEmpty(Map<String, dynamic> 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')}';
}
}