feat(#103): Numéro de dossier – backend

- Colonne numero_dossier (utilisateurs, assistantes_maternelles, parents)
- Table numero_dossier_sequence, format AAAA-NNNNNN, séquence par année
- Génération à la soumission AM et parent (famille)
- Backfill existants (famille = co_parent ou enfants partagés)
- API PATCH /users/:id/numero-dossier (gestionnaire/admin)
- Garde-fous: max 2 parents/dossier, pas de mélange AM/parent

Made-with: Cursor
This commit is contained in:
MARTIN Julien 2026-03-12 22:12:33 +01:00
parent 34a36b069e
commit dfd58d9b6c
13 changed files with 350 additions and 2 deletions

View File

@ -48,4 +48,7 @@ export class AssistanteMaternelle {
@Column( { name: 'place_disponible', type: 'integer', nullable: true }) @Column( { name: 'place_disponible', type: 'integer', nullable: true })
places_available?: number; 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;
} }

View File

@ -1,5 +1,5 @@
import { import {
Entity, PrimaryColumn, OneToOne, JoinColumn, Entity, PrimaryColumn, Column, OneToOne, JoinColumn,
ManyToOne, OneToMany ManyToOne, OneToMany
} from 'typeorm'; } from 'typeorm';
import { Users } from './users.entity'; import { Users } from './users.entity';
@ -21,6 +21,10 @@ export class Parents {
@JoinColumn({ name: 'id_co_parent', referencedColumnName: 'id' }) @JoinColumn({ name: 'id_co_parent', referencedColumnName: 'id' })
co_parent?: Users; 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 // Lien vers enfants via la table enfants_parents
@OneToMany(() => ParentsChildren, pc => pc.parent) @OneToMany(() => ParentsChildren, pc => pc.parent)
parentChildren: ParentsChildren[]; parentChildren: ParentsChildren[];

View File

@ -152,6 +152,10 @@ export class Users {
@Column({ nullable: true, name: 'relais_id' }) @Column({ nullable: true, name: 'relais_id' })
relaisId?: string; 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 }) @ManyToOne(() => Relais, relais => relais.gestionnaires, { nullable: true })
@JoinColumn({ name: 'relais_id' }) @JoinColumn({ name: 'relais_id' })
relais?: Relais; relais?: Relais;

View File

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { NumeroDossierService } from './numero-dossier.service';
@Module({
providers: [NumeroDossierService],
exports: [NumeroDossierService],
})
export class NumeroDossierModule {}

View File

@ -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 };
}
}

View File

