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 $$;