- 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
233 lines
7.4 KiB
Dart
233 lines
7.4 KiB
Dart
/// 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 l’API).
|
||
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,
|
||
);
|
||
}
|
||
}
|