petitspas/backend/src/routes/parents/parents.service.ts

289 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Parents>,
@InjectRepository(Users)
private readonly usersRepository: Repository<Users>,
@InjectRepository(DossierFamille)
private readonly dossierFamilleRepository: Repository<DossierFamille>,
) {}
// Création dun parent
async create(dto: CreateParentDto): Promise<Parents> {
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<Parents[]> {
return this.parentsRepository.find({
relations: ['user', 'co_parent', 'parentChildren', 'dossiers'],
});
}
// Récupérer un parent par user_id
async findOne(user_id: string): Promise<Parents> {
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<Parents> {
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<PendingFamilyDto[]> {
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<DossierFamilleCompletDto> {
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<string, DossierFamilleEnfantDto>();
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<string[]> {
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);
}
}