feat(#157): autoriser détachement dernier parent + flag sans_responsable.

Supprime la garde totalLinks<=1 ; GET/findOne enfants exposent
sans_responsable pour les orphelins toujours listés.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MARTIN Julien 2026-07-17 19:31:18 +02:00
parent 2ececa711b
commit ef7512dc1e
2 changed files with 22 additions and 14 deletions

View File

@ -146,16 +146,27 @@ export class EnfantsService {
throw new ForbiddenException('Accès interdit'); throw new ForbiddenException('Accès interdit');
} }
// Liste des enfants (admin/gestionnaire) /** Flag API #157 — true si aucun lien enfants_parents. */
async findAll(): Promise<Children[]> { private withSansResponsable(child: Children): Children & { sans_responsable: boolean } {
return this.childrenRepository.find({ return Object.assign(child, {
relations: ['parentLinks', 'parentLinks.parent', 'parentLinks.parent.user'], sans_responsable: !child.parentLinks || child.parentLinks.length === 0,
order: { last_name: 'ASC', first_name: 'ASC' },
}); });
} }
// Liste des enfants (admin/gestionnaire) — inclut les orphelins (parentLinks: [])
async findAll(): Promise<Array<Children & { sans_responsable: boolean }>> {
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 // Récupérer un enfant par id
async findOne(id: string, currentUser: Users): Promise<Children> { async findOne(
id: string,
currentUser: Users,
): Promise<Children & { sans_responsable: boolean }> {
const child = await this.childrenRepository.findOne({ const child = await this.childrenRepository.findOne({
where: { id }, where: { id },
relations: ['parentLinks', 'parentLinks.parent', 'parentLinks.parent.user'], relations: ['parentLinks', 'parentLinks.parent', 'parentLinks.parent.user'],
@ -172,14 +183,14 @@ export class EnfantsService {
case RoleType.ADMINISTRATEUR: case RoleType.ADMINISTRATEUR:
case RoleType.SUPER_ADMIN: case RoleType.SUPER_ADMIN:
case RoleType.GESTIONNAIRE: case RoleType.GESTIONNAIRE:
// accès complet // accès complet (y compris orphelins)
break; break;
default: default:
throw new ForbiddenException('Accès interdit'); throw new ForbiddenException('Accès interdit');
} }
return child; return this.withSansResponsable(child);
} }

View File

@ -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<Parents> { async detachEnfant(parentUserId: string, enfantId: string): Promise<Parents> {
await this.findOne(parentUserId); await this.findOne(parentUserId);
@ -150,11 +152,6 @@ export class ParentsService {
throw new NotFoundException('Lien parent-enfant introuvable'); 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 }); await this.parentsChildrenRepository.delete({ parentId: parentUserId, enfantId });
return this.findOne(parentUserId); return this.findOne(parentUserId);
} }