Supprime la garde totalLinks<=1 ; GET/findOne enfants exposent sans_responsable pour les orphelins toujours listés. Co-authored-by: Cursor <cursoragent@cursor.com>
218 lines
6.9 KiB
TypeScript
218 lines
6.9 KiB
TypeScript
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<Children>,
|
|
@InjectRepository(Parents)
|
|
private readonly parentsRepository: Repository<Parents>,
|
|
@InjectRepository(ParentsChildren)
|
|
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
|
|
) { }
|
|
|
|
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<Children> {
|
|
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');
|
|
}
|
|
|
|
/** 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<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
|
|
async findOne(
|
|
id: string,
|
|
currentUser: Users,
|
|
): Promise<Children & { sans_responsable: boolean }> {
|
|
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 (y compris orphelins)
|
|
break;
|
|
|
|
default:
|
|
throw new ForbiddenException('Accès interdit');
|
|
}
|
|
|
|
return this.withSansResponsable(child);
|
|
}
|
|
|
|
|
|
// Mise à jour
|
|
async update(id: string, dto: Partial<CreateEnfantsDto>, currentUser: Users): Promise<Children> {
|
|
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<Children> = { ...rest } as Partial<Children>;
|
|
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<void> {
|
|
await this.childrenRepository.delete(id);
|
|
}
|
|
}
|