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:
@@ -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) => ({
|
||||
|
||||
@@ -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<Parents>,
|
||||
@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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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' })
|
||||
|
||||
@@ -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<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> {
|
||||
if (currentUser.role !== RoleType.SUPER_ADMIN) {
|
||||
throw new ForbiddenException('Accès réservé aux super admins');
|
||||
|
||||
Reference in New Issue
Block a user