Compare commits
No commits in common. "0e6e473e84c93f788138d5eaa2721d105fd6a897" and "f6fabc521e6fca1d49306c97fb1da0e6d1de047f" have entirely different histories.
0e6e473e84
...
f6fabc521e
@ -17,7 +17,6 @@ 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: [
|
||||||
@ -56,7 +55,6 @@ import { DossiersModule } from './routes/dossiers/dossiers.module';
|
|||||||
AppConfigModule,
|
AppConfigModule,
|
||||||
DocumentsLegauxModule,
|
DocumentsLegauxModule,
|
||||||
RelaisModule,
|
RelaisModule,
|
||||||
DossiersModule,
|
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
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,28 +1,4 @@
|
|||||||
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 {}
|
||||||
|
|||||||
@ -1,81 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { StatutUtilisateurType } from 'src/entities/users.entity';
|
import { StatutUtilisateurType } from 'src/entities/users.entity';
|
||||||
import { StatutEnfantType, GenreType } from 'src/entities/children.entity';
|
import { StatutDossierType } from 'src/entities/dossiers.entity';
|
||||||
|
import { StatutEnfantType } from 'src/entities/children.entity';
|
||||||
|
|
||||||
/** Parent dans le dossier famille (infos utilisateur + parent) */
|
/** Parent dans le dossier famille (infos utilisateur + parent) */
|
||||||
export class DossierFamilleParentDto {
|
export class DossierFamilleParentDto {
|
||||||
@ -14,12 +15,6 @@ export class DossierFamilleParentDto {
|
|||||||
nom?: string;
|
nom?: string;
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
telephone?: string;
|
telephone?: string;
|
||||||
@ApiProperty({ required: false })
|
|
||||||
adresse?: string;
|
|
||||||
@ApiProperty({ required: false })
|
|
||||||
ville?: string;
|
|
||||||
@ApiProperty({ required: false })
|
|
||||||
code_postal?: string;
|
|
||||||
@ApiProperty({ enum: StatutUtilisateurType })
|
@ApiProperty({ enum: StatutUtilisateurType })
|
||||||
statut: StatutUtilisateurType;
|
statut: StatutUtilisateurType;
|
||||||
@ApiProperty({ required: false, description: 'Id du co-parent si couple' })
|
@ApiProperty({ required: false, description: 'Id du co-parent si couple' })
|
||||||
@ -34,8 +29,6 @@ export class DossierFamilleEnfantDto {
|
|||||||
first_name?: string;
|
first_name?: string;
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
last_name?: string;
|
last_name?: string;
|
||||||
@ApiProperty({ required: false, enum: GenreType })
|
|
||||||
genre?: GenreType;
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
birth_date?: Date;
|
birth_date?: Date;
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
@ -44,6 +37,26 @@ export class DossierFamilleEnfantDto {
|
|||||||
status: StatutEnfantType;
|
status: StatutEnfantType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Dossier (parent+enfant) avec presentation */
|
||||||
|
export class DossierFamillePresentationDto {
|
||||||
|
@ApiProperty()
|
||||||
|
id: string;
|
||||||
|
@ApiProperty()
|
||||||
|
id_parent: string;
|
||||||
|
@ApiProperty()
|
||||||
|
id_enfant: string;
|
||||||
|
@ApiProperty({ required: false, description: 'Texte de présentation' })
|
||||||
|
presentation?: string;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
type_contrat?: string;
|
||||||
|
@ApiProperty()
|
||||||
|
repas: boolean;
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
budget?: number;
|
||||||
|
@ApiProperty({ enum: StatutDossierType })
|
||||||
|
statut: StatutDossierType;
|
||||||
|
}
|
||||||
|
|
||||||
/** Réponse GET /parents/dossier-famille/:numeroDossier – dossier famille complet. Ticket #119 */
|
/** Réponse GET /parents/dossier-famille/:numeroDossier – dossier famille complet. Ticket #119 */
|
||||||
export class DossierFamilleCompletDto {
|
export class DossierFamilleCompletDto {
|
||||||
@ApiProperty({ example: '2026-000001', description: 'Numéro de dossier famille' })
|
@ApiProperty({ example: '2026-000001', description: 'Numéro de dossier famille' })
|
||||||
@ -52,6 +65,6 @@ export class DossierFamilleCompletDto {
|
|||||||
parents: DossierFamilleParentDto[];
|
parents: DossierFamilleParentDto[];
|
||||||
@ApiProperty({ type: [DossierFamilleEnfantDto], description: 'Enfants de la famille' })
|
@ApiProperty({ type: [DossierFamilleEnfantDto], description: 'Enfants de la famille' })
|
||||||
enfants: DossierFamilleEnfantDto[];
|
enfants: DossierFamilleEnfantDto[];
|
||||||
@ApiProperty({ required: false, description: 'Texte de présentation / motivation (un seul par famille)' })
|
@ApiProperty({ type: [DossierFamillePresentationDto], description: 'Dossiers (présentation par parent/enfant)' })
|
||||||
texte_motivation?: string;
|
presentation: DossierFamillePresentationDto[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,4 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class ParentPendingSummaryDto {
|
|
||||||
@ApiProperty({ description: 'UUID utilisateur' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
email: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ nullable: true })
|
|
||||||
telephone?: string | null;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ nullable: true })
|
|
||||||
code_postal?: string | null;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ nullable: true })
|
|
||||||
ville?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PendingFamilyDto {
|
export class PendingFamilyDto {
|
||||||
@ApiProperty({ example: 'Famille Dupont', description: 'Libellé affiché pour la famille' })
|
@ApiProperty({ example: 'Famille Dupont', description: 'Libellé affiché pour la famille' })
|
||||||
@ -34,30 +17,4 @@ export class PendingFamilyDto {
|
|||||||
description: 'Numéro de dossier famille (format AAAA-NNNNNN)',
|
description: 'Numéro de dossier famille (format AAAA-NNNNNN)',
|
||||||
})
|
})
|
||||||
numero_dossier: string | null;
|
numero_dossier: string | null;
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
nullable: true,
|
|
||||||
example: '2026-01-12T10:00:00.000Z',
|
|
||||||
description: 'Date de référence dossier soumis / en attente : MIN(cree_le) des parents en_attente du groupe (ISO 8601)',
|
|
||||||
})
|
|
||||||
date_soumission: string | null;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
example: 3,
|
|
||||||
description: 'Nombre d’enfants distincts liés aux parents de la famille (enfants_parents)',
|
|
||||||
})
|
|
||||||
nombre_enfants: number;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
type: [String],
|
|
||||||
example: ['parent1@example.com', 'parent2@example.com'],
|
|
||||||
description: 'Emails des parents du groupe (ordre stable : nom, prénom)',
|
|
||||||
})
|
|
||||||
emails?: string[];
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
type: [ParentPendingSummaryDto],
|
|
||||||
description: 'Résumé des parents (ordre stable, aligné sur parentIds/emails)',
|
|
||||||
})
|
|
||||||
parents?: ParentPendingSummaryDto[];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export class ParentsController {
|
|||||||
@Get('pending-families')
|
@Get('pending-families')
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE)
|
||||||
@ApiOperation({ summary: 'Liste des familles en attente (une entrée par famille)' })
|
@ApiOperation({ summary: 'Liste des familles en attente (une entrée par famille)' })
|
||||||
@ApiResponse({ status: 200, description: 'Liste des familles (libellé, parentIds, numero_dossier, date_soumission, nombre_enfants, emails)', type: [PendingFamilyDto] })
|
@ApiResponse({ status: 200, description: 'Liste des familles (libellé, parentIds, numero_dossier)', type: [PendingFamilyDto] })
|
||||||
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
||||||
getPendingFamilies(): Promise<PendingFamilyDto[]> {
|
getPendingFamilies(): Promise<PendingFamilyDto[]> {
|
||||||
return this.parentsService.getPendingFamilies();
|
return this.parentsService.getPendingFamilies();
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
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';
|
||||||
@ -11,7 +10,7 @@ import { UserModule } from '../user/user.module';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Parents, Users, DossierFamille, DossierFamilleEnfant]),
|
TypeOrmModule.forFeature([Parents, Users]),
|
||||||
forwardRef(() => UserModule),
|
forwardRef(() => UserModule),
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { In, 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';
|
||||||
@ -16,6 +15,7 @@ import {
|
|||||||
DossierFamilleCompletDto,
|
DossierFamilleCompletDto,
|
||||||
DossierFamilleParentDto,
|
DossierFamilleParentDto,
|
||||||
DossierFamilleEnfantDto,
|
DossierFamilleEnfantDto,
|
||||||
|
DossierFamillePresentationDto,
|
||||||
} from './dto/dossier-famille-complet.dto';
|
} from './dto/dossier-famille-complet.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -25,8 +25,6 @@ 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
|
||||||
@ -87,15 +85,7 @@ 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[]> {
|
||||||
let raw: {
|
let raw: { libelle: string; parentIds: unknown; numero_dossier: string | null }[];
|
||||||
libelle: string;
|
|
||||||
parentIds: unknown;
|
|
||||||
numero_dossier: string | null;
|
|
||||||
date_soumission: Date | string | null;
|
|
||||||
nombre_enfants: string | number | null;
|
|
||||||
emails: unknown;
|
|
||||||
parents: unknown;
|
|
||||||
}[];
|
|
||||||
try {
|
try {
|
||||||
raw = await this.parentsRepository.query(`
|
raw = await this.parentsRepository.query(`
|
||||||
WITH RECURSIVE
|
WITH RECURSIVE
|
||||||
@ -122,27 +112,8 @@ export class ParentsService {
|
|||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
'Famille ' || string_agg(u.nom, ' - ' ORDER BY u.nom, u.prenom) AS libelle,
|
'Famille ' || string_agg(u.nom, ' - ' ORDER BY u.nom, u.prenom) AS libelle,
|
||||||
array_agg(p.id_utilisateur ORDER BY u.nom, u.prenom, u.id) AS "parentIds",
|
array_agg(DISTINCT p.id_utilisateur ORDER BY p.id_utilisateur) AS "parentIds",
|
||||||
(array_agg(p.numero_dossier))[1] AS numero_dossier,
|
(array_agg(p.numero_dossier))[1] AS numero_dossier
|
||||||
MIN(u.cree_le) AS date_soumission,
|
|
||||||
COALESCE((
|
|
||||||
SELECT COUNT(DISTINCT ep.id_enfant)::int
|
|
||||||
FROM enfants_parents ep
|
|
||||||
WHERE ep.id_parent IN (
|
|
||||||
SELECT frx.id FROM family_rep frx WHERE frx.rep = fr.rep
|
|
||||||
)
|
|
||||||
), 0) AS nombre_enfants,
|
|
||||||
array_agg(u.email ORDER BY u.nom, u.prenom, u.id) AS emails,
|
|
||||||
json_agg(
|
|
||||||
json_build_object(
|
|
||||||
'id', u.id::text,
|
|
||||||
'email', u.email,
|
|
||||||
'telephone', u.telephone,
|
|
||||||
'code_postal', u.code_postal,
|
|
||||||
'ville', u.ville
|
|
||||||
)
|
|
||||||
ORDER BY u.nom, u.prenom, u.id
|
|
||||||
) AS parents
|
|
||||||
FROM family_rep fr
|
FROM family_rep fr
|
||||||
JOIN parents p ON p.id_utilisateur = fr.id
|
JOIN parents p ON p.id_utilisateur = fr.id
|
||||||
JOIN utilisateurs u ON u.id = p.id_utilisateur
|
JOIN utilisateurs u ON u.id = p.id_utilisateur
|
||||||
@ -158,56 +129,9 @@ export class ParentsService {
|
|||||||
libelle: r.libelle ?? '',
|
libelle: r.libelle ?? '',
|
||||||
parentIds: this.normalizeParentIds(r.parentIds),
|
parentIds: this.normalizeParentIds(r.parentIds),
|
||||||
numero_dossier: r.numero_dossier ?? null,
|
numero_dossier: r.numero_dossier ?? null,
|
||||||
date_soumission: this.toIsoDateTimeOrNull(r.date_soumission),
|
|
||||||
nombre_enfants: this.normalizeNombreEnfants(r.nombre_enfants),
|
|
||||||
emails: this.normalizeEmails(r.emails),
|
|
||||||
parents: this.normalizeParents(r.parents),
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private toIsoDateTimeOrNull(value: Date | string | null | undefined): string | null {
|
|
||||||
if (value == null) return null;
|
|
||||||
if (value instanceof Date) return value.toISOString();
|
|
||||||
const d = new Date(value);
|
|
||||||
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeNombreEnfants(v: string | number | null | undefined): number {
|
|
||||||
if (v == null) return 0;
|
|
||||||
const n = typeof v === 'number' ? v : parseInt(String(v), 10);
|
|
||||||
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeEmails(emails: unknown): string[] {
|
|
||||||
if (Array.isArray(emails)) return emails.map(String);
|
|
||||||
if (typeof emails === 'string') {
|
|
||||||
const s = emails.replace(/^\{|\}$/g, '').trim();
|
|
||||||
return s ? s.split(',').map((x) => x.trim()) : [];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeParents(parents: unknown): { id: string; email: string; telephone: string | null; code_postal: string | null; ville: string | null }[] {
|
|
||||||
if (Array.isArray(parents)) {
|
|
||||||
return parents.map((p: any) => ({
|
|
||||||
id: String(p?.id ?? ''),
|
|
||||||
email: String(p?.email ?? ''),
|
|
||||||
telephone: p?.telephone != null ? String(p.telephone) : null,
|
|
||||||
code_postal: p?.code_postal != null ? String(p.code_postal) : null,
|
|
||||||
ville: p?.ville != null ? String(p.ville) : null,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (typeof parents === 'string') {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(parents);
|
|
||||||
return this.normalizeParents(parsed);
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convertit parentIds (array ou chaîne PG) en string[] pour éviter 500 si le driver renvoie une chaîne. */
|
/** Convertit parentIds (array ou chaîne PG) en string[] pour éviter 500 si le driver renvoie une chaîne. */
|
||||||
private normalizeParentIds(parentIds: unknown): string[] {
|
private normalizeParentIds(parentIds: unknown): string[] {
|
||||||
if (Array.isArray(parentIds)) return parentIds.map(String);
|
if (Array.isArray(parentIds)) return parentIds.map(String);
|
||||||
@ -241,17 +165,7 @@ export class ParentsService {
|
|||||||
relations: ['user', 'co_parent', 'parentChildren', 'parentChildren.child', 'dossiers', 'dossiers.child'],
|
relations: ['user', 'co_parent', 'parentChildren', 'parentChildren.child', 'dossiers', 'dossiers.child'],
|
||||||
});
|
});
|
||||||
const enfantsMap = new Map<string, DossierFamilleEnfantDto>();
|
const enfantsMap = new Map<string, DossierFamilleEnfantDto>();
|
||||||
let texte_motivation: string | undefined;
|
const presentationList: DossierFamillePresentationDto[] = [];
|
||||||
|
|
||||||
// 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) {
|
for (const p of parents) {
|
||||||
// Enfants via parentChildren
|
// Enfants via parentChildren
|
||||||
if (p.parentChildren) {
|
if (p.parentChildren) {
|
||||||
@ -261,7 +175,6 @@ export class ParentsService {
|
|||||||
id: pc.child.id,
|
id: pc.child.id,
|
||||||
first_name: pc.child.first_name,
|
first_name: pc.child.first_name,
|
||||||
last_name: pc.child.last_name,
|
last_name: pc.child.last_name,
|
||||||
genre: pc.child.gender,
|
|
||||||
birth_date: pc.child.birth_date,
|
birth_date: pc.child.birth_date,
|
||||||
due_date: pc.child.due_date,
|
due_date: pc.child.due_date,
|
||||||
status: pc.child.status,
|
status: pc.child.status,
|
||||||
@ -269,21 +182,28 @@ export class ParentsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback : anciens dossiers (un texte, on prend le premier)
|
// Dossiers (présentation)
|
||||||
if (texte_motivation == null && p.dossiers?.length) {
|
if (p.dossiers) {
|
||||||
texte_motivation = p.dossiers[0].presentation ?? undefined;
|
for (const d of p.dossiers) {
|
||||||
|
presentationList.push({
|
||||||
|
id: d.id,
|
||||||
|
id_parent: p.user_id,
|
||||||
|
id_enfant: d.child?.id ?? '',
|
||||||
|
presentation: d.presentation,
|
||||||
|
type_contrat: d.type_contrat,
|
||||||
|
repas: d.meals,
|
||||||
|
budget: d.budget != null ? Number(d.budget) : undefined,
|
||||||
|
statut: d.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentsDto: DossierFamilleParentDto[] = parents.map((p) => ({
|
const parentsDto: DossierFamilleParentDto[] = parents.map((p) => ({
|
||||||
user_id: p.user_id,
|
user_id: p.user_id,
|
||||||
email: p.user.email,
|
email: p.user.email,
|
||||||
prenom: p.user.prenom,
|
prenom: p.user.prenom,
|
||||||
nom: p.user.nom,
|
nom: p.user.nom,
|
||||||
telephone: p.user.telephone,
|
telephone: p.user.telephone,
|
||||||
adresse: p.user.adresse,
|
|
||||||
ville: p.user.ville,
|
|
||||||
code_postal: p.user.code_postal,
|
|
||||||
statut: p.user.statut,
|
statut: p.user.statut,
|
||||||
co_parent_id: p.co_parent?.id,
|
co_parent_id: p.co_parent?.id,
|
||||||
}));
|
}));
|
||||||
@ -291,7 +211,7 @@ export class ParentsService {
|
|||||||
numero_dossier: num,
|
numero_dossier: num,
|
||||||
parents: parentsDto,
|
parents: parentsDto,
|
||||||
enfants: Array.from(enfantsMap.values()),
|
enfants: Array.from(enfantsMap.values()),
|
||||||
texte_motivation,
|
presentation: presentationList,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
-- 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);
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
Loading…
x
Reference in New Issue
Block a user