feat(#132): photo à la création enfant staff (multipart + bools).

Transform "true"/"false" pour class-validator ; multipart staff
avec FileInterceptor('photo') inchangé côté stockage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MARTIN Julien 2026-07-17 18:08:52 +02:00
parent 26e9738ec7
commit f7ac628445
3 changed files with 26 additions and 6 deletions

View File

@ -1,4 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
IsDateString, IsDateString,
@ -12,6 +13,17 @@ import {
} from 'class-validator'; } from 'class-validator';
import { GenreType, StatutEnfantType } from 'src/entities/children.entity'; 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 { export class CreateEnfantsDto {
@ApiProperty({ enum: StatutEnfantType, example: StatutEnfantType.SANS_GARDE }) @ApiProperty({ enum: StatutEnfantType, example: StatutEnfantType.SANS_GARDE })
@IsEnum(StatutEnfantType) @IsEnum(StatutEnfantType)
@ -53,6 +65,7 @@ export class CreateEnfantsDto {
photo_url?: string; photo_url?: string;
@ApiProperty({ default: false }) @ApiProperty({ default: false })
@Transform(toBoolean)
@IsBoolean() @IsBoolean()
consent_photo: boolean; consent_photo: boolean;
@ -62,6 +75,7 @@ export class CreateEnfantsDto {
consent_photo_at?: string; consent_photo_at?: string;
@ApiProperty({ default: false }) @ApiProperty({ default: false })
@Transform(toBoolean)
@IsBoolean() @IsBoolean()
is_multiple: boolean; is_multiple: boolean;

View File

@ -58,8 +58,8 @@ const photoMulterOptions = {
}; };
/** /**
* Multer uniquement si Content-Type multipart (parcours parent). * Multer uniquement si Content-Type multipart (parent ou staff + photo).
* Les appels staff JSON (#132) passent sans interceptor fichier. * JSON sans photo (#132) passe sans interceptor fichier.
*/ */
@Injectable() @Injectable()
class OptionalEnfantPhotoInterceptor implements NestInterceptor { class OptionalEnfantPhotoInterceptor implements NestInterceptor {
@ -96,10 +96,15 @@ export class EnfantsController {
@ApiOperation({ @ApiOperation({
summary: 'Créer un enfant', summary: 'Créer un enfant',
description: 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') @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) @UseInterceptors(OptionalEnfantPhotoInterceptor)
create( create(
@Body() dto: CreateEnfantsDto, @Body() dto: CreateEnfantsDto,

View File

@ -37,7 +37,8 @@ export class EnfantsService {
/** /**
* Création d'un enfant. * Création d'un enfant.
* - PARENT : rattache au parent connecté (multipart photo optionnel). * - 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( async create(
dto: CreateEnfantsDto, dto: CreateEnfantsDto,
@ -67,7 +68,7 @@ export class EnfantsService {
}); });
if (exist) throw new ConflictException('Cet enfant existe déjà'); 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 photoUrl = dto.photo_url;
let consentAt: Date | undefined; let consentAt: Date | undefined;
if (photoFile) { if (photoFile) {