268 lines
9.9 KiB
TypeScript
268 lines
9.9 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
ConflictException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { IsNull, Repository } from 'typeorm';
|
|
import { RoleType, Users } from 'src/entities/users.entity';
|
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
|
import { AmChildren } from 'src/entities/am_children.entity';
|
|
import { Children, StatutEnfantType } from 'src/entities/children.entity';
|
|
import { CreateAssistanteDto } from '../user/dto/create_assistante.dto';
|
|
import { UpdateAssistanteDto } from '../user/dto/update_assistante.dto';
|
|
import { UpdateAmFicheAdminDto } from './dto/update-am-fiche-admin.dto';
|
|
import { validateNir } from 'src/common/utils/nir.util';
|
|
|
|
const AM_CHILDREN_RELATIONS = ['user', 'amChildren', 'amChildren.child'] as const;
|
|
|
|
@Injectable()
|
|
export class AssistantesMaternellesService {
|
|
constructor(
|
|
@InjectRepository(AssistanteMaternelle)
|
|
private readonly assistantesMaternelleRepository: Repository<AssistanteMaternelle>,
|
|
@InjectRepository(Users)
|
|
private readonly usersRepository: Repository<Users>,
|
|
@InjectRepository(AmChildren)
|
|
private readonly amChildrenRepository: Repository<AmChildren>,
|
|
@InjectRepository(Children)
|
|
private readonly childrenRepository: Repository<Children>,
|
|
) {}
|
|
|
|
async create(dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
|
|
const user = await this.usersRepository.findOneBy({ id: dto.user_id });
|
|
if (!user) throw new NotFoundException('Utilisateur introuvable');
|
|
if (user.role !== RoleType.ASSISTANTE_MATERNELLE) {
|
|
throw new BadRequestException('Accès réservé aux assistantes maternelles');
|
|
}
|
|
|
|
const exist = await this.assistantesMaternelleRepository.findOneBy({ user_id: dto.user_id });
|
|
if (exist) throw new ConflictException('Assistante maternelle déjà existante');
|
|
|
|
const entity = this.assistantesMaternelleRepository.create({
|
|
user_id: dto.user_id,
|
|
user: { ...user, role: RoleType.ASSISTANTE_MATERNELLE },
|
|
approval_number: dto.approval_number,
|
|
nir: dto.nir,
|
|
max_children: dto.max_children,
|
|
biography: dto.biography,
|
|
available: dto.available ?? true,
|
|
residence_city: dto.residence_city,
|
|
agreement_date: dto.agreement_date ? new Date(dto.agreement_date) : undefined,
|
|
years_experience: dto.years_experience,
|
|
specialty: dto.specialty,
|
|
places_available: dto.places_available,
|
|
});
|
|
|
|
return this.assistantesMaternelleRepository.save(entity);
|
|
}
|
|
|
|
async findAll(): Promise<AssistanteMaternelle[]> {
|
|
return this.assistantesMaternelleRepository.find({
|
|
relations: [...AM_CHILDREN_RELATIONS],
|
|
});
|
|
}
|
|
|
|
async findOne(user_id: string): Promise<AssistanteMaternelle> {
|
|
const assistante = await this.assistantesMaternelleRepository.findOne({
|
|
where: { user_id },
|
|
relations: [...AM_CHILDREN_RELATIONS],
|
|
});
|
|
if (!assistante) throw new NotFoundException('Assistante maternelle introuvable');
|
|
return assistante;
|
|
}
|
|
|
|
async update(id: string, dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
|
|
await this.assistantesMaternelleRepository.update(id, dto);
|
|
return this.findOne(id);
|
|
}
|
|
|
|
/**
|
|
* Mise à jour fiche AM (identité + champs pro) par admin/gestionnaire. Ticket #131.
|
|
*/
|
|
async updateFicheAdmin(amUserId: string, dto: UpdateAmFicheAdminDto): Promise<AssistanteMaternelle> {
|
|
const am = await this.findOne(amUserId);
|
|
const user = am.user;
|
|
|
|
if (dto.email && dto.email !== user.email) {
|
|
const existing = await this.usersRepository.findOne({ where: { email: dto.email } });
|
|
if (existing && existing.id !== user.id) {
|
|
throw new ConflictException('Cet email est déjà utilisé');
|
|
}
|
|
user.email = dto.email;
|
|
}
|
|
|
|
if (dto.nom !== undefined) user.nom = dto.nom;
|
|
if (dto.prenom !== undefined) user.prenom = dto.prenom;
|
|
if (dto.telephone !== undefined) user.telephone = dto.telephone;
|
|
if (dto.adresse !== undefined) user.adresse = dto.adresse;
|
|
if (dto.ville !== undefined) user.ville = dto.ville;
|
|
if (dto.code_postal !== undefined) user.code_postal = dto.code_postal;
|
|
if (dto.statut !== undefined) user.statut = dto.statut;
|
|
if (dto.date_naissance !== undefined) {
|
|
user.date_naissance = dto.date_naissance ? new Date(dto.date_naissance) : undefined;
|
|
}
|
|
if (dto.lieu_naissance_ville !== undefined) {
|
|
user.lieu_naissance_ville = dto.lieu_naissance_ville || undefined;
|
|
}
|
|
if (dto.lieu_naissance_pays !== undefined) {
|
|
user.lieu_naissance_pays = dto.lieu_naissance_pays || undefined;
|
|
}
|
|
|
|
await this.usersRepository.save(user);
|
|
|
|
const amPatch: Partial<AssistanteMaternelle> = {};
|
|
if (dto.approval_number !== undefined) amPatch.approval_number = dto.approval_number;
|
|
if (dto.residence_city !== undefined) amPatch.residence_city = dto.residence_city;
|
|
if (dto.max_children !== undefined) amPatch.max_children = dto.max_children;
|
|
if (dto.places_available !== undefined) amPatch.places_available = dto.places_available;
|
|
if (dto.biography !== undefined) amPatch.biography = dto.biography;
|
|
if (dto.available !== undefined) amPatch.available = dto.available;
|
|
if (dto.agreement_date !== undefined) {
|
|
amPatch.agreement_date = dto.agreement_date ? new Date(dto.agreement_date) : undefined;
|
|
}
|
|
|
|
if (dto.nir !== undefined) {
|
|
const nirNormalized = dto.nir.replace(/\s/g, '').toUpperCase();
|
|
if (nirNormalized) {
|
|
const dateNaissanceForNir =
|
|
dto.date_naissance ??
|
|
(user.date_naissance instanceof Date
|
|
? user.date_naissance.toISOString().slice(0, 10)
|
|
: user.date_naissance
|
|
? String(user.date_naissance).slice(0, 10)
|
|
: undefined);
|
|
const nirValidation = validateNir(nirNormalized, {
|
|
dateNaissance: dateNaissanceForNir,
|
|
});
|
|
if (!nirValidation.valid) {
|
|
throw new BadRequestException(nirValidation.error || 'NIR invalide');
|
|
}
|
|
const nirDejaUtilise = await this.assistantesMaternelleRepository.findOne({
|
|
where: { nir: nirNormalized },
|
|
});
|
|
if (nirDejaUtilise && nirDejaUtilise.user_id !== amUserId) {
|
|
throw new ConflictException(
|
|
'Un compte assistante maternelle avec ce numéro NIR existe déjà.',
|
|
);
|
|
}
|
|
amPatch.nir = nirNormalized;
|
|
}
|
|
// NIR vide : ne pas effacer (colonne NOT NULL en BDD) — le front renvoie toujours la clé.
|
|
}
|
|
|
|
if (Object.keys(amPatch).length > 0) {
|
|
await this.assistantesMaternelleRepository.update(amUserId, amPatch);
|
|
}
|
|
|
|
return this.findOne(amUserId);
|
|
}
|
|
|
|
/**
|
|
* Rattacher un enfant à une AM (placement actif). Ticket #131.
|
|
* Passe le statut enfant à `garde` (sauf a_naitre / scolarise).
|
|
*/
|
|
async attachEnfant(amUserId: string, enfantId: string, createdBy?: Users): Promise<AssistanteMaternelle> {
|
|
const am = await this.findOne(amUserId);
|
|
|
|
const existingForAm = await this.amChildrenRepository.findOne({
|
|
where: { amId: amUserId, enfantId, date_fin: IsNull() },
|
|
});
|
|
if (existingForAm) {
|
|
throw new ConflictException('Cet enfant est déjà rattaché à cette assistante maternelle');
|
|
}
|
|
|
|
const child = await this.childrenRepository.findOne({ where: { id: enfantId } });
|
|
if (!child) {
|
|
throw new NotFoundException('Enfant introuvable');
|
|
}
|
|
|
|
const activeForChild = await this.amChildrenRepository.findOne({
|
|
where: { enfantId, date_fin: IsNull() },
|
|
});
|
|
if (activeForChild && activeForChild.amId !== amUserId) {
|
|
throw new ConflictException(
|
|
'Cet enfant est déjà en garde chez une autre assistante maternelle',
|
|
);
|
|
}
|
|
|
|
const activeCount = await this.amChildrenRepository.count({
|
|
where: { amId: amUserId, date_fin: IsNull() },
|
|
});
|
|
if (am.max_children != null && activeCount >= am.max_children) {
|
|
throw new BadRequestException(
|
|
`Capacité maximale atteinte (${am.max_children} enfant(s))`,
|
|
);
|
|
}
|
|
|
|
await this.amChildrenRepository.save(
|
|
this.amChildrenRepository.create({
|
|
amId: amUserId,
|
|
enfantId,
|
|
date_debut: new Date(),
|
|
cree_par: createdBy?.id,
|
|
}),
|
|
);
|
|
|
|
await this.applyGardeStatusOnAttach(child);
|
|
|
|
return this.findOne(amUserId);
|
|
}
|
|
|
|
/**
|
|
* Clôturer le placement AM ↔ enfant. Ticket #131.
|
|
* Repasse l'enfant en `sans_garde` s'il n'a plus de placement actif.
|
|
*/
|
|
async detachEnfant(amUserId: string, enfantId: string): Promise<AssistanteMaternelle> {
|
|
await this.findOne(amUserId);
|
|
|
|
const link = await this.amChildrenRepository.findOne({
|
|
where: { amId: amUserId, enfantId, date_fin: IsNull() },
|
|
relations: ['child'],
|
|
});
|
|
if (!link) {
|
|
throw new NotFoundException('Lien assistante maternelle-enfant introuvable');
|
|
}
|
|
|
|
link.date_fin = new Date();
|
|
await this.amChildrenRepository.save(link);
|
|
|
|
const remaining = await this.amChildrenRepository.count({
|
|
where: { enfantId, date_fin: IsNull() },
|
|
});
|
|
if (remaining === 0 && link.child) {
|
|
await this.applySansGardeStatusOnDetach(link.child);
|
|
}
|
|
|
|
return this.findOne(amUserId);
|
|
}
|
|
|
|
private async applyGardeStatusOnAttach(child: Children): Promise<void> {
|
|
if (
|
|
child.status === StatutEnfantType.A_NAITRE ||
|
|
child.status === StatutEnfantType.SCOLARISE
|
|
) {
|
|
return;
|
|
}
|
|
child.status = StatutEnfantType.GARDE;
|
|
await this.childrenRepository.save(child);
|
|
}
|
|
|
|
private async applySansGardeStatusOnDetach(child: Children): Promise<void> {
|
|
if (
|
|
child.status === StatutEnfantType.A_NAITRE ||
|
|
child.status === StatutEnfantType.SCOLARISE
|
|
) {
|
|
return;
|
|
}
|
|
child.status = StatutEnfantType.SANS_GARDE;
|
|
await this.childrenRepository.save(child);
|
|
}
|
|
|
|
async remove(id: string): Promise<{ message: string }> {
|
|
await this.assistantesMaternelleRepository.delete(id);
|
|
return { message: 'Assistante maternelle supprimée' };
|
|
}
|
|
}
|