diff --git a/backend/src/entities/users.entity.ts b/backend/src/entities/users.entity.ts index 94d12bb..6382e4b 100644 --- a/backend/src/entities/users.entity.ts +++ b/backend/src/entities/users.entity.ts @@ -138,6 +138,12 @@ export class Users { @Column({ name: 'date_naissance', type: 'date', nullable: true }) date_naissance?: Date; + @Column({ name: 'lieu_naissance_ville', length: 100, nullable: true }) + lieu_naissance_ville?: string; + + @Column({ name: 'lieu_naissance_pays', length: 100, nullable: true }) + lieu_naissance_pays?: string; + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) cree_le: Date; diff --git a/backend/src/modules/mail/mail.service.ts b/backend/src/modules/mail/mail.service.ts index df5eec9..10f9fc9 100644 --- a/backend/src/modules/mail/mail.service.ts +++ b/backend/src/modules/mail/mail.service.ts @@ -99,10 +99,10 @@ export class MailService { } /** - * Accusé de réception inscription parent : demande enregistrée + n° de dossier. - * L’utilisateur est en attente de validation ; pas de lien création MDP à ce stade. + * Accusé de réception inscription (parent ou assistante maternelle) : + * demande enregistrée + n° de dossier. En attente de validation ; pas de lien création MDP à ce stade. */ - async sendParentRegistrationPendingEmail( + async sendRegistrationPendingEmail( to: string, prenom: string, nom: string, diff --git a/backend/src/routes/auth/auth.controller.ts b/backend/src/routes/auth/auth.controller.ts index ccec2a4..a68e74e 100644 --- a/backend/src/routes/auth/auth.controller.ts +++ b/backend/src/routes/auth/auth.controller.ts @@ -5,6 +5,7 @@ import { Public } from 'src/common/decorators/public.decorator'; import { RegisterDto } from './dto/register.dto'; import { RegisterParentCompletDto } from './dto/register-parent-complet.dto'; import { RegisterAMCompletDto } from './dto/register-am-complet.dto'; +import { RegisterAmResponseDto } from './dto/register-am-response.dto'; import { ChangePasswordRequiredDto } from './dto/change-password.dto'; import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthGuard } from 'src/common/guards/auth.guard'; @@ -62,10 +63,10 @@ export class AuthController { description: 'Crée User AM + entrée assistantes_maternelles (identité + infos pro + photo + CGU) en une transaction. Si photo_base64 est fourni sans photo_filename, le serveur utilise le nom par défaut photo_am.jpg (extension du fichier stocké = type du data-URL).', }) - @ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation' }) + @ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation', type: RegisterAmResponseDto }) @ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' }) @ApiResponse({ status: 409, description: 'Email déjà utilisé' }) - async inscrireAMComplet(@Body() dto: RegisterAMCompletDto) { + async inscrireAMComplet(@Body() dto: RegisterAMCompletDto): Promise { return this.authService.inscrireAMComplet(dto); } diff --git a/backend/src/routes/auth/auth.service.ts b/backend/src/routes/auth/auth.service.ts index 5365d7e..0874aec 100644 --- a/backend/src/routes/auth/auth.service.ts +++ b/backend/src/routes/auth/auth.service.ts @@ -376,14 +376,14 @@ export class AuthService { const numeroDossier = resultat.parent1.numero_dossier ?? ''; try { - await this.mailService.sendParentRegistrationPendingEmail( + await this.mailService.sendRegistrationPendingEmail( resultat.parent1.email, resultat.parent1.prenom ?? '', resultat.parent1.nom ?? '', numeroDossier, ); if (resultat.parent2) { - await this.mailService.sendParentRegistrationPendingEmail( + await this.mailService.sendRegistrationPendingEmail( resultat.parent2.email, resultat.parent2.prenom ?? '', resultat.parent2.nom ?? '', @@ -496,6 +496,8 @@ export class AuthService { consentement_photo: dto.consentement_photo, date_consentement_photo: dateConsentementPhoto, date_naissance: dto.date_naissance ? new Date(dto.date_naissance) : undefined, + lieu_naissance_ville: dto.lieu_naissance_ville, + lieu_naissance_pays: dto.lieu_naissance_pays, numero_dossier: numeroDossier, }); const userEnregistre = await manager.save(Users, user); @@ -506,6 +508,7 @@ export class AuthService { approval_number: dto.numero_agrement, nir: nirNormalized, max_children: dto.capacite_accueil, + places_available: dto.capacite_accueil, biography: dto.biographie, residence_city: dto.ville ?? undefined, agreement_date: dto.date_agrement ? new Date(dto.date_agrement) : undefined, @@ -523,11 +526,28 @@ export class AuthService { throw err; } + const numeroDossier = resultat.user.numero_dossier ?? ''; + + try { + await this.mailService.sendRegistrationPendingEmail( + resultat.user.email, + resultat.user.prenom ?? '', + resultat.user.nom ?? '', + numeroDossier, + ); + } catch (err) { + this.logger.error( + "[inscrireAMComplet] Échec envoi email d'accusé de réception (inscription conservée)", + err instanceof Error ? err.stack : String(err), + ); + } + return { message: 'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.', user_id: resultat.user.id, statut: StatutUtilisateurType.EN_ATTENTE, + numero_dossier: numeroDossier, }; } diff --git a/backend/src/routes/auth/dto/register-am-complet.dto.ts b/backend/src/routes/auth/dto/register-am-complet.dto.ts index 833602d..16d0c89 100644 --- a/backend/src/routes/auth/dto/register-am-complet.dto.ts +++ b/backend/src/routes/auth/dto/register-am-complet.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, @@ -96,17 +97,21 @@ export class RegisterAMCompletDto { @IsDateString() date_naissance?: string; - @ApiProperty({ example: 'Paris', required: false, description: 'Ville de naissance' }) - @IsOptional() + @ApiProperty({ example: 'Paris', description: 'Ville de naissance (obligatoire)' }) + @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) @IsString() + @IsNotEmpty({ message: 'La ville de naissance est requise' }) + @MinLength(2, { message: 'La ville de naissance doit contenir au moins 2 caractères' }) @MaxLength(100) - lieu_naissance_ville?: string; + lieu_naissance_ville: string; - @ApiProperty({ example: 'France', required: false, description: 'Pays de naissance' }) - @IsOptional() + @ApiProperty({ example: 'France', description: 'Pays de naissance (obligatoire)' }) + @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) @IsString() + @IsNotEmpty({ message: 'Le pays de naissance est requis' }) + @MinLength(2, { message: 'Le pays de naissance doit contenir au moins 2 caractères' }) @MaxLength(100) - lieu_naissance_pays?: string; + lieu_naissance_pays: string; @ApiProperty({ example: '123456789012345', description: 'NIR 15 caractères (chiffres, ou 2A/2B pour la Corse)' }) @IsString() diff --git a/backend/src/routes/auth/dto/register-am-response.dto.ts b/backend/src/routes/auth/dto/register-am-response.dto.ts new file mode 100644 index 0000000..31819b2 --- /dev/null +++ b/backend/src/routes/auth/dto/register-am-response.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { StatutUtilisateurType } from 'src/entities/users.entity'; + +/** Réponse 201 POST /auth/register/am (alignée sur les champs clés de /auth/register/parent). */ +export class RegisterAmResponseDto { + @ApiProperty() + message: string; + + @ApiProperty({ format: 'uuid' }) + user_id: string; + + @ApiProperty({ enum: StatutUtilisateurType, example: StatutUtilisateurType.EN_ATTENTE }) + statut: StatutUtilisateurType; + + @ApiProperty({ example: '2026-000015', description: 'Numéro de dossier attribué à l’inscription' }) + numero_dossier: string; +} diff --git a/backend/src/routes/dossiers/dossiers.service.ts b/backend/src/routes/dossiers/dossiers.service.ts index 59d9f47..f9715e6 100644 --- a/backend/src/routes/dossiers/dossiers.service.ts +++ b/backend/src/routes/dossiers/dossiers.service.ts @@ -62,7 +62,23 @@ export class DossiersService { throw new NotFoundException('Aucun dossier trouvé pour ce numéro.'); } - private toDossierAmUserDto(user: { id: string; email: string; prenom?: string; nom?: string; telephone?: string; adresse?: string; ville?: string; code_postal?: string; profession?: string; date_naissance?: Date; photo_url?: string; statut: any }): DossierAmUserDto { + private toDossierAmUserDto(user: { + id: string; + email: string; + prenom?: string; + nom?: string; + telephone?: string; + adresse?: string; + ville?: string; + code_postal?: string; + profession?: string; + date_naissance?: Date; + lieu_naissance_ville?: string; + lieu_naissance_pays?: string; + photo_url?: string; + consentement_photo?: boolean; + statut: any; + }): DossierAmUserDto { return { id: user.id, email: user.email, @@ -74,7 +90,10 @@ export class DossiersService { code_postal: user.code_postal, profession: user.profession, date_naissance: user.date_naissance, + lieu_naissance_ville: user.lieu_naissance_ville, + lieu_naissance_pays: user.lieu_naissance_pays, photo_url: user.photo_url, + consentement_photo: user.consentement_photo, statut: user.statut, }; } diff --git a/backend/src/routes/dossiers/dto/dossier-am-complet.dto.ts b/backend/src/routes/dossiers/dto/dossier-am-complet.dto.ts index 0ddc8a1..c0c42fc 100644 --- a/backend/src/routes/dossiers/dto/dossier-am-complet.dto.ts +++ b/backend/src/routes/dossiers/dto/dossier-am-complet.dto.ts @@ -23,8 +23,14 @@ export class DossierAmUserDto { profession?: string; @ApiProperty({ required: false }) date_naissance?: Date; + @ApiProperty({ required: false, description: 'Ville de naissance' }) + lieu_naissance_ville?: string; + @ApiProperty({ required: false, description: 'Pays de naissance' }) + lieu_naissance_pays?: string; @ApiProperty({ required: false }) photo_url?: string; + @ApiProperty({ required: false, description: 'Consentement utilisation photo' }) + consentement_photo?: boolean; @ApiProperty({ enum: StatutUtilisateurType }) statut: StatutUtilisateurType; } diff --git a/database/BDD.sql b/database/BDD.sql index 670336f..3cd7e3a 100644 --- a/database/BDD.sql +++ b/database/BDD.sql @@ -55,6 +55,8 @@ CREATE TABLE utilisateurs ( telephone VARCHAR(20), -- Unifié (mobile privilégié) adresse TEXT, date_naissance DATE, + lieu_naissance_ville VARCHAR(100), + lieu_naissance_pays VARCHAR(100), photo_url TEXT, -- Obligatoire pour AM, non utilisé pour parents consentement_photo BOOLEAN DEFAULT false, date_consentement_photo TIMESTAMPTZ, @@ -125,6 +127,33 @@ CREATE TABLE enfants_parents ( PRIMARY KEY (id_parent, id_enfant) ); +-- ========================================================== +-- Table : dossier_famille (inscription parent, schéma simplifié — ticket #119) +-- ========================================================== +CREATE TABLE dossier_famille ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero_dossier VARCHAR(20) NOT NULL, + id_parent UUID NOT NULL REFERENCES parents(id_utilisateur) ON DELETE CASCADE, + presentation TEXT, + statut statut_dossier_type NOT NULL DEFAULT 'envoye', + cree_le TIMESTAMPTZ NOT NULL DEFAULT now(), + modifie_le TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_dossier_famille_numero ON dossier_famille(numero_dossier); +CREATE INDEX idx_dossier_famille_id_parent ON dossier_famille(id_parent); + +-- ========================================================== +-- Table : dossier_famille_enfants +-- ========================================================== +CREATE TABLE dossier_famille_enfants ( + id_dossier_famille UUID NOT NULL REFERENCES dossier_famille(id) ON DELETE CASCADE, + id_enfant UUID NOT NULL REFERENCES enfants(id) ON DELETE CASCADE, + PRIMARY KEY (id_dossier_famille, id_enfant) +); + +CREATE INDEX idx_dossier_famille_enfants_enfant ON dossier_famille_enfants(id_enfant); + -- ========================================================== -- Table : dossiers -- ========================================================== @@ -376,6 +405,10 @@ ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS token_reprise VARCHAR(255) NUL ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS token_reprise_expire_le TIMESTAMPTZ NULL; CREATE INDEX IF NOT EXISTS idx_utilisateurs_token_reprise ON utilisateurs(token_reprise) WHERE token_reprise IS NOT NULL; +-- Lieu de naissance (aligné CREATE TABLE utilisateurs — idempotent si colonnes déjà présentes) +ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS lieu_naissance_ville VARCHAR(100) NULL; +ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS lieu_naissance_pays VARCHAR(100) NULL; + -- ========================================================== -- Seed : Documents légaux génériques v1 -- ========================================================== diff --git a/database/migrations/2026_lieu_naissance_utilisateurs.sql b/database/migrations/2026_lieu_naissance_utilisateurs.sql new file mode 100644 index 0000000..bafc5ea --- /dev/null +++ b/database/migrations/2026_lieu_naissance_utilisateurs.sql @@ -0,0 +1,4 @@ +-- Lieu de naissance (inscription AM, affichage dossier / validation). Nullable pour l'historique. +ALTER TABLE utilisateurs + ADD COLUMN IF NOT EXISTS lieu_naissance_ville VARCHAR(100) NULL, + ADD COLUMN IF NOT EXISTS lieu_naissance_pays VARCHAR(100) NULL; diff --git a/docs/TEMP_FRONT_alignement_AM.md b/docs/TEMP_FRONT_alignement_AM.md new file mode 100644 index 0000000..6a53adc --- /dev/null +++ b/docs/TEMP_FRONT_alignement_AM.md @@ -0,0 +1,44 @@ +# TEMP — Alignement front / API (inscription AM & validation gestionnaire) + +> **Fichier temporaire** : à supprimer ou renommer une fois le front livré. + +Ce document décrit les changements **côté API** et ce que **Flutter** doit faire pour rester aligné. Aucune modification front n’a été faite dans le chantier backend associé. + +## 1. `POST /auth/register/am` — lieu de naissance obligatoire + +- **`lieu_naissance_ville`** et **`lieu_naissance_pays`** sont **obligatoires** (non vides après trim, min. **2 caractères** chacun, max 100). +- Réponses **400** si manquants ou invalides (messages class-validator). +- **Action front** : champs obligatoires dans le parcours AM (étapes identité / naissance), validation UI avant envoi ; afficher les erreurs renvoyées par l’API. + +## 2. Réponse `GET /dossiers/:numeroDossier` (type `am`) + +Sous `dossier.user`, l’API peut inclure : + +| Clé JSON | Description | +|----------|-------------| +| `date_naissance` | Date (si renseignée à l’inscription) | +| `lieu_naissance_ville` | Ville de naissance | +| `lieu_naissance_pays` | Pays de naissance | +| `consentement_photo` | Booléen (exposé dans `dossier.user`) | + +À la **racine** de `dossier` (objet AM), champs déjà renvoyés par le backend : `disponible`, `annees_experience`, `specialite`, `nb_max_enfants`, `place_disponible`, etc. + +**Action front** : + +- Étendre **`AppUser.fromJson` / `toJson`** (`lib/models/user.dart`) pour mapper `date_naissance`, `lieu_naissance_ville`, `lieu_naissance_pays`, `consentement_photo`. +- Étendre **`DossierAM.fromJson`** (`lib/models/dossier_unifie.dart`) pour parser `disponible`, `annees_experience`, `specialite` à la racine du dossier (noms snake_case comme dans la réponse JSON Nest). + +## 3. `ValidationAmWizard` (admin) + +Afficher pour cohérence avec le formulaire d’inscription : + +- **Informations personnelles** : date de naissance, ville / pays de naissance, consentement photo (Oui/Non). +- **Informations professionnelles** : disponibilité, années d’expérience, spécialité (afficher « – » si `null`). + +## 4. `place_disponible` à l’inscription + +- Le backend initialise **`place_disponible`** sur la fiche AM à la **même valeur** que **`capacite_accueil`** à la création. Le wizard peut donc afficher une valeur cohérente avec la capacité sans champ séparé côté public. + +--- + +*Dernière mise à jour : alignement backend branche `feature/120-inscription-am-photo-backend`.*