import { BadRequestException, ConflictException, Injectable, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { Parents } from 'src/entities/parents.entity'; import { DossierFamille } from 'src/entities/dossier_famille.entity'; import { RoleType, Users } from 'src/entities/users.entity'; import { CreateParentDto } from '../user/dto/create_parent.dto'; import { UpdateParentsDto } from '../user/dto/update_parent.dto'; import { PendingFamilyDto } from './dto/pending-family.dto'; import { DossierFamilleCompletDto, DossierFamilleParentDto, DossierFamilleEnfantDto, DossierFamillePresentationDto, } from './dto/dossier-famille-complet.dto'; @Injectable() export class ParentsService { constructor( @InjectRepository(Parents) private readonly parentsRepository: Repository, @InjectRepository(Users) private readonly usersRepository: Repository, @InjectRepository(DossierFamille) private readonly dossierFamilleRepository: Repository, ) {} // Création d’un parent async create(dto: CreateParentDto): Promise { const user = await this.usersRepository.findOneBy({ id: dto.user_id }); if (!user) throw new NotFoundException('Utilisateur introuvable'); if (user.role !== RoleType.PARENT) { throw new BadRequestException('Accès réservé aux parents'); } const exist = await this.parentsRepository.findOneBy({ user_id: dto.user_id }); if (exist) throw new ConflictException('Ce parent existe déjà'); let co_parent: Users | null = null; if (dto.co_parent_id) { co_parent = await this.usersRepository.findOneBy({ id: dto.co_parent_id }); if (!co_parent) throw new NotFoundException('Co-parent introuvable'); if (co_parent.role !== RoleType.PARENT) { throw new BadRequestException('Accès réservé aux parents'); } } const entity = this.parentsRepository.create({ user_id: dto.user_id, user, co_parent: co_parent ?? undefined, }); return this.parentsRepository.save(entity); } // Liste des parents async findAll(): Promise { return this.parentsRepository.find({ relations: ['user', 'co_parent', 'parentChildren', 'dossiers'], }); } // Récupérer un parent par user_id async findOne(user_id: string): Promise { const parent = await this.parentsRepository.findOne({ where: { user_id }, relations: ['user', 'co_parent', 'parentChildren', 'dossiers'], }); if (!parent) throw new NotFoundException('Parent introuvable'); return parent; } // Mise à jour async update(id: string, dto: UpdateParentsDto): Promise { await this.parentsRepository.update(id, dto); return this.findOne(id); } /** * Liste des familles en attente (une entrée par famille). * Famille = lien co_parent ou partage d'enfants (même logique que backfill #103). * Uniquement les parents dont l'utilisateur a statut = en_attente. */ async getPendingFamilies(): Promise { let raw: { libelle: string; parentIds: unknown; numero_dossier: string | null }[]; try { raw = await this.parentsRepository.query(` WITH RECURSIVE links AS ( SELECT p.id_utilisateur AS p1, p.id_co_parent AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL UNION ALL SELECT p.id_co_parent AS p1, p.id_utilisateur AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL UNION ALL SELECT ep1.id_parent AS p1, ep2.id_parent AS p2 FROM enfants_parents ep1 JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent UNION ALL SELECT ep2.id_parent AS p1, ep1.id_parent AS p2 FROM enfants_parents ep1 JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent ), rec AS ( SELECT id_utilisateur AS id, id_utilisateur AS rep FROM parents UNION SELECT l.p2 AS id, LEAST(rec_alias.rep, l.p2) AS rep FROM links l JOIN rec rec_alias ON rec_alias.id = l.p1 ), family_rep AS ( SELECT id, (MIN(rep::text))::uuid AS rep FROM rec GROUP BY id ) SELECT 'Famille ' || string_agg(u.nom, ' - ' ORDER BY u.nom, u.prenom) AS libelle, array_agg(DISTINCT p.id_utilisateur ORDER BY p.id_utilisateur) AS "parentIds", (array_agg(p.numero_dossier))[1] AS numero_dossier FROM family_rep fr JOIN parents p ON p.id_utilisateur = fr.id JOIN utilisateurs u ON u.id = p.id_utilisateur WHERE u.role = 'parent' AND u.statut = 'en_attente' GROUP BY fr.rep ORDER BY libelle `); } catch (err) { throw err; } if (!Array.isArray(raw)) return []; return raw.map((r) => ({ libelle: r.libelle ?? '', parentIds: this.normalizeParentIds(r.parentIds), numero_dossier: r.numero_dossier ?? null, })); } /** Convertit parentIds (array ou chaîne PG) en string[] pour éviter 500 si le driver renvoie une chaîne. */ private normalizeParentIds(parentIds: unknown): string[] { if (Array.isArray(parentIds)) return parentIds.map(String); if (typeof parentIds === 'string') { const s = parentIds.replace(/^\{|\}$/g, '').trim(); return s ? s.split(',').map((x) => x.trim()) : []; } return []; } /** * Dossier famille complet par numéro de dossier. Ticket #119. * Rôles : admin, gestionnaire. * @throws NotFoundException si aucun parent avec ce numéro de dossier */ async getDossierFamilleByNumero(numeroDossier: string): Promise { const num = numeroDossier?.trim(); if (!num) { throw new NotFoundException('Numéro de dossier requis.'); } const firstParent = await this.parentsRepository.findOne({ where: { numero_dossier: num }, relations: ['user'], }); if (!firstParent || !firstParent.user) { throw new NotFoundException('Aucun dossier famille trouvé pour ce numéro.'); } const familyUserIds = await this.getFamilyUserIds(firstParent.user_id); const parents = await this.parentsRepository.find({ where: { user_id: In(familyUserIds) }, relations: ['user', 'co_parent', 'parentChildren', 'parentChildren.child', 'dossiers', 'dossiers.child'], }); const enfantsMap = new Map(); let presentationList: DossierFamillePresentationDto[] = []; // Un dossier = une famille : priorité à dossier_famille (un texte, N enfants) const dossierFamille = await this.dossierFamilleRepository.findOne({ where: { numero_dossier: num }, relations: ['parent', 'enfants', 'enfants.enfant'], }); if (dossierFamille?.enfants?.length) { const idParent = dossierFamille.parent?.user_id ?? familyUserIds[0]; for (const dfe of dossierFamille.enfants) { const idEnfant = dfe.enfant?.id ?? dfe.id_enfant; if (idEnfant) { presentationList.push({ id: dossierFamille.id, id_parent: idParent, id_enfant: idEnfant, presentation: dossierFamille.presentation, statut: dossierFamille.statut, }); } } } for (const p of parents) { // Enfants via parentChildren if (p.parentChildren) { for (const pc of p.parentChildren) { if (pc.child && !enfantsMap.has(pc.child.id)) { enfantsMap.set(pc.child.id, { id: pc.child.id, first_name: pc.child.first_name, last_name: pc.child.last_name, genre: pc.child.gender, birth_date: pc.child.birth_date, due_date: pc.child.due_date, status: pc.child.status, }); } } } // Si pas de dossier_famille : fallback sur anciens dossiers (parent+enfant) if (presentationList.length === 0 && p.dossiers) { for (const d of p.dossiers) { presentationList.push({ id: d.id, id_parent: p.user_id, id_enfant: d.child?.id ?? '', presentation: d.presentation, statut: d.status, }); } } } const parentsDto: DossierFamilleParentDto[] = parents.map((p) => ({ user_id: p.user_id, email: p.user.email, prenom: p.user.prenom, nom: p.user.nom, telephone: p.user.telephone, adresse: p.user.adresse, ville: p.user.ville, code_postal: p.user.code_postal, statut: p.user.statut, co_parent_id: p.co_parent?.id, })); return { numero_dossier: num, parents: parentsDto, enfants: Array.from(enfantsMap.values()), presentation: presentationList, }; } /** * Retourne les user_id de tous les parents de la même famille (co_parent ou enfants partagés). * @throws NotFoundException si parentId n'est pas un parent */ async getFamilyUserIds(parentId: string): Promise { const raw = await this.parentsRepository.query( ` WITH RECURSIVE links AS ( SELECT p.id_utilisateur AS p1, p.id_co_parent AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL UNION ALL SELECT p.id_co_parent AS p1, p.id_utilisateur AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL UNION ALL SELECT ep1.id_parent AS p1, ep2.id_parent AS p2 FROM enfants_parents ep1 JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent UNION ALL SELECT ep2.id_parent AS p1, ep1.id_parent AS p2 FROM enfants_parents ep1 JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent ), rec AS ( SELECT id_utilisateur AS id, id_utilisateur AS rep FROM parents UNION SELECT l.p2 AS id, LEAST(rec_alias.rep, l.p2) AS rep FROM links l JOIN rec rec_alias ON rec_alias.id = l.p1 ), family_rep AS ( SELECT id, (MIN(rep::text))::uuid AS rep FROM rec GROUP BY id ), input_rep AS ( SELECT rep FROM family_rep WHERE id = $1::uuid LIMIT 1 ) SELECT fr.id::text AS id FROM family_rep fr CROSS JOIN input_rep ir WHERE fr.rep = ir.rep `, [parentId], ); if (!raw || raw.length === 0) { throw new NotFoundException('Parent introuvable ou pas encore enregistré en base.'); } return raw.map((r: { id: string }) => r.id); } }