feat(#131): fiches parent/AM éditable, placement AM↔enfant, statuts garde/sans_garde
Squash merge develop → master. - Fiche parent éditable (co-parent, PATCH fiche, GET /parents) - Fiche AM 3 onglets (PATCH fiche, rattacher/détacher enfants) - Table enfants_assistantes_maternelles + enum garde/sans_garde - Migration SQL + BDD.sql canonique - Correctifs recette : @Get() parents, DTO fiche AM, fix NIR Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,11 +12,14 @@ import { AssistantesMaternellesService } from './assistantes_maternelles.service
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||
import { Roles } from 'src/common/decorators/roles.decorator';
|
||||
import { RoleType } from 'src/entities/users.entity';
|
||||
import { RoleType, Users } from 'src/entities/users.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 { RolesGuard } from 'src/common/guards/roles.guard';
|
||||
import { AuthGuard } from 'src/common/guards/auth.guard';
|
||||
import { User } from 'src/common/decorators/user.decorator';
|
||||
import { mapAmForApi, mapAmsForApi } from './assistantes_maternelles.mapper';
|
||||
|
||||
@ApiTags("Assistantes Maternelles")
|
||||
@ApiBearerAuth('access-token')
|
||||
@@ -31,28 +34,74 @@ export class AssistantesMaternellesController {
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
|
||||
@ApiBody({ type: CreateAssistanteDto })
|
||||
@Post()
|
||||
create(@Body() dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
|
||||
return this.assistantesMaternellesService.create(dto);
|
||||
async create(@Body() dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
|
||||
const am = await this.assistantesMaternellesService.create(dto);
|
||||
return mapAmForApi(am);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Récupérer la liste des nounous' })
|
||||
@ApiOperation({ summary: 'Récupérer la liste des nounous (inclut amChildren actifs) — ticket #131' })
|
||||
@ApiResponse({ status: 200, description: 'Liste des nounous' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
|
||||
getAll(): Promise<AssistanteMaternelle[]> {
|
||||
return this.assistantesMaternellesService.findAll();
|
||||
async getAll(): Promise<AssistanteMaternelle[]> {
|
||||
const ams = await this.assistantesMaternellesService.findAll();
|
||||
return mapAmsForApi(ams);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Get(':id')
|
||||
@ApiParam({ name: 'id', description: "UUID de la nounou" })
|
||||
@ApiOperation({ summary: 'Récupérer une nounou par id' })
|
||||
@ApiOperation({ summary: 'Récupérer une nounou par id (inclut amChildren) — ticket #131' })
|
||||
@ApiResponse({ status: 200, description: 'Détails de la nounou' })
|
||||
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
|
||||
getOne(@Param('id') user_id: string): Promise<AssistanteMaternelle> {
|
||||
return this.assistantesMaternellesService.findOne(user_id);
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
||||
async getOne(@Param('id') user_id: string): Promise<AssistanteMaternelle> {
|
||||
const am = await this.assistantesMaternellesService.findOne(user_id);
|
||||
return mapAmForApi(am);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Patch(':id/fiche')
|
||||
@ApiBody({ type: UpdateAmFicheAdminDto })
|
||||
@ApiOperation({ summary: 'Mettre à jour la fiche AM (identité + pro) — ticket #131' })
|
||||
@ApiParam({ name: 'id', description: "UUID utilisateur de l'AM" })
|
||||
@ApiResponse({ status: 200, description: 'Fiche AM mise à jour' })
|
||||
async updateFicheAdmin(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateAmFicheAdminDto,
|
||||
): Promise<AssistanteMaternelle> {
|
||||
const am = await this.assistantesMaternellesService.updateFicheAdmin(id, dto);
|
||||
return mapAmForApi(am);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Post(':id/enfants/:enfantId')
|
||||
@ApiOperation({ summary: 'Rattacher un enfant à une AM (statut enfant → garde) — ticket #131' })
|
||||
@ApiParam({ name: 'id', description: "UUID utilisateur de l'AM" })
|
||||
@ApiParam({ name: 'enfantId', description: "UUID de l'enfant" })
|
||||
@ApiResponse({ status: 200, description: 'AM avec enfants mis à jour' })
|
||||
async attachEnfant(
|
||||
@Param('id') id: string,
|
||||
@Param('enfantId') enfantId: string,
|
||||
@User() currentUser: Users,
|
||||
): Promise<AssistanteMaternelle> {
|
||||
const am = await this.assistantesMaternellesService.attachEnfant(id, enfantId, currentUser);
|
||||
return mapAmForApi(am);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Delete(':id/enfants/:enfantId')
|
||||
@ApiOperation({ summary: "Clôturer le placement d'un enfant chez une AM — ticket #131" })
|
||||
@ApiParam({ name: 'id', description: "UUID utilisateur de l'AM" })
|
||||
@ApiParam({ name: 'enfantId', description: "UUID de l'enfant" })
|
||||
@ApiResponse({ status: 200, description: 'AM avec enfants mis à jour' })
|
||||
async detachEnfant(
|
||||
@Param('id') id: string,
|
||||
@Param('enfantId') enfantId: string,
|
||||
): Promise<AssistanteMaternelle> {
|
||||
const am = await this.assistantesMaternellesService.detachEnfant(id, enfantId);
|
||||
return mapAmForApi(am);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||
@@ -63,14 +112,15 @@ export class AssistantesMaternellesController {
|
||||
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
|
||||
@ApiParam({ name: 'id', description: "UUID de la nounou" })
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
|
||||
return this.assistantesMaternellesService.update(id, dto);
|
||||
async update(@Param('id') id: string, @Body() dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
|
||||
const am = await this.assistantesMaternellesService.update(id, dto);
|
||||
return mapAmForApi(am);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Supprimer une nounou' })
|
||||
@ApiResponse({ status: 200, description: 'Nounou supprimée avec succès' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins, gestionnaires et administrateurs' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
||||
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
|
||||
@ApiParam({ name: 'id', description: "UUID de la nounou" })
|
||||
@Delete(':id')
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||
import { AmChildren } from 'src/entities/am_children.entity';
|
||||
import { sanitizeUserForApi } from '../../common/utils/sanitize-user-for-api';
|
||||
|
||||
/**
|
||||
* Sérialisation API fiche AM — ticket #131.
|
||||
* Expose `amChildren` actifs (date_fin null) avec enfant imbriqué, sans secrets user.
|
||||
*/
|
||||
export function mapAmForApi(am: AssistanteMaternelle): AssistanteMaternelle {
|
||||
const activeChildren = (am.amChildren ?? []).filter((link) => !link.date_fin);
|
||||
|
||||
return {
|
||||
...am,
|
||||
user: sanitizeUserForApi(am.user)!,
|
||||
amChildren: activeChildren.map((link) => ({
|
||||
...link,
|
||||
child: link.child,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapAmsForApi(ams: AssistanteMaternelle[]): AssistanteMaternelle[] {
|
||||
return ams.map(mapAmForApi);
|
||||
}
|
||||
|
||||
export function filterActiveAmChildren(links: AmChildren[] | undefined): AmChildren[] {
|
||||
return (links ?? []).filter((link) => !link.date_fin);
|
||||
}
|
||||
@@ -2,12 +2,14 @@ import { Module } from '@nestjs/common';
|
||||
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
|
||||
import { AssistantesMaternellesController } from './assistantes_maternelles.controller';
|
||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||
import { AmChildren } from 'src/entities/am_children.entity';
|
||||
import { Children } from 'src/entities/children.entity';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Users } from 'src/entities/users.entity';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AssistanteMaternelle, Users]),
|
||||
imports: [TypeOrmModule.forFeature([AssistanteMaternelle, AmChildren, Children, Users]),
|
||||
AuthModule
|
||||
],
|
||||
controllers: [AssistantesMaternellesController],
|
||||
|
||||
@@ -5,11 +5,17 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from '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 {
|
||||
@@ -17,10 +23,13 @@ export class AssistantesMaternellesService {
|
||||
@InjectRepository(AssistanteMaternelle)
|
||||
private readonly assistantesMaternelleRepository: Repository<AssistanteMaternelle>,
|
||||
@InjectRepository(Users)
|
||||
private readonly usersRepository: Repository<Users>
|
||||
private readonly usersRepository: Repository<Users>,
|
||||
@InjectRepository(AmChildren)
|
||||
private readonly amChildrenRepository: Repository<AmChildren>,
|
||||
@InjectRepository(Children)
|
||||
private readonly childrenRepository: Repository<Children>,
|
||||
) {}
|
||||
|
||||
// Création d’une assistante maternelle
|
||||
async create(dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
|
||||
const user = await this.usersRepository.findOneBy({ id: dto.user_id });
|
||||
if (!user) throw new NotFoundException('Utilisateur introuvable');
|
||||
@@ -49,30 +58,208 @@ export class AssistantesMaternellesService {
|
||||
return this.assistantesMaternelleRepository.save(entity);
|
||||
}
|
||||
|
||||
// Liste des assistantes maternelles
|
||||
async findAll(): Promise<AssistanteMaternelle[]> {
|
||||
return this.assistantesMaternelleRepository.find({
|
||||
relations: ['user'],
|
||||
relations: [...AM_CHILDREN_RELATIONS],
|
||||
});
|
||||
}
|
||||
|
||||
// Récupérer une assistante maternelle par user_id
|
||||
async findOne(user_id: string): Promise<AssistanteMaternelle> {
|
||||
const assistante = await this.assistantesMaternelleRepository.findOne({
|
||||
where: { user_id },
|
||||
relations: ['user'],
|
||||
relations: [...AM_CHILDREN_RELATIONS],
|
||||
});
|
||||
if (!assistante) throw new NotFoundException('Assistante maternelle introuvable');
|
||||
return assistante;
|
||||
}
|
||||
|
||||
// Mise à jour
|
||||
async update(id: string, dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
|
||||
await this.assistantesMaternelleRepository.update(id, dto);
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
// Suppression d’une assistante maternelle
|
||||
/**
|
||||
* 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' };
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsEmail,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
MaxLength,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { StatutUtilisateurType } from 'src/entities/users.entity';
|
||||
|
||||
/** Mise à jour fiche AM par admin/gestionnaire (doc 28 §6.1, ticket #131). */
|
||||
export class UpdateAmFicheAdminDto {
|
||||
@ApiPropertyOptional({ example: 'MARTIN' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
nom?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Claire' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
prenom?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'claire@example.com' })
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '0612345678' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
telephone?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '5 place Bellecour' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
adresse?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Lyon' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(150)
|
||||
ville?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '69002' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(10)
|
||||
code_postal?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: StatutUtilisateurType })
|
||||
@IsOptional()
|
||||
@IsEnum(StatutUtilisateurType)
|
||||
statut?: StatutUtilisateurType;
|
||||
|
||||
@ApiPropertyOptional({ example: '123456789012345' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(15)
|
||||
nir?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '1985-03-12' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
date_naissance?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Lyon' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
lieu_naissance_ville?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'France' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
lieu_naissance_pays?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'AGR-2024-12345' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(50)
|
||||
approval_number?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '2020-01-15' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
agreement_date?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Lyon' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
residence_city?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 4 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(10)
|
||||
max_children?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 2 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(10)
|
||||
places_available?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
biography?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
available?: boolean;
|
||||
}
|
||||
@@ -176,7 +176,7 @@ describe('AuthService (#118 create-password API)', () => {
|
||||
parentsServiceMock.getDossierFamilleByNumero.mockResolvedValue({
|
||||
numero_dossier: '2026-000021',
|
||||
parents: [{ user_id: 'p1', email: 'claire@test.fr', statut: StatutUtilisateurType.REFUSE }],
|
||||
enfants: [{ id: 'e1', first_name: 'Emma', status: 'actif' }],
|
||||
enfants: [{ id: 'e1', first_name: 'Emma', status: 'sans_garde' }],
|
||||
texte_motivation: 'Motivation test',
|
||||
});
|
||||
|
||||
|
||||
@@ -551,7 +551,7 @@ export class AuthService {
|
||||
? new Date(enfantDto.date_previsionnelle_naissance)
|
||||
: undefined;
|
||||
enfant.photo_url = urlPhoto || undefined;
|
||||
enfant.status = enfantDto.date_naissance ? StatutEnfantType.ACTIF : StatutEnfantType.A_NAITRE;
|
||||
enfant.status = enfantDto.date_naissance ? StatutEnfantType.SANS_GARDE : StatutEnfantType.A_NAITRE;
|
||||
enfant.consent_photo = false;
|
||||
enfant.is_multiple = enfantDto.grossesse_multiple || false;
|
||||
|
||||
@@ -1063,7 +1063,7 @@ export class AuthService {
|
||||
if (enfantDto.genre !== undefined) enfant.gender = enfantDto.genre;
|
||||
if (enfantDto.date_naissance !== undefined) {
|
||||
enfant.birth_date = new Date(enfantDto.date_naissance);
|
||||
enfant.status = StatutEnfantType.ACTIF;
|
||||
enfant.status = StatutEnfantType.SANS_GARDE;
|
||||
}
|
||||
if (enfantDto.date_previsionnelle_naissance !== undefined) {
|
||||
enfant.due_date = new Date(enfantDto.date_previsionnelle_naissance);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { GenreType, StatutEnfantType } from 'src/entities/children.entity';
|
||||
|
||||
export class CreateEnfantsDto {
|
||||
@ApiProperty({ enum: StatutEnfantType, example: StatutEnfantType.ACTIF })
|
||||
@ApiProperty({ enum: StatutEnfantType, example: StatutEnfantType.SANS_GARDE })
|
||||
@IsEnum(StatutEnfantType)
|
||||
@IsNotEmpty()
|
||||
status: StatutEnfantType;
|
||||
|
||||
@@ -83,7 +83,12 @@ export class EnfantsController {
|
||||
return this.enfantsService.findOne(id, currentUser);
|
||||
}
|
||||
|
||||
@Roles(RoleType.ADMINISTRATEUR, RoleType.SUPER_ADMIN, RoleType.PARENT)
|
||||
@Roles(
|
||||
RoleType.PARENT,
|
||||
RoleType.ADMINISTRATEUR,
|
||||
RoleType.SUPER_ADMIN,
|
||||
RoleType.GESTIONNAIRE,
|
||||
)
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id', new ParseUUIDPipe()) id: string,
|
||||
|
||||
@@ -34,7 +34,7 @@ export class EnfantsService {
|
||||
|
||||
// Vérif métier simple
|
||||
if (dto.status !== StatutEnfantType.A_NAITRE && !dto.birth_date) {
|
||||
throw new BadRequestException('Un enfant actif doit avoir une date de naissance');
|
||||
throw new BadRequestException('Un enfant né doit avoir une date de naissance');
|
||||
}
|
||||
|
||||
// Vérif doublon éventuel (ex: même prénom + date de naissance pour ce parent)
|
||||
@@ -78,10 +78,10 @@ export class EnfantsService {
|
||||
return this.findOne(child.id, currentUser);
|
||||
}
|
||||
|
||||
// Liste des enfants
|
||||
// Liste des enfants (admin/gestionnaire)
|
||||
async findAll(): Promise<Children[]> {
|
||||
return this.childrenRepository.find({
|
||||
relations: ['parentLinks'],
|
||||
relations: ['parentLinks', 'parentLinks.parent', 'parentLinks.parent.user'],
|
||||
order: { last_name: 'ASC', first_name: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsEmail,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
import { StatutUtilisateurType } from 'src/entities/users.entity';
|
||||
|
||||
/** Mise à jour fiche parent par admin/gestionnaire (doc 28 §6.1, ticket #131). */
|
||||
export class UpdateParentFicheAdminDto {
|
||||
@ApiPropertyOptional({ example: 'Dupont' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
nom?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Marie' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
prenom?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'marie.dupont@example.com' })
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '+33612345678' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
telephone?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '10 rue de la Paix' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
adresse?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Paris' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(150)
|
||||
ville?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '75001' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(10)
|
||||
code_postal?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: StatutUtilisateurType })
|
||||
@IsOptional()
|
||||
@IsEnum(StatutUtilisateurType)
|
||||
statut?: StatutUtilisateurType;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
@@ -16,11 +17,13 @@ import { RoleType, StatutUtilisateurType } from 'src/entities/users.entity';
|
||||
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateParentDto } from '../user/dto/create_parent.dto';
|
||||
import { UpdateParentsDto } from '../user/dto/update_parent.dto';
|
||||
import { UpdateParentFicheAdminDto } from './dto/update-parent-fiche-admin.dto';
|
||||
import { AuthGuard } from 'src/common/guards/auth.guard';
|
||||
import { RolesGuard } from 'src/common/guards/roles.guard';
|
||||
import { User } from 'src/common/decorators/user.decorator';
|
||||
import { PendingFamilyDto } from './dto/pending-family.dto';
|
||||
import { DossierFamilleCompletDto } from './dto/dossier-famille-complet.dto';
|
||||
import { mapParentForApi, mapParentsForApi } from './parents.mapper';
|
||||
|
||||
@ApiTags('Parents')
|
||||
@Controller('parents')
|
||||
@@ -79,21 +82,25 @@ export class ParentsController {
|
||||
return validated;
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Get()
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Liste des parents (user, co_parent, parentChildren) — ticket #131' })
|
||||
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
||||
getAll(): Promise<Parents[]> {
|
||||
return this.parentsService.findAll();
|
||||
async getAll(): Promise<Parents[]> {
|
||||
const parents = await this.parentsService.findAll();
|
||||
return mapParentsForApi(parents);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Détail parent par user_id (inclut co_parent si id_co_parent renseigné) — ticket #131' })
|
||||
@ApiResponse({ status: 200, type: Parents, description: 'Détails du parent par ID utilisateur' })
|
||||
@ApiResponse({ status: 404, description: 'Parent non trouvé' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
||||
getOne(@Param('id') user_id: string): Promise<Parents> {
|
||||
return this.parentsService.findOne(user_id);
|
||||
async getOne(@Param('id') user_id: string): Promise<Parents> {
|
||||
const parent = await this.parentsService.findOne(user_id);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||
@@ -101,8 +108,51 @@ export class ParentsController {
|
||||
@ApiBody({ type: CreateParentDto })
|
||||
@ApiResponse({ status: 201, type: Parents, description: 'Parent créé avec succès' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
||||
create(@Body() dto: CreateParentDto): Promise<Parents> {
|
||||
return this.parentsService.create(dto);
|
||||
async create(@Body() dto: CreateParentDto): Promise<Parents> {
|
||||
const parent = await this.parentsService.create(dto);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Patch(':id/fiche')
|
||||
@ApiOperation({ summary: 'Mettre à jour la fiche parent (admin/gestionnaire) — ticket #131' })
|
||||
@ApiParam({ name: 'id', description: "UUID utilisateur du parent" })
|
||||
@ApiBody({ type: UpdateParentFicheAdminDto })
|
||||
@ApiResponse({ status: 200, type: Parents, description: 'Fiche parent mise à jour' })
|
||||
async updateFicheAdmin(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateParentFicheAdminDto,
|
||||
): Promise<Parents> {
|
||||
const parent = await this.parentsService.updateFicheAdmin(id, dto);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Post(':id/enfants/:enfantId')
|
||||
@ApiOperation({ summary: 'Rattacher un enfant à un parent — ticket #115' })
|
||||
@ApiParam({ name: 'id', description: "UUID utilisateur du parent" })
|
||||
@ApiParam({ name: 'enfantId', description: "UUID de l'enfant" })
|
||||
@ApiResponse({ status: 200, type: Parents, description: 'Parent avec enfants mis à jour' })
|
||||
async attachEnfant(
|
||||
@Param('id') id: string,
|
||||
@Param('enfantId') enfantId: string,
|
||||
): Promise<Parents> {
|
||||
const parent = await this.parentsService.attachEnfant(id, enfantId);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Delete(':id/enfants/:enfantId')
|
||||
@ApiOperation({ summary: "Détacher un enfant d'un parent — ticket #115" })
|
||||
@ApiParam({ name: 'id', description: "UUID utilisateur du parent" })
|
||||
@ApiParam({ name: 'enfantId', description: "UUID de l'enfant" })
|
||||
@ApiResponse({ status: 200, type: Parents, description: 'Parent avec enfants mis à jour' })
|
||||
async detachEnfant(
|
||||
@Param('id') id: string,
|
||||
@Param('enfantId') enfantId: string,
|
||||
): Promise<Parents> {
|
||||
const parent = await this.parentsService.detachEnfant(id, enfantId);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||
@@ -111,7 +161,8 @@ export class ParentsController {
|
||||
@ApiResponse({ status: 200, type: Parents, description: 'Parent mis à jour avec succès' })
|
||||
@ApiResponse({ status: 404, description: 'Parent introuvable' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
||||
update(@Param('id') id: string, @Body() dto: UpdateParentsDto): Promise<Parents> {
|
||||
return this.parentsService.update(id, dto);
|
||||
async update(@Param('id') id: string, @Body() dto: UpdateParentsDto): Promise<Parents> {
|
||||
const parent = await this.parentsService.update(id, dto);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { mapParentForApi } from './parents.mapper';
|
||||
import { Parents } from '../../entities/parents.entity';
|
||||
import { RoleType, StatutUtilisateurType, Users } from '../../entities/users.entity';
|
||||
|
||||
describe('mapParentForApi', () => {
|
||||
it('expose co_parent avec prenom/nom sans secrets', () => {
|
||||
const coParent = {
|
||||
id: 'cp1',
|
||||
email: 'co@b.fr',
|
||||
prenom: 'Clara',
|
||||
nom: 'Co',
|
||||
role: RoleType.PARENT,
|
||||
statut: StatutUtilisateurType.ACTIF,
|
||||
password: 'secret',
|
||||
} as Users;
|
||||
|
||||
const parent = {
|
||||
user_id: 'u1',
|
||||
numero_dossier: '2026-000042',
|
||||
user: {
|
||||
id: 'u1',
|
||||
email: 'p@b.fr',
|
||||
prenom: 'Paul',
|
||||
nom: 'Parent',
|
||||
role: RoleType.PARENT,
|
||||
password: 'secret',
|
||||
} as Users,
|
||||
co_parent: coParent,
|
||||
parentChildren: [],
|
||||
} as Parents;
|
||||
|
||||
const out = mapParentForApi(parent);
|
||||
expect(out.co_parent?.prenom).toBe('Clara');
|
||||
expect(out.co_parent?.nom).toBe('Co');
|
||||
expect(out.co_parent?.password).toBeUndefined();
|
||||
expect(out.user.password).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Parents } from 'src/entities/parents.entity';
|
||||
import { sanitizeUserForApi } from '../../common/utils/sanitize-user-for-api';
|
||||
|
||||
/**
|
||||
* Sérialisation API fiche parent — ticket #131.
|
||||
* Garantit `user`, `co_parent` (si présent) et relations sans champs sensibles.
|
||||
*/
|
||||
export function mapParentForApi(parent: Parents): Parents {
|
||||
return {
|
||||
...parent,
|
||||
user: sanitizeUserForApi(parent.user)!,
|
||||
co_parent: sanitizeUserForApi(parent.co_parent),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapParentsForApi(parents: Parents[]): Parents[] {
|
||||
return parents.map(mapParentForApi);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { Parents } from 'src/entities/parents.entity';
|
||||
import { DossierFamille, DossierFamilleEnfant } from 'src/entities/dossier_famille.entity';
|
||||
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||
import { ParentsController } from './parents.controller';
|
||||
import { ParentsService } from './parents.service';
|
||||
import { Users } from 'src/entities/users.entity';
|
||||
@@ -11,7 +12,7 @@ import { UserModule } from '../user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Parents, Users, DossierFamille, DossierFamilleEnfant]),
|
||||
TypeOrmModule.forFeature([Parents, Users, DossierFamille, DossierFamilleEnfant, ParentsChildren]),
|
||||
forwardRef(() => UserModule),
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
DossierFamilleParentDto,
|
||||
DossierFamilleEnfantDto,
|
||||
} from './dto/dossier-famille-complet.dto';
|
||||
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||
import { Children } from 'src/entities/children.entity';
|
||||
import { UpdateParentFicheAdminDto } from './dto/update-parent-fiche-admin.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ParentsService {
|
||||
@@ -28,6 +30,8 @@ export class ParentsService {
|
||||
private readonly usersRepository: Repository<Users>,
|
||||
@InjectRepository(DossierFamille)
|
||||
private readonly dossierFamilleRepository: Repository<DossierFamille>,
|
||||
@InjectRepository(ParentsChildren)
|
||||
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
|
||||
) {}
|
||||
|
||||
// Création d’un parent
|
||||
@@ -62,7 +66,7 @@ export class ParentsService {
|
||||
// Liste des parents
|
||||
async findAll(): Promise<Parents[]> {
|
||||
return this.parentsRepository.find({
|
||||
relations: ['user', 'co_parent', 'parentChildren', 'dossiers'],
|
||||
relations: ['user', 'co_parent', 'parentChildren', 'parentChildren.child', 'dossiers'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,7 +74,7 @@ export class ParentsService {
|
||||
async findOne(user_id: string): Promise<Parents> {
|
||||
const parent = await this.parentsRepository.findOne({
|
||||
where: { user_id },
|
||||
relations: ['user', 'co_parent', 'parentChildren', 'dossiers'],
|
||||
relations: ['user', 'co_parent', 'parentChildren', 'parentChildren.child', 'dossiers'],
|
||||
});
|
||||
if (!parent) throw new NotFoundException('Parent introuvable');
|
||||
return parent;
|
||||
@@ -82,6 +86,79 @@ export class ParentsService {
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mise à jour fiche parent (champs user + statut) par admin/gestionnaire. Ticket #131 / doc 28 §6.1.
|
||||
*/
|
||||
async updateFicheAdmin(parentUserId: string, dto: UpdateParentFicheAdminDto): Promise<Parents> {
|
||||
const parent = await this.findOne(parentUserId);
|
||||
const user = parent.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;
|
||||
|
||||
await this.usersRepository.save(user);
|
||||
return this.findOne(parentUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rattacher un enfant existant à un parent (enfants_parents). Ticket #115 / doc 28 §6.2.
|
||||
*/
|
||||
async attachEnfant(parentUserId: string, enfantId: string): Promise<Parents> {
|
||||
await this.findOne(parentUserId);
|
||||
|
||||
const existing = await this.parentsChildrenRepository.findOne({
|
||||
where: { parentId: parentUserId, enfantId },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException('Cet enfant est déjà rattaché à ce parent');
|
||||
}
|
||||
|
||||
const child = await this.parentsRepository.manager.findOne(Children, { where: { id: enfantId } });
|
||||
if (!child) {
|
||||
throw new NotFoundException('Enfant introuvable');
|
||||
}
|
||||
|
||||
await this.parentsChildrenRepository.save(
|
||||
this.parentsChildrenRepository.create({ parentId: parentUserId, enfantId }),
|
||||
);
|
||||
return this.findOne(parentUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Détacher un enfant d'un parent sans supprimer l'enfant. Ticket #115 / doc 28 §6.2.
|
||||
*/
|
||||
async detachEnfant(parentUserId: string, enfantId: string): Promise<Parents> {
|
||||
await this.findOne(parentUserId);
|
||||
|
||||
const link = await this.parentsChildrenRepository.findOne({
|
||||
where: { parentId: parentUserId, enfantId },
|
||||
});
|
||||
if (!link) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste des familles en attente (une entrée par famille).
|
||||
* Famille = lien co_parent ou partage d'enfants (même logique que backfill #103).
|
||||
|
||||
Reference in New Issue
Block a user