diff --git a/backend/src/entities/assistantes_maternelles.entity.ts b/backend/src/entities/assistantes_maternelles.entity.ts index f8793e6..77db5f1 100644 --- a/backend/src/entities/assistantes_maternelles.entity.ts +++ b/backend/src/entities/assistantes_maternelles.entity.ts @@ -48,4 +48,7 @@ export class AssistanteMaternelle { @Column( { name: 'place_disponible', type: 'integer', nullable: true }) places_available?: number; + /** Numéro de dossier (format AAAA-NNNNNN), même valeur que sur utilisateurs (ticket #103) */ + @Column({ name: 'numero_dossier', length: 20, nullable: true }) + numero_dossier?: string; } diff --git a/backend/src/entities/parents.entity.ts b/backend/src/entities/parents.entity.ts index b66843c..5f55d44 100644 --- a/backend/src/entities/parents.entity.ts +++ b/backend/src/entities/parents.entity.ts @@ -1,5 +1,5 @@ import { - Entity, PrimaryColumn, OneToOne, JoinColumn, + Entity, PrimaryColumn, Column, OneToOne, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; import { Users } from './users.entity'; @@ -21,6 +21,10 @@ export class Parents { @JoinColumn({ name: 'id_co_parent', referencedColumnName: 'id' }) co_parent?: Users; + /** Numéro de dossier famille (format AAAA-NNNNNN), attribué à la soumission (ticket #103) */ + @Column({ name: 'numero_dossier', length: 20, nullable: true }) + numero_dossier?: string; + // Lien vers enfants via la table enfants_parents @OneToMany(() => ParentsChildren, pc => pc.parent) parentChildren: ParentsChildren[]; diff --git a/backend/src/entities/users.entity.ts b/backend/src/entities/users.entity.ts index 649ddda..ca5d2ef 100644 --- a/backend/src/entities/users.entity.ts +++ b/backend/src/entities/users.entity.ts @@ -152,6 +152,10 @@ export class Users { @Column({ nullable: true, name: 'relais_id' }) relaisId?: string; + /** Numéro de dossier (format AAAA-NNNNNN), attribué à la soumission (ticket #103) */ + @Column({ nullable: true, name: 'numero_dossier', length: 20 }) + numero_dossier?: string; + @ManyToOne(() => Relais, relais => relais.gestionnaires, { nullable: true }) @JoinColumn({ name: 'relais_id' }) relais?: Relais; diff --git a/backend/src/modules/numero-dossier/numero-dossier.module.ts b/backend/src/modules/numero-dossier/numero-dossier.module.ts new file mode 100644 index 0000000..a5724bc --- /dev/null +++ b/backend/src/modules/numero-dossier/numero-dossier.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { NumeroDossierService } from './numero-dossier.service'; + +@Module({ + providers: [NumeroDossierService], + exports: [NumeroDossierService], +}) +export class NumeroDossierModule {} diff --git a/backend/src/modules/numero-dossier/numero-dossier.service.ts b/backend/src/modules/numero-dossier/numero-dossier.service.ts new file mode 100644 index 0000000..2cbb2a2 --- /dev/null +++ b/backend/src/modules/numero-dossier/numero-dossier.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { EntityManager } from 'typeorm'; + +const FORMAT_MAX_SEQUENCE = 990000; + +/** + * Service de génération du numéro de dossier (ticket #103). + * Format AAAA-NNNNNN (année + 6 chiffres), séquence par année. + * Si séquence >= 990000, overflowWarning est true (alerte gestionnaire). + */ +@Injectable() +export class NumeroDossierService { + /** + * Génère le prochain numéro de dossier dans le cadre d'une transaction. + * À appeler avec le manager de la transaction pour garantir l'unicité. + */ + async getNextNumeroDossier(manager: EntityManager): Promise<{ + numero: string; + overflowWarning: boolean; + }> { + const year = new Date().getFullYear(); + + // Garantir l'existence de la ligne pour l'année + await manager.query( + `INSERT INTO numero_dossier_sequence (annee, prochain) + VALUES ($1, 1) + ON CONFLICT (annee) DO NOTHING`, + [year], + ); + + // Prendre le prochain numéro et incrémenter (FOR UPDATE pour concurrence) + const selectRows = await manager.query( + `SELECT prochain FROM numero_dossier_sequence WHERE annee = $1 FOR UPDATE`, + [year], + ); + const currentVal = selectRows?.[0]?.prochain ?? 1; + + await manager.query( + `UPDATE numero_dossier_sequence SET prochain = prochain + 1 WHERE annee = $1`, + [year], + ); + + const nextVal = currentVal; + const overflowWarning = nextVal >= FORMAT_MAX_SEQUENCE; + if (overflowWarning) { + // Log pour alerte gestionnaire (ticket #103) + console.warn( + `[NumeroDossierService] Séquence année ${year} >= ${FORMAT_MAX_SEQUENCE} (valeur ${nextVal}). Prévoir renouvellement ou format.`, + ); + } + + const numero = `${year}-${String(nextVal).padStart(6, '0')}`; + return { numero, overflowWarning }; + } +} diff --git a/backend/src/routes/auth/auth.module.ts b/backend/src/routes/auth/auth.module.ts index 3d15615..2a4513a 100644 --- a/backend/src/routes/auth/auth.module.ts +++ b/backend/src/routes/auth/auth.module.ts @@ -10,12 +10,14 @@ import { Parents } from 'src/entities/parents.entity'; import { Children } from 'src/entities/children.entity'; import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity'; import { AppConfigModule } from 'src/modules/config'; +import { NumeroDossierModule } from 'src/modules/numero-dossier/numero-dossier.module'; @Module({ imports: [ TypeOrmModule.forFeature([Users, Parents, Children, AssistanteMaternelle]), forwardRef(() => UserModule), AppConfigModule, + NumeroDossierModule, JwtModule.registerAsync({ imports: [ConfigModule], useFactory: (config: ConfigService) => ({ diff --git a/backend/src/routes/auth/auth.service.ts b/backend/src/routes/auth/auth.service.ts index 76f2ddd..45e7ee5 100644 --- a/backend/src/routes/auth/auth.service.ts +++ b/backend/src/routes/auth/auth.service.ts @@ -24,6 +24,7 @@ import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entit import { LoginDto } from './dto/login.dto'; import { AppConfigService } from 'src/modules/config/config.service'; import { validateNir } from 'src/common/utils/nir.util'; +import { NumeroDossierService } from 'src/modules/numero-dossier/numero-dossier.service'; @Injectable() export class AuthService { @@ -32,6 +33,7 @@ export class AuthService { private readonly jwtService: JwtService, private readonly configService: ConfigService, private readonly appConfigService: AppConfigService, + private readonly numeroDossierService: NumeroDossierService, @InjectRepository(Parents) private readonly parentsRepo: Repository, @InjectRepository(Users) @@ -194,6 +196,8 @@ export class AuthService { dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken); const resultat = await this.usersRepo.manager.transaction(async (manager) => { + const { numero: numeroDossier } = await this.numeroDossierService.getNextNumeroDossier(manager); + const parent1 = manager.create(Users, { email: dto.email, prenom: dto.prenom, @@ -206,6 +210,7 @@ export class AuthService { ville: dto.ville, token_creation_mdp: tokenCreationMdp, token_creation_mdp_expire_le: dateExpiration, + numero_dossier: numeroDossier, }); const parent1Enregistre = await manager.save(Users, parent1); @@ -230,6 +235,7 @@ export class AuthService { ville: dto.co_parent_meme_adresse ? dto.ville : dto.co_parent_ville, token_creation_mdp: tokenCoParent, token_creation_mdp_expire_le: dateExpirationCoParent, + numero_dossier: numeroDossier, }); parent2Enregistre = await manager.save(Users, parent2); @@ -237,6 +243,7 @@ export class AuthService { const entiteParent = manager.create(Parents, { user_id: parent1Enregistre.id, + numero_dossier: numeroDossier, }); entiteParent.user = parent1Enregistre; if (parent2Enregistre) { @@ -248,6 +255,7 @@ export class AuthService { if (parent2Enregistre) { const entiteCoParent = manager.create(Parents, { user_id: parent2Enregistre.id, + numero_dossier: numeroDossier, }); entiteCoParent.user = parent2Enregistre; entiteCoParent.co_parent = parent1Enregistre; @@ -360,6 +368,8 @@ export class AuthService { dto.consentement_photo ? new Date() : undefined; const resultat = await this.usersRepo.manager.transaction(async (manager) => { + const { numero: numeroDossier } = await this.numeroDossierService.getNextNumeroDossier(manager); + const user = manager.create(Users, { email: dto.email, prenom: dto.prenom, @@ -376,6 +386,7 @@ export class AuthService { consentement_photo: dto.consentement_photo, date_consentement_photo: dateConsentementPhoto, date_naissance: dto.date_naissance ? new Date(dto.date_naissance) : undefined, + numero_dossier: numeroDossier, }); const userEnregistre = await manager.save(Users, user); @@ -389,6 +400,7 @@ export class AuthService { residence_city: dto.ville ?? undefined, agreement_date: dto.date_agrement ? new Date(dto.date_agrement) : undefined, available: true, + numero_dossier: numeroDossier, }); await amRepo.save(am); diff --git a/backend/src/routes/user/dto/affecter-numero-dossier.dto.ts b/backend/src/routes/user/dto/affecter-numero-dossier.dto.ts new file mode 100644 index 0000000..5227823 --- /dev/null +++ b/backend/src/routes/user/dto/affecter-numero-dossier.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, Matches } from 'class-validator'; + +/** Format AAAA-NNNNNN (année + 6 chiffres) */ +const NUMERO_DOSSIER_REGEX = /^\d{4}-\d{6}$/; + +export class AffecterNumeroDossierDto { + @ApiProperty({ example: '2026-000004', description: 'Numéro de dossier (AAAA-NNNNNN)' }) + @IsNotEmpty({ message: 'Le numéro de dossier est requis' }) + @Matches(NUMERO_DOSSIER_REGEX, { + message: 'Le numéro de dossier doit être au format AAAA-NNNNNN (ex: 2026-000001)', + }) + numero_dossier: string; +} diff --git a/backend/src/routes/user/user.controller.ts b/backend/src/routes/user/user.controller.ts index 84682bc..d7df010 100644 --- a/backend/src/routes/user/user.controller.ts +++ b/backend/src/routes/user/user.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthGuard } from 'src/common/guards/auth.guard'; +import { RolesGuard } from 'src/common/guards/roles.guard'; import { Roles } from 'src/common/decorators/roles.decorator'; import { User } from 'src/common/decorators/user.decorator'; import { RoleType, Users } from 'src/entities/users.entity'; @@ -8,10 +9,11 @@ import { UserService } from './user.service'; import { CreateUserDto } from './dto/create_user.dto'; import { CreateAdminDto } from './dto/create_admin.dto'; import { UpdateUserDto } from './dto/update_user.dto'; +import { AffecterNumeroDossierDto } from './dto/affecter-numero-dossier.dto'; @ApiTags('Utilisateurs') @ApiBearerAuth('access-token') -@UseGuards(AuthGuard) +@UseGuards(AuthGuard, RolesGuard) @Controller('users') export class UserController { constructor(private readonly userService: UserService) { } @@ -78,6 +80,23 @@ export class UserController { return this.userService.updateUser(id, dto, currentUser); } + @Patch(':id/numero-dossier') + @Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE) + @ApiOperation({ + summary: 'Affecter un numéro de dossier à un utilisateur', + description: 'Permet de rapprocher deux dossiers ou d’attribuer un numéro existant à un parent/AM. Réservé aux gestionnaires et administrateurs.', + }) + @ApiParam({ name: 'id', description: "UUID de l'utilisateur (parent ou AM)" }) + @ApiResponse({ status: 200, description: 'Numéro de dossier affecté' }) + @ApiResponse({ status: 400, description: 'Format invalide, rôle non éligible, ou dossier déjà associé à 2 parents' }) + @ApiResponse({ status: 404, description: 'Utilisateur introuvable' }) + affecterNumeroDossier( + @Param('id') id: string, + @Body() dto: AffecterNumeroDossierDto, + ) { + return this.userService.affecterNumeroDossier(id, dto.numero_dossier); + } + @Patch(':id/valider') @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR) @ApiOperation({ summary: 'Valider un compte utilisateur' }) diff --git a/backend/src/routes/user/user.service.ts b/backend/src/routes/user/user.service.ts index 69ccaac..3ef45b3 100644 --- a/backend/src/routes/user/user.service.ts +++ b/backend/src/routes/user/user.service.ts @@ -270,6 +270,64 @@ export class UserService { await this.validationRepository.save(suspend); return savedUser; } + /** + * Affecter ou modifier le numéro de dossier d'un utilisateur (parent ou AM). + * Permet au gestionnaire/admin de rapprocher deux dossiers (même numéro pour plusieurs personnes). + * Garde-fou : au plus 2 parents par numéro de dossier (couple / co-parents). + */ + async affecterNumeroDossier(userId: string, numeroDossier: string): Promise { + const user = await this.usersRepository.findOne({ where: { id: userId } }); + if (!user) throw new NotFoundException('Utilisateur introuvable'); + + if (user.role !== RoleType.PARENT && user.role !== RoleType.ASSISTANTE_MATERNELLE) { + throw new BadRequestException( + 'Le numéro de dossier ne peut être affecté qu\'à un parent ou une assistante maternelle', + ); + } + + if (user.role === RoleType.PARENT) { + const uneAMALe = await this.assistantesRepository.count({ + where: { numero_dossier: numeroDossier }, + }); + if (uneAMALe > 0) { + throw new BadRequestException( + 'Ce numéro de dossier est celui d\'une assistante maternelle. Un numéro AM ne peut pas être affecté à un parent.', + ); + } + const parentsAvecCeNumero = await this.parentsRepository.count({ + where: { numero_dossier: numeroDossier }, + }); + const userADejaCeNumero = user.numero_dossier === numeroDossier; + if (!userADejaCeNumero && parentsAvecCeNumero >= 2) { + throw new BadRequestException( + 'Un numéro de dossier ne peut être associé qu\'à 2 parents au maximum (couple / co-parents). Ce dossier a déjà 2 parents.', + ); + } + } + + if (user.role === RoleType.ASSISTANTE_MATERNELLE) { + const unParentLA = await this.parentsRepository.count({ + where: { numero_dossier: numeroDossier }, + }); + if (unParentLA > 0) { + throw new BadRequestException( + 'Ce numéro de dossier est celui d\'une famille (parent). Un numéro famille ne peut pas être affecté à une assistante maternelle.', + ); + } + } + + user.numero_dossier = numeroDossier; + const savedUser = await this.usersRepository.save(user); + + if (user.role === RoleType.PARENT) { + await this.parentsRepository.update({ user_id: userId }, { numero_dossier: numeroDossier }); + } else { + await this.assistantesRepository.update({ user_id: userId }, { numero_dossier: numeroDossier }); + } + + return savedUser; + } + async remove(id: string, currentUser: Users): Promise { if (currentUser.role !== RoleType.SUPER_ADMIN) { throw new ForbiddenException('Accès réservé aux super admins'); diff --git a/database/BDD.sql b/database/BDD.sql index 46a741e..fc04af8 100644 --- a/database/BDD.sql +++ b/database/BDD.sql @@ -355,6 +355,20 @@ ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS privacy_acceptee_le TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS relais_id UUID REFERENCES relais(id) ON DELETE SET NULL; +-- ========================================================== +-- Ticket #103 : Numéro de dossier (AAAA-NNNNNN, séquence par année) +-- ========================================================== +CREATE TABLE IF NOT EXISTS numero_dossier_sequence ( + annee INT PRIMARY KEY, + prochain INT NOT NULL DEFAULT 1 +); +ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS numero_dossier VARCHAR(20) NULL; +ALTER TABLE assistantes_maternelles ADD COLUMN IF NOT EXISTS numero_dossier VARCHAR(20) NULL; +ALTER TABLE parents ADD COLUMN IF NOT EXISTS numero_dossier VARCHAR(20) NULL; +CREATE INDEX IF NOT EXISTS idx_utilisateurs_numero_dossier ON utilisateurs(numero_dossier) WHERE numero_dossier IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_assistantes_maternelles_numero_dossier ON assistantes_maternelles(numero_dossier) WHERE numero_dossier IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_parents_numero_dossier ON parents(numero_dossier) WHERE numero_dossier IS NOT NULL; + -- ========================================================== -- Seed : Documents légaux génériques v1 -- ========================================================== diff --git a/database/migrations/2026_numero_dossier.sql b/database/migrations/2026_numero_dossier.sql new file mode 100644 index 0000000..4af0866 --- /dev/null +++ b/database/migrations/2026_numero_dossier.sql @@ -0,0 +1,33 @@ +-- Migration #103 : Numéro de dossier (format AAAA-NNNNNN, séquence par année) +-- Colonnes sur utilisateurs, assistantes_maternelles, parents. +-- Table de séquence par année pour génération unique. + +BEGIN; + +-- Table de séquence : une ligne par année, prochain = prochain numéro à attribuer (1..999999) +CREATE TABLE IF NOT EXISTS numero_dossier_sequence ( + annee INT PRIMARY KEY, + prochain INT NOT NULL DEFAULT 1 +); + +-- Colonne sur utilisateurs (AM et parents : numéro attribué à la soumission) +ALTER TABLE utilisateurs + ADD COLUMN IF NOT EXISTS numero_dossier VARCHAR(20) NULL; + +-- Colonne sur assistantes_maternelles (redondant avec users pour accès direct) +ALTER TABLE assistantes_maternelles + ADD COLUMN IF NOT EXISTS numero_dossier VARCHAR(20) NULL; + +-- Colonne sur parents (un numéro par famille, même valeur sur les deux lignes si co-parent) +ALTER TABLE parents + ADD COLUMN IF NOT EXISTS numero_dossier VARCHAR(20) NULL; + +-- Index pour recherche par numéro +CREATE INDEX IF NOT EXISTS idx_utilisateurs_numero_dossier + ON utilisateurs(numero_dossier) WHERE numero_dossier IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_assistantes_maternelles_numero_dossier + ON assistantes_maternelles(numero_dossier) WHERE numero_dossier IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_parents_numero_dossier + ON parents(numero_dossier) WHERE numero_dossier IS NOT NULL; + +COMMIT; diff --git a/database/migrations/2026_numero_dossier_backfill.sql b/database/migrations/2026_numero_dossier_backfill.sql new file mode 100644 index 0000000..ef2590e --- /dev/null +++ b/database/migrations/2026_numero_dossier_backfill.sql @@ -0,0 +1,122 @@ +-- Backfill #103 : attribuer un numero_dossier aux entrées existantes (NULL) +-- Famille = lien co_parent OU partage d'au moins un enfant (même dossier). +-- Ordre : par année, AM puis familles (une entrée par famille), séquence 000001, 000002... +-- À exécuter après 2026_numero_dossier.sql + +DO $$ +DECLARE + yr INT; + seq INT; + num TEXT; + r RECORD; + family_user_ids UUID[]; +BEGIN + -- Réinitialiser pour rejouer le backfill (cohérence AM + familles) + UPDATE parents SET numero_dossier = NULL; + UPDATE utilisateurs SET numero_dossier = NULL + WHERE role IN ('parent', 'assistante_maternelle'); + UPDATE assistantes_maternelles SET numero_dossier = NULL; + + FOR yr IN + SELECT DISTINCT EXTRACT(YEAR FROM u.cree_le)::INT + FROM utilisateurs u + WHERE ( + (u.role = 'assistante_maternelle' AND u.numero_dossier IS NULL) + OR EXISTS (SELECT 1 FROM parents p WHERE p.id_utilisateur = u.id AND p.numero_dossier IS NULL) + ) + ORDER BY 1 + LOOP + seq := 0; + + -- 1) AM : par ordre de création + FOR r IN + SELECT u.id + FROM utilisateurs u + WHERE u.role = 'assistante_maternelle' + AND u.numero_dossier IS NULL + AND EXTRACT(YEAR FROM u.cree_le) = yr + ORDER BY u.cree_le + LOOP + seq := seq + 1; + num := yr || '-' || LPAD(seq::TEXT, 6, '0'); + UPDATE utilisateurs SET numero_dossier = num WHERE id = r.id; + UPDATE assistantes_maternelles SET numero_dossier = num WHERE id_utilisateur = r.id; + END LOOP; + + -- 2) Familles : une entrée par "dossier" (co_parent OU enfants partagés) + -- family_rep = min(id) de la composante connexe (lien co_parent + partage d'enfants) + FOR r IN + WITH RECURSIVE + links AS ( + SELECT p.id_utilisateur AS p1, p.id_co_parent AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL + UNION ALL + SELECT p.id_co_parent AS p1, p.id_utilisateur AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL + UNION ALL + SELECT ep1.id_parent AS p1, ep2.id_parent AS p2 + FROM enfants_parents ep1 + JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent + UNION ALL + SELECT ep2.id_parent AS p1, ep1.id_parent AS p2 + FROM enfants_parents ep1 + JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent + ), + rec AS ( + SELECT id_utilisateur AS id, id_utilisateur AS rep FROM parents + UNION + SELECT l.p2 AS id, LEAST(rec_alias.rep, l.p2) AS rep FROM links l JOIN rec rec_alias ON rec_alias.id = l.p1 + ), + family_rep AS ( + SELECT id, MIN(rep::text)::uuid AS rep FROM rec GROUP BY id + ), + fam_ordered AS ( + SELECT fr.rep AS family_rep, MIN(u.cree_le) AS cree_le + FROM family_rep fr + JOIN parents p ON p.id_utilisateur = fr.id + JOIN utilisateurs u ON u.id = p.id_utilisateur + WHERE p.numero_dossier IS NULL + AND EXTRACT(YEAR FROM u.cree_le) = yr + GROUP BY fr.rep + ORDER BY MIN(u.cree_le) + ) + SELECT fo.family_rep + FROM fam_ordered fo + LOOP + seq := seq + 1; + num := yr || '-' || LPAD(seq::TEXT, 6, '0'); + + WITH RECURSIVE + links AS ( + SELECT p.id_utilisateur AS p1, p.id_co_parent AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL + UNION ALL + SELECT p.id_co_parent AS p1, p.id_utilisateur AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL + UNION ALL + SELECT ep1.id_parent AS p1, ep2.id_parent AS p2 + FROM enfants_parents ep1 + JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent + UNION ALL + SELECT ep2.id_parent AS p1, ep1.id_parent AS p2 + FROM enfants_parents ep1 + JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent + ), + rec AS ( + SELECT id_utilisateur AS id, id_utilisateur AS rep FROM parents + UNION + SELECT l.p2 AS id, LEAST(rec_alias.rep, l.p2) AS rep FROM links l JOIN rec rec_alias ON rec_alias.id = l.p1 + ), + family_rep AS ( + SELECT id, MIN(rep::text)::uuid AS rep FROM rec GROUP BY id + ) + SELECT array_agg(DISTINCT fr.id) INTO family_user_ids + FROM family_rep fr + WHERE fr.rep = r.family_rep; + + UPDATE utilisateurs SET numero_dossier = num WHERE id = ANY(family_user_ids); + UPDATE parents SET numero_dossier = num WHERE id_utilisateur = ANY(family_user_ids); + END LOOP; + + INSERT INTO numero_dossier_sequence (annee, prochain) + VALUES (yr, seq + 1) + ON CONFLICT (annee) DO UPDATE + SET prochain = GREATEST(numero_dossier_sequence.prochain, seq + 1); + END LOOP; +END $$; diff --git a/docs/23_LISTE-TICKETS.md b/docs/23_LISTE-TICKETS.md index 5a879ac..4e31cfd 100644 --- a/docs/23_LISTE-TICKETS.md +++ b/docs/23_LISTE-TICKETS.md @@ -1,7 +1,7 @@ # 🎫 Liste Complète des Tickets - Projet P'titsPas -**Version** : 1.5 -**Date** : 24 Février 2026 +**Version** : 1.6 +**Date** : 25 Février 2026 **Auteur** : Équipe PtitsPas **Estimation totale** : ~208h @@ -9,7 +9,7 @@ ## 🔗 Liste des tickets Gitea -**Les numéros de section dans ce document = numéros d’issues Gitea.** Ticket #14 dans le doc = issue Gitea #14, etc. Source : dépôt `jmartin/petitspas` (état au 9 février 2026). +**Les numéros de section dans ce document = numéros d’issues Gitea.** Ticket #14 dans le doc = issue Gitea #14, etc. Source : dépôt `jmartin/petitspas` (état au 25 février 2026). | Gitea # | Titre (dépôt) | Statut | |--------|----------------|--------| @@ -25,21 +25,86 @@ | 12 | [Backend] Guard Configuration Initiale | ✅ Fermé | | 13 | [Backend] Adaptation MailService pour config dynamique | ✅ Fermé | | 14 | [Frontend] Panneau Paramètres / Configuration (première config + accès permanent) | Ouvert | -| 15 | [Frontend] Écran Paramètres (accès permanent) | Ouvert | +| 15 | [Frontend] Écran Paramètres (accès permanent) / Intégration panneau | Ouvert | | 16 | [Doc] Documentation configuration on-premise | Ouvert | | 17 | [Backend] API Création gestionnaire | ✅ Terminé | +| 18 | [Backend] API Inscription Parent - REFONTE (Workflow complet 6 étapes) | ✅ Terminé | +| 19 | [Backend] API Inscription Parent (étape 2 - Parent 2) | ✅ Terminé | +| 20 | [Backend] API Inscription Parent (étape 3 - Enfants) | ✅ Terminé | +| 21 | [Backend] API Inscription Parent (étape 4-6 - Finalisation) | ✅ Terminé | +| 24 | [Backend] API Création mot de passe | Ouvert | +| 25 | [Backend] API Liste comptes en attente | Ouvert | +| 26 | [Backend] API Validation/Refus comptes | Ouvert | +| 27 | [Backend] Service Email - Installation Nodemailer | Ouvert | +| 28 | [Backend] Templates Email - Validation | Ouvert | +| 29 | [Backend] Templates Email - Refus | Ouvert | +| 30 | [Backend] Connexion - Vérification statut | Ouvert | +| 31 | [Backend] Changement MDP obligatoire première connexion | Ouvert | +| 32 | [Backend] Service Documents Légaux | Ouvert | +| 33 | [Backend] API Documents Légaux | Ouvert | +| 34 | [Backend] Traçabilité acceptations documents | Ouvert | +| 35 | [Frontend] Écran Création Gestionnaire | Ouvert | +| 36 | [Frontend] Inscription Parent - Étape 1 (Parent 1) | ✅ Terminé | +| 37 | [Frontend] Inscription Parent - Étape 2 (Parent 2) | Ouvert | +| 38 | [Frontend] Inscription Parent - Étape 3 (Enfants) | ✅ Terminé | +| 39 | [Frontend] Inscription Parent - Étapes 4-6 (Finalisation) | ✅ Terminé | +| 40 | [Frontend] Inscription AM - Panneau 1 (Identité) | ✅ Terminé | +| 41 | [Frontend] Inscription AM - Panneau 2 (Infos pro) | ✅ Terminé | +| 42 | [Frontend] Inscription AM - Finalisation | ✅ Terminé | +| 43 | [Frontend] Écran Création Mot de Passe | Ouvert | +| 44 | [Frontend] Dashboard Gestionnaire - Structure | ✅ Terminé | +| 45 | [Frontend] Dashboard Gestionnaire - Liste Parents | Ouvert | +| 46 | [Frontend] Dashboard Gestionnaire - Liste AM | Ouvert | +| 47 | [Frontend] Écran Changement MDP Obligatoire | Ouvert | +| 48 | [Frontend] Gestion Erreurs & Messages | Ouvert | +| 49 | [Frontend] Écran Gestion Documents Légaux (Admin) | Ouvert | +| 50 | [Frontend] Affichage dynamique CGU lors inscription | Ouvert | +| 51 | [Frontend] Écran Logs Admin (optionnel v1.1) | Ouvert | +| 52 | [Tests] Tests unitaires Backend | Ouvert | +| 53 | [Tests] Tests intégration Backend | Ouvert | +| 54 | [Tests] Tests E2E Frontend | Ouvert | +| 55 | [Doc] Documentation API OpenAPI/Swagger | Ouvert | +| 56 | [Backend] Service Upload & Stockage fichiers | Ouvert | +| 58 | [Backend] Service Logging (Winston) | Ouvert | +| 59 | [Infra] Volume Docker pour uploads | Ouvert | +| 60 | [Infra] Volume Docker pour documents légaux | Ouvert | +| 61 | [Doc] Guide installation & configuration | Ouvert | +| 62 | [Doc] Amendement CDC v1.4 - Suppression SMS | Ouvert | +| 63 | [Doc] Rédaction CGU/Privacy génériques v1 | Ouvert | +| 78 | [Frontend] Refonte Infrastructure Formulaires Multi-modes | ✅ Terminé | +| 79 | [Frontend] Renommer "Nanny" en "Assistante Maternelle" (AM) | ✅ Terminé | +| 81 | [Frontend] Corrections suite refactoring widgets | ✅ Terminé | +| 83 | [Frontend] Adapter RegisterChoiceScreen pour mobile | ✅ Terminé | +| 86 / 88 | Doublons fermés (voir #12, #14, #15) | ✅ Fermé | +| 89 | Log des appels API en mode debug | Ouvert | | 91 | [Frontend] Inscription AM – Branchement soumission formulaire à l'API | Ouvert | -| 101 | [Frontend] Inscription Parent – Branchement soumission formulaire à l'API | Ouvert | | 92 | [Frontend] Dashboard Admin - Données réelles et branchement API | ✅ Terminé | -| 93 | [Frontend] Panneau Admin - Homogeneiser la presentation des onglets | ✅ Fermé | -| 94 | [Backend] Relais - modele, API CRUD et liaison gestionnaire | ✅ Terminé | -| 95 | [Frontend] Admin - gestion des relais et rattachement gestionnaire | ✅ Fermé | +| 93 | [Frontend] Panneau Admin - Homogénéisation des onglets | ✅ Fermé | +| 94 | [Backend] Relais - Modèle, API CRUD et liaison gestionnaire | ✅ Terminé | +| 95 | [Frontend] Admin - Gestion des Relais et rattachement gestionnaire | ✅ Fermé | | 96 | [Frontend] Admin - Création administrateur via modale (sans relais) | ✅ Terminé | | 97 | [Backend] Harmoniser API création administrateur avec le contrat frontend | ✅ Terminé | -| 89 | Log des appels API en mode debug | Ouvert | +| 101 | [Frontend] Inscription Parent – Branchement soumission formulaire à l'API | Ouvert | +| 103 | Numéro de dossier – backend | Ouvert | +| 104 | Numéro de dossier – frontend | Ouvert | +| 105 | Statut « refusé » | Ouvert | +| 106 | Liste familles en attente | Ouvert | +| 107 | Onglet « À valider » + listes | Ouvert | +| 108 | Validation dossier famille | Ouvert | +| 109 | Modale de validation | Ouvert | +| 110 | Refus sans suppression | Ouvert | +| 111 | Reprise après refus – backend | Ouvert | +| 112 | Reprise après refus – frontend | Ouvert | +| 113 | Doublons à l'inscription | Ouvert | +| 114 | Doublons – alerte gestionnaire | Ouvert | +| 115 | Rattachement parent – backend | Ouvert | +| 116 | Rattachement parent – frontend | Ouvert | +| 117 | Évolution du cahier des charges | Ouvert | *Gitea #1 et #2 = anciens tickets de test (fermés). Liste complète : https://git.ptits-pas.fr/jmartin/petitspas/issues* +*Tickets #103 à #117 = périmètre « Validation des nouveaux comptes par le gestionnaire » (plan + sync via `backend/scripts/sync-gitea-validation-tickets.js`).* + --- ## 📊 Vue d'ensemble @@ -1412,7 +1477,7 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit --- -**Dernière mise à jour** : 24 Février 2026 +**Dernière mise à jour** : 25 Février 2026 **Version** : 1.6 -**Statut** : ✅ Aligné avec le dépôt Gitea +**Statut** : ✅ Aligné avec le dépôt Gitea (tickets #103-#117 créés)