diff --git a/backend/src/routes/parents/dto/pending-family.dto.ts b/backend/src/routes/parents/dto/pending-family.dto.ts index e7706d3..ab996dd 100644 --- a/backend/src/routes/parents/dto/pending-family.dto.ts +++ b/backend/src/routes/parents/dto/pending-family.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class PendingFamilyDto { @ApiProperty({ example: 'Famille Dupont', description: 'Libellé affiché pour la famille' }) @@ -17,4 +17,24 @@ export class PendingFamilyDto { description: 'Numéro de dossier famille (format AAAA-NNNNNN)', }) numero_dossier: string | null; + + @ApiProperty({ + nullable: true, + example: '2026-01-12T10:00:00.000Z', + description: 'Date de référence dossier soumis / en attente : MIN(cree_le) des parents en_attente du groupe (ISO 8601)', + }) + date_soumission: string | null; + + @ApiProperty({ + example: 3, + description: 'Nombre d’enfants distincts liés aux parents de la famille (enfants_parents)', + }) + nombre_enfants: number; + + @ApiPropertyOptional({ + type: [String], + example: ['parent1@example.com', 'parent2@example.com'], + description: 'Emails des parents du groupe (ordre stable : nom, prénom)', + }) + emails?: string[]; } diff --git a/backend/src/routes/parents/parents.controller.ts b/backend/src/routes/parents/parents.controller.ts index d9c7546..84c7006 100644 --- a/backend/src/routes/parents/parents.controller.ts +++ b/backend/src/routes/parents/parents.controller.ts @@ -34,7 +34,7 @@ export class ParentsController { @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: 200, description: 'Liste des familles (libellé, parentIds, numero_dossier, date_soumission, nombre_enfants, emails)', type: [PendingFamilyDto] }) @ApiResponse({ status: 403, description: 'Accès refusé' }) getPendingFamilies(): Promise { return this.parentsService.getPendingFamilies(); diff --git a/backend/src/routes/parents/parents.service.ts b/backend/src/routes/parents/parents.service.ts index 54db406..93f4325 100644 --- a/backend/src/routes/parents/parents.service.ts +++ b/backend/src/routes/parents/parents.service.ts @@ -87,7 +87,14 @@ export class ParentsService { * Uniquement les parents dont l'utilisateur a statut = en_attente. */ async getPendingFamilies(): Promise { - let raw: { libelle: string; parentIds: unknown; numero_dossier: string | null }[]; + let raw: { + libelle: string; + parentIds: unknown; + numero_dossier: string | null; + date_soumission: Date | string | null; + nombre_enfants: string | number | null; + emails: unknown; + }[]; try { raw = await this.parentsRepository.query(` WITH RECURSIVE @@ -115,7 +122,16 @@ export class ParentsService { 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 + (array_agg(p.numero_dossier))[1] AS numero_dossier, + MIN(u.cree_le) AS date_soumission, + COALESCE(( + SELECT COUNT(DISTINCT ep.id_enfant)::int + FROM enfants_parents ep + WHERE ep.id_parent IN ( + SELECT frx.id FROM family_rep frx WHERE frx.rep = fr.rep + ) + ), 0) AS nombre_enfants, + array_agg(u.email ORDER BY u.nom, u.prenom) AS emails FROM family_rep fr JOIN parents p ON p.id_utilisateur = fr.id JOIN utilisateurs u ON u.id = p.id_utilisateur @@ -131,9 +147,34 @@ export class ParentsService { libelle: r.libelle ?? '', parentIds: this.normalizeParentIds(r.parentIds), numero_dossier: r.numero_dossier ?? null, + date_soumission: this.toIsoDateTimeOrNull(r.date_soumission), + nombre_enfants: this.normalizeNombreEnfants(r.nombre_enfants), + emails: this.normalizeEmails(r.emails), })); } + private toIsoDateTimeOrNull(value: Date | string | null | undefined): string | null { + if (value == null) return null; + if (value instanceof Date) return value.toISOString(); + const d = new Date(value); + return Number.isNaN(d.getTime()) ? null : d.toISOString(); + } + + private normalizeNombreEnfants(v: string | number | null | undefined): number { + if (v == null) return 0; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + return Number.isFinite(n) && n >= 0 ? n : 0; + } + + private normalizeEmails(emails: unknown): string[] { + if (Array.isArray(emails)) return emails.map(String); + if (typeof emails === 'string') { + const s = emails.replace(/^\{|\}$/g, '').trim(); + return s ? s.split(',').map((x) => x.trim()) : []; + } + return []; + } + /** 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);