From f7ac628445c9063442bbb50b629d38cf280c401a Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Fri, 17 Jul 2026 18:08:52 +0200 Subject: [PATCH] =?UTF-8?q?feat(#132):=20photo=20=C3=A0=20la=20cr=C3=A9ati?= =?UTF-8?q?on=20enfant=20staff=20(multipart=20+=20bools).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transform "true"/"false" pour class-validator ; multipart staff avec FileInterceptor('photo') inchangé côté stockage. Co-authored-by: Cursor --- .../src/routes/enfants/dto/create_enfants.dto.ts | 14 ++++++++++++++ backend/src/routes/enfants/enfants.controller.ts | 13 +++++++++---- backend/src/routes/enfants/enfants.service.ts | 5 +++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/backend/src/routes/enfants/dto/create_enfants.dto.ts b/backend/src/routes/enfants/dto/create_enfants.dto.ts index edfc5cb..0d66d46 100644 --- a/backend/src/routes/enfants/dto/create_enfants.dto.ts +++ b/backend/src/routes/enfants/dto/create_enfants.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; import { IsBoolean, IsDateString, @@ -12,6 +13,17 @@ import { } from 'class-validator'; import { GenreType, StatutEnfantType } from 'src/entities/children.entity'; +/** Multipart envoie des strings ("true"/"false") — JSON envoie déjà des booleans. */ +function toBoolean({ value }: { value: unknown }): boolean | unknown { + if (typeof value === 'boolean') return value; + if (typeof value === 'string') { + const v = value.trim().toLowerCase(); + if (v === 'true' || v === '1') return true; + if (v === 'false' || v === '0' || v === '') return false; + } + return value; +} + export class CreateEnfantsDto { @ApiProperty({ enum: StatutEnfantType, example: StatutEnfantType.SANS_GARDE }) @IsEnum(StatutEnfantType) @@ -53,6 +65,7 @@ export class CreateEnfantsDto { photo_url?: string; @ApiProperty({ default: false }) + @Transform(toBoolean) @IsBoolean() consent_photo: boolean; @@ -62,6 +75,7 @@ export class CreateEnfantsDto { consent_photo_at?: string; @ApiProperty({ default: false }) + @Transform(toBoolean) @IsBoolean() is_multiple: boolean; diff --git a/backend/src/routes/enfants/enfants.controller.ts b/backend/src/routes/enfants/enfants.controller.ts index 5505704..492c885 100644 --- a/backend/src/routes/enfants/enfants.controller.ts +++ b/backend/src/routes/enfants/enfants.controller.ts @@ -58,8 +58,8 @@ const photoMulterOptions = { }; /** - * Multer uniquement si Content-Type multipart (parcours parent). - * Les appels staff JSON (#132) passent sans interceptor fichier. + * Multer uniquement si Content-Type multipart (parent ou staff + photo). + * JSON sans photo (#132) passe sans interceptor fichier. */ @Injectable() class OptionalEnfantPhotoInterceptor implements NestInterceptor { @@ -96,10 +96,15 @@ export class EnfantsController { @ApiOperation({ summary: 'Créer un enfant', description: - 'PARENT : multipart éventuel, rattache au compte connecté. Staff : JSON + parent_user_id (foyer). Ticket #132.', + 'PARENT : multipart éventuel, rattache au compte connecté. ' + + 'Staff : parent_user_id obligatoire ; JSON sans photo OK ; avec photo → multipart (champ fichier `photo`, max 5 Mo). Ticket #132.', }) @ApiConsumes('application/json', 'multipart/form-data') - @ApiBody({ type: CreateEnfantsDto }) + @ApiBody({ + description: + 'Champs métier (+ parent_user_id côté staff). Fichier optionnel `photo` en multipart.', + type: CreateEnfantsDto, + }) @UseInterceptors(OptionalEnfantPhotoInterceptor) create( @Body() dto: CreateEnfantsDto, diff --git a/backend/src/routes/enfants/enfants.service.ts b/backend/src/routes/enfants/enfants.service.ts index fc3a301..bd77d68 100644 --- a/backend/src/routes/enfants/enfants.service.ts +++ b/backend/src/routes/enfants/enfants.service.ts @@ -37,7 +37,8 @@ export class EnfantsService { /** * Création d'un enfant. * - PARENT : rattache au parent connecté (multipart photo optionnel). - * - Staff : JSON + `parent_user_id` obligatoire (foyer existant). Ticket #132. + * - Staff : `parent_user_id` obligatoire ; JSON sans photo OK ; + * avec photo → multipart (même stockage `/uploads/photos/...`). Ticket #132. */ async create( dto: CreateEnfantsDto, @@ -67,7 +68,7 @@ export class EnfantsService { }); if (exist) throw new ConflictException('Cet enfant existe déjà'); - // Gestion de la photo uploadée (parcours parent multipart) + // Gestion de la photo uploadée (multipart parent ou staff) let photoUrl = dto.photo_url; let consentAt: Date | undefined; if (photoFile) {