Compare commits

..

7 Commits

Author SHA1 Message Date
c934466e47 Merge pull request 'feat(#37): Inscription Parent - Étape 2 (Parent 2)' (#74) from feature/37-frontend-inscription-parent-step2 into master
feat(#37): Inscription Parent - Étape 2 (Parent 2)
2025-12-01 22:36:31 +00:00
90d8fa8669 feat(#37): Inscription Parent - Étape 2 (Parent 2)
Frontend Step2:
- Suppression des champs mot de passe et confirmation
- Correction de l'indicateur d'étape: 2/5 → 2/6
- Améliorations visuelles (mêmes que Step1):
  * Taille des labels: 18 → 22px
  * Taille de police des champs: 18 → 20px
  * Espacement entre champs: 20 → 32px
  * Meilleure répartition verticale avec spaceEvenly

Note: Le champ password est conservé dans le modèle ParentData pour compatibilité
2025-12-01 23:36:02 +01:00
90cdf16709 docs: Correction numérotation tickets et ajout statuts terminés
- Correction numérotation pour correspondre à Gitea (#36-#63)
- Ajout tickets #34 et #35 (réservés)
- Marquage tickets terminés avec :
  * #3, #4, #7 (BDD)
  * #18, #19, #20, #21 (Backend API Parent)
  * #36 (Frontend Step1)
- Correction doublons (#38, #39, #41, #42, #47, #48)
- Renumération tickets Frontend et Tests
2025-12-01 23:28:08 +01:00
bde97c24db Merge pull request 'feat(#36): Inscription Parent - Étape 1 (Parent 1)' (#73) from feature/36-frontend-inscription-parent-step1 into master
feat(#36): Inscription Parent - Étape 1 (Parent 1)
2025-12-01 22:21:02 +00:00
9ae6533b4d feat(#36): Inscription Parent - Étape 1 (Parent 1)
Backend:
- Retrait des champs non-CDC: profession, situation_familiale, date_naissance
- Nettoyage des DTOs RegisterParentCompletDto et RegisterParentDto
- Mise à jour de la logique dans auth.service.ts (inscrireParentComplet et legacy)

Frontend Step1:
- Suppression des champs mot de passe et confirmation
- Correction de l'indicateur d'étape: 1/5 → 1/6
- Améliorations visuelles:
  * Taille des labels: 18 → 22px
  * Taille de police des champs: 18 → 20px
  * Espacement entre champs: 20 → 32px
  * Meilleure répartition verticale avec spaceEvenly

Note: Le champ password est conservé dans le modèle ParentData pour compatibilité avec Step2
2025-12-01 23:19:58 +01:00
579b6cae90 [Backend] API Inscription Parent - REFONTE Workflow 6 etapes (#72)
Co-authored-by: Julien Martin <julien.martin@ptits-pas.fr>
Co-committed-by: Julien Martin <julien.martin@ptits-pas.fr>
2025-12-01 21:43:36 +00:00
9aea26805d Merge pull request '[Backend] Endpoint inscription parent + Nettoyage code #9' (#71) from feature/9-endpoints-inscription-parent into master 2025-11-30 15:10:21 +00:00
14 changed files with 972 additions and 161 deletions

View File

@ -32,6 +32,9 @@ COPY --from=builder /app/dist ./dist
RUN addgroup -g 1001 -S nodejs RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001 RUN adduser -S nestjs -u 1001
# Créer le dossier uploads et donner les permissions
RUN mkdir -p /app/uploads/photos && chown -R nestjs:nodejs /app/uploads
USER nestjs USER nestjs
EXPOSE 3000 EXPOSE 3000

View File

@ -4,6 +4,7 @@ import { AuthService } from './auth.service';
import { Public } from 'src/common/decorators/public.decorator'; import { Public } from 'src/common/decorators/public.decorator';
import { RegisterDto } from './dto/register.dto'; import { RegisterDto } from './dto/register.dto';
import { RegisterParentDto } from './dto/register-parent.dto'; import { RegisterParentDto } from './dto/register-parent.dto';
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard'; import { AuthGuard } from 'src/common/guards/auth.guard';
import type { Request } from 'express'; import type { Request } from 'express';
@ -39,10 +40,23 @@ export class AuthController {
@Public() @Public()
@Post('register/parent') @Post('register/parent')
@ApiOperation({ summary: 'Inscription Parent (étape 1/6)' }) @ApiOperation({
summary: 'Inscription Parent COMPLÈTE - Workflow 6 étapes',
description: 'Crée Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU en une transaction'
})
@ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation' })
@ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' })
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
async inscrireParentComplet(@Body() dto: RegisterParentCompletDto) {
return this.authService.inscrireParentComplet(dto);
}
@Public()
@Post('register/parent/legacy')
@ApiOperation({ summary: '[OBSOLÈTE] Inscription Parent (étape 1/6 uniquement)' })
@ApiResponse({ status: 201, description: 'Inscription réussie' }) @ApiResponse({ status: 201, description: 'Inscription réussie' })
@ApiResponse({ status: 409, description: 'Email déjà utilisé' }) @ApiResponse({ status: 409, description: 'Email déjà utilisé' })
async registerParent(@Body() dto: RegisterParentDto) { async registerParentLegacy(@Body() dto: RegisterParentDto) {
return this.authService.registerParent(dto); return this.authService.registerParent(dto);
} }

View File

@ -7,11 +7,12 @@ import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { Users } from 'src/entities/users.entity'; import { Users } from 'src/entities/users.entity';
import { Parents } from 'src/entities/parents.entity'; import { Parents } from 'src/entities/parents.entity';
import { Children } from 'src/entities/children.entity';
import { AppConfigModule } from 'src/modules/config'; import { AppConfigModule } from 'src/modules/config';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Users, Parents]), TypeOrmModule.forFeature([Users, Parents, Children]),
forwardRef(() => UserModule), forwardRef(() => UserModule),
AppConfigModule, AppConfigModule,
JwtModule.registerAsync({ JwtModule.registerAsync({

View File

@ -2,6 +2,7 @@ import {
ConflictException, ConflictException,
Injectable, Injectable,
UnauthorizedException, UnauthorizedException,
BadRequestException,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@ -9,11 +10,16 @@ import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import * as fs from 'fs/promises';
import * as path from 'path';
import { RegisterDto } from './dto/register.dto'; import { RegisterDto } from './dto/register.dto';
import { RegisterParentDto } from './dto/register-parent.dto'; import { RegisterParentDto } from './dto/register-parent.dto';
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity'; import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity';
import { Parents } from 'src/entities/parents.entity'; import { Parents } from 'src/entities/parents.entity';
import { Children, StatutEnfantType } from 'src/entities/children.entity';
import { ParentsChildren } from 'src/entities/parents_children.entity';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { AppConfigService } from 'src/modules/config/config.service'; import { AppConfigService } from 'src/modules/config/config.service';
@ -28,6 +34,8 @@ export class AuthService {
private readonly parentsRepo: Repository<Parents>, private readonly parentsRepo: Repository<Parents>,
@InjectRepository(Users) @InjectRepository(Users)
private readonly usersRepo: Repository<Users>, private readonly usersRepo: Repository<Users>,
@InjectRepository(Children)
private readonly childrenRepo: Repository<Children>,
) { } ) { }
/** /**
@ -192,16 +200,10 @@ export class AuthService {
adresse: dto.adresse, adresse: dto.adresse,
code_postal: dto.code_postal, code_postal: dto.code_postal,
ville: dto.ville, ville: dto.ville,
profession: dto.profession,
situation_familiale: dto.situation_familiale,
token_creation_mdp: tokenCreationMdp, token_creation_mdp: tokenCreationMdp,
token_creation_mdp_expire_le: tokenExpiration, token_creation_mdp_expire_le: tokenExpiration,
}); });
if (dto.date_naissance) {
parent1.date_naissance = new Date(dto.date_naissance);
}
const savedParent1 = await manager.save(Users, parent1); const savedParent1 = await manager.save(Users, parent1);
// Créer Parent 2 si renseigné // Créer Parent 2 si renseigné
@ -274,9 +276,186 @@ export class AuthService {
}; };
} }
/**
* Inscription Parent COMPLÈTE - Workflow CDC 6 étapes en 1 transaction
* Gère : Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU
*/
async inscrireParentComplet(dto: RegisterParentCompletDto) {
if (!dto.acceptation_cgu || !dto.acceptation_privacy) {
throw new BadRequestException('L\'acceptation des CGU et de la politique de confidentialité est obligatoire');
}
if (!dto.enfants || dto.enfants.length === 0) {
throw new BadRequestException('Au moins un enfant est requis');
}
const existe = await this.usersService.findByEmailOrNull(dto.email);
if (existe) {
throw new ConflictException('Un compte avec cet email existe déjà');
}
if (dto.co_parent_email) {
const coParentExiste = await this.usersService.findByEmailOrNull(dto.co_parent_email);
if (coParentExiste) {
throw new ConflictException('L\'email du co-parent est déjà utilisé');
}
}
const joursExpirationToken = await this.appConfigService.get<number>(
'password_reset_token_expiry_days',
7,
);
const tokenCreationMdp = crypto.randomUUID();
const dateExpiration = new Date();
dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken);
const resultat = await this.usersRepo.manager.transaction(async (manager) => {
const parent1 = manager.create(Users, {
email: dto.email,
prenom: dto.prenom,
nom: dto.nom,
role: RoleType.PARENT,
statut: StatutUtilisateurType.EN_ATTENTE,
telephone: dto.telephone,
adresse: dto.adresse,
code_postal: dto.code_postal,
ville: dto.ville,
token_creation_mdp: tokenCreationMdp,
token_creation_mdp_expire_le: dateExpiration,
});
const parent1Enregistre = await manager.save(Users, parent1);
let parent2Enregistre: Users | null = null;
let tokenCoParent: string | null = null;
if (dto.co_parent_email && dto.co_parent_prenom && dto.co_parent_nom) {
tokenCoParent = crypto.randomUUID();
const dateExpirationCoParent = new Date();
dateExpirationCoParent.setDate(dateExpirationCoParent.getDate() + joursExpirationToken);
const parent2 = manager.create(Users, {
email: dto.co_parent_email,
prenom: dto.co_parent_prenom,
nom: dto.co_parent_nom,
role: RoleType.PARENT,
statut: StatutUtilisateurType.EN_ATTENTE,
telephone: dto.co_parent_telephone,
adresse: dto.co_parent_meme_adresse ? dto.adresse : dto.co_parent_adresse,
code_postal: dto.co_parent_meme_adresse ? dto.code_postal : dto.co_parent_code_postal,
ville: dto.co_parent_meme_adresse ? dto.ville : dto.co_parent_ville,
token_creation_mdp: tokenCoParent,
token_creation_mdp_expire_le: dateExpirationCoParent,
});
parent2Enregistre = await manager.save(Users, parent2);
}
const entiteParent = manager.create(Parents, {
user_id: parent1Enregistre.id,
});
entiteParent.user = parent1Enregistre;
if (parent2Enregistre) {
entiteParent.co_parent = parent2Enregistre;
}
await manager.save(Parents, entiteParent);
if (parent2Enregistre) {
const entiteCoParent = manager.create(Parents, {
user_id: parent2Enregistre.id,
});
entiteCoParent.user = parent2Enregistre;
entiteCoParent.co_parent = parent1Enregistre;
await manager.save(Parents, entiteCoParent);
}
const enfantsEnregistres: Children[] = [];
for (const enfantDto of dto.enfants) {
let urlPhoto: string | null = null;
if (enfantDto.photo_base64 && enfantDto.photo_filename) {
urlPhoto = await this.sauvegarderPhotoDepuisBase64(
enfantDto.photo_base64,
enfantDto.photo_filename,
);
}
const enfant = new Children();
enfant.first_name = enfantDto.prenom;
enfant.last_name = enfantDto.nom || dto.nom;
enfant.gender = enfantDto.genre;
enfant.birth_date = enfantDto.date_naissance ? new Date(enfantDto.date_naissance) : undefined;
enfant.due_date = enfantDto.date_previsionnelle_naissance
? new Date(enfantDto.date_previsionnelle_naissance)
: undefined;
enfant.photo_url = urlPhoto || undefined;
enfant.status = enfantDto.date_naissance ? StatutEnfantType.ACTIF : StatutEnfantType.A_NAITRE;
enfant.consent_photo = false;
enfant.is_multiple = enfantDto.grossesse_multiple || false;
const enfantEnregistre = await manager.save(Children, enfant);
enfantsEnregistres.push(enfantEnregistre);
const lienParentEnfant1 = manager.create(ParentsChildren, {
parentId: parent1Enregistre.id,
enfantId: enfantEnregistre.id,
});
await manager.save(ParentsChildren, lienParentEnfant1);
if (parent2Enregistre) {
const lienParentEnfant2 = manager.create(ParentsChildren, {
parentId: parent2Enregistre.id,
enfantId: enfantEnregistre.id,
});
await manager.save(ParentsChildren, lienParentEnfant2);
}
}
return {
parent1: parent1Enregistre,
parent2: parent2Enregistre,
enfants: enfantsEnregistres,
tokenCreationMdp,
tokenCoParent,
};
});
return {
message: 'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
parent_id: resultat.parent1.id,
co_parent_id: resultat.parent2?.id,
enfants_ids: resultat.enfants.map(e => e.id),
statut: StatutUtilisateurType.EN_ATTENTE,
};
}
/**
* Sauvegarde une photo depuis base64 vers le système de fichiers
*/
private async sauvegarderPhotoDepuisBase64(donneesBase64: string, nomFichier: string): Promise<string> {
const correspondances = donneesBase64.match(/^data:image\/(\w+);base64,(.+)$/);
if (!correspondances) {
throw new BadRequestException('Format de photo invalide (doit être base64)');
}
const extension = correspondances[1];
const tamponImage = Buffer.from(correspondances[2], 'base64');
const dossierUpload = '/app/uploads/photos';
await fs.mkdir(dossierUpload, { recursive: true });
const nomFichierUnique = `${Date.now()}-${crypto.randomUUID()}.${extension}`;
const cheminFichier = path.join(dossierUpload, nomFichierUnique);
await fs.writeFile(cheminFichier, tamponImage);
return `/uploads/photos/${nomFichierUnique}`;
}
async logout(userId: string) { async logout(userId: string) {
// Pour le moment envoyer un message clair
return { success: true, message: 'Deconnexion'} return { success: true, message: 'Deconnexion'}
} }
} }

View File

@ -0,0 +1,63 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsString,
IsNotEmpty,
IsOptional,
IsEnum,
IsDateString,
IsBoolean,
MinLength,
MaxLength,
} from 'class-validator';
import { GenreType } from 'src/entities/children.entity';
export class EnfantInscriptionDto {
@ApiProperty({ example: 'Emma', required: false, description: 'Prénom de l\'enfant (obligatoire si déjà né)' })
@IsOptional()
@IsString()
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
@MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' })
prenom?: string;
@ApiProperty({ example: 'MARTIN', required: false, description: 'Nom de l\'enfant (hérité des parents si non fourni)' })
@IsOptional()
@IsString()
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
@MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' })
nom?: string;
@ApiProperty({ example: '2023-02-15', required: false, description: 'Date de naissance (si enfant déjà né)' })
@IsOptional()
@IsDateString()
date_naissance?: string;
@ApiProperty({ example: '2025-06-15', required: false, description: 'Date prévisionnelle de naissance (si enfant à naître)' })
@IsOptional()
@IsDateString()
date_previsionnelle_naissance?: string;
@ApiProperty({ enum: GenreType, example: GenreType.F })
@IsEnum(GenreType, { message: 'Le genre doit être H, F ou Autre' })
@IsNotEmpty({ message: 'Le genre est requis' })
genre: GenreType;
@ApiProperty({
example: 'data:image/jpeg;base64,/9j/4AAQSkZJRg...',
required: false,
description: 'Photo de l\'enfant en base64 (obligatoire si déjà né)'
})
@IsOptional()
@IsString()
photo_base64?: string;
@ApiProperty({ example: 'emma_martin.jpg', required: false, description: 'Nom du fichier photo' })
@IsOptional()
@IsString()
photo_filename?: string;
@ApiProperty({ example: false, required: false, description: 'Grossesse multiple (jumeaux, triplés, etc.)' })
@IsOptional()
@IsBoolean()
grossesse_multiple?: boolean;
}

View File

@ -0,0 +1,166 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
IsDateString,
IsEnum,
IsBoolean,
IsArray,
ValidateNested,
MinLength,
MaxLength,
Matches,
} from 'class-validator';
import { Type } from 'class-transformer';
import { SituationFamilialeType } from 'src/entities/users.entity';
import { EnfantInscriptionDto } from './enfant-inscription.dto';
export class RegisterParentCompletDto {
// ============================================
// ÉTAPE 1 : PARENT 1 (Obligatoire)
// ============================================
@ApiProperty({ example: 'claire.martin@ptits-pas.fr' })
@IsEmail({}, { message: 'Email invalide' })
@IsNotEmpty({ message: 'L\'email est requis' })
email: string;
@ApiProperty({ example: 'Claire' })
@IsString()
@IsNotEmpty({ message: 'Le prénom est requis' })
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
@MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' })
prenom: string;
@ApiProperty({ example: 'MARTIN' })
@IsString()
@IsNotEmpty({ message: 'Le nom est requis' })
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
@MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' })
nom: string;
@ApiProperty({ example: '0689567890' })
@IsString()
@IsNotEmpty({ message: 'Le téléphone est requis' })
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
message: 'Le numéro de téléphone doit être valide (ex: 0689567890 ou +33689567890)',
})
telephone: string;
@ApiProperty({ example: '5 Avenue du Général de Gaulle', required: false })
@IsOptional()
@IsString()
adresse?: string;
@ApiProperty({ example: '95870', required: false })
@IsOptional()
@IsString()
@MaxLength(10)
code_postal?: string;
@ApiProperty({ example: 'Bezons', required: false })
@IsOptional()
@IsString()
@MaxLength(150)
ville?: string;
// ============================================
// ÉTAPE 2 : PARENT 2 / CO-PARENT (Optionnel)
// ============================================
@ApiProperty({ example: 'thomas.martin@ptits-pas.fr', required: false })
@IsOptional()
@IsEmail({}, { message: 'Email du co-parent invalide' })
co_parent_email?: string;
@ApiProperty({ example: 'Thomas', required: false })
@IsOptional()
@IsString()
co_parent_prenom?: string;
@ApiProperty({ example: 'MARTIN', required: false })
@IsOptional()
@IsString()
co_parent_nom?: string;
@ApiProperty({ example: '0678456789', required: false })
@IsOptional()
@IsString()
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
message: 'Le numéro de téléphone du co-parent doit être valide',
})
co_parent_telephone?: string;
@ApiProperty({ example: true, description: 'Le co-parent habite à la même adresse', required: false })
@IsOptional()
@IsBoolean()
co_parent_meme_adresse?: boolean;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
co_parent_adresse?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
co_parent_code_postal?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
co_parent_ville?: string;
// ============================================
// ÉTAPE 3 : ENFANT(S) (Au moins 1 requis)
// ============================================
@ApiProperty({
type: [EnfantInscriptionDto],
description: 'Liste des enfants (au moins 1 requis)',
example: [{
prenom: 'Emma',
nom: 'MARTIN',
date_naissance: '2023-02-15',
genre: 'F',
photo_base64: 'data:image/jpeg;base64,...',
photo_filename: 'emma_martin.jpg'
}]
})
@IsArray({ message: 'La liste des enfants doit être un tableau' })
@IsNotEmpty({ message: 'Au moins un enfant est requis' })
@ValidateNested({ each: true })
@Type(() => EnfantInscriptionDto)
enfants: EnfantInscriptionDto[];
// ============================================
// ÉTAPE 4 : PRÉSENTATION DU DOSSIER (Optionnel)
// ============================================
@ApiProperty({
example: 'Nous recherchons une assistante maternelle bienveillante pour nos triplés...',
required: false,
description: 'Présentation du dossier (max 2000 caractères)'
})
@IsOptional()
@IsString()
@MaxLength(2000, { message: 'La présentation ne peut pas dépasser 2000 caractères' })
presentation_dossier?: string;
// ============================================
// ÉTAPE 5 : ACCEPTATION CGU (Obligatoire)
// ============================================
@ApiProperty({ example: true, description: 'Acceptation des Conditions Générales d\'Utilisation' })
@IsBoolean()
@IsNotEmpty({ message: 'L\'acceptation des CGU est requise' })
acceptation_cgu: boolean;
@ApiProperty({ example: true, description: 'Acceptation de la Politique de confidentialité' })
@IsBoolean()
@IsNotEmpty({ message: 'L\'acceptation de la politique de confidentialité est requise' })
acceptation_privacy: boolean;
}

View File

@ -59,22 +59,6 @@ export class RegisterParentDto {
@MaxLength(150) @MaxLength(150)
ville?: string; ville?: string;
@ApiProperty({ example: 'Infirmière', required: false })
@IsOptional()
@IsString()
@MaxLength(150)
profession?: string;
@ApiProperty({ enum: SituationFamilialeType, example: SituationFamilialeType.MARIE, required: false })
@IsOptional()
@IsEnum(SituationFamilialeType)
situation_familiale?: SituationFamilialeType;
@ApiProperty({ example: '1990-04-03', required: false })
@IsOptional()
@IsDateString()
date_naissance?: string;
// === Informations co-parent (optionnel) === // === Informations co-parent (optionnel) ===
@ApiProperty({ example: 'thomas.martin@ptits-pas.fr', required: false }) @ApiProperty({ example: 'thomas.martin@ptits-pas.fr', required: false })
@IsOptional() @IsOptional()

View File

@ -29,10 +29,10 @@ export class CreateEnfantsDto {
@MaxLength(100) @MaxLength(100)
last_name?: string; last_name?: string;
@ApiProperty({ enum: GenreType, required: false }) @ApiProperty({ enum: GenreType })
@IsOptional()
@IsEnum(GenreType) @IsEnum(GenreType)
gender?: GenreType; @IsNotEmpty()
gender: GenreType;
@ApiProperty({ example: '2018-06-24', required: false }) @ApiProperty({ example: '2018-06-24', required: false })
@ValidateIf(o => o.status !== StatutEnfantType.A_NAITRE) @ValidateIf(o => o.status !== StatutEnfantType.A_NAITRE)

View File

@ -8,8 +8,13 @@ import {
Patch, Patch,
Post, Post,
UseGuards, UseGuards,
UseInterceptors,
UploadedFile,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBearerAuth, ApiTags, ApiConsumes } from '@nestjs/swagger';
import { diskStorage } from 'multer';
import { extname } from 'path';
import { EnfantsService } from './enfants.service'; import { EnfantsService } from './enfants.service';
import { CreateEnfantsDto } from './dto/create_enfants.dto'; import { CreateEnfantsDto } from './dto/create_enfants.dto';
import { UpdateEnfantsDto } from './dto/update_enfants.dto'; import { UpdateEnfantsDto } from './dto/update_enfants.dto';
@ -28,8 +33,34 @@ export class EnfantsController {
@Roles(RoleType.PARENT) @Roles(RoleType.PARENT)
@Post() @Post()
create(@Body() dto: CreateEnfantsDto, @User() currentUser: Users) { @ApiConsumes('multipart/form-data')
return this.enfantsService.create(dto, currentUser); @UseInterceptors(
FileInterceptor('photo', {
storage: diskStorage({
destination: './uploads/photos',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = extname(file.originalname);
cb(null, `enfant-${uniqueSuffix}${ext}`);
},
}),
fileFilter: (req, file, cb) => {
if (!file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) {
return cb(new Error('Seules les images sont autorisées'), false);
}
cb(null, true);
},
limits: {
fileSize: 5 * 1024 * 1024,
},
}),
)
create(
@Body() dto: CreateEnfantsDto,
@UploadedFile() photo: Express.Multer.File,
@User() currentUser: Users,
) {
return this.enfantsService.create(dto, currentUser, photo);
} }
@Roles(RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN) @Roles(RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)

View File

@ -24,10 +24,11 @@ export class EnfantsService {
private readonly parentsChildrenRepository: Repository<ParentsChildren>, private readonly parentsChildrenRepository: Repository<ParentsChildren>,
) { } ) { }
// Création dun enfant // Création d'un enfant
async create(dto: CreateEnfantsDto, currentUser: Users): Promise<Children> { async create(dto: CreateEnfantsDto, currentUser: Users, photoFile?: Express.Multer.File): Promise<Children> {
const parent = await this.parentsRepository.findOne({ const parent = await this.parentsRepository.findOne({
where: { user_id: currentUser.id }, where: { user_id: currentUser.id },
relations: ['co_parent'],
}); });
if (!parent) throw new NotFoundException('Parent introuvable'); if (!parent) throw new NotFoundException('Parent introuvable');
@ -46,17 +47,34 @@ 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
if (photoFile) {
dto.photo_url = `/uploads/photos/${photoFile.filename}`;
if (dto.consent_photo) {
dto.consent_photo_at = new Date().toISOString();
}
}
// Création // Création
const child = this.childrenRepository.create(dto); const child = this.childrenRepository.create(dto);
await this.childrenRepository.save(child); await this.childrenRepository.save(child);
// Lien parent-enfant // Lien parent-enfant (Parent 1)
const parentLink = this.parentsChildrenRepository.create({ const parentLink = this.parentsChildrenRepository.create({
parentId: parent.user_id, parentId: parent.user_id,
enfantId: child.id, enfantId: child.id,
}); });
await this.parentsChildrenRepository.save(parentLink); await this.parentsChildrenRepository.save(parentLink);
// Rattachement automatique au co-parent s'il existe
if (parent.co_parent) {
const coParentLink = this.parentsChildrenRepository.create({
parentId: parent.co_parent.id,
enfantId: child.id,
});
await this.parentsChildrenRepository.save(coParentLink);
}
return this.findOne(child.id, currentUser); return this.findOne(child.id, currentUser);
} }

View File

@ -58,32 +58,34 @@ Ajouter un champ pour stocker la présentation du dossier parent (étape 4 de l'
--- ---
### Ticket #3 : [BDD] Ajout gestion tokens création mot de passe ### Ticket #3 : [BDD] Ajout gestion tokens création mot de passe
**Estimation** : 30min **Estimation** : 30min
**Labels** : `bdd`, `p0-bloquant`, `security` **Labels** : `bdd`, `p0-bloquant`, `security`
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-28)
**Description** : **Description** :
Ajouter les champs nécessaires pour gérer les tokens de création de mot de passe (workflow sans MDP lors inscription). Ajouter les champs nécessaires pour gérer les tokens de création de mot de passe (workflow sans MDP lors inscription).
**Tâches** : **Tâches** :
- [ ] Ajouter `password_reset_token` UUID dans `utilisateurs` - [x] Ajouter `password_reset_token` UUID dans `utilisateurs`
- [ ] Ajouter `password_reset_expires` TIMESTAMPTZ dans `utilisateurs` - [x] Ajouter `password_reset_expires` TIMESTAMPTZ dans `utilisateurs`
- [ ] Créer migration Prisma - [x] Créer migration Prisma
- [ ] Tester migration - [x] Tester migration
--- ---
### Ticket #4 : [BDD] Ajout champ genre obligatoire enfants ### Ticket #4 : [BDD] Ajout champ genre obligatoire enfants
**Estimation** : 30min **Estimation** : 30min
**Labels** : `bdd`, `p0-bloquant`, `cdc` **Labels** : `bdd`, `p0-bloquant`, `cdc`
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-28)
**Description** : **Description** :
Ajouter le champ `genre` obligatoire (H/F) dans la table `enfants`. Ajouter le champ `genre` obligatoire (H/F) dans la table `enfants`.
**Tâches** : **Tâches** :
- [ ] Ajouter `genre` ENUM('H', 'F') NOT NULL dans `enfants` - [x] Ajouter `genre` ENUM('H', 'F') NOT NULL dans `enfants`
- [ ] Créer migration Prisma - [x] Créer migration Prisma
- [ ] Tester migration - [x] Tester migration
--- ---
@ -122,9 +124,10 @@ Créer la table `configuration` pour stocker les paramètres système (SMTP, app
--- ---
### Ticket #7 : [BDD] Tables documents légaux & acceptations ### Ticket #7 : [BDD] Tables documents légaux & acceptations
**Estimation** : 2h **Estimation** : 2h
**Labels** : `bdd`, `p0-bloquant`, `rgpd`, `juridique` **Labels** : `bdd`, `p0-bloquant`, `rgpd`, `juridique`
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-30 - Ticket #68 sur Gitea)
**Description** : **Description** :
Créer les tables pour gérer les versions des documents légaux (CGU/Privacy) et tracer les acceptations utilisateurs. Créer les tables pour gérer les versions des documents légaux (CGU/Privacy) et tracer les acceptations utilisateurs.
@ -334,12 +337,13 @@ Ajouter la gestion du co-parent (Parent 2) dans l'endpoint d'inscription.
--- ---
### Ticket #18 : [Backend] API Inscription Parent (étape 3 - Enfants) ### Ticket #18 : [Backend] API Inscription Parent - REFONTE (Workflow complet 6 étapes) ✅
**Estimation** : 4h **Estimation** : 4h
**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload` **Labels** : `backend`, `p2`, `auth`, `cdc`, `upload`
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
**Description** : **Description** :
Créer l'endpoint pour ajouter des enfants lors de l'inscription parent. Refonte complète de l'API d'inscription parent pour gérer le workflow complet en 6 étapes dans une seule transaction.
**Tâches** : **Tâches** :
- [ ] Endpoint `POST /api/v1/enfants` - [ ] Endpoint `POST /api/v1/enfants`
@ -352,12 +356,13 @@ Créer l'endpoint pour ajouter des enfants lors de l'inscription parent.
--- ---
### Ticket #19 : [Backend] API Inscription Parent (étape 4-6 - Finalisation) ### Ticket #19 : [Backend] API Inscription Parent (étape 2 - Parent 2) ✅
**Estimation** : 2h **Estimation** : 2h
**Labels** : `backend`, `p2`, `auth`, `cdc` **Labels** : `backend`, `p2`, `auth`, `cdc`
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
**Description** : **Description** :
Finaliser l'inscription parent (présentation, CGU, récapitulatif). Gestion du co-parent (Parent 2) dans l'endpoint d'inscription (intégré dans la refonte #18).
**Tâches** : **Tâches** :
- [ ] Enregistrement présentation dossier - [ ] Enregistrement présentation dossier
@ -367,12 +372,13 @@ Finaliser l'inscription parent (présentation, CGU, récapitulatif).
--- ---
### Ticket #20 : [Backend] API Inscription AM (panneau 1 - Identité) ### Ticket #20 : [Backend] API Inscription Parent (étape 3 - Enfants) ✅
**Estimation** : 4h **Estimation** : 4h
**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload` **Labels** : `backend`, `p2`, `auth`, `cdc`, `upload`
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
**Description** : **Description** :
Créer l'endpoint d'inscription Assistante Maternelle (panneau 1/5 : identité). Gestion des enfants dans l'endpoint d'inscription (intégré dans la refonte #18).
**Tâches** : **Tâches** :
- [ ] Endpoint `POST /api/v1/auth/register/am` - [ ] Endpoint `POST /api/v1/auth/register/am`
@ -386,12 +392,13 @@ Créer l'endpoint d'inscription Assistante Maternelle (panneau 1/5 : identité).
--- ---
### Ticket #21 : [Backend] API Inscription AM (panneau 2 - Infos pro) ### Ticket #21 : [Backend] API Inscription Parent (étape 4-6 - Finalisation) ✅
**Estimation** : 3h **Estimation** : 3h
**Labels** : `backend`, `p2`, `auth`, `cdc` **Labels** : `backend`, `p2`, `auth`, `cdc`
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
**Description** : **Description** :
Ajouter les informations professionnelles de l'AM (panneau 2/5). Finalisation de l'inscription parent (présentation, CGU, récapitulatif - intégré dans la refonte #18).
**Tâches** : **Tâches** :
- [ ] Validation NIR (15 chiffres obligatoire) - [ ] Validation NIR (15 chiffres obligatoire)
@ -617,22 +624,33 @@ Créer l'écran de création de gestionnaire (super admin uniquement).
--- ---
### Ticket #34 : [Frontend] Inscription Parent - Étape 1 (Parent 1) ### Ticket #34 : [Réservé - Non utilisé]
---
### Ticket #35 : [Réservé - Non utilisé]
---
### Ticket #36 : [Frontend] Inscription Parent - Étape 1 (Parent 1) ✅
**Estimation** : 3h **Estimation** : 3h
**Labels** : `frontend`, `p3`, `auth`, `cdc` **Labels** : `frontend`, `p3`, `auth`, `cdc`
**Statut** : ✅ TERMINÉ (PR #73 mergée le 2025-12-01)
**Description** : **Description** :
Créer le formulaire d'inscription parent - étape 1/6 (informations Parent 1). Créer le formulaire d'inscription parent - étape 1/6 (informations Parent 1).
**Tâches** : **Tâches** :
- [ ] Formulaire identité Parent 1 - [x] Formulaire identité Parent 1
- [ ] Validation côté client - [x] Validation côté client
- [ ] Pas de champ mot de passe - [x] Pas de champ mot de passe
- [ ] Navigation vers étape 2 - [x] Navigation vers étape 2
- [x] Améliorations visuelles (labels 22px, champs 20px, espacement 32px)
- [x] Correction indicateur étape 1/6
--- ---
### Ticket #35 : [Frontend] Inscription Parent - Étape 2 (Parent 2) ### Ticket #37 : [Frontend] Inscription Parent - Étape 2 (Parent 2)
**Estimation** : 3h **Estimation** : 3h
**Labels** : `frontend`, `p3`, `auth`, `cdc` **Labels** : `frontend`, `p3`, `auth`, `cdc`
@ -644,10 +662,13 @@ Créer le formulaire d'inscription parent - étape 2/6 (informations Parent 2 op
- [ ] Formulaire identité Parent 2 (conditionnel) - [ ] Formulaire identité Parent 2 (conditionnel)
- [ ] Checkbox "Même adresse" - [ ] Checkbox "Même adresse"
- [ ] Navigation vers étape 3 - [ ] Navigation vers étape 3
- [ ] Pas de champ mot de passe
- [ ] Améliorations visuelles (mêmes que Step1)
- [ ] Correction indicateur étape 2/6
--- ---
### Ticket #36 : [Frontend] Inscription Parent - Étape 3 (Enfants) ### Ticket #38 : [Frontend] Inscription Parent - Étape 3 (Enfants)
**Estimation** : 4h **Estimation** : 4h
**Labels** : `frontend`, `p3`, `auth`, `cdc`, `upload` **Labels** : `frontend`, `p3`, `auth`, `cdc`, `upload`
@ -664,7 +685,7 @@ Créer le formulaire d'inscription parent - étape 3/6 (informations enfants).
--- ---
### Ticket #37 : [Frontend] Inscription Parent - Étapes 4-6 (Finalisation) ### Ticket #39 : [Frontend] Inscription Parent - Étapes 4-6 (Finalisation)
**Estimation** : 4h **Estimation** : 4h
**Labels** : `frontend`, `p3`, `auth`, `cdc` **Labels** : `frontend`, `p3`, `auth`, `cdc`
@ -681,7 +702,7 @@ Créer les étapes finales de l'inscription parent (présentation, CGU, récapit
--- ---
### Ticket #38 : [Frontend] Inscription AM - Panneau 1 (Identité) ### Ticket #40 : [Frontend] Inscription AM - Panneau 1 (Identité)
**Estimation** : 3h **Estimation** : 3h
**Labels** : `frontend`, `p3`, `auth`, `cdc`, `upload` **Labels** : `frontend`, `p3`, `auth`, `cdc`, `upload`
@ -697,7 +718,7 @@ Créer le formulaire d'inscription AM - panneau 1/5 (identité).
--- ---
### Ticket #39 : [Frontend] Inscription AM - Panneau 2 (Infos pro) ### Ticket #41 : [Frontend] Inscription AM - Panneau 2 (Infos pro)
**Estimation** : 3h **Estimation** : 3h
**Labels** : `frontend`, `p3`, `auth`, `cdc` **Labels** : `frontend`, `p3`, `auth`, `cdc`
@ -713,7 +734,7 @@ Créer le formulaire d'inscription AM - panneau 2/5 (informations professionnell
--- ---
### Ticket #40 : [Frontend] Inscription AM - Finalisation ### Ticket #42 : [Frontend] Inscription AM - Finalisation
**Estimation** : 3h **Estimation** : 3h
**Labels** : `frontend`, `p3`, `auth`, `cdc` **Labels** : `frontend`, `p3`, `auth`, `cdc`
@ -729,7 +750,7 @@ Créer les étapes finales de l'inscription AM (présentation, CGU, récapitulat
--- ---
### Ticket #41 : [Frontend] Écran Création Mot de Passe ### Ticket #43 : [Frontend] Écran Création Mot de Passe
**Estimation** : 3h **Estimation** : 3h
**Labels** : `frontend`, `p3`, `auth` **Labels** : `frontend`, `p3`, `auth`
@ -746,7 +767,7 @@ Créer l'écran de création de mot de passe (lien reçu par email).
--- ---
### Ticket #42 : [Frontend] Dashboard Gestionnaire - Structure ### Ticket #44 : [Frontend] Dashboard Gestionnaire - Structure
**Estimation** : 2h **Estimation** : 2h
**Labels** : `frontend`, `p3`, `gestionnaire` **Labels** : `frontend`, `p3`, `gestionnaire`
@ -760,7 +781,7 @@ Créer la structure du dashboard gestionnaire avec 2 onglets.
--- ---
### Ticket #43 : [Frontend] Dashboard Gestionnaire - Liste Parents ### Ticket #45 : [Frontend] Dashboard Gestionnaire - Liste Parents
**Estimation** : 4h **Estimation** : 4h
**Labels** : `frontend`, `p3`, `gestionnaire` **Labels** : `frontend`, `p3`, `gestionnaire`
@ -776,7 +797,7 @@ Créer la liste des parents en attente de validation.
--- ---
### Ticket #44 : [Frontend] Dashboard Gestionnaire - Liste AM ### Ticket #46 : [Frontend] Dashboard Gestionnaire - Liste AM
**Estimation** : 4h **Estimation** : 4h
**Labels** : `frontend`, `p3`, `gestionnaire` **Labels** : `frontend`, `p3`, `gestionnaire`
@ -793,7 +814,7 @@ Créer la liste des assistantes maternelles en attente de validation.
--- ---
### Ticket #45 : [Frontend] Écran Changement MDP Obligatoire ### Ticket #47 : [Frontend] Écran Changement MDP Obligatoire
**Estimation** : 2h **Estimation** : 2h
**Labels** : `frontend`, `p3`, `auth`, `security` **Labels** : `frontend`, `p3`, `auth`, `security`
@ -809,7 +830,7 @@ Créer l'écran de changement de mot de passe obligatoire (première connexion g
--- ---
### Ticket #46 : [Frontend] Gestion Erreurs & Messages ### Ticket #48 : [Frontend] Gestion Erreurs & Messages
**Estimation** : 2h **Estimation** : 2h
**Labels** : `frontend`, `p3`, `ux` **Labels** : `frontend`, `p3`, `ux`
@ -823,7 +844,7 @@ Créer un système de gestion des erreurs et messages utilisateur.
--- ---
### Ticket #47 : [Frontend] Écran Gestion Documents Légaux (Admin) ### Ticket #49 : [Frontend] Écran Gestion Documents Légaux (Admin)
**Estimation** : 5h **Estimation** : 5h
**Labels** : `frontend`, `p3`, `juridique`, `admin` **Labels** : `frontend`, `p3`, `juridique`, `admin`
@ -842,7 +863,7 @@ Créer l'écran de gestion des documents légaux (CGU/Privacy) pour l'admin.
--- ---
### Ticket #48 : [Frontend] Affichage dynamique CGU lors inscription ### Ticket #50 : [Frontend] Affichage dynamique CGU lors inscription
**Estimation** : 2h **Estimation** : 2h
**Labels** : `frontend`, `p3`, `juridique` **Labels** : `frontend`, `p3`, `juridique`
@ -856,9 +877,24 @@ Afficher dynamiquement les CGU/Privacy lors de l'inscription (avec numéro de ve
--- ---
### Ticket #51 : [Frontend] Écran Logs Admin (optionnel v1.1)
**Estimation** : 4h
**Labels** : `frontend`, `p3`, `admin`, `logs`
**Description** :
Créer l'écran de consultation des logs système (optionnel pour v1.1).
**Tâches** :
- [ ] Appel API logs
- [ ] Filtres (date, niveau, utilisateur)
- [ ] Pagination
- [ ] Export CSV
---
## 🔵 PRIORITÉ 4 : Tests & Documentation ## 🔵 PRIORITÉ 4 : Tests & Documentation
### Ticket #49 : [Tests] Tests unitaires Backend ### Ticket #52 : [Tests] Tests unitaires Backend
**Estimation** : 8h **Estimation** : 8h
**Labels** : `tests`, `p4`, `backend` **Labels** : `tests`, `p4`, `backend`

344
docs/99_REGLES-CODAGE.md Normal file
View File

@ -0,0 +1,344 @@
# 📐 Règles de Codage - Projet P'titsPas
**Version** : 1.0
**Date** : 1er Décembre 2025
**Statut** : ✅ Actif
---
## 🌍 Langue du Code
### Principe Général
**Tout le code doit être écrit en FRANÇAIS**, sauf les termes techniques qui restent en **ANGLAIS**.
---
## ✅ Ce qui doit être en FRANÇAIS
### 1. Noms de variables
```typescript
// ✅ BON
const utilisateurConnecte = await this.trouverUtilisateur(id);
const enfantsEnregistres = [];
const tokenCreationMotDePasse = crypto.randomUUID();
// ❌ MAUVAIS
const loggedUser = await this.findUser(id);
const savedChildren = [];
const passwordCreationToken = crypto.randomUUID();
```
### 2. Noms de fonctions/méthodes
```typescript
// ✅ BON
async inscrireParentComplet(dto: DtoInscriptionParentComplet) { }
async creerGestionnaire(dto: DtoCreationGestionnaire) { }
async validerCompte(idUtilisateur: string) { }
// ❌ MAUVAIS
async registerParentComplete(dto: RegisterParentCompleteDto) { }
async createManager(dto: CreateManagerDto) { }
async validateAccount(userId: string) { }
```
### 3. Noms de classes/interfaces/types
```typescript
// ✅ BON
export class DtoInscriptionParentComplet { }
export class ServiceAuthentification { }
export interface OptionsConfiguration { }
export type StatutUtilisateur = 'actif' | 'en_attente' | 'suspendu';
// ❌ MAUVAIS
export class RegisterParentCompleteDto { }
export class AuthService { }
export interface ConfigOptions { }
export type UserStatus = 'active' | 'pending' | 'suspended';
```
### 4. Noms de fichiers
```typescript
// ✅ BON
inscription-parent-complet.dto.ts
service-authentification.ts
entite-utilisateurs.ts
controleur-configuration.ts
// ❌ MAUVAIS
register-parent-complete.dto.ts
auth.service.ts
users.entity.ts
config.controller.ts
```
### 5. Propriétés d'entités/DTOs
```typescript
// ✅ BON
export class Enfants {
@Column({ name: 'prenom' })
prenom: string;
@Column({ name: 'date_naissance' })
dateNaissance: Date;
@Column({ name: 'consentement_photo' })
consentementPhoto: boolean;
}
// ❌ MAUVAIS
export class Children {
@Column({ name: 'first_name' })
firstName: string;
@Column({ name: 'birth_date' })
birthDate: Date;
@Column({ name: 'consent_photo' })
consentPhoto: boolean;
}
```
### 6. Commentaires
```typescript
// ✅ BON
// Créer Parent 1 + Parent 2 (si existe) + entités parents
// Vérifier que l'email n'existe pas déjà
// Transaction : Créer utilisateur + entité métier
// ❌ MAUVAIS
// Create Parent 1 + Parent 2 (if exists) + parent entities
// Check if email already exists
// Transaction: Create user + business entity
```
### 7. Messages d'erreur/succès
```typescript
// ✅ BON
throw new ConflictException('Un compte avec cet email existe déjà');
return { message: 'Inscription réussie. Votre dossier est en attente de validation.' };
// ❌ MAUVAIS
throw new ConflictException('An account with this email already exists');
return { message: 'Registration successful. Your application is pending validation.' };
```
### 8. Logs
```typescript
// ✅ BON
this.logger.log('📦 Chargement de 16 configurations en cache');
this.logger.error('Erreur lors de la création du parent');
// ❌ MAUVAIS
this.logger.log('📦 Loading 16 configurations in cache');
this.logger.error('Error creating parent');
```
---
## ✅ Ce qui RESTE en ANGLAIS (Termes Techniques)
### 1. Patterns de conception
- `singleton`
- `factory`
- `repository`
- `observer`
- `decorator`
### 2. Architecture/Framework
- `backend` / `frontend`
- `controller`
- `service`
- `middleware`
- `guard`
- `interceptor`
- `pipe`
- `filter`
- `module`
- `provider`
### 3. Concepts techniques
- `entity` (TypeORM)
- `DTO` (Data Transfer Object)
- `API` / `endpoint`
- `token` (JWT)
- `hash` (bcrypt)
- `cache`
- `query`
- `transaction`
- `migration`
- `seed`
### 4. Bibliothèques/Technologies
- `NestJS`
- `TypeORM`
- `PostgreSQL`
- `Docker`
- `Git`
- `JWT`
- `bcrypt`
- `Multer`
- `Nodemailer`
### 5. Mots-clés TypeScript/JavaScript
- `async` / `await`
- `const` / `let` / `var`
- `function`
- `class`
- `interface`
- `type`
- `enum`
- `import` / `export`
- `return`
- `throw`
---
## 📋 Exemples Complets
### Exemple 1 : Service d'authentification
```typescript
// ✅ BON
@Injectable()
export class ServiceAuthentification {
constructor(
private readonly serviceUtilisateurs: ServiceUtilisateurs,
private readonly serviceJwt: JwtService,
@InjectRepository(Utilisateurs)
private readonly depotUtilisateurs: Repository<Utilisateurs>,
) {}
async inscrireParentComplet(dto: DtoInscriptionParentComplet) {
// Vérifier que l'email n'existe pas
const existe = await this.serviceUtilisateurs.trouverParEmail(dto.email);
if (existe) {
throw new ConflictException('Un compte avec cet email existe déjà');
}
// Générer le token de création de mot de passe
const tokenCreationMdp = crypto.randomUUID();
const dateExpiration = new Date();
dateExpiration.setDate(dateExpiration.getDate() + 7);
// Transaction : Créer parent + enfants
const resultat = await this.depotUtilisateurs.manager.transaction(async (manager) => {
const parent1 = new Utilisateurs();
parent1.email = dto.email;
parent1.prenom = dto.prenom;
parent1.nom = dto.nom;
parent1.tokenCreationMdp = tokenCreationMdp;
const parentEnregistre = await manager.save(Utilisateurs, parent1);
return { parent: parentEnregistre, token: tokenCreationMdp };
});
return {
message: 'Inscription réussie. Votre dossier est en attente de validation.',
idParent: resultat.parent.id,
statut: 'en_attente',
};
}
}
```
### Exemple 2 : Entité Enfants
```typescript
// ✅ BON
@Entity('enfants')
export class Enfants {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'prenom', length: 100 })
prenom: string;
@Column({ name: 'nom', length: 100 })
nom: string;
@Column({
type: 'enum',
enum: TypeGenre,
name: 'genre'
})
genre: TypeGenre;
@Column({ type: 'date', name: 'date_naissance', nullable: true })
dateNaissance?: Date;
@Column({ type: 'date', name: 'date_prevue_naissance', nullable: true })
datePrevueNaissance?: Date;
@Column({ name: 'photo_url', type: 'text', nullable: true })
photoUrl?: string;
@Column({ name: 'consentement_photo', type: 'boolean', default: false })
consentementPhoto: boolean;
@Column({ name: 'est_multiple', type: 'boolean', default: false })
estMultiple: boolean;
@Column({
type: 'enum',
enum: StatutEnfantType,
name: 'statut'
})
statut: StatutEnfantType;
}
```
---
## 🔄 Migration Progressive
### Stratégie
1. ✅ **Nouveau code** : Appliquer la règle immédiatement
2. ⏳ **Code existant** : Migrer progressivement lors des modifications
3. ❌ **Ne PAS refactoriser** tout le code d'un coup
### Priorités de migration
1. **Haute priorité** : Nouveaux fichiers, nouvelles fonctionnalités
2. **Moyenne priorité** : Fichiers modifiés fréquemment
3. **Basse priorité** : Code stable non modifié
### Exemple de migration progressive
```typescript
// Avant (ancien code - OK pour l'instant)
export class Children { }
// Après modification (nouveau code - appliquer la règle)
export class Enfants { }
```
---
## 🚫 Exceptions
### Cas où l'anglais est toléré
1. **Noms de colonnes en BDD** : Si la BDD existe déjà (ex: `first_name` en BDD → `prenom` en TypeScript)
2. **APIs externes** : Noms imposés par des bibliothèques tierces
3. **Standards** : `id`, `uuid`, `url`, `email`, `password` (termes universels)
---
## ✅ Checklist Avant Commit
- [ ] Noms de variables en français
- [ ] Noms de fonctions/méthodes en français
- [ ] Noms de classes/interfaces en français
- [ ] Noms de fichiers en français
- [ ] Propriétés d'entités/DTOs en français
- [ ] Commentaires en français
- [ ] Messages d'erreur/succès en français
- [ ] Termes techniques restent en anglais
- [ ] Pas de `console.log` (utiliser `this.logger`)
- [ ] Pas de code commenté
- [ ] Types TypeScript corrects (pas de `any`)
- [ ] Imports propres (pas d'imports inutilisés)
---
**Dernière mise à jour** : 1er Décembre 2025
**Auteur** : Équipe P'titsPas

View File

@ -22,11 +22,9 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
final _firstNameController = TextEditingController(); final _firstNameController = TextEditingController();
final _phoneController = TextEditingController(); final _phoneController = TextEditingController();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _passwordController = TextEditingController(); final _addressController = TextEditingController();
final _confirmPasswordController = TextEditingController(); final _postalCodeController = TextEditingController();
final _addressController = TextEditingController(); // Rue seule final _cityController = TextEditingController();
final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); // Restauré
@override @override
void initState() { void initState() {
@ -48,8 +46,6 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
_lastNameController.text = genLastName; _lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone(); _phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName); _emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
} }
@override @override
@ -58,8 +54,6 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
_firstNameController.dispose(); _firstNameController.dispose();
_phoneController.dispose(); _phoneController.dispose();
_emailController.dispose(); _emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_addressController.dispose(); _addressController.dispose();
_postalCodeController.dispose(); _postalCodeController.dispose();
_cityController.dispose(); _cityController.dispose();
@ -88,9 +82,9 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Indicateur d'étape (à rendre dynamique) // Indicateur d'étape
Text( Text(
'Étape 1/5', 'Étape 1/6',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
@ -121,54 +115,43 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
key: _formKey, key: _formKey,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
const SizedBox(height: 10),
Row( Row(
children: [ children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4% Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 32),
Row( Row(
children: [ children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4% Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 32),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) {
if (value == null || value.isEmpty) return 'Mot de passe requis';
if (value.length < 6) return '6 caractères minimum';
return null;
})),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) {
if (value == null || value.isEmpty) return 'Confirmation requise';
if (value != _passwordController.text) return 'Ne correspond pas';
return null;
})),
],
),
const SizedBox(height: 20),
CustomAppTextField( CustomAppTextField(
controller: _addressController, controller: _addressController,
labelText: 'Adresse (N° et Rue)', labelText: 'Adresse (N° et Rue)',
hintText: 'Numéro et nom de votre rue', hintText: 'Numéro et nom de votre rue',
style: CustomAppTextFieldStyle.beige, style: CustomAppTextFieldStyle.beige,
fieldWidth: double.infinity, fieldWidth: double.infinity,
labelFontSize: 22.0,
inputFontSize: 20.0,
), ),
const SizedBox(height: 20), const SizedBox(height: 32),
Row( Row(
children: [ children: [
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
const SizedBox(width: 20), const SizedBox(width: 20),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
], ],
), ),
const SizedBox(height: 10),
], ],
), ),
), ),
@ -205,12 +188,12 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
ParentData( ParentData(
firstName: _firstNameController.text, firstName: _firstNameController.text,
lastName: _lastNameController.text, lastName: _lastNameController.text,
address: _addressController.text, // Rue address: _addressController.text,
postalCode: _postalCodeController.text, // Ajout postalCode: _postalCodeController.text,
city: _cityController.text, // Ajout city: _cityController.text,
phone: _phoneController.text, phone: _phoneController.text,
email: _emailController.text, email: _emailController.text,
password: _passwordController.text, password: '', // Pas de mot de passe à cette étape
) )
); );
Navigator.pushNamed(context, '/parent-register/step2', arguments: _registrationData); Navigator.pushNamed(context, '/parent-register/step2', arguments: _registrationData);

View File

@ -22,16 +22,14 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2 bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2
bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi
// Contrôleurs pour les champs du parent 2 (restauration CP et Ville) // Contrôleurs pour les champs du parent 2
final _lastNameController = TextEditingController(); final _lastNameController = TextEditingController();
final _firstNameController = TextEditingController(); final _firstNameController = TextEditingController();
final _phoneController = TextEditingController(); final _phoneController = TextEditingController();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _passwordController = TextEditingController(); final _addressController = TextEditingController();
final _confirmPasswordController = TextEditingController(); final _postalCodeController = TextEditingController();
final _addressController = TextEditingController(); // Rue seule final _cityController = TextEditingController();
final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); // Restauré
@override @override
void initState() { void initState() {
@ -49,17 +47,13 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
_lastNameController.text = genLastName; _lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone(); _phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName); _emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
_sameAddressAsParent1 = DataGenerator.boolean(); _sameAddressAsParent1 = DataGenerator.boolean();
if (!_sameAddressAsParent1) { if (!_sameAddressAsParent1) {
// Générer adresse, CP, Ville séparément
_addressController.text = DataGenerator.address(); _addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode(); _postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city(); _cityController.text = DataGenerator.city();
} else { } else {
// Vider les champs si même adresse (seront désactivés)
_addressController.clear(); _addressController.clear();
_postalCodeController.clear(); _postalCodeController.clear();
_cityController.clear(); _cityController.clear();
@ -72,8 +66,6 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
_firstNameController.dispose(); _firstNameController.dispose();
_phoneController.dispose(); _phoneController.dispose();
_emailController.dispose(); _emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_addressController.dispose(); _addressController.dispose();
_postalCodeController.dispose(); _postalCodeController.dispose();
_cityController.dispose(); _cityController.dispose();
@ -98,7 +90,7 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text('Étape 2/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), Text('Étape 2/6', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'Informations du Deuxième Parent (Optionnel)', 'Informations du Deuxième Parent (Optionnel)',
@ -117,7 +109,9 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
const SizedBox(height: 10),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@ -156,40 +150,33 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
]), ]),
), ),
]), ]),
const SizedBox(height: 25), const SizedBox(height: 32),
Row( Row(
children: [ children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4% Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 32),
Row( Row(
children: [ children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4% Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 32),
CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0),
const SizedBox(height: 32),
Row( Row(
children: [ children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v.length < 6 ? '6 car. min' : null)) : null)), Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmer mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v != _passwordController.text ? 'Différent' : null)) : null)),
],
),
const SizedBox(height: 20),
CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
const SizedBox(width: 20), const SizedBox(width: 20),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
], ],
), ),
const SizedBox(height: 10),
], ],
), ),
), ),
@ -225,7 +212,7 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
city: _sameAddressAsParent1 ? _registrationData.parent1.city : _cityController.text, city: _sameAddressAsParent1 ? _registrationData.parent1.city : _cityController.text,
phone: _phoneController.text, phone: _phoneController.text,
email: _emailController.text, email: _emailController.text,
password: _passwordController.text, password: '', // Pas de mot de passe à cette étape
) )
); );
} else { } else {
@ -244,8 +231,10 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
void _clearParent2Fields() { void _clearParent2Fields() {
_formKey.currentState?.reset(); _formKey.currentState?.reset();
_lastNameController.clear(); _firstNameController.clear(); _phoneController.clear(); _lastNameController.clear();
_emailController.clear(); _passwordController.clear(); _confirmPasswordController.clear(); _firstNameController.clear();
_phoneController.clear();
_emailController.clear();
_addressController.clear(); _addressController.clear();
_postalCodeController.clear(); _postalCodeController.clear();
_cityController.clear(); _cityController.clear();