@ -10,12 +10,14 @@ import { Parents } from 'src/entities/parents.entity';
import { Children } from 'src/entities/children.entity'; import { Children } from 'src/entities/children.entity';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity'; import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { AppConfigModule } from 'src/modules/config'; import { AppConfigModule } from 'src/modules/config';
import { NumeroDossierModule } from 'src/modules/numero-dossier/numero-dossier.module';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Users, Parents, Children, AssistanteMaternelle]), TypeOrmModule.forFeature([Users, Parents, Children, AssistanteMaternelle]),
forwardRef(() => UserModule), forwardRef(() => UserModule),
AppConfigModule, AppConfigModule,
NumeroDossierModule,
JwtModule.registerAsync({ JwtModule.registerAsync({
imports: [ConfigModule], imports: [ConfigModule],
useFactory: (config: ConfigService) => ({ useFactory: (config: ConfigService) => ({

View File

@ -24,6 +24,7 @@ import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entit
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { AppConfigService } from 'src/modules/config/config.service'; import { AppConfigService } from 'src/modules/config/config.service';
import { validateNir } from 'src/common/utils/nir.util'; import { validateNir } from 'src/common/utils/nir.util';
import { NumeroDossierService } from 'src/modules/numero-dossier/numero-dossier.service';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
@ -32,6 +33,7 @@ export class AuthService {
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly appConfigService: AppConfigService, private readonly appConfigService: AppConfigService,
private readonly numeroDossierService: NumeroDossierService,
@InjectRepository(Parents) @InjectRepository(Parents)
private readonly parentsRepo: Repository<Parents>, private readonly parentsRepo: Repository<Parents>,
@InjectRepository(Users) @InjectRepository(Users)
@ -194,6 +196,8 @@ export class AuthService {
dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken); dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken);
const resultat = await this.usersRepo.manager.transaction(async (manager) => { const resultat = await this.usersRepo.manager.transaction(async (manager) => {
const { numero: numeroDossier } = await this.numeroDossierService.getNextNumeroDossier(manager);
const parent1 = manager.create(Users, { const parent1 = manager.create(Users, {
email: dto.email, email: dto.email,
prenom: dto.prenom, prenom: dto.prenom,
@ -206,6 +210,7 @@ export class AuthService {
ville: dto.ville, ville: dto.ville,
token_creation_mdp: tokenCreationMdp, token_creation_mdp: tokenCreationMdp,
token_creation_mdp_expire_le: dateExpiration, token_creation_mdp_expire_le: dateExpiration,
numero_dossier: numeroDossier,
}); });
const parent1Enregistre = await manager.save(Users, parent1); 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, ville: dto.co_parent_meme_adresse ? dto.ville : dto.co_parent_ville,
token_creation_mdp: tokenCoParent, token_creation_mdp: tokenCoParent,
token_creation_mdp_expire_le: dateExpirationCoParent, token_creation_mdp_expire_le: dateExpirationCoParent,
numero_dossier: numeroDossier,
}); });
parent2Enregistre = await manager.save(Users, parent2); parent2Enregistre = await manager.save(Users, parent2);
@ -237,6 +243,7 @@ export class AuthService {
const entiteParent = manager.create(Parents, { const entiteParent = manager.create(Parents, {
user_id: parent1Enregistre.id, user_id: parent1Enregistre.id,
numero_dossier: numeroDossier,
}); });
entiteParent.user = parent1Enregistre; entiteParent.user = parent1Enregistre;
if (parent2Enregistre) { if (parent2Enregistre) {
@ -248,6 +255,7 @@ export class AuthService {
if (parent2Enregistre) { if (parent2Enregistre) {
const entiteCoParent = manager.create(Parents, { const entiteCoParent = manager.create(Parents, {
user_id: parent2Enregistre.id, user_id: parent2Enregistre.id,
numero_dossier: numeroDossier,
}); });
entiteCoParent.user = parent2Enregistre; entiteCoParent.user = parent2Enregistre;
entiteCoParent.co_parent = parent1Enregistre; entiteCoParent.co_parent = parent1Enregistre;
@ -360,6 +368,8 @@ export class AuthService {
dto.consentement_photo ? new Date() : undefined; dto.consentement_photo ? new Date() : undefined;
const resultat = await this.usersRepo.manager.transaction(async (manager) => { const resultat = await this.usersRepo.manager.transaction(async (manager) => {
const { numero: numeroDossier } = await this.numeroDossierService.getNextNumeroDossier(manager);
const user = manager.create(Users, { const user = manager.create(Users, {
email: dto.email, email: dto.email,
prenom: dto.prenom, prenom: dto.prenom,
@ -376,6 +386,7 @@ export class AuthService {
consentement_photo: dto.consentement_photo, consentement_photo: dto.consentement_photo,
date_consentement_photo: dateConsentementPhoto, date_consentement_photo: dateConsentementPhoto,
date_naissance: dto.date_naissance ? new Date(dto.date_naissance) : undefined, date_naissance: dto.date_naissance ? new Date(dto.date_naissance) : undefined,
numero_dossier: numeroDossier,
}); });
const userEnregistre = await manager.save(Users, user); const userEnregistre = await manager.save(Users, user);
@ -389,6 +400,7 @@ export class AuthService {
residence_city: dto.ville ?? undefined, residence_city: dto.ville ?? undefined,
agreement_date: dto.date_agrement ? new Date(dto.date_agrement) : undefined, agreement_date: dto.date_agrement ? new Date(dto.date_agrement) : undefined,
available: true, available: true,
numero_dossier: numeroDossier,
}); });
await amRepo.save(am); await amRepo.save(am);

View File

@ -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;
}

View File

@ -1,6 +1,7 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard'; 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 { Roles } from 'src/common/decorators/roles.decorator';
import { User } from 'src/common/decorators/user.decorator'; import { User } from 'src/common/decorators/user.decorator';
import { RoleType, Users } from 'src/entities/users.entity'; 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 { CreateUserDto } from './dto/create_user.dto';
import { CreateAdminDto } from './dto/create_admin.dto'; import { CreateAdminDto } from './dto/create_admin.dto';
import { UpdateUserDto } from './dto/update_user.dto'; import { UpdateUserDto } from './dto/update_user.dto';
import { AffecterNumeroDossierDto } from './dto/affecter-numero-dossier.dto';
@ApiTags('Utilisateurs') @ApiTags('Utilisateurs')
@ApiBearerAuth('access-token') @ApiBearerAuth('access-token')
@UseGuards(AuthGuard) @UseGuards(AuthGuard, RolesGuard)
@Controller('users') @Controller('users')
export class UserController { export class UserController {
constructor(private readonly userService: UserService) { } constructor(private readonly userService: UserService) { }
@ -78,6 +80,23 @@ export class UserController {
return this.userService.updateUser(id, dto, currentUser); 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 dattribuer 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') @Patch(':id/valider')
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Valider un compte utilisateur' }) @ApiOperation({ summary: 'Valider un compte utilisateur' })

View File

@ -270,6 +270,64 @@ export class UserService {
await this.validationRepository.save(suspend); await this.validationRepository.save(suspend);
return savedUser; 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<Users> {
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<void> { async remove(id: string, currentUser: Users): Promise<void> {
if (currentUser.role !== RoleType.SUPER_ADMIN) { if (currentUser.role !== RoleType.SUPER_ADMIN) {
throw new ForbiddenException('Accès réservé aux super admins'); throw new ForbiddenException('Accès réservé aux super admins');

View File

@ -355,6 +355,20 @@ ALTER TABLE utilisateurs
ADD COLUMN IF NOT EXISTS privacy_acceptee_le TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS privacy_acceptee_le TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS relais_id UUID REFERENCES relais(id) ON DELETE SET NULL; 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 -- Seed : Documents légaux génériques v1
-- ========================================================== -- ==========================================================

View File

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

View File

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