import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Parents } from 'src/entities/parents.entity'; import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity'; import { StatutUtilisateurType, Users } from 'src/entities/users.entity'; import { ParentsService } from '../parents/parents.service'; import { SuppressionService } from '../suppressions/suppression.service'; import { DossierUnifieDto } from './dto/dossier-unifie.dto'; import { DossierAmCompletDto, DossierAmUserDto } from './dto/dossier-am-complet.dto'; import { DossierListItemDto } from './dto/dossier-list-item.dto'; /** * Dossiers unifiés — détail (#119) + liste (#153) + sans_enfant (#159). */ @Injectable() export class DossiersService { constructor( @InjectRepository(Parents) private readonly parentsRepository: Repository, @InjectRepository(AssistanteMaternelle) private readonly amRepository: Repository, private readonly parentsService: ParentsService, private readonly suppressionService: SuppressionService, ) {} /** * Liste unifiée tous dossiers (familles + AM) ayant un numero_dossier. * Ticket #153 — optionnel `q` filtre n° / nom / prénom / email (côté serveur). */ async listDossiers(q?: string): Promise { const items: DossierListItemDto[] = [ ...(await this.listFamilleItems()), ...(await this.listAmItems()), ]; const needle = (q ?? '').trim().toLowerCase(); const filtered = needle ? items.filter((item) => this.matchesQuery(item, needle)) : items; filtered.sort((a, b) => { // À valider d'abord, puis n° dossier décroissant if (a.a_valider !== b.a_valider) return a.a_valider ? -1 : 1; return b.numero_dossier.localeCompare(a.numero_dossier, 'fr'); }); for (const item of filtered) { if (item.type === 'famille') { const n = await this.suppressionService.countEnfantsForNumero( item.numero_dossier, ); item.sans_enfant = n === 0; } else { item.sans_enfant = false; } } return filtered; } private async listFamilleItems(): Promise { const parents = await this.parentsRepository .createQueryBuilder('p') .innerJoinAndSelect('p.user', 'u') .leftJoinAndSelect('p.co_parent', 'cp') .where('p.numero_dossier IS NOT NULL') .andWhere("TRIM(p.numero_dossier) <> ''") .getMany(); const byNum = new Map(); for (const p of parents) { const num = (p.numero_dossier ?? '').trim(); if (!num) continue; const group = byNum.get(num) ?? []; group.push(p); byNum.set(num, group); } const items: DossierListItemDto[] = []; for (const [numero_dossier, group] of byNum) { const usersMap = new Map(); for (const p of group) { if (p.user) usersMap.set(p.user.id, p.user); if (p.co_parent) usersMap.set(p.co_parent.id, p.co_parent); } const users = [...usersMap.values()].sort((a, b) => { const an = `${a.nom ?? ''} ${a.prenom ?? ''}`.toLowerCase(); const bn = `${b.nom ?? ''} ${b.prenom ?? ''}`.toLowerCase(); return an.localeCompare(bn, 'fr') || a.id.localeCompare(b.id); }); if (users.length === 0) continue; const names = users.map((u) => this.formatPersonName(u)).filter(Boolean); const libelle = names.length === 0 ? `Dossier ${numero_dossier}` : names.length === 1 ? names[0] : names.join(' & '); const emails = users.map((u) => u.email).filter(Boolean); const user_ids = users.map((u) => u.id); const a_valider = users.some((u) => u.statut === StatutUtilisateurType.EN_ATTENTE); const statut = a_valider ? StatutUtilisateurType.EN_ATTENTE : (users[0].statut ?? StatutUtilisateurType.ACTIF); const date_reference = this.minCreeLeIso(users); items.push({ type: 'famille', numero_dossier, libelle, emails, user_ids, statut, a_valider, date_reference, }); } return items; } private async listAmItems(): Promise { const ams = await this.amRepository .createQueryBuilder('am') .innerJoinAndSelect('am.user', 'u') .where('am.numero_dossier IS NOT NULL') .andWhere("TRIM(am.numero_dossier) <> ''") .getMany(); const byNum = new Map(); for (const am of ams) { const num = (am.numero_dossier ?? '').trim(); if (!num || !am.user) continue; // Un n° = une AM ; garder le premier if (!byNum.has(num)) byNum.set(num, am); } const items: DossierListItemDto[] = []; for (const [numero_dossier, am] of byNum) { const u = am.user!; const libelle = this.formatPersonName(u) || `AM ${numero_dossier}`; const a_valider = u.statut === StatutUtilisateurType.EN_ATTENTE; items.push({ type: 'assistante_maternelle', numero_dossier, libelle, emails: u.email ? [u.email] : [], user_ids: [u.id], statut: u.statut ?? StatutUtilisateurType.ACTIF, a_valider, date_reference: this.minCreeLeIso([u]), }); } return items; } private matchesQuery(item: DossierListItemDto, needle: string): boolean { const hay = [ item.numero_dossier, item.libelle, ...item.emails, item.statut, item.type, ] .join(' ') .toLowerCase(); return hay.includes(needle); } private formatPersonName(u: Users): string { const prenom = (u.prenom ?? '').trim(); const nom = (u.nom ?? '').trim(); const nomFmt = nom ? nom.toUpperCase() : ''; return [prenom, nomFmt].filter(Boolean).join(' '); } private minCreeLeIso(users: Users[]): string | null { let min: Date | null = null; for (const u of users) { const d = u.cree_le; if (!d) continue; const date = d instanceof Date ? d : new Date(d); if (Number.isNaN(date.getTime())) continue; if (!min || date < min) min = date; } return min ? min.toISOString() : null; } async getDossierByNumero(numeroDossier: string): Promise { const num = numeroDossier?.trim(); if (!num) { throw new NotFoundException('Numéro de dossier requis.'); } // 1) Famille : un parent a ce numéro ? const parentWithNum = await this.parentsRepository.findOne({ where: { numero_dossier: num }, select: ['user_id'], }); if (parentWithNum) { const dossier = await this.parentsService.getDossierFamilleByNumero(num); return { type: 'family', dossier }; } // 2) AM : une assistante maternelle a ce numéro ? const am = await this.amRepository.findOne({ where: { numero_dossier: num }, relations: ['user'], }); if (am?.user) { const dossier: DossierAmCompletDto = { numero_dossier: num, user: this.toDossierAmUserDto(am.user), numero_agrement: am.approval_number, nir: am.nir, biographie: am.biography, disponible: am.available, ville_residence: am.residence_city, date_agrement: am.agreement_date, annees_experience: am.years_experience, specialite: am.specialty, nb_max_enfants: am.max_children, place_disponible: am.places_available, }; return { type: 'am', dossier }; } throw new NotFoundException('Aucun dossier trouvé pour ce numéro.'); } private toDossierAmUserDto(user: { id: string; email: string; prenom?: string; nom?: string; telephone?: string; adresse?: string; ville?: string; code_postal?: string; profession?: string; date_naissance?: Date; lieu_naissance_ville?: string; lieu_naissance_pays?: string; photo_url?: string; consentement_photo?: boolean; statut: any; }): DossierAmUserDto { return { id: user.id, email: user.email, prenom: user.prenom, nom: user.nom, telephone: user.telephone, adresse: user.adresse, ville: user.ville, code_postal: user.code_postal, profession: user.profession, date_naissance: user.date_naissance, lieu_naissance_ville: user.lieu_naissance_ville, lieu_naissance_pays: user.lieu_naissance_pays, photo_url: user.photo_url, consentement_photo: user.consentement_photo, statut: user.statut, }; } }