diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 0197cd2..ce8dbc2 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -17,6 +17,7 @@ import { EnfantsModule } from './routes/enfants/enfants.module'; import { AppConfigModule } from './modules/config/config.module'; import { DocumentsLegauxModule } from './modules/documents-legaux'; import { RelaisModule } from './routes/relais/relais.module'; +import { DossiersModule } from './routes/dossiers/dossiers.module'; @Module({ imports: [ @@ -55,6 +56,7 @@ import { RelaisModule } from './routes/relais/relais.module'; AppConfigModule, DocumentsLegauxModule, RelaisModule, + DossiersModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/routes/dossiers/dossiers.controller.ts b/backend/src/routes/dossiers/dossiers.controller.ts new file mode 100644 index 0000000..2f3f677 --- /dev/null +++ b/backend/src/routes/dossiers/dossiers.controller.ts @@ -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 { + return this.dossiersService.getDossierByNumero(numeroDossier); + } +} diff --git a/backend/src/routes/dossiers/dossiers.module.ts b/backend/src/routes/dossiers/dossiers.module.ts index 66026d2..e6ba196 100644 --- a/backend/src/routes/dossiers/dossiers.module.ts +++ b/backend/src/routes/dossiers/dossiers.module.ts @@ -1,4 +1,28 @@ 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 {} diff --git a/backend/src/routes/dossiers/dossiers.service.ts b/backend/src/routes/dossiers/dossiers.service.ts new file mode 100644 index 0000000..59d9f47 --- /dev/null +++ b/backend/src/routes/dossiers/dossiers.service.ts @@ -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, + @InjectRepository(AssistanteMaternelle) + private readonly amRepository: Repository, + private readonly parentsService: ParentsService, + ) {} + + async getDossierByNumero(numeroDossier: string): Promise { + 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, + }; + } +} diff --git a/backend/src/routes/dossiers/dto/dossier-am-complet.dto.ts b/backend/src/routes/dossiers/dto/dossier-am-complet.dto.ts new file mode 100644 index 0000000..0ddc8a1 --- /dev/null +++ b/backend/src/routes/dossiers/dto/dossier-am-complet.dto.ts @@ -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; +} diff --git a/backend/src/routes/dossiers/dto/dossier-unifie.dto.ts b/backend/src/routes/dossiers/dto/dossier-unifie.dto.ts new file mode 100644 index 0000000..a51ae1b --- /dev/null +++ b/backend/src/routes/dossiers/dto/dossier-unifie.dto.ts @@ -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; +}