import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Children, StatutEnfantType } from 'src/entities/children.entity'; import { Parents } from 'src/entities/parents.entity'; import { ParentsChildren } from 'src/entities/parents_children.entity'; import { RoleType, Users } from 'src/entities/users.entity'; import { CreateEnfantsDto } from './dto/create_enfants.dto'; const STAFF_ROLES: RoleType[] = [ RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR, RoleType.SUPER_ADMIN, ]; @Injectable() export class EnfantsService { constructor( @InjectRepository(Children) private readonly childrenRepository: Repository, @InjectRepository(Parents) private readonly parentsRepository: Repository, @InjectRepository(ParentsChildren) private readonly parentsChildrenRepository: Repository, ) { } private isStaff(user: Users): boolean { return STAFF_ROLES.includes(user.role); } /** * Création d'un enfant. * - PARENT : rattache au parent connecté (multipart photo optionnel). * - Staff : `parent_user_id` obligatoire ; JSON sans photo OK ; * avec photo → multipart (même stockage `/uploads/photos/...`). Ticket #132. */ async create( dto: CreateEnfantsDto, currentUser: Users, photoFile?: Express.Multer.File, ): Promise { const pivotUserId = this.resolvePivotParentUserId(dto, currentUser); const parent = await this.parentsRepository.findOne({ where: { user_id: pivotUserId }, relations: ['co_parent'], }); if (!parent) throw new NotFoundException('Parent introuvable'); // Vérif métier simple (aligné comportement historique parent) if (dto.status !== StatutEnfantType.A_NAITRE && !dto.birth_date) { throw new BadRequestException('Un enfant né doit avoir une date de naissance'); } // Vérif doublon éventuel (ex: même prénom + date de naissance) const exist = await this.childrenRepository.findOne({ where: { first_name: dto.first_name, last_name: dto.last_name, birth_date: dto.birth_date ? new Date(dto.birth_date) : undefined, }, }); if (exist) throw new ConflictException('Cet enfant existe déjà'); // Gestion de la photo uploadée (multipart parent ou staff) let photoUrl = dto.photo_url; let consentAt: Date | undefined; if (photoFile) { photoUrl = `/uploads/photos/${photoFile.filename}`; if (dto.consent_photo) { consentAt = new Date(); } } else if (dto.consent_photo) { consentAt = dto.consent_photo_at ? new Date(dto.consent_photo_at) : new Date(); } const child = this.childrenRepository.create({ status: dto.status, first_name: dto.first_name, last_name: dto.last_name, gender: dto.gender, birth_date: dto.birth_date ? new Date(dto.birth_date) : undefined, due_date: dto.due_date ? new Date(dto.due_date) : undefined, photo_url: photoUrl, consent_photo: !!dto.consent_photo, consent_photo_at: consentAt, is_multiple: !!dto.is_multiple, }); await this.childrenRepository.save(child); // Lien parent-enfant (pivot) await this.parentsChildrenRepository.save( this.parentsChildrenRepository.create({ parentId: parent.user_id, enfantId: child.id, }), ); // Rattachement automatique au co-parent s'il existe if (parent.co_parent) { await this.parentsChildrenRepository.save( this.parentsChildrenRepository.create({ parentId: parent.co_parent.id, enfantId: child.id, }), ); } return this.findOne(child.id, currentUser); } private resolvePivotParentUserId( dto: CreateEnfantsDto, currentUser: Users, ): string { if (this.isStaff(currentUser)) { const id = dto.parent_user_id?.trim(); if (!id) { throw new BadRequestException( 'parent_user_id est obligatoire pour créer un enfant (staff)', ); } return id; } if (currentUser.role === RoleType.PARENT) { if ( dto.parent_user_id && dto.parent_user_id.trim() !== currentUser.id ) { throw new ForbiddenException( 'Un parent ne peut pas créer un enfant pour un autre compte', ); } return currentUser.id; } 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' }, }); } // Récupérer un enfant par id async findOne(id: string, currentUser: Users): Promise { const child = await this.childrenRepository.findOne({ where: { id }, relations: ['parentLinks', 'parentLinks.parent', 'parentLinks.parent.user'], }); if (!child) throw new NotFoundException('Enfant introuvable'); switch (currentUser.role) { case RoleType.PARENT: if (!child.parentLinks.some(link => link.parentId === currentUser.id)) { throw new ForbiddenException('Cet enfant ne vous appartient pas'); } break; case RoleType.ADMINISTRATEUR: case RoleType.SUPER_ADMIN: case RoleType.GESTIONNAIRE: // accès complet break; default: throw new ForbiddenException('Accès interdit'); } return child; } // Mise à jour async update(id: string, dto: Partial, currentUser: Users): Promise { const child = await this.childrenRepository.findOne({ where: { id } }); if (!child) throw new NotFoundException('Enfant introuvable'); const { parent_user_id: _ignored, ...rest } = dto; const patch: Partial = { ...rest } as Partial; if (dto.consent_photo !== undefined) { patch.consent_photo = dto.consent_photo; patch.consent_photo_at = dto.consent_photo ? new Date() : null!; } await this.childrenRepository.update(id, patch); return this.findOne(id, currentUser); } // Suppression async remove(id: string): Promise { await this.childrenRepository.delete(id); } }