diff --git a/backend/src/routes/parents/dto/pending-family.dto.ts b/backend/src/routes/parents/dto/pending-family.dto.ts new file mode 100644 index 0000000..e7706d3 --- /dev/null +++ b/backend/src/routes/parents/dto/pending-family.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class PendingFamilyDto { + @ApiProperty({ example: 'Famille Dupont', description: 'Libellé affiché pour la famille' }) + libelle: string; + + @ApiProperty({ + type: [String], + example: ['uuid-parent-1', 'uuid-parent-2'], + description: 'IDs utilisateur des parents de la famille', + }) + parentIds: string[]; + + @ApiProperty({ + nullable: true, + example: '2026-000001', + description: 'Numéro de dossier famille (format AAAA-NNNNNN)', + }) + numero_dossier: string | null; +} diff --git a/backend/src/routes/parents/parents.controller.ts b/backend/src/routes/parents/parents.controller.ts index e4a9ee2..a6ed455 100644 --- a/backend/src/routes/parents/parents.controller.ts +++ b/backend/src/routes/parents/parents.controller.ts @@ -6,20 +6,34 @@ import { Param, Patch, Post, + UseGuards, } from '@nestjs/common'; import { ParentsService } from './parents.service'; import { Parents } from 'src/entities/parents.entity'; import { Roles } from 'src/common/decorators/roles.decorator'; import { RoleType } from 'src/entities/users.entity'; -import { ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { CreateParentDto } from '../user/dto/create_parent.dto'; import { UpdateParentsDto } from '../user/dto/update_parent.dto'; +import { AuthGuard } from 'src/common/guards/auth.guard'; +import { RolesGuard } from 'src/common/guards/roles.guard'; +import { PendingFamilyDto } from './dto/pending-family.dto'; @ApiTags('Parents') @Controller('parents') +@UseGuards(AuthGuard, RolesGuard) export class ParentsController { constructor(private readonly parentsService: ParentsService) {} + @Get('pending-families') + @Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE) + @ApiOperation({ summary: 'Liste des familles en attente (une entrée par famille)' }) + @ApiResponse({ status: 200, description: 'Liste des familles (libellé, parentIds, numero_dossier)', type: [PendingFamilyDto] }) + @ApiResponse({ status: 403, description: 'Accès refusé' }) + getPendingFamilies(): Promise { + return this.parentsService.getPendingFamilies(); + } + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR) @Get() @ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' }) diff --git a/backend/src/routes/parents/parents.service.ts b/backend/src/routes/parents/parents.service.ts index d2cafee..e2c50e6 100644 --- a/backend/src/routes/parents/parents.service.ts +++ b/backend/src/routes/parents/parents.service.ts @@ -10,6 +10,7 @@ import { Parents } from 'src/entities/parents.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'; @Injectable() export class ParentsService { @@ -71,4 +72,51 @@ export class ParentsService { 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 { + 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 + ) + 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 + `); + return raw.map((r: { libelle: string; parentIds: unknown; numero_dossier: string | null }) => ({ + libelle: r.libelle, + parentIds: Array.isArray(r.parentIds) ? r.parentIds.map(String) : [], + numero_dossier: r.numero_dossier ?? null, + })); + } }