240 lines
7.4 KiB
Dart
240 lines
7.4 KiB
Dart
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
||
import 'package:p_tits_pas/models/parent_model.dart';
|
||
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||
|
||
/// Ligne de liste unifiée dossiers (famille ou AM) — ticket #153.
|
||
enum DossierListType { famille, assistanteMaternelle }
|
||
|
||
class DossierListItem {
|
||
final DossierListType type;
|
||
final String numeroDossier;
|
||
final String libelle;
|
||
final List<String> emails;
|
||
final String? statut;
|
||
/// Photo profil (AM) — affichée à la place de l’icône si présente.
|
||
final String? photoUrl;
|
||
/// Dossier famille sans enfant lié (#159 / #160).
|
||
final bool sansEnfant;
|
||
/// Nombre d’enfants du foyer (famille uniquement ; null pour AM).
|
||
final int? enfantsCount;
|
||
|
||
const DossierListItem({
|
||
required this.type,
|
||
required this.numeroDossier,
|
||
required this.libelle,
|
||
this.emails = const [],
|
||
this.statut,
|
||
this.photoUrl,
|
||
this.sansEnfant = false,
|
||
this.enfantsCount,
|
||
});
|
||
|
||
DossierListItem copyWith({
|
||
DossierListType? type,
|
||
String? numeroDossier,
|
||
String? libelle,
|
||
List<String>? emails,
|
||
String? statut,
|
||
String? photoUrl,
|
||
bool? sansEnfant,
|
||
int? enfantsCount,
|
||
}) {
|
||
return DossierListItem(
|
||
type: type ?? this.type,
|
||
numeroDossier: numeroDossier ?? this.numeroDossier,
|
||
libelle: libelle ?? this.libelle,
|
||
emails: emails ?? this.emails,
|
||
statut: statut ?? this.statut,
|
||
photoUrl: photoUrl ?? this.photoUrl,
|
||
sansEnfant: sansEnfant ?? this.sansEnfant,
|
||
enfantsCount: enfantsCount ?? this.enfantsCount,
|
||
);
|
||
}
|
||
|
||
bool get isFamille => type == DossierListType.famille;
|
||
bool get isAm => type == DossierListType.assistanteMaternelle;
|
||
|
||
String get typeLabel => isFamille ? 'Famille' : 'AM';
|
||
|
||
/// Sous-titre carte : `NOM Prénom` ou `NOM Prénom - NOM Prénom`.
|
||
String get namesLine => libelle;
|
||
|
||
String get emailsLine => emails.where((e) => e.trim().isNotEmpty).join(' · ');
|
||
|
||
/// Titre carte : numéro de dossier seul.
|
||
String get titleLine => numeroDossier;
|
||
|
||
bool matchesQuery(String query) {
|
||
final q = query.trim().toLowerCase();
|
||
if (q.isEmpty) return true;
|
||
if (numeroDossier.toLowerCase().contains(q)) return true;
|
||
if (libelle.toLowerCase().contains(q)) return true;
|
||
for (final e in emails) {
|
||
if (e.toLowerCase().contains(q)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// Une ligne par `numero_dossier` (foyer dédupliqué).
|
||
static List<DossierListItem> fromParents(List<ParentModel> parents) {
|
||
final byDossier = <String, List<ParentModel>>{};
|
||
for (final p in parents) {
|
||
final num = (p.user.numeroDossier ?? '').trim();
|
||
if (num.isEmpty) continue;
|
||
byDossier.putIfAbsent(num, () => []).add(p);
|
||
}
|
||
|
||
final items = <DossierListItem>[];
|
||
for (final entry in byDossier.entries) {
|
||
final seenIds = <String>{};
|
||
final names = <String>[];
|
||
final emails = <String>[];
|
||
final statuts = <String>[];
|
||
|
||
void consider(
|
||
String? id,
|
||
String? nom,
|
||
String? prenom,
|
||
String? email,
|
||
String? statut,
|
||
) {
|
||
final uid = (id ?? '').trim();
|
||
if (uid.isEmpty || !seenIds.add(uid)) return;
|
||
final label = formatDossierPersonLabel(
|
||
nom: nom,
|
||
prenom: prenom,
|
||
email: email,
|
||
);
|
||
if (label.isNotEmpty) names.add(label);
|
||
final e = (email ?? '').trim();
|
||
if (e.isNotEmpty) emails.add(e);
|
||
final s = (statut ?? '').trim();
|
||
if (s.isNotEmpty) statuts.add(s);
|
||
}
|
||
|
||
for (final p in entry.value) {
|
||
consider(
|
||
p.user.id,
|
||
p.user.nom,
|
||
p.user.prenom,
|
||
p.user.email,
|
||
p.user.statut,
|
||
);
|
||
final co = p.coParent;
|
||
if (co != null) {
|
||
consider(co.id, co.nom, co.prenom, co.email, co.statut);
|
||
}
|
||
}
|
||
|
||
final childIds = <String>{};
|
||
var maxCountFallback = 0;
|
||
for (final p in entry.value) {
|
||
for (final c in p.children) {
|
||
final id = c.id.trim();
|
||
if (id.isNotEmpty) childIds.add(id);
|
||
}
|
||
final n = p.children.isNotEmpty ? p.children.length : p.childrenCount;
|
||
if (n > maxCountFallback) maxCountFallback = n;
|
||
}
|
||
final enfantsCount =
|
||
childIds.isNotEmpty ? childIds.length : maxCountFallback;
|
||
|
||
items.add(
|
||
DossierListItem(
|
||
type: DossierListType.famille,
|
||
numeroDossier: entry.key,
|
||
libelle: names.isNotEmpty ? names.join(' - ') : 'Famille',
|
||
emails: emails,
|
||
statut: _preferStatut(statuts),
|
||
enfantsCount: enfantsCount,
|
||
sansEnfant: enfantsCount == 0,
|
||
),
|
||
);
|
||
}
|
||
return items;
|
||
}
|
||
|
||
static List<DossierListItem> fromAssistantes(
|
||
List<AssistanteMaternelleModel> ams,
|
||
) {
|
||
final byDossier = <String, AssistanteMaternelleModel>{};
|
||
for (final am in ams) {
|
||
final num = (am.user.numeroDossier ?? '').trim();
|
||
if (num.isEmpty) continue;
|
||
byDossier.putIfAbsent(num, () => am);
|
||
}
|
||
|
||
return byDossier.entries.map((e) {
|
||
final u = e.value.user;
|
||
final name = formatDossierPersonLabel(
|
||
nom: u.nom,
|
||
prenom: u.prenom,
|
||
email: u.email,
|
||
);
|
||
final photo = (u.photoUrl ?? '').trim();
|
||
return DossierListItem(
|
||
type: DossierListType.assistanteMaternelle,
|
||
numeroDossier: e.key,
|
||
libelle: name.isNotEmpty ? name : 'AM',
|
||
emails: u.email.trim().isEmpty ? const [] : [u.email.trim()],
|
||
statut: u.statut?.trim(),
|
||
photoUrl: photo.isEmpty ? null : photo,
|
||
);
|
||
}).toList();
|
||
}
|
||
|
||
/// Priorité affichage : en_attente > suspendu > refuse > actif > autre.
|
||
static String? _preferStatut(List<String> raw) {
|
||
if (raw.isEmpty) return null;
|
||
const order = ['en_attente', 'suspendu', 'refuse', 'actif'];
|
||
for (final wanted in order) {
|
||
for (final s in raw) {
|
||
if (s.toLowerCase() == wanted) return s;
|
||
}
|
||
}
|
||
return raw.first;
|
||
}
|
||
}
|
||
|
||
/// Affichage carte dossier : `NOM Prénom` (repli email).
|
||
String formatDossierPersonLabel({
|
||
String? nom,
|
||
String? prenom,
|
||
String? email,
|
||
}) {
|
||
final n = (nom ?? '').trim().toUpperCase();
|
||
final p = formatPersonNameCase(prenom ?? '');
|
||
if (n.isNotEmpty && p.isNotEmpty) return '$n $p';
|
||
if (n.isNotEmpty) return n;
|
||
if (p.isNotEmpty) return p;
|
||
return (email ?? '').trim();
|
||
}
|
||
|
||
/// Reformate un libellé famille API (`A & B` / `Famille …`) en `NOM Prénom - …`.
|
||
String formatDossierFamilyNamesLine(String libelle) {
|
||
var raw = libelle.trim();
|
||
if (raw.isEmpty) return '';
|
||
raw = raw.replaceFirst(RegExp(r'^famille\s+', caseSensitive: false), '');
|
||
raw = raw
|
||
.replaceAll(RegExp(r'\s+&\s+'), ' - ')
|
||
.replaceAll(RegExp(r'\s+et\s+', caseSensitive: false), ' - ');
|
||
final parts = raw
|
||
.split(RegExp(r'\s+-\s+'))
|
||
.map((part) => _formatLoosePersonSegment(part.trim()))
|
||
.where((s) => s.isNotEmpty)
|
||
.toList();
|
||
return parts.join(' - ');
|
||
}
|
||
|
||
/// Segment libre type « martin sophie » ou « DURAND Amélie » → `NOM Prénom`.
|
||
String _formatLoosePersonSegment(String segment) {
|
||
final words =
|
||
segment.split(RegExp(r'\s+')).where((w) => w.isNotEmpty).toList();
|
||
if (words.isEmpty) return '';
|
||
if (words.length == 1) return words.first.toUpperCase();
|
||
// Convention affichage : premier mot = NOM, reste = prénom(s).
|
||
final nom = words.first.toUpperCase();
|
||
final prenom = formatPersonNameCase(words.sublist(1).join(' '));
|
||
return '$nom $prenom';
|
||
}
|