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 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
EXPOSE 3000

View File

@ -4,6 +4,7 @@ import { AuthService } from './auth.service';
import { Public } from 'src/common/decorators/public.decorator';
import { RegisterDto } from './dto/register.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 { AuthGuard } from 'src/common/guards/auth.guard';
import type { Request } from 'express';
@ -39,10 +40,23 @@ export class AuthController {
@Public()
@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: 409, description: 'Email déjà utilisé' })
async registerParent(@Body() dto: RegisterParentDto) {
async registerParentLegacy(@Body() dto: RegisterParentDto) {
return this.authService.registerParent(dto);
}

View File

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

View File

@ -2,6 +2,7 @@ import {
ConflictException,
Injectable,
UnauthorizedException,
BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@ -9,11 +10,16 @@ import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';
import * as fs from 'fs/promises';
import * as path from 'path';
import { RegisterDto } from './dto/register.dto';
import { RegisterParentDto } from './dto/register-parent.dto';
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
import { ConfigService } from '@nestjs/config';
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.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 { AppConfigService } from 'src/modules/config/config.service';
@ -28,6 +34,8 @@ export class AuthService {
private readonly parentsRepo: Repository<Parents>,
@InjectRepository(Users)
private readonly usersRepo: Repository<Users>,
@InjectRepository(Children)
private readonly childrenRepo: Repository<Children>,
) { }
/**
@ -192,16 +200,10 @@ export class AuthService {
adresse: dto.adresse,
code_postal: dto.code_postal,
ville: dto.ville,
profession: dto.profession,
situation_familiale: dto.situation_familiale,
token_creation_mdp: tokenCreationMdp,
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);
// 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) {
// Pour le moment envoyer un message clair
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)
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) ===
@ApiProperty({ example: 'thomas.martin@ptits-pas.fr', required: false })
@IsOptional()

View File

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

View File

@ -8,8 +8,13 @@ import {
Patch,
Post,
UseGuards,
UseInterceptors,
UploadedFile,
} 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 { CreateEnfantsDto } from './dto/create_enfants.dto';
import { UpdateEnfantsDto } from './dto/update_enfants.dto';
@ -28,8 +33,34 @@ export class EnfantsController {
@Roles(RoleType.PARENT)
@Post()
create(@Body() dto: CreateEnfantsDto, @User() currentUser: Users) {
return this.enfantsService.create(dto, currentUser);
@ApiConsumes('multipart/form-data')
@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)

View File

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

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
**Labels** : `bdd`, `p0-bloquant`, `security`
**Labels** : `bdd`, `p0-bloquant`, `security`
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-28)
**Description** :
Ajouter les champs nécessaires pour gérer les tokens de création de mot de passe (workflow sans MDP lors inscription).
**Tâches** :
- [ ] Ajouter `password_reset_token` UUID dans `utilisateurs`
- [ ] Ajouter `password_reset_expires` TIMESTAMPTZ dans `utilisateurs`
- [ ] Créer migration Prisma
- [ ] Tester migration
- [x] Ajouter `password_reset_token` UUID dans `utilisateurs`
- [x] Ajouter `password_reset_expires` TIMESTAMPTZ dans `utilisateurs`
- [x] Créer migration Prisma
- [x] Tester migration
---
### Ticket #4 : [BDD] Ajout champ genre obligatoire enfants
### Ticket #4 : [BDD] Ajout champ genre obligatoire enfants
**Estimation** : 30min
**Labels** : `bdd`, `p0-bloquant`, `cdc`
**Labels** : `bdd`, `p0-bloquant`, `cdc`
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-28)
**Description** :
Ajouter le champ `genre` obligatoire (H/F) dans la table `enfants`.
**Tâches** :
- [ ] Ajouter `genre` ENUM('H', 'F') NOT NULL dans `enfants`
- [ ] Créer migration Prisma
- [ ] Tester migration
- [x] Ajouter `genre` ENUM('H', 'F') NOT NULL dans `enfants`
- [x] Créer migration Prisma
- [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
**Labels** : `bdd`, `p0-bloquant`, `rgpd`, `juridique`
**Labels** : `bdd`, `p0-bloquant`, `rgpd`, `juridique`
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-30 - Ticket #68 sur Gitea)
**Description** :
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
**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload`
**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload`
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
**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** :
- [ ] 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
**Labels** : `backend`, `p2`, `auth`, `cdc`
**Labels** : `backend`, `p2`, `auth`, `cdc`
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
**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** :
- [ ] 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
**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload`
**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload`
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
**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** :
- [ ] 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
**Labels** : `backend`, `p2`, `auth`, `cdc`
**Labels** : `backend`, `p2`, `auth`, `cdc`
**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01)
**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** :
- [ ] 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
**Labels** : `frontend`, `p3`, `auth`, `cdc`
**Labels** : `frontend`, `p3`, `auth`, `cdc`
**Statut** : ✅ TERMINÉ (PR #73 mergée le 2025-12-01)
**Description** :
Créer le formulaire d'inscription parent - étape 1/6 (informations Parent 1).
**Tâches** :
- [ ] Formulaire identité Parent 1
- [ ] Validation côté client
- [ ] Pas de champ mot de passe
- [ ] Navigation vers étape 2
- [x] Formulaire identité Parent 1
- [x] Validation côté client
- [x] Pas de champ mot de passe
- [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
**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)
- [ ] Checkbox "Même adresse"
- [ ] 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
**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
**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
**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
**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
**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
**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
**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
**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
**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
**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
**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
**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
**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
### Ticket #49 : [Tests] Tests unitaires Backend
### Ticket #52 : [Tests] Tests unitaires Backend
**Estimation** : 8h
**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 _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _addressController = TextEditingController(); // Rue seule
final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); // Restauré
final _addressController = TextEditingController();
final _postalCodeController = TextEditingController();
final _cityController = TextEditingController();
@override
void initState() {
@ -48,8 +46,6 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
}
@override
@ -58,8 +54,6 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
_firstNameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_addressController.dispose();
_postalCodeController.dispose();
_cityController.dispose();
@ -88,9 +82,9 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Indicateur d'étape (à rendre dynamique)
// Indicateur d'étape
Text(
'Étape 1/5',
'Étape 1/6',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 10),
@ -121,54 +115,43 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(height: 10),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
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: _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()),
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(
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: 1, child: const SizedBox()), // Espace de 4%
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: _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()),
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),
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),
const SizedBox(height: 32),
CustomAppTextField(
controller: _addressController,
labelText: 'Adresse (N° et Rue)',
hintText: 'Numéro et nom de votre rue',
style: CustomAppTextFieldStyle.beige,
fieldWidth: double.infinity,
labelFontSize: 22.0,
inputFontSize: 20.0,
),
const SizedBox(height: 20),
const SizedBox(height: 32),
Row(
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),
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(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _addressController.text, // Rue
postalCode: _postalCodeController.text, // Ajout
city: _cityController.text, // Ajout
address: _addressController.text,
postalCode: _postalCodeController.text,
city: _cityController.text,
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
password: '', // Pas de mot de passe à cette étape
)
);
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 _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 _firstNameController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _addressController = TextEditingController(); // Rue seule
final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); // Restauré
final _addressController = TextEditingController();
final _postalCodeController = TextEditingController();
final _cityController = TextEditingController();
@override
void initState() {
@ -49,17 +47,13 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
_sameAddressAsParent1 = DataGenerator.boolean();
if (!_sameAddressAsParent1) {
// Générer adresse, CP, Ville séparément
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
} else {
// Vider les champs si même adresse (seront désactivés)
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
@ -72,8 +66,6 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
_firstNameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_addressController.dispose();
_postalCodeController.dispose();
_cityController.dispose();
@ -98,7 +90,7 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
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),
Text(
'Informations du Deuxième Parent (Optionnel)',
@ -117,7 +109,9 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(height: 10),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -156,40 +150,33 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
]),
),
]),
const SizedBox(height: 25),
const SizedBox(height: 32),
Row(
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: 1, child: const SizedBox()), // Espace de 4%
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: _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()),
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(
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: 1, child: const SizedBox()), // Espace de 4%
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: _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()),
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(
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: 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)),
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)),
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,
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
password: '', // Pas de mot de passe à cette étape
)
);
} else {
@ -244,8 +231,10 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
void _clearParent2Fields() {
_formKey.currentState?.reset();
_lastNameController.clear(); _firstNameController.clear(); _phoneController.clear();
_emailController.clear(); _passwordController.clear(); _confirmPasswordController.clear();
_lastNameController.clear();
_firstNameController.clear();
_phoneController.clear();
_emailController.clear();
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();