petitspas/frontend/lib/models/pending_family.dart
Julien Martin cde676c4f9 feat: alignement master sur develop (squash)
- Dossiers unifiés #119, pending-families enrichi, validation admin (wizards)
- Front: modèles dossier_unifie / pending_family, NIR, auth
- Migrations dossier_famille, scripts de test API
- Résolution conflits: parents.*, docs tickets, auth_service, nir_utils

Made-with: Cursor
2026-03-26 00:20:47 +01:00

233 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.

/// Résumé affichable pour un parent (liste pending-families).
class PendingParentLine {
final String? email;
final String? telephone;
final String? codePostal;
final String? ville;
const PendingParentLine({
this.email,
this.telephone,
this.codePostal,
this.ville,
});
bool get isEmpty {
final e = email?.trim();
final t = telephone?.trim();
final loc = _locationTrimmed;
return (e == null || e.isEmpty) &&
(t == null || t.isEmpty) &&
(loc == null || loc.isEmpty);
}
String? get _locationTrimmed {
final cp = codePostal?.trim();
final v = ville?.trim();
final loc = [if (cp != null && cp.isNotEmpty) cp, if (v != null && v.isNotEmpty) v]
.join(' ')
.trim();
return loc.isEmpty ? null : loc;
}
}
/// Famille en attente de validation (GET /parents/pending-families). Ticket #107.
///
/// Contrat API : `libelle`, `parentIds`, `numero_dossier`, `date_soumission`,
/// `nombre_enfants`, `emails`, éventuellement `parents` / tableaux parallèles.
class PendingFamily {
final String libelle;
final List<String> parentIds;
final String? numeroDossier;
/// Date affichée : `date_soumission` (ISO), sinon alias `cree_le` / etc.
final DateTime? dateSoumission;
/// Emails seuls (API) — le sous-titre utilise de préférence [parentLines].
final List<String> emails;
/// Une entrée par parent : email, tél., CP ville (si fournis par lAPI).
final List<PendingParentLine> parentLines;
final int nombreEnfants;
/// Compat : premier email.
final String? email;
PendingFamily({
required this.libelle,
required this.parentIds,
this.numeroDossier,
this.dateSoumission,
this.emails = const [],
this.parentLines = const [],
this.nombreEnfants = 0,
this.email,
});
static DateTime? _parseDate(dynamic v) {
if (v == null) return null;
if (v is DateTime) return v;
if (v is String) {
return DateTime.tryParse(v);
}
return null;
}
static List<String> _parseStringList(dynamic raw) {
if (raw is! List) return [];
return raw
.map((e) => e?.toString().trim() ?? '')
.where((s) => s.isNotEmpty)
.toList();
}
static List<PendingParentLine> _parseParentLinesFromMaps(dynamic raw) {
if (raw is! List) return [];
final out = <PendingParentLine>[];
for (final e in raw) {
if (e is! Map) continue;
final m = Map<String, dynamic>.from(e);
final em = m['email']?.toString().trim();
final tel = m['telephone']?.toString().trim();
final cp = (m['code_postal'] ?? m['codePostal'])?.toString().trim();
final ville = m['ville']?.toString().trim();
out.add(PendingParentLine(
email: em != null && em.isNotEmpty ? em : null,
telephone: tel != null && tel.isNotEmpty ? tel : null,
codePostal: cp != null && cp.isNotEmpty ? cp : null,
ville: ville != null && ville.isNotEmpty ? ville : null,
));
}
return out;
}
/// Construit [parentLines] : objets `parents`, tableaux parallèles, ou emails + champs racine.
static List<PendingParentLine> _buildParentLines(
Map<String, dynamic> json,
List<String> emails,
) {
final fromMaps = _parseParentLinesFromMaps(
json['parents'] ?? json['resume_parents'] ?? json['parent_summaries'] ?? json['parent_lines'],
);
if (fromMaps.isNotEmpty) {
return fromMaps;
}
List<String>? parallel(dynamic keySingular, dynamic keyPlural) {
final pl = json[keyPlural];
if (pl is List) return _parseStringList(pl);
final s = json[keySingular];
if (s is String && s.trim().isNotEmpty) return [s.trim()];
return null;
}
final tels = parallel('telephone', 'telephones');
final cps = parallel('code_postal', 'code_postaux') ?? parallel('codePostal', 'codes_postaux');
final villes = parallel('ville', 'villes');
if (emails.isNotEmpty &&
((tels?.isNotEmpty ?? false) ||
(cps?.isNotEmpty ?? false) ||
(villes?.isNotEmpty ?? false))) {
return List.generate(emails.length, (i) {
return PendingParentLine(
email: emails[i],
telephone: tels != null && i < tels.length ? tels[i] : null,
codePostal: cps != null && i < cps.length ? cps[i] : null,
ville: villes != null && i < villes.length ? villes[i] : null,
);
});
}
final rootTel = json['telephone']?.toString().trim();
final rootTelOk = rootTel != null && rootTel.isNotEmpty ? rootTel : null;
final rootCp = (json['code_postal'] ?? json['codePostal'])?.toString().trim();
final rootCpOk = rootCp != null && rootCp.isNotEmpty ? rootCp : null;
final rootVille = json['ville']?.toString().trim();
final rootVilleOk = rootVille != null && rootVille.isNotEmpty ? rootVille : null;
if (emails.isNotEmpty) {
return List.generate(emails.length, (i) {
return PendingParentLine(
email: emails[i],
telephone: i == 0 ? rootTelOk : null,
codePostal: i == 0 ? rootCpOk : null,
ville: i == 0 ? rootVilleOk : null,
);
});
}
if (rootTelOk != null || rootCpOk != null || rootVilleOk != null) {
final em = json['email']?.toString().trim();
return [
PendingParentLine(
email: em != null && em.isNotEmpty ? em : null,
telephone: rootTelOk,
codePostal: rootCpOk,
ville: rootVilleOk,
),
];
}
return [];
}
factory PendingFamily.fromJson(Map<String, dynamic> json) {
final parentIdsRaw = json['parentIds'] ?? json['parent_ids'];
final List<String> ids = parentIdsRaw is List
? (parentIdsRaw).map((e) => e?.toString() ?? '').where((s) => s.isNotEmpty).toList()
: [];
final libelle = json['libelle'];
final libelleStr = libelle is String ? libelle : (libelle?.toString() ?? 'Famille');
final nd = json['numero_dossier'] ?? json['numeroDossier'];
final numeroDossier = (nd is String && nd.isNotEmpty) ? nd : null;
DateTime? dateSoumission = _parseDate(
json['date_soumission'] ?? json['dateSoumission'],
);
dateSoumission ??= _parseDate(
json['cree_le'] ?? json['creeLe'] ?? json['date_inscription'],
);
List<String> emails = _parseStringList(json['emails']);
if (emails.isEmpty) {
final emailRaw = json['email'];
if (emailRaw is String && emailRaw.trim().isNotEmpty) {
emails = [emailRaw.trim()];
}
}
final String? emailCompat = emails.isNotEmpty
? emails.first
: (json['email'] is String && (json['email'] as String).trim().isNotEmpty
? (json['email'] as String).trim()
: null);
final nbRaw = json['nombre_enfants'] ?? json['nombreEnfants'];
int nombreEnfants = 0;
if (nbRaw is int) {
nombreEnfants = nbRaw;
} else if (nbRaw is num) {
nombreEnfants = nbRaw.toInt();
} else if (nbRaw != null) {
nombreEnfants = int.tryParse(nbRaw.toString()) ?? 0;
}
var parentLines = _buildParentLines(json, emails);
if (parentLines.isEmpty && emails.isNotEmpty) {
parentLines = emails.map((e) => PendingParentLine(email: e)).toList();
}
return PendingFamily(
libelle: libelleStr.isEmpty ? 'Famille' : libelleStr,
parentIds: ids,
numeroDossier: numeroDossier,
dateSoumission: dateSoumission,
emails: emails,
parentLines: parentLines,
nombreEnfants: nombreEnfants,
email: emailCompat,
);
}
}