feat(front): inscription parent — soumission réelle vers POST /auth/register/parent (#101)
- ParentRegistrationPayload : validation client et mapping UserRegistrationData → JSON API - AuthService.registerParent - Étape 5 : appel API, chargement, erreurs en dialogue, succès inchangé puis /login Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
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}$');
|
||||
|
||||
/// 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 'L’email 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 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) {
|
||||
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 '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.isUnbornChild) {
|
||||
final due = _ddMmYyyyToIso(c.dob);
|
||||
if (due == null) {
|
||||
return '$label : indiquez une date de naissance prévisionnelle.';
|
||||
}
|
||||
} 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.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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': 'F',
|
||||
'grossesse_multiple': c.multipleBirth,
|
||||
};
|
||||
|
||||
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')}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user