289 lines
10 KiB
TypeScript
289 lines
10 KiB
TypeScript
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 d’un 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);
|
||
}
|
||
}
|