From ef7512dc1e6ca18acfb5a83c32ef27abb3696051 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Fri, 17 Jul 2026 19:31:18 +0200 Subject: [PATCH] =?UTF-8?q?feat(#157):=20autoriser=20d=C3=A9tachement=20de?= =?UTF-8?q?rnier=20parent=20+=20flag=20sans=5Fresponsable.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supprime la garde totalLinks<=1 ; GET/findOne enfants exposent sans_responsable pour les orphelins toujours listés. Co-authored-by: Cursor --- backend/src/routes/enfants/enfants.service.ts | 27 +++++++++++++------ backend/src/routes/parents/parents.service.ts | 9 +++---- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/backend/src/routes/enfants/enfants.service.ts b/backend/src/routes/enfants/enfants.service.ts index bd77d68..4e0d393 100644 --- a/backend/src/routes/enfants/enfants.service.ts +++ b/backend/src/routes/enfants/enfants.service.ts @@ -146,16 +146,27 @@ export class EnfantsService { throw new ForbiddenException('Accès interdit'); } - // Liste des enfants (admin/gestionnaire) - async findAll(): Promise { - return this.childrenRepository.find({ - relations: ['parentLinks', 'parentLinks.parent', 'parentLinks.parent.user'], - order: { last_name: 'ASC', first_name: 'ASC' }, + /** Flag API #157 — true si aucun lien enfants_parents. */ + private withSansResponsable(child: Children): Children & { sans_responsable: boolean } { + return Object.assign(child, { + sans_responsable: !child.parentLinks || child.parentLinks.length === 0, }); } + // Liste des enfants (admin/gestionnaire) — inclut les orphelins (parentLinks: []) + async findAll(): Promise> { + const children = await this.childrenRepository.find({ + relations: ['parentLinks', 'parentLinks.parent', 'parentLinks.parent.user'], + order: { last_name: 'ASC', first_name: 'ASC' }, + }); + return children.map((c) => this.withSansResponsable(c)); + } + // Récupérer un enfant par id - async findOne(id: string, currentUser: Users): Promise { + async findOne( + id: string, + currentUser: Users, + ): Promise { const child = await this.childrenRepository.findOne({ where: { id }, relations: ['parentLinks', 'parentLinks.parent', 'parentLinks.parent.user'], @@ -172,14 +183,14 @@ export class EnfantsService { case RoleType.ADMINISTRATEUR: case RoleType.SUPER_ADMIN: case RoleType.GESTIONNAIRE: - // accès complet + // accès complet (y compris orphelins) break; default: throw new ForbiddenException('Accès interdit'); } - return child; + return this.withSansResponsable(child); } diff --git a/backend/src/routes/parents/parents.service.ts b/backend/src/routes/parents/parents.service.ts index 2696593..ac00f0a 100644 --- a/backend/src/routes/parents/parents.service.ts +++ b/backend/src/routes/parents/parents.service.ts @@ -138,7 +138,9 @@ export class ParentsService { } /** - * Détacher un enfant d'un parent sans supprimer l'enfant. Ticket #115 / doc 28 §6.2. + * Détacher un enfant d'un parent sans supprimer l'enfant. + * Autorise le détachement du dernier responsable (#157) — l'enfant reste listé + * via GET /enfants avec parentLinks vides (alerte front). */ async detachEnfant(parentUserId: string, enfantId: string): Promise { await this.findOne(parentUserId); @@ -150,11 +152,6 @@ export class ParentsService { throw new NotFoundException('Lien parent-enfant introuvable'); } - const totalLinks = await this.parentsChildrenRepository.count({ where: { enfantId } }); - if (totalLinks <= 1) { - throw new BadRequestException('Un enfant doit rester rattaché à au moins un responsable'); - } - await this.parentsChildrenRepository.delete({ parentId: parentUserId, enfantId }); return this.findOne(parentUserId); }