Compare commits
75 Commits
master
...
feature/11
| Author | SHA1 | Date | |
|---|---|---|---|
| 76ba28a7ce | |||
| 1772744c81 | |||
| 5465117238 | |||
| 6e2343087e | |||
| f6fabc521e | |||
| 5390276ecd | |||
| 7e9306de01 | |||
| d832559027 | |||
| 54ad1d2aa1 | |||
| 86b28abe51 | |||
| 86d8189038 | |||
| dbcb3611d4 | |||
| 1fa70f4052 | |||
| 393a527c37 | |||
| dfd58d9b6c | |||
| 34a36b069e | |||
| 2fa546e6b7 | |||
| 8636b16659 | |||
| 7e17e5ff8d | |||
| e8b6d906e6 | |||
| ae0be04964 | |||
| 447f3d4137 | |||
| 721f40599b | |||
| a9c6b9e15b | |||
| 38c003ef6f | |||
| 3dbddbb8c4 | |||
| f46740c6ab | |||
| 85bfef7a6b | |||
| 3c2ecdff7a | |||
| 8b83702bd2 | |||
| 19b8be684f | |||
| 5950d85876 | |||
| 4339e1e53d | |||
| defa438edf | |||
| e990d576cf | |||
| e8c6665a06 | |||
| a4e6cfc50e | |||
| 80d69a5463 | |||
| 0579fda553 | |||
| d14550a1cf | |||
| 2645cf1cd6 | |||
| e2ebc6a0a1 | |||
| 090ce6e13b | |||
| d66bdd04be | |||
| d8572e7fd6 | |||
| 222d7c702f | |||
| 537c46127f | |||
| ed18dcab10 | |||
| bb92f010bd | |||
| 42bb872c41 | |||
| fac3ae9baa | |||
| 5c28981ac5 | |||
| 57ce5af0f4 | |||
| c1204a3050 | |||
| 9d4363b2a7 | |||
| af06ab1e66 | |||
| aa148354ec | |||
| a10dc5a195 | |||
| 04c0b05aae | |||
| d0b730c8ab | |||
| bc8362bdb7 | |||
| ac3178903d | |||
| aec1990ec9 | |||
| 5da2ab9005 | |||
| b2d6414fab | |||
| fbafef8f2c | |||
| 135c7c2255 | |||
| 9cce326046 | |||
| d697083f54 | |||
| ae786426fd | |||
| e4f7a35f0f | |||
| 8a6768b316 | |||
| 3892a8beab | |||
| d39bc55be3 | |||
| e0debf0394 |
@ -17,6 +17,7 @@ import { EnfantsModule } from './routes/enfants/enfants.module';
|
|||||||
import { AppConfigModule } from './modules/config/config.module';
|
import { AppConfigModule } from './modules/config/config.module';
|
||||||
import { DocumentsLegauxModule } from './modules/documents-legaux';
|
import { DocumentsLegauxModule } from './modules/documents-legaux';
|
||||||
import { RelaisModule } from './routes/relais/relais.module';
|
import { RelaisModule } from './routes/relais/relais.module';
|
||||||
|
import { DossiersModule } from './routes/dossiers/dossiers.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -55,6 +56,7 @@ import { RelaisModule } from './routes/relais/relais.module';
|
|||||||
AppConfigModule,
|
AppConfigModule,
|
||||||
DocumentsLegauxModule,
|
DocumentsLegauxModule,
|
||||||
RelaisModule,
|
RelaisModule,
|
||||||
|
DossiersModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
65
backend/src/entities/dossier_famille.entity.ts
Normal file
65
backend/src/entities/dossier_famille.entity.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Parents } from './parents.entity';
|
||||||
|
import { Children } from './children.entity';
|
||||||
|
import { StatutDossierType } from './dossiers.entity';
|
||||||
|
|
||||||
|
/** Un dossier = une famille, N enfants (texte de motivation unique, liste d'enfants). */
|
||||||
|
@Entity('dossier_famille')
|
||||||
|
export class DossierFamille {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'numero_dossier', length: 20 })
|
||||||
|
numero_dossier: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Parents, { onDelete: 'CASCADE', nullable: false })
|
||||||
|
@JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' })
|
||||||
|
parent: Parents;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
presentation?: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'enum',
|
||||||
|
enum: StatutDossierType,
|
||||||
|
enumName: 'statut_dossier_type',
|
||||||
|
default: StatutDossierType.ENVOYE,
|
||||||
|
name: 'statut',
|
||||||
|
})
|
||||||
|
statut: StatutDossierType;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
|
||||||
|
cree_le: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
|
||||||
|
modifie_le: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => DossierFamilleEnfant, (dfe) => dfe.dossier_famille)
|
||||||
|
enfants: DossierFamilleEnfant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('dossier_famille_enfants')
|
||||||
|
export class DossierFamilleEnfant {
|
||||||
|
@Column({ name: 'id_dossier_famille', primary: true })
|
||||||
|
id_dossier_famille: string;
|
||||||
|
|
||||||
|
@Column({ name: 'id_enfant', primary: true })
|
||||||
|
id_enfant: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => DossierFamille, (df) => df.enfants, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'id_dossier_famille' })
|
||||||
|
dossier_famille: DossierFamille;
|
||||||
|
|
||||||
|
@ManyToOne(() => Children, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'id_enfant' })
|
||||||
|
enfant: Children;
|
||||||
|
}
|
||||||
@ -43,6 +43,8 @@ export class AuthService {
|
|||||||
private readonly usersRepo: Repository<Users>,
|
private readonly usersRepo: Repository<Users>,
|
||||||
@InjectRepository(Children)
|
@InjectRepository(Children)
|
||||||
private readonly childrenRepo: Repository<Children>,
|
private readonly childrenRepo: Repository<Children>,
|
||||||
|
@InjectRepository(AssistanteMaternelle)
|
||||||
|
private readonly assistantesMaternellesRepo: Repository<AssistanteMaternelle>,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -189,6 +191,11 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dto.co_parent_email) {
|
if (dto.co_parent_email) {
|
||||||
|
if (dto.email.trim().toLowerCase() === dto.co_parent_email.trim().toLowerCase()) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'L\'email du parent et du co-parent doivent être différents.',
|
||||||
|
);
|
||||||
|
}
|
||||||
const coParentExiste = await this.usersService.findByEmailOrNull(dto.co_parent_email);
|
const coParentExiste = await this.usersService.findByEmailOrNull(dto.co_parent_email);
|
||||||
if (coParentExiste) {
|
if (coParentExiste) {
|
||||||
throw new ConflictException('L\'email du co-parent est déjà utilisé');
|
throw new ConflictException('L\'email du co-parent est déjà utilisé');
|
||||||
@ -360,6 +367,27 @@ export class AuthService {
|
|||||||
throw new ConflictException('Un compte avec cet email existe déjà');
|
throw new ConflictException('Un compte avec cet email existe déjà');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nirDejaUtilise = await this.assistantesMaternellesRepo.findOne({
|
||||||
|
where: { nir: nirNormalized },
|
||||||
|
});
|
||||||
|
if (nirDejaUtilise) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'Un compte assistante maternelle avec ce numéro NIR existe déjà.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const numeroAgrement = (dto.numero_agrement || '').trim();
|
||||||
|
if (numeroAgrement) {
|
||||||
|
const agrementDejaUtilise = await this.assistantesMaternellesRepo.findOne({
|
||||||
|
where: { approval_number: numeroAgrement },
|
||||||
|
});
|
||||||
|
if (agrementDejaUtilise) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'Un compte assistante maternelle avec ce numéro d\'agrément existe déjà.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const joursExpirationToken = await this.appConfigService.get<number>(
|
const joursExpirationToken = await this.appConfigService.get<number>(
|
||||||
'password_reset_token_expiry_days',
|
'password_reset_token_expiry_days',
|
||||||
7,
|
7,
|
||||||
|
|||||||
26
backend/src/routes/dossiers/dossiers.controller.ts
Normal file
26
backend/src/routes/dossiers/dossiers.controller.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Roles } from 'src/common/decorators/roles.decorator';
|
||||||
|
import { RoleType } from 'src/entities/users.entity';
|
||||||
|
import { AuthGuard } from 'src/common/guards/auth.guard';
|
||||||
|
import { RolesGuard } from 'src/common/guards/roles.guard';
|
||||||
|
import { DossiersService } from './dossiers.service';
|
||||||
|
import { DossierUnifieDto } from './dto/dossier-unifie.dto';
|
||||||
|
|
||||||
|
@ApiTags('Dossiers')
|
||||||
|
@Controller('dossiers')
|
||||||
|
@UseGuards(AuthGuard, RolesGuard)
|
||||||
|
export class DossiersController {
|
||||||
|
constructor(private readonly dossiersService: DossiersService) {}
|
||||||
|
|
||||||
|
@Get(':numeroDossier')
|
||||||
|
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE)
|
||||||
|
@ApiOperation({ summary: 'Dossier complet par numéro (AM ou famille) – Ticket #119' })
|
||||||
|
@ApiParam({ name: 'numeroDossier', description: 'Numéro de dossier (ex: 2026-000001)' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Dossier famille ou AM', type: DossierUnifieDto })
|
||||||
|
@ApiResponse({ status: 404, description: 'Aucun dossier pour ce numéro' })
|
||||||
|
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
||||||
|
getDossier(@Param('numeroDossier') numeroDossier: string): Promise<DossierUnifieDto> {
|
||||||
|
return this.dossiersService.getDossierByNumero(numeroDossier);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,28 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
|
import { ParentsModule } from '../parents/parents.module';
|
||||||
|
import { DossiersController } from './dossiers.controller';
|
||||||
|
import { DossiersService } from './dossiers.service';
|
||||||
|
|
||||||
@Module({})
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Parents, AssistanteMaternelle]),
|
||||||
|
ParentsModule,
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
secret: config.get('jwt.accessSecret'),
|
||||||
|
signOptions: { expiresIn: config.get('jwt.accessExpiresIn') },
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [DossiersController],
|
||||||
|
providers: [DossiersService],
|
||||||
|
exports: [DossiersService],
|
||||||
|
})
|
||||||
export class DossiersModule {}
|
export class DossiersModule {}
|
||||||
|
|||||||
81
backend/src/routes/dossiers/dossiers.service.ts
Normal file
81
backend/src/routes/dossiers/dossiers.service.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
|
import { ParentsService } from '../parents/parents.service';
|
||||||
|
import { DossierUnifieDto } from './dto/dossier-unifie.dto';
|
||||||
|
import { DossierAmCompletDto, DossierAmUserDto } from './dto/dossier-am-complet.dto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint unifié GET /dossiers/:numeroDossier – AM ou famille. Ticket #119.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class DossiersService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Parents)
|
||||||
|
private readonly parentsRepository: Repository<Parents>,
|
||||||
|
@InjectRepository(AssistanteMaternelle)
|
||||||
|
private readonly amRepository: Repository<AssistanteMaternelle>,
|
||||||
|
private readonly parentsService: ParentsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getDossierByNumero(numeroDossier: string): Promise<DossierUnifieDto> {
|
||||||
|
const num = numeroDossier?.trim();
|
||||||
|
if (!num) {
|
||||||
|
throw new NotFoundException('Numéro de dossier requis.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Famille : un parent a ce numéro ?
|
||||||
|
const parentWithNum = await this.parentsRepository.findOne({
|
||||||
|
where: { numero_dossier: num },
|
||||||
|
select: ['user_id'],
|
||||||
|
});
|
||||||
|
if (parentWithNum) {
|
||||||
|
const dossier = await this.parentsService.getDossierFamilleByNumero(num);
|
||||||
|
return { type: 'family', dossier };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) AM : une assistante maternelle a ce numéro ?
|
||||||
|
const am = await this.amRepository.findOne({
|
||||||
|
where: { numero_dossier: num },
|
||||||
|
relations: ['user'],
|
||||||
|
});
|
||||||
|
if (am?.user) {
|
||||||
|
const dossier: DossierAmCompletDto = {
|
||||||
|
numero_dossier: num,
|
||||||
|
user: this.toDossierAmUserDto(am.user),
|
||||||
|
numero_agrement: am.approval_number,
|
||||||
|
nir: am.nir,
|
||||||
|
biographie: am.biography,
|
||||||
|
disponible: am.available,
|
||||||
|
ville_residence: am.residence_city,
|
||||||
|
date_agrement: am.agreement_date,
|
||||||
|
annees_experience: am.years_experience,
|
||||||
|
specialite: am.specialty,
|
||||||
|
nb_max_enfants: am.max_children,
|
||||||
|
place_disponible: am.places_available,
|
||||||
|
};
|
||||||
|
return { type: 'am', dossier };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
prenom: user.prenom,
|
||||||
|
nom: user.nom,
|
||||||
|
telephone: user.telephone,
|
||||||
|
adresse: user.adresse,
|
||||||
|
ville: user.ville,
|
||||||
|
code_postal: user.code_postal,
|
||||||
|
profession: user.profession,
|
||||||
|
date_naissance: user.date_naissance,
|
||||||
|
photo_url: user.photo_url,
|
||||||
|
statut: user.statut,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
58
backend/src/routes/dossiers/dto/dossier-am-complet.dto.ts
Normal file
58
backend/src/routes/dossiers/dto/dossier-am-complet.dto.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { StatutUtilisateurType } from 'src/entities/users.entity';
|
||||||
|
|
||||||
|
/** Utilisateur AM sans données sensibles (pour dossier AM complet). Ticket #119 */
|
||||||
|
export class DossierAmUserDto {
|
||||||
|
@ApiProperty()
|
||||||
|
id: string;
|
||||||
|
@ApiProperty()
|
||||||
|
email: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
prenom?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
nom?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
telephone?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
adresse?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
ville?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
code_postal?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
profession?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
date_naissance?: Date;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
photo_url?: string;
|
||||||
|
@ApiProperty({ enum: StatutUtilisateurType })
|
||||||
|
statut: StatutUtilisateurType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dossier AM complet (fiche AM sans secrets). Ticket #119 */
|
||||||
|
export class DossierAmCompletDto {
|
||||||
|
@ApiProperty({ example: '2026-000003', description: 'Numéro de dossier AM' })
|
||||||
|
numero_dossier: string;
|
||||||
|
@ApiProperty({ type: DossierAmUserDto, description: 'Utilisateur (sans mot de passe ni tokens)' })
|
||||||
|
user: DossierAmUserDto;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
numero_agrement?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
nir?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
biographie?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
disponible?: boolean;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
ville_residence?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
date_agrement?: Date;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
annees_experience?: number;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
specialite?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
nb_max_enfants?: number;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
place_disponible?: number;
|
||||||
|
}
|
||||||
14
backend/src/routes/dossiers/dto/dossier-unifie.dto.ts
Normal file
14
backend/src/routes/dossiers/dto/dossier-unifie.dto.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { DossierFamilleCompletDto } from '../../parents/dto/dossier-famille-complet.dto';
|
||||||
|
import { DossierAmCompletDto } from './dossier-am-complet.dto';
|
||||||
|
|
||||||
|
/** Réponse unifiée GET /dossiers/:numeroDossier – AM ou famille. Ticket #119 */
|
||||||
|
export class DossierUnifieDto {
|
||||||
|
@ApiProperty({ enum: ['family', 'am'], description: 'Type de dossier' })
|
||||||
|
type: 'family' | 'am';
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Dossier famille (si type=family) ou dossier AM (si type=am)',
|
||||||
|
})
|
||||||
|
dossier: DossierFamilleCompletDto | DossierAmCompletDto;
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { StatutUtilisateurType } from 'src/entities/users.entity';
|
||||||
|
import { StatutEnfantType, GenreType } from 'src/entities/children.entity';
|
||||||
|
|
||||||
|
/** Parent dans le dossier famille (infos utilisateur + parent) */
|
||||||
|
export class DossierFamilleParentDto {
|
||||||
|
@ApiProperty()
|
||||||
|
user_id: string;
|
||||||
|
@ApiProperty()
|
||||||
|
email: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
prenom?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
nom?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
telephone?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
adresse?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
ville?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
code_postal?: string;
|
||||||
|
@ApiProperty({ enum: StatutUtilisateurType })
|
||||||
|
statut: StatutUtilisateurType;
|
||||||
|
@ApiProperty({ required: false, description: 'Id du co-parent si couple' })
|
||||||
|
co_parent_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enfant dans le dossier famille */
|
||||||
|
export class DossierFamilleEnfantDto {
|
||||||
|
@ApiProperty()
|
||||||
|
id: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
first_name?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
last_name?: string;
|
||||||
|
@ApiProperty({ required: false, enum: GenreType })
|
||||||
|
genre?: GenreType;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
birth_date?: Date;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
due_date?: Date;
|
||||||
|
@ApiProperty({ enum: StatutEnfantType })
|
||||||
|
status: StatutEnfantType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Réponse GET /parents/dossier-famille/:numeroDossier – dossier famille complet. Ticket #119 */
|
||||||
|
export class DossierFamilleCompletDto {
|
||||||
|
@ApiProperty({ example: '2026-000001', description: 'Numéro de dossier famille' })
|
||||||
|
numero_dossier: string;
|
||||||
|
@ApiProperty({ type: [DossierFamilleParentDto] })
|
||||||
|
parents: DossierFamilleParentDto[];
|
||||||
|
@ApiProperty({ type: [DossierFamilleEnfantDto], description: 'Enfants de la famille' })
|
||||||
|
enfants: DossierFamilleEnfantDto[];
|
||||||
|
@ApiProperty({ required: false, description: 'Texte de présentation / motivation (un seul par famille)' })
|
||||||
|
texte_motivation?: string;
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ import { AuthGuard } from 'src/common/guards/auth.guard';
|
|||||||
import { RolesGuard } from 'src/common/guards/roles.guard';
|
import { RolesGuard } from 'src/common/guards/roles.guard';
|
||||||
import { User } from 'src/common/decorators/user.decorator';
|
import { User } from 'src/common/decorators/user.decorator';
|
||||||
import { PendingFamilyDto } from './dto/pending-family.dto';
|
import { PendingFamilyDto } from './dto/pending-family.dto';
|
||||||
|
import { DossierFamilleCompletDto } from './dto/dossier-famille-complet.dto';
|
||||||
|
|
||||||
@ApiTags('Parents')
|
@ApiTags('Parents')
|
||||||
@Controller('parents')
|
@Controller('parents')
|
||||||
@ -39,6 +40,17 @@ export class ParentsController {
|
|||||||
return this.parentsService.getPendingFamilies();
|
return this.parentsService.getPendingFamilies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('dossier-famille/:numeroDossier')
|
||||||
|
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE)
|
||||||
|
@ApiOperation({ summary: 'Dossier famille complet par numéro de dossier (Ticket #119)' })
|
||||||
|
@ApiParam({ name: 'numeroDossier', description: 'Numéro de dossier (ex: 2026-000001)' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Dossier famille (numero_dossier, parents, enfants, presentation)', type: DossierFamilleCompletDto })
|
||||||
|
@ApiResponse({ status: 404, description: 'Aucun dossier pour ce numéro' })
|
||||||
|
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
||||||
|
getDossierFamille(@Param('numeroDossier') numeroDossier: string): Promise<DossierFamilleCompletDto> {
|
||||||
|
return this.parentsService.getDossierFamilleByNumero(numeroDossier);
|
||||||
|
}
|
||||||
|
|
||||||
@Post(':parentId/valider-dossier')
|
@Post(':parentId/valider-dossier')
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE)
|
||||||
@ApiOperation({ summary: 'Valider tout le dossier famille (les 2 parents en une fois)' })
|
@ApiOperation({ summary: 'Valider tout le dossier famille (les 2 parents en une fois)' })
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { Module, forwardRef } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { Parents } from 'src/entities/parents.entity';
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
|
import { DossierFamille, DossierFamilleEnfant } from 'src/entities/dossier_famille.entity';
|
||||||
import { ParentsController } from './parents.controller';
|
import { ParentsController } from './parents.controller';
|
||||||
import { ParentsService } from './parents.service';
|
import { ParentsService } from './parents.service';
|
||||||
import { Users } from 'src/entities/users.entity';
|
import { Users } from 'src/entities/users.entity';
|
||||||
@ -8,8 +11,16 @@ import { UserModule } from '../user/user.module';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Parents, Users]),
|
TypeOrmModule.forFeature([Parents, Users, DossierFamille, DossierFamilleEnfant]),
|
||||||
forwardRef(() => UserModule),
|
forwardRef(() => UserModule),
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
secret: config.get('jwt.accessSecret'),
|
||||||
|
signOptions: { expiresIn: config.get('jwt.accessExpiresIn') },
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
controllers: [ParentsController],
|
controllers: [ParentsController],
|
||||||
providers: [ParentsService],
|
providers: [ParentsService],
|
||||||
|
|||||||
@ -5,12 +5,18 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { In, Repository } from 'typeorm';
|
||||||
import { Parents } from 'src/entities/parents.entity';
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
|
import { DossierFamille } from 'src/entities/dossier_famille.entity';
|
||||||
import { RoleType, Users } from 'src/entities/users.entity';
|
import { RoleType, Users } from 'src/entities/users.entity';
|
||||||
import { CreateParentDto } from '../user/dto/create_parent.dto';
|
import { CreateParentDto } from '../user/dto/create_parent.dto';
|
||||||
import { UpdateParentsDto } from '../user/dto/update_parent.dto';
|
import { UpdateParentsDto } from '../user/dto/update_parent.dto';
|
||||||
import { PendingFamilyDto } from './dto/pending-family.dto';
|
import { PendingFamilyDto } from './dto/pending-family.dto';
|
||||||
|
import {
|
||||||
|
DossierFamilleCompletDto,
|
||||||
|
DossierFamilleParentDto,
|
||||||
|
DossierFamilleEnfantDto,
|
||||||
|
} from './dto/dossier-famille-complet.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ParentsService {
|
export class ParentsService {
|
||||||
@ -19,6 +25,8 @@ export class ParentsService {
|
|||||||
private readonly parentsRepository: Repository<Parents>,
|
private readonly parentsRepository: Repository<Parents>,
|
||||||
@InjectRepository(Users)
|
@InjectRepository(Users)
|
||||||
private readonly usersRepository: Repository<Users>,
|
private readonly usersRepository: Repository<Users>,
|
||||||
|
@InjectRepository(DossierFamille)
|
||||||
|
private readonly dossierFamilleRepository: Repository<DossierFamille>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Création d’un parent
|
// Création d’un parent
|
||||||
@ -79,47 +87,140 @@ export class ParentsService {
|
|||||||
* Uniquement les parents dont l'utilisateur a statut = en_attente.
|
* Uniquement les parents dont l'utilisateur a statut = en_attente.
|
||||||
*/
|
*/
|
||||||
async getPendingFamilies(): Promise<PendingFamilyDto[]> {
|
async getPendingFamilies(): Promise<PendingFamilyDto[]> {
|
||||||
const raw = await this.parentsRepository.query(`
|
let raw: { libelle: string; parentIds: unknown; numero_dossier: string | null }[];
|
||||||
WITH RECURSIVE
|
try {
|
||||||
links AS (
|
raw = await this.parentsRepository.query(`
|
||||||
SELECT p.id_utilisateur AS p1, p.id_co_parent AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL
|
WITH RECURSIVE
|
||||||
UNION ALL
|
links AS (
|
||||||
SELECT p.id_co_parent AS p1, p.id_utilisateur AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL
|
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
|
UNION ALL
|
||||||
SELECT ep1.id_parent AS p1, ep2.id_parent AS p2
|
SELECT p.id_co_parent AS p1, p.id_utilisateur AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL
|
||||||
FROM enfants_parents ep1
|
UNION ALL
|
||||||
JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
|
SELECT ep1.id_parent AS p1, ep2.id_parent AS p2
|
||||||
UNION ALL
|
FROM enfants_parents ep1
|
||||||
SELECT ep2.id_parent AS p1, ep1.id_parent AS p2
|
JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
|
||||||
FROM enfants_parents ep1
|
UNION ALL
|
||||||
JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
|
SELECT ep2.id_parent AS p1, ep1.id_parent AS p2
|
||||||
),
|
FROM enfants_parents ep1
|
||||||
rec AS (
|
JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
|
||||||
SELECT id_utilisateur AS id, id_utilisateur AS rep FROM parents
|
),
|
||||||
UNION
|
rec AS (
|
||||||
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
|
SELECT id_utilisateur AS id, id_utilisateur AS rep FROM parents
|
||||||
),
|
UNION
|
||||||
family_rep AS (
|
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
|
||||||
SELECT id, (MIN(rep::text))::uuid AS rep FROM rec GROUP BY id
|
),
|
||||||
)
|
family_rep AS (
|
||||||
SELECT
|
SELECT id, (MIN(rep::text))::uuid AS rep FROM rec GROUP BY id
|
||||||
'Famille ' || string_agg(u.nom, ' - ' ORDER BY u.nom, u.prenom) AS libelle,
|
)
|
||||||
array_agg(DISTINCT p.id_utilisateur ORDER BY p.id_utilisateur) AS "parentIds",
|
SELECT
|
||||||
(array_agg(p.numero_dossier))[1] AS numero_dossier
|
'Famille ' || string_agg(u.nom, ' - ' ORDER BY u.nom, u.prenom) AS libelle,
|
||||||
FROM family_rep fr
|
array_agg(DISTINCT p.id_utilisateur ORDER BY p.id_utilisateur) AS "parentIds",
|
||||||
JOIN parents p ON p.id_utilisateur = fr.id
|
(array_agg(p.numero_dossier))[1] AS numero_dossier
|
||||||
JOIN utilisateurs u ON u.id = p.id_utilisateur
|
FROM family_rep fr
|
||||||
WHERE u.role = 'parent' AND u.statut = 'en_attente'
|
JOIN parents p ON p.id_utilisateur = fr.id
|
||||||
GROUP BY fr.rep
|
JOIN utilisateurs u ON u.id = p.id_utilisateur
|
||||||
ORDER BY libelle
|
WHERE u.role = 'parent' AND u.statut = 'en_attente'
|
||||||
`);
|
GROUP BY fr.rep
|
||||||
return raw.map((r: { libelle: string; parentIds: unknown; numero_dossier: string | null }) => ({
|
ORDER BY libelle
|
||||||
libelle: r.libelle,
|
`);
|
||||||
parentIds: Array.isArray(r.parentIds) ? r.parentIds.map(String) : [],
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(raw)) return [];
|
||||||
|
return raw.map((r) => ({
|
||||||
|
libelle: r.libelle ?? '',
|
||||||
|
parentIds: this.normalizeParentIds(r.parentIds),
|
||||||
numero_dossier: r.numero_dossier ?? null,
|
numero_dossier: r.numero_dossier ?? null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Convertit parentIds (array ou chaîne PG) en string[] pour éviter 500 si le driver renvoie une chaîne. */
|
||||||
|
private normalizeParentIds(parentIds: unknown): string[] {
|
||||||
|
if (Array.isArray(parentIds)) return parentIds.map(String);
|
||||||
|
if (typeof parentIds === 'string') {
|
||||||
|
const s = parentIds.replace(/^\{|\}$/g, '').trim();
|
||||||
|
return s ? s.split(',').map((x) => x.trim()) : [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dossier famille complet par numéro de dossier. Ticket #119.
|
||||||
|
* Rôles : admin, gestionnaire.
|
||||||
|
* @throws NotFoundException si aucun parent avec ce numéro de dossier
|
||||||
|
*/
|
||||||
|
async getDossierFamilleByNumero(numeroDossier: string): Promise<DossierFamilleCompletDto> {
|
||||||
|
const num = numeroDossier?.trim();
|
||||||
|
if (!num) {
|
||||||
|
throw new NotFoundException('Numéro de dossier requis.');
|
||||||
|
}
|
||||||
|
const firstParent = await this.parentsRepository.findOne({
|
||||||
|
where: { numero_dossier: num },
|
||||||
|
relations: ['user'],
|
||||||
|
});
|
||||||
|
if (!firstParent || !firstParent.user) {
|
||||||
|
throw new NotFoundException('Aucun dossier famille trouvé pour ce numéro.');
|
||||||
|
}
|
||||||
|
const familyUserIds = await this.getFamilyUserIds(firstParent.user_id);
|
||||||
|
const parents = await this.parentsRepository.find({
|
||||||
|
where: { user_id: In(familyUserIds) },
|
||||||
|
relations: ['user', 'co_parent', 'parentChildren', 'parentChildren.child', 'dossiers', 'dossiers.child'],
|
||||||
|
});
|
||||||
|
const enfantsMap = new Map<string, DossierFamilleEnfantDto>();
|
||||||
|
let texte_motivation: string | undefined;
|
||||||
|
|
||||||
|
// Un dossier = une famille, un seul texte de motivation
|
||||||
|
const dossierFamille = await this.dossierFamilleRepository.findOne({
|
||||||
|
where: { numero_dossier: num },
|
||||||
|
relations: ['parent', 'enfants', 'enfants.enfant'],
|
||||||
|
});
|
||||||
|
if (dossierFamille?.presentation) {
|
||||||
|
texte_motivation = dossierFamille.presentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of parents) {
|
||||||
|
// Enfants via parentChildren
|
||||||
|
if (p.parentChildren) {
|
||||||
|
for (const pc of p.parentChildren) {
|
||||||
|
if (pc.child && !enfantsMap.has(pc.child.id)) {
|
||||||
|
enfantsMap.set(pc.child.id, {
|
||||||
|
id: pc.child.id,
|
||||||
|
first_name: pc.child.first_name,
|
||||||
|
last_name: pc.child.last_name,
|
||||||
|
genre: pc.child.gender,
|
||||||
|
birth_date: pc.child.birth_date,
|
||||||
|
due_date: pc.child.due_date,
|
||||||
|
status: pc.child.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback : anciens dossiers (un texte, on prend le premier)
|
||||||
|
if (texte_motivation == null && p.dossiers?.length) {
|
||||||
|
texte_motivation = p.dossiers[0].presentation ?? undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentsDto: DossierFamilleParentDto[] = parents.map((p) => ({
|
||||||
|
user_id: p.user_id,
|
||||||
|
email: p.user.email,
|
||||||
|
prenom: p.user.prenom,
|
||||||
|
nom: p.user.nom,
|
||||||
|
telephone: p.user.telephone,
|
||||||
|
adresse: p.user.adresse,
|
||||||
|
ville: p.user.ville,
|
||||||
|
code_postal: p.user.code_postal,
|
||||||
|
statut: p.user.statut,
|
||||||
|
co_parent_id: p.co_parent?.id,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
numero_dossier: num,
|
||||||
|
parents: parentsDto,
|
||||||
|
enfants: Array.from(enfantsMap.values()),
|
||||||
|
texte_motivation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne les user_id de tous les parents de la même famille (co_parent ou enfants partagés).
|
* Retourne les user_id de tous les parents de la même famille (co_parent ou enfants partagés).
|
||||||
* @throws NotFoundException si parentId n'est pas un parent
|
* @throws NotFoundException si parentId n'est pas un parent
|
||||||
|
|||||||
27
database/migrations/2026_dossier_famille.sql
Normal file
27
database/migrations/2026_dossier_famille.sql
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
-- Un dossier = une famille, N enfants. Ticket #119 évolution.
|
||||||
|
-- Table: un enregistrement par famille (lien via numero_dossier / id_parent).
|
||||||
|
CREATE TABLE IF NOT EXISTS 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,
|
||||||
|
type_contrat VARCHAR(50),
|
||||||
|
repas BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
budget NUMERIC(10,2),
|
||||||
|
planning_souhaite JSONB,
|
||||||
|
statut statut_dossier_type NOT NULL DEFAULT 'envoye',
|
||||||
|
cree_le TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
modifie_le TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dossier_famille_numero ON dossier_famille(numero_dossier);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dossier_famille_id_parent ON dossier_famille(id_parent);
|
||||||
|
|
||||||
|
-- Enfants concernés par ce dossier famille (N par dossier).
|
||||||
|
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS idx_dossier_famille_enfants_enfant ON dossier_famille_enfants(id_enfant);
|
||||||
5
database/migrations/2026_dossier_famille_simplifier.sql
Normal file
5
database/migrations/2026_dossier_famille_simplifier.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- Dossier famille = inscription uniquement, pas les données de dossier de garde (repas, type_contrat, budget, etc.)
|
||||||
|
ALTER TABLE dossier_famille DROP COLUMN IF EXISTS repas;
|
||||||
|
ALTER TABLE dossier_famille DROP COLUMN IF EXISTS type_contrat;
|
||||||
|
ALTER TABLE dossier_famille DROP COLUMN IF EXISTS budget;
|
||||||
|
ALTER TABLE dossier_famille DROP COLUMN IF EXISTS planning_souhaite;
|
||||||
@ -33,13 +33,13 @@
|
|||||||
| 20 | [Backend] API Inscription Parent (étape 3 - Enfants) | ✅ Terminé |
|
| 20 | [Backend] API Inscription Parent (étape 3 - Enfants) | ✅ Terminé |
|
||||||
| 21 | [Backend] API Inscription Parent (étape 4-6 - Finalisation) | ✅ Terminé |
|
| 21 | [Backend] API Inscription Parent (étape 4-6 - Finalisation) | ✅ Terminé |
|
||||||
| 24 | [Backend] API Création mot de passe | Ouvert |
|
| 24 | [Backend] API Création mot de passe | Ouvert |
|
||||||
| 25 | [Backend] API Liste comptes en attente | Ouvert |
|
| 25 | [Backend] API Liste comptes en attente | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||||
| 26 | [Backend] API Validation/Refus comptes | Ouvert |
|
| 26 | [Backend] API Validation/Refus comptes | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||||
| 27 | [Backend] Service Email - Installation Nodemailer | Ouvert |
|
| 27 | [Backend] Service Email - Installation Nodemailer | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||||
| 28 | [Backend] Templates Email - Validation | Ouvert |
|
| 28 | [Backend] Templates Email - Validation | Ouvert |
|
||||||
| 29 | [Backend] Templates Email - Refus | Ouvert |
|
| 29 | [Backend] Templates Email - Refus | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||||
| 30 | [Backend] Connexion - Vérification statut | Ouvert |
|
| 30 | [Backend] Connexion - Vérification statut | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||||
| 31 | [Backend] Changement MDP obligatoire première connexion | Ouvert |
|
| 31 | [Backend] Changement MDP obligatoire première connexion | ✅ Terminé |
|
||||||
| 32 | [Backend] Service Documents Légaux | Ouvert |
|
| 32 | [Backend] Service Documents Légaux | Ouvert |
|
||||||
| 33 | [Backend] API Documents Légaux | Ouvert |
|
| 33 | [Backend] API Documents Légaux | Ouvert |
|
||||||
| 34 | [Backend] Traçabilité acceptations documents | Ouvert |
|
| 34 | [Backend] Traçabilité acceptations documents | Ouvert |
|
||||||
@ -53,8 +53,8 @@
|
|||||||
| 42 | [Frontend] Inscription AM - Finalisation | ✅ Terminé |
|
| 42 | [Frontend] Inscription AM - Finalisation | ✅ Terminé |
|
||||||
| 43 | [Frontend] Écran Création Mot de Passe | Ouvert |
|
| 43 | [Frontend] Écran Création Mot de Passe | Ouvert |
|
||||||
| 44 | [Frontend] Dashboard Gestionnaire - Structure | ✅ Terminé |
|
| 44 | [Frontend] Dashboard Gestionnaire - Structure | ✅ Terminé |
|
||||||
| 45 | [Frontend] Dashboard Gestionnaire - Liste Parents | Ouvert |
|
| 45 | [Frontend] Dashboard Gestionnaire - Liste Parents | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||||
| 46 | [Frontend] Dashboard Gestionnaire - Liste AM | Ouvert |
|
| 46 | [Frontend] Dashboard Gestionnaire - Liste AM | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||||
| 47 | [Frontend] Écran Changement MDP Obligatoire | Ouvert |
|
| 47 | [Frontend] Écran Changement MDP Obligatoire | Ouvert |
|
||||||
| 48 | [Frontend] Gestion Erreurs & Messages | Ouvert |
|
| 48 | [Frontend] Gestion Erreurs & Messages | Ouvert |
|
||||||
| 49 | [Frontend] Écran Gestion Documents Légaux (Admin) | Ouvert |
|
| 49 | [Frontend] Écran Gestion Documents Légaux (Admin) | Ouvert |
|
||||||
@ -103,7 +103,7 @@
|
|||||||
|
|
||||||
*Gitea #1 et #2 = anciens tickets de test (fermés). Liste complète : https://git.ptits-pas.fr/jmartin/petitspas/issues*
|
*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`).*
|
*Tickets #103 à #117 = périmètre « Validation des nouveaux comptes par le gestionnaire » (voir plan de spec).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -641,17 +641,18 @@ Modifier l'endpoint de connexion pour bloquer les comptes en attente ou suspendu
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Ticket #31 : [Backend] Changement MDP obligatoire première connexion
|
### Ticket #31 : [Backend] Changement MDP obligatoire première connexion ✅
|
||||||
**Estimation** : 2h
|
**Estimation** : 2h
|
||||||
**Labels** : `backend`, `p2`, `auth`, `security`
|
**Labels** : `backend`, `p2`, `auth`, `security`
|
||||||
|
**Statut** : ✅ TERMINÉ
|
||||||
|
|
||||||
**Description** :
|
**Description** :
|
||||||
Implémenter le changement de mot de passe obligatoire pour les gestionnaires/admins à la première connexion.
|
Implémenter le changement de mot de passe obligatoire pour les gestionnaires/admins à la première connexion.
|
||||||
|
|
||||||
**Tâches** :
|
**Tâches** :
|
||||||
- [ ] Endpoint `POST /api/v1/auth/change-password-required`
|
- [x] Endpoint `POST /api/v1/auth/change-password-required`
|
||||||
- [ ] Vérification flag `changement_mdp_obligatoire`
|
- [x] Vérification flag `changement_mdp_obligatoire`
|
||||||
- [ ] Mise à jour flag après changement
|
- [x] Mise à jour flag après changement
|
||||||
- [ ] Tests unitaires
|
- [ ] Tests unitaires
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -15,18 +15,9 @@ if [ -z "$GITEA_TOKEN" ]; then
|
|||||||
GITEA_TOKEN=$(cat .gitea-token)
|
GITEA_TOKEN=$(cat .gitea-token)
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -z "$GITEA_TOKEN" ] && [ -f ~/.bashrc ]; then
|
|
||||||
eval "$(grep '^export GITEA_TOKEN=' ~/.bashrc 2>/dev/null)" || true
|
|
||||||
fi
|
|
||||||
if [ -z "$GITEA_TOKEN" ] && [ -f docs/BRIEFING-FRONTEND.md ]; then
|
|
||||||
token_from_briefing=$(sed -n 's/.*Token: *\(giteabu_[a-f0-9]*\).*/\1/p' docs/BRIEFING-FRONTEND.md 2>/dev/null | head -1)
|
|
||||||
if [ -n "$token_from_briefing" ]; then
|
|
||||||
GITEA_TOKEN="$token_from_briefing"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$GITEA_TOKEN" ]; then
|
if [ -z "$GITEA_TOKEN" ]; then
|
||||||
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea (voir docs/PROCEDURE-API-GITEA.md)."
|
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user