/// 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 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 emails; /// Une entrée par parent : email, tél., CP ville (si fournis par l’API). final List 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 _parseStringList(dynamic raw) { if (raw is! List) return []; return raw .map((e) => e?.toString().trim() ?? '') .where((s) => s.isNotEmpty) .toList(); } static List _parseParentLinesFromMaps(dynamic raw) { if (raw is! List) return []; final out = []; for (final e in raw) { if (e is! Map) continue; final m = Map.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 _buildParentLines( Map json, List emails, ) { final fromMaps = _parseParentLinesFromMaps( json['parents'] ?? json['resume_parents'] ?? json['parent_summaries'] ?? json['parent_lines'], ); if (fromMaps.isNotEmpty) { return fromMaps; } List? 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 json) { final parentIdsRaw = json['parentIds'] ?? json['parent_ids']; final List 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 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, ); } }