feat(admin): onglet À valider, dossier unifié et modales de validation

- Onglet « À valider » (AM + familles), pending-families et détail dossier par numéro.
- Wizards validation AM et famille, modale commune, chargement via GET /dossiers/:num.
- UI : cartes enfants, photo AM (cadre uniforme, ratio), NIR affiché formaté (espaces autour du tiret).
- Backend : DTO / routes parents pending ; scripts de test et mise à jour issue Gitea.

Tickets #107, #119.

Made-with: Cursor
This commit is contained in:
2026-03-26 00:15:18 +01:00
parent d832559027
commit 2e02f60601
36 changed files with 3303 additions and 173 deletions
@@ -17,4 +17,22 @@ export class PendingFamilyDto {
description: 'Numéro de dossier famille (format AAAA-NNNNNN)',
})
numero_dossier: string | null;
@ApiProperty({ nullable: true, example: 'parent@mail.fr', description: 'Email du premier parent (ordre nom/prénom)' })
email: string | null;
@ApiProperty({ nullable: true, description: 'Date de création du compte la plus ancienne (famille)' })
cree_le: Date | string | null;
@ApiProperty({ nullable: true, example: '0612345678' })
telephone: string | null;
@ApiProperty({ nullable: true, example: '75001' })
code_postal: string | null;
@ApiProperty({ nullable: true, example: 'Paris' })
ville: string | null;
@ApiProperty({ example: 2, description: 'Nombre denfants liés à la famille' })
nombre_enfants: number;
}
@@ -33,7 +33,12 @@ 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, email, cree_le, téléphone, CP, ville, nombre_enfants)',
type: [PendingFamilyDto],
})
@ApiResponse({ status: 403, description: 'Accès refusé' })
getPendingFamilies(): Promise<PendingFamilyDto[]> {
return this.parentsService.getPendingFamilies();
+35 -6
View File
@@ -105,7 +105,17 @@ 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,
(array_agg(u.email ORDER BY u.nom, u.prenom))[1] AS email,
MIN(u.cree_le) AS cree_le,
(array_agg(u.telephone ORDER BY u.nom, u.prenom))[1] AS telephone,
(array_agg(u.code_postal ORDER BY u.nom, u.prenom))[1] AS code_postal,
(array_agg(u.ville ORDER BY u.nom, u.prenom))[1] AS ville,
(
SELECT COUNT(DISTINCT ep.id_enfant)::int
FROM enfants_parents ep
WHERE ep.id_parent IN (SELECT fr2.id FROM family_rep fr2 WHERE fr2.rep = fr.rep)
) AS nombre_enfants
FROM family_rep fr
JOIN parents p ON p.id_utilisateur = fr.id
JOIN utilisateurs u ON u.id = p.id_utilisateur
@@ -113,11 +123,30 @@ export class ParentsService {
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,
}));
return raw.map(
(r: {
libelle: string;
parentIds: unknown;
numero_dossier: string | null;
email: string | null;
cree_le: Date | string | null;
telephone: string | null;
code_postal: string | null;
ville: string | null;
nombre_enfants: string | number | null;
}) => ({
libelle: r.libelle,
parentIds: Array.isArray(r.parentIds) ? r.parentIds.map(String) : [],
numero_dossier: r.numero_dossier ?? null,
email: r.email ?? null,
cree_le: r.cree_le ?? null,
telephone: r.telephone ?? null,
code_postal: r.code_postal ?? null,
ville: r.ville ?? null,
nombre_enfants:
r.nombre_enfants == null ? 0 : Number(r.nombre_enfants),
}),
);
}
/**