diff --git a/src/app.module.ts b/src/app.module.ts index dcb4e79..d42393d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @@ -8,6 +8,12 @@ import databaseConfig from './config/database.config'; import jwtConfig from './config/jwt.config'; import { configValidationSchema } from './config/validation.schema'; import { AuthModule } from './routes/auth/auth.module'; +import { UserModule } from './routes/user/user.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AllExceptionsFilter } from './common/filters/all_exceptions.filters'; +import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; +import { TransformInterceptor } from './common/interceptors/transform.interceptor'; +import { RolesGuard } from './common/guards/roles.guard'; @Module({ imports: [ @@ -21,9 +27,30 @@ import { AuthModule } from './routes/auth/auth.module'; isGlobal: true, validationSchema: configValidationSchema, }), + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + type: 'postgres', + host: config.get('database.host'), + port: config.get('database.port'), + username: config.get('database.username'), + password: config.get('database.password'), + database: config.get('database.name'), + entities: [__dirname + '/**/*.entity{.ts,.js}'], + synchronize: false, + migrations: [__dirname + '/migrations/**/*{.ts,.js}'], + logging: true, + }), + }), AuthModule, + UserModule, ], controllers: [AppController], - providers: [AppService], + providers: [AppService, + { provide: APP_FILTER, useClass: AllExceptionsFilter }, + { provide: APP_INTERCEPTOR, useClass: TransformInterceptor }, + { provide: APP_GUARD, useClass: RolesGuard } + ], }) export class AppModule {} diff --git a/src/common/base.controller.ts b/src/common/base.controller.ts new file mode 100644 index 0000000..d1d3224 --- /dev/null +++ b/src/common/base.controller.ts @@ -0,0 +1,33 @@ +import { Body, Delete, Get, Param, Patch, Post } from "@nestjs/common"; +import { BaseService } from "./base.service"; +import type { DeepPartial, ObjectLiteral } from "typeorm"; +import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity.js"; + +export class BaseController { + constructor(protected readonly service: BaseService) { } + + @Get() + getAll(relations: string[] = []) { + return this.service.findAll(relations); + } + + @Get(':id') + getOne(@Param('id') id: string) { + return this.service.findOne(id); + } + + @Post() + create(@Body() data: DeepPartial) { + return this.service.create(data); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() data: QueryDeepPartialEntity) { + return this.service.update(id, data); + } + + @Delete(':id') + delete(@Param('id') id: string) { + return this.service.delete(id); + } +} \ No newline at end of file diff --git a/src/common/base.service.ts b/src/common/base.service.ts new file mode 100644 index 0000000..90917b4 --- /dev/null +++ b/src/common/base.service.ts @@ -0,0 +1,28 @@ +import { DeepPartial, ObjectLiteral, Repository } from "typeorm"; +import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity.js"; + +export class BaseService { + constructor(protected readonly repo: Repository) { } + + findAll(relations: string[] = []) { + return this.repo.find({ relations }); + } + + findOne(id: string, relations: string[] = []) { + return this.repo.findOne({ where: { id } as any, relations }); + } + + create(data: DeepPartial) { + const entity = this.repo.create(data); + return this.repo.save(entity); + } + + async update(id: string, data: QueryDeepPartialEntity) { + await this.repo.update(id, data); + return this.findOne(id); + } + + delete(id: string) { + return this.repo.delete(id); + } +} \ No newline at end of file diff --git a/src/common/decorators/roles.decorator.ts b/src/common/decorators/roles.decorator.ts new file mode 100644 index 0000000..b1efe4b --- /dev/null +++ b/src/common/decorators/roles.decorator.ts @@ -0,0 +1,3 @@ +import { SetMetadata } from "@nestjs/common"; + +export const Roles = (...roles: string[]) => SetMetadata("roles", roles); diff --git a/src/common/decorators/user.decorator.ts b/src/common/decorators/user.decorator.ts new file mode 100644 index 0000000..efbca73 --- /dev/null +++ b/src/common/decorators/user.decorator.ts @@ -0,0 +1,7 @@ +import { createParamDecorator, ExecutionContext } from "@nestjs/common"; + +export const User = createParamDecorator((data: string | undefined, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const user = request.user; + return data ? user?.[data] : user; +}); diff --git a/src/common/dto/date_range_query.dto.ts b/src/common/dto/date_range_query.dto.ts new file mode 100644 index 0000000..973e4aa --- /dev/null +++ b/src/common/dto/date_range_query.dto.ts @@ -0,0 +1,11 @@ +import { IsDateString, IsOptional } from "class-validator"; + +export class DateRangeQueryDto { + @IsOptional() + @IsDateString() + start?: string; + + @IsOptional() + @IsDateString() + end?: string; +} diff --git a/src/common/dto/id_param.dto.ts b/src/common/dto/id_param.dto.ts new file mode 100644 index 0000000..f632953 --- /dev/null +++ b/src/common/dto/id_param.dto.ts @@ -0,0 +1,6 @@ +import { IsUUID } from "class-validator"; + +export class IdParamDto { + @IsUUID() + id: string; +} diff --git a/src/common/dto/pagination.query.ts b/src/common/dto/pagination.query.ts new file mode 100644 index 0000000..371e358 --- /dev/null +++ b/src/common/dto/pagination.query.ts @@ -0,0 +1,11 @@ +import { IsOptional, IsPositive } from "class-validator"; + +export class PaginationQueryDto { + @IsOptional() + @IsPositive() + offset?: number; + + @IsOptional() + @IsPositive() + limit?: number; +} \ No newline at end of file diff --git a/src/common/dto/search_query.dto.ts b/src/common/dto/search_query.dto.ts new file mode 100644 index 0000000..b256af1 --- /dev/null +++ b/src/common/dto/search_query.dto.ts @@ -0,0 +1,8 @@ +import { IsOptional, IsString, MinLength } from "class-validator"; + +export class SearchQueryDto { + @IsOptional() + @IsString() + @MinLength(2) + q?: string; +} diff --git a/src/common/filters/all_exceptions.filters.ts b/src/common/filters/all_exceptions.filters.ts new file mode 100644 index 0000000..4d46609 --- /dev/null +++ b/src/common/filters/all_exceptions.filters.ts @@ -0,0 +1,27 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common"; + +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.getResponse() + : { message: 'Internal server error' }; + + response.status(status).json({ + success: false, + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + message, + }); + } +} \ No newline at end of file diff --git a/src/common/guards/roles.guard.ts b/src/common/guards/roles.guard.ts new file mode 100644 index 0000000..6df212e --- /dev/null +++ b/src/common/guards/roles.guard.ts @@ -0,0 +1,21 @@ +import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { Observable } from "rxjs"; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + canActivate(context: ExecutionContext): boolean | Promise | Observable { + const requiredRoles = this.reflector.get('roles', context.getHandler()); + if (!requiredRoles || requiredRoles.length === 0) { + return true; // Si aucun role est requis -> accès autorise + } + + const request = context.switchToHttp().getRequest(); + const user = request.user; + if (!user || !user.role) { + return false; // Si l'utilisateur est pas authentifie ou a pas de role -> accès refusé + } + return requiredRoles.includes(user.role); + } +} \ No newline at end of file diff --git a/src/common/interceptors/transform.interceptor.ts b/src/common/interceptors/transform.interceptor.ts new file mode 100644 index 0000000..300de4a --- /dev/null +++ b/src/common/interceptors/transform.interceptor.ts @@ -0,0 +1,15 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; +import { map, Observable, timestamp } from "rxjs"; + +@Injectable() +export class TransformInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map((data) => ({ + success: true, + timestamp: new Date().toISOString(), + data + })), + ); + } +} \ No newline at end of file diff --git a/src/entities/assistantes_maternelles.entity.ts b/src/entities/assistantes_maternelles.entity.ts index 7dfb7ad..36499a1 100644 --- a/src/entities/assistantes_maternelles.entity.ts +++ b/src/entities/assistantes_maternelles.entity.ts @@ -1,41 +1,42 @@ -import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from "typeorm"; -import { Users } from "./user.entity"; +import { + Entity, PrimaryColumn, Column, OneToOne, JoinColumn +} from 'typeorm'; +import { Users } from './users.entity'; @Entity('assistantes_maternelles') export class AssistanteMaternelle { - // Declarer les proprietes ici - @PrimaryColumn('uuid') - user_id: string; + // PK = FK vers utilisateurs.id + @PrimaryColumn('uuid', { name: 'id_utilisateur' }) + user_id: string; - @OneToOne(() => Users, user => user.assistanteMaternelle, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'user_id' }) - user: Users; + @OneToOne(() => Users, user => user.assistanteMaternelle, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user: Users; - @Column({type: 'varchar'}) - approval_number: string; - - @Column({type: 'date'}) - birthdate: Date; + @Column({ name: 'numero_agrement', length: 50, nullable: true }) + approval_number?: string; - @Column({type: 'varchar'}) - birthplace_city: string; + @Column({ name: 'date_naissance', type: 'date', nullable: true }) + birthdate?: Date; - @Column({type: 'varchar'}) - birthplace_country: string; + @Column({ name: 'ville_naissance', length: 100, nullable: true }) + birthplace_city?: string; - @Column({type: 'text'}) - nir_encrypted: string; + @Column({ name: 'pays_naissance', length: 2, nullable: true }) + birthplace_country?: string; - @Column({type: 'int'}) - max_children: number; + @Column({ name: 'nir_chiffre', length: 15, nullable: true }) + nir?: string; - @Column({type: 'text'}) - bio: string; + @Column({ name: 'nb_max_enfants', type: 'int', nullable: true }) + max_children?: number; - @Column({type: 'boolean', default: true}) - is_available: boolean; + @Column({ name: 'biographie', type: 'text', nullable: true }) + biography?: string; - @Column({type: 'varchar'}) - city: string; + @Column({ name: 'disponible', type: 'boolean', default: true }) + available: boolean; -} \ No newline at end of file + @Column({ name: 'ville_residence', length: 100, nullable: true }) + city?: string; +} diff --git a/src/entities/avenants_contrats.entity.ts b/src/entities/avenants_contrats.entity.ts new file mode 100644 index 0000000..bb3725f --- /dev/null +++ b/src/entities/avenants_contrats.entity.ts @@ -0,0 +1,42 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; +import { Contrat } from "./contrats.entity"; +import { Users } from "./users.entity"; + +export enum StatutAvenantType { + PROPOSE = 'propose', + ACCEPTE = 'accepte', + REFUSE = 'refuse', +} + +@Entity('avenants_contrats') +export class AvenantContrat { + // Define your columns and relationships here + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Contrat, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_contrat' }) + contrat: Contrat; + + @Column({ type: 'jsonb', nullable: true, name: 'modifications' }) + modifications?: any; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'initie_par', referencedColumnName: 'id' }) + initiator?: Users; + + @Column({ + type: 'enum', + enum: StatutAvenantType, + enumName: 'statut_avenant_type', + default: StatutAvenantType.PROPOSE, + name: 'statut' + }) + statut: StatutAvenantType; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/entities/children.entity.ts b/src/entities/children.entity.ts index 5218878..645c3d9 100644 --- a/src/entities/children.entity.ts +++ b/src/entities/children.entity.ts @@ -1,60 +1,74 @@ -import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryColumn, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; -import { Parents } from "./parents.entity"; -import { ParentsChildren } from "./parents_children.entity"; -import { Dossier } from "./dossiers.entity"; +import { + Entity, PrimaryGeneratedColumn, Column, + OneToMany, ManyToMany, CreateDateColumn, JoinTable +} from 'typeorm'; +import { Parents } from './parents.entity'; +import { ParentsChildren } from './parents_children.entity'; +import { Dossier } from './dossiers.entity'; -export enum ChildStatus { - A_NAITRE = 'A_NAÎTRE', - ACTIF = 'ACTIF', - SCOLARISE = 'SCOLARISE' +export enum StatutEnfantType { + A_NAITRE = 'a_naitre', + ACTIF = 'actif', + SCOLARISE = 'scolarise', } -@Entity('children') +export enum GenreType { + H = 'H', + F = 'F', + AUTRE = 'Autre', +} + +@Entity('enfants') export class Children { + @PrimaryGeneratedColumn('uuid') + id: string; - @PrimaryGeneratedColumn('uuid') - id: string; + @Column({ + type: 'enum', + enum: StatutEnfantType, + enumName: 'statut_enfant_type', + name: 'statut' + }) + status: StatutEnfantType; - @Column({ nullable: true }) - first_name: string; + @Column({ name: 'prenom', length: 100 }) + first_name: string; - @Column({ nullable: true }) - last_name: string; + @Column({ name: 'nom', length: 100 }) + last_name: string; - @Column({ type: 'date', nullable: true }) - birthdate: Date; + @Column({ + type: 'enum', + enum: GenreType, + enumName: 'genre_type', + nullable: true, + name: 'genre' + }) + gender?: GenreType; - @Column({ type: 'date', nullable: true }) - due_date: Date; + @Column({ type: 'date', nullable: true, name: 'date_naissance' }) + birthdate?: Date; - @Column({ nullable: true }) - photo_url: string; + @Column({ type: 'date', nullable: true, name: 'date_prevue_naissance' }) + due_date?: Date; - @Column({ nullable: true }) - consent_photo: boolean; + @Column({ nullable: true, name: 'photo_url' }) + photo_url?: string; - @Column({ type: 'timestamp', nullable: true }) - consent_photo_at: Date; + @Column({ default: false, name: 'consentement_photo' }) + consent_photo: boolean; - @Column({nullable: true }) - is_multiple: boolean + @Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' }) + consent_photo_at?: Date; - @Column({ - type: 'enum', - enum: ChildStatus, - default: ChildStatus.A_NAITRE, - }) - status: ChildStatus; + @Column({ default: false, name: 'est_multiple' }) + is_multiple: boolean; - @CreateDateColumn() - created_at: Date; + // Lien via table de jointure enfants_parents + @OneToMany(() => ParentsChildren, pc => pc.child) + parentLinks: ParentsChildren[]; - @UpdateDateColumn() - updated_at: Date; - - @OneToMany(() => ParentsChildren, pc => pc.child, { onDelete: 'CASCADE' }) - parentChildren: ParentsChildren[]; - - @OneToMany(() => Dossier, d => d.child) - dossiers: Dossier[]; + // Relation avec Dossier + @OneToMany(() => Dossier, d => d.child) + dossiers: Dossier[]; } diff --git a/src/entities/contrats.entity.ts b/src/entities/contrats.entity.ts new file mode 100644 index 0000000..f6428bd --- /dev/null +++ b/src/entities/contrats.entity.ts @@ -0,0 +1,53 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; +import { Dossier } from "./dossiers.entity"; + +export enum StatutContratType { + BROUILLON = 'brouillon', + EN_ATTENTE_SIGNATURE = 'en_attente_signature', + VALIDE = 'valide', + RESILIE = 'resilie', +} + +@Entity('contrats') +export class Contrat { + // Define your columns and relationships here + + @PrimaryGeneratedColumn('uuid') + id: string; + + @OneToOne(() => Dossier, {onDelete: 'CASCADE'} ) + @JoinColumn({ name: 'id_dossier'}) + dossier: Dossier; + + @Column({type: 'jsonb', nullable: true, name: 'planning'}) + planning?: any; + + @Column({type: 'numeric', precision: 6, scale: 2, nullable: true, name: 'tarif_horaire'}) + hourly_rate?: string; + + @Column({type: 'numeric', precision: 6, scale: 2, nullable: true, name: 'indemnites_repas'}) + meal_indemnity?: string; + + @Column({ + type: 'enum', + enum: StatutContratType, + default: StatutContratType.BROUILLON, + name: 'statut' + }) + statut: StatutContratType; + + @Column({type: 'boolean', default: false, name: 'signe_parent'}) + signed_by_parent: boolean; + + @Column({type: 'boolean', default: false, name: 'signe_am'}) + signed_by_am: boolean; + + @Column({type: 'timestamptz', nullable: true, name: 'finalise_le'}) + finalized_at?: Date; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + updated_at: Date; +} diff --git a/src/entities/dossiers.entity.ts b/src/entities/dossiers.entity.ts index 3d29d5a..bca56f4 100644 --- a/src/entities/dossiers.entity.ts +++ b/src/entities/dossiers.entity.ts @@ -1,41 +1,60 @@ -import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; -import { Parents } from "./parents.entity"; -import { Children } from "./children.entity"; +import { + Entity, PrimaryGeneratedColumn, Column, + ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn, JoinColumn +} from 'typeorm'; +import { Parents } from './parents.entity'; +import { Children } from './children.entity'; +import { Message } from './messages.entity'; + +export enum StatutDossierType { + ENVOYE = 'envoye', + ACCEPTE = 'accepte', + REFUSE = 'refuse', +} @Entity('dossiers') export class Dossier { - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryGeneratedColumn('uuid') + id: string; - @ManyToOne(() => Parents, p => p.dossiers, {onDelete: 'CASCADE'} ) - @JoinColumn({ name: 'parent_id', referencedColumnName: 'user_id' }) - parent: Parents; + @ManyToOne(() => Parents, p => p.dossiers, { onDelete: 'CASCADE', nullable: false }) + @JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' }) + parent: Parents; - @ManyToOne(() => Children, c => c.dossiers, {onDelete:'CASCADE'}) - @JoinColumn({ name: 'child_id', referencedColumnName: 'id' }) - child: Children; + @ManyToOne(() => Children, c => c.dossiers, { onDelete: 'CASCADE', nullable: false }) + @JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' }) + child: Children; - @Column({type: 'text', nullable: true}) - presentation: string; + @Column({ type: 'text', nullable: true, name: 'presentation' }) + presentation?: string; - @Column({type: 'varchar', nullable: true}) - type_contract: string; + @Column({ type: 'varchar', length: 50, nullable: true, name: 'type_contrat' }) + type_contrat?: string; - @Column({type:'boolean', nullable: true}) - meals: boolean; + @Column({ type: 'boolean', default: false, name: 'repas' }) + meals: boolean; - @Column({type: 'decimal', nullable: true}) - budget: number; + @Column({ type: 'numeric', precision: 10, scale: 2, nullable: true, name: 'budget' }) + budget?: number; - @Column({type: 'json', nullable: true}) - desired_schedule: any; + @Column({ type: 'jsonb', nullable: true, name: 'planning_souhaite' }) + desired_schedule?: any; - @Column({type: 'varchar', default: 'sent'}) - status: string; - - @CreateDateColumn() - created_at: Date; + @Column({ + type: 'enum', + enum: StatutDossierType, + enumName: 'statut_dossier_type', + default: StatutDossierType.ENVOYE, + name: 'statut' + }) + status: StatutDossierType; - @UpdateDateColumn() - updated_at: Date; -} + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + updated_at: Date; + + @OneToMany(() => Message, m => m.dossier) + messages: Message[]; +} \ No newline at end of file diff --git a/src/entities/evenements.entity.ts b/src/entities/evenements.entity.ts new file mode 100644 index 0000000..9d7a73c --- /dev/null +++ b/src/entities/evenements.entity.ts @@ -0,0 +1,79 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; +import { Children } from "./children.entity"; +import { Users } from "./users.entity"; +import { Parents } from "./parents.entity"; + +export enum TypeEvenementType { + ABSENCE_ENFANT = 'absence_enfant', + CONGE_AM = 'conge_am', + CONGE_PARENT = 'conge_parent', + ARRET_MALADIE_AM = 'arret_maladie_am', + EVENEMENT_RPE = 'evenement_rpe', +} + +export enum StatutEvenementType { + PROPOSE = 'propose', + VALIDE = 'valide', + REFUSE = 'refuse', +} + +@Entity('evenements') +export class Evenement { + // Define your columns and relationships here + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + type: 'enum', + enum: TypeEvenementType, + enumName: 'type_evenement_type', + name: 'type' + }) + type: TypeEvenementType; + + @ManyToOne(() => Children, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' }) + child?: Children; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'id_am', referencedColumnName: 'id' }) + assistanteMaternelle?: Users; + + @ManyToOne(() => Parents, { nullable: true }) + @JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' }) + parent?: Parents; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'cree_par', referencedColumnName: 'id' }) + created_by?: Users; + + @Column({ type: 'timestamptz', nullable: true, name: 'date_debut' }) + start_date?: Date; + + @Column({ type: 'timestamptz', nullable: true, name: 'date_fin' }) + end_date?: Date; + + @Column({ type: 'text', nullable: true, name: 'commentaires' }) + comments?: string; + + @Column({ + type: 'enum', + enum: StatutEvenementType, + enumName: 'statut_evenement_type', + name: 'statut', + default: StatutEvenementType.PROPOSE + }) + status: StatutEvenementType; + + @Column({type: 'timestamptz', nullable: true, name: 'delai_grace'}) + grace_deadline?: Date; + + @Column({type: 'boolean', default: false, name: 'urgent'}) + urgent: boolean; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + updated_at: Date; +} diff --git a/src/entities/messages.entity.ts b/src/entities/messages.entity.ts new file mode 100644 index 0000000..0083bf5 --- /dev/null +++ b/src/entities/messages.entity.ts @@ -0,0 +1,29 @@ +import { + Entity, PrimaryGeneratedColumn, Column, + ManyToOne, JoinColumn, CreateDateColumn +} from 'typeorm'; +import { Dossier } from './dossiers.entity'; +import { Users } from './users.entity'; + +@Entity('messages') +export class Message { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Dossier, d => d.messages, { onDelete: 'CASCADE', nullable: false }) + @JoinColumn({ name: 'id_dossier' }) + dossier: Dossier; + + @ManyToOne(() => Users, u => u.messages, { onDelete: 'CASCADE', nullable: false }) + @JoinColumn({ name: 'id_expediteur' }) + sender: Users; + + @Column({ type: 'text', name: 'contenu' }) + content: string; + + @Column({ type: 'boolean', name: 're_redige_par_ia', default: false }) + reRedigeParIA: boolean; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; +} diff --git a/src/entities/notifications.entity.ts b/src/entities/notifications.entity.ts new file mode 100644 index 0000000..5f1dbe8 --- /dev/null +++ b/src/entities/notifications.entity.ts @@ -0,0 +1,23 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Users } from "./users.entity"; + +@Entity('notifications') +export class Notification { + // Define your columns and relationships here + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Users, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user: Users; + + @Column({ type: 'text', name: 'contenu' }) + content: string; + + @Column({type: 'boolean', name: 'lu', default: false}) + read: boolean; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + +} diff --git a/src/entities/parents.entity.ts b/src/entities/parents.entity.ts index 3980813..8114adb 100644 --- a/src/entities/parents.entity.ts +++ b/src/entities/parents.entity.ts @@ -1,28 +1,31 @@ -import { Column, Entity, JoinColumn, OneToMany, OneToOne, PrimaryColumn } from "typeorm"; -import { Users } from "./user.entity"; -import { ParentsChildren } from "./parents_children.entity"; -import { Dossier } from "./dossiers.entity"; +import { + Entity, PrimaryColumn, OneToOne, JoinColumn, + ManyToOne, OneToMany +} from 'typeorm'; +import { Users } from './users.entity'; +import { ParentsChildren } from './parents_children.entity'; +import { Dossier } from './dossiers.entity'; @Entity('parents') export class Parents { + // PK = FK vers utilisateurs.id + @PrimaryColumn('uuid', { name: 'id_utilisateur' }) + user_id: string; - @PrimaryColumn('uuid') - user_id: string; + @OneToOne(() => Users, user => user.parent, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user: Users; - @OneToOne(() => Users, user => user.parent, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'user_id' }) - user: Users; + // Co-parent (nullable) → FK vers utilisateurs.id + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'id_co_parent', referencedColumnName: 'id' }) + co_parent?: Users; - @Column({ type: 'uuid', nullable: true }) - co_parent_id?: string; + // Lien vers enfants via la table enfants_parents + @OneToMany(() => ParentsChildren, pc => pc.parent) + parentChildren: ParentsChildren[]; - @OneToOne(() => Users) - @JoinColumn({ name: 'co_parent_id' }) - co_parent?: Users; - - @OneToMany(() => ParentsChildren, pc => pc.parent, { onDelete: 'CASCADE' }) - parentChildren: ParentsChildren[]; - - @OneToMany(() => Dossier, d => d.parent) - dossiers: Dossier[]; + // Lien vers les dossiers de ce parent + @OneToMany(() => Dossier, d => d.parent) + dossiers: Dossier[]; } diff --git a/src/entities/parents_children.entity.ts b/src/entities/parents_children.entity.ts index 6f4f598..8bd183f 100644 --- a/src/entities/parents_children.entity.ts +++ b/src/entities/parents_children.entity.ts @@ -1,21 +1,22 @@ -import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm"; -import { Parents } from "./parents.entity"; -import { Children } from "./children.entity"; +import { + Entity, ManyToOne, JoinColumn, PrimaryColumn +} from 'typeorm'; +import { Parents } from './parents.entity'; +import { Children } from './children.entity'; -@Entity('parents_children') +@Entity('enfants_parents') export class ParentsChildren { - - @PrimaryColumn('uuid') - parent_id: string; + @PrimaryColumn('uuid', { name: 'id_parent' }) + id_parent: string; - @PrimaryColumn('uuid') - child_id: string; + @PrimaryColumn('uuid', { name: 'id_enfant' }) + id_enfant: string; - @ManyToOne(() => Parents, (parent) => parent.parentChildren, {onDelete: 'CASCADE'}) - @JoinColumn({ name: 'parent_id', referencedColumnName: 'user_id' }) - parent: Parents; + @ManyToOne(() => Parents, p => p.parentChildren, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' }) + parent: Parents; - @ManyToOne(() => Children, (child) => child.parentChildren, {onDelete: 'CASCADE'}) - @JoinColumn({ name: 'child_id', referencedColumnName: 'id' }) - child: Children; -} \ No newline at end of file + @ManyToOne(() => Children, c => c.parentLinks, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' }) + child: Children; +} diff --git a/src/entities/signalements_bugs.entity.ts b/src/entities/signalements_bugs.entity.ts new file mode 100644 index 0000000..3759c6f --- /dev/null +++ b/src/entities/signalements_bugs.entity.ts @@ -0,0 +1,19 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; +import { Users } from "./users.entity"; + +@Entity('signalements_bugs') +export class SignalementBug { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Users, {nullable: true}) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user?: Users; + + @Column({ type: 'text', name: 'description'}) + description: string; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + +} diff --git a/src/entities/uploads.entity.ts b/src/entities/uploads.entity.ts new file mode 100644 index 0000000..77470f8 --- /dev/null +++ b/src/entities/uploads.entity.ts @@ -0,0 +1,21 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Users } from "./users.entity"; + +@Entity('uploads') +export class Upload { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Users, { onDelete: 'SET NULL', nullable: true }) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user?: Users; + + @Column({ type: 'text', name: 'fichier_url' }) + file_url: string; + + @Column({type: 'varchar', length: 50, nullable: true, name: 'type'}) + type?: string; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; +} diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts deleted file mode 100644 index 449d227..0000000 --- a/src/entities/user.entity.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - Entity, PrimaryGeneratedColumn, Column, - CreateDateColumn, UpdateDateColumn, - OneToOne -} from 'typeorm'; -import { AssistanteMaternelle } from './assistantes_maternelles.entity'; -import { Parents } from './parents.entity'; -export enum UserRole { - PARENT = 'PARENT', - ASSISTANT = 'ASSISTANT', - GESTIONNAIRE = 'GESTIONNAIRE', - ADMIN = 'ADMIN', -} - -@Entity('users') -export class Users { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ unique: true }) - email: string; - - @Column() - password_hash: string; - - @Column() - first_name: string; - - @Column() - last_name: string; - - @Column({ - type: 'enum', - enum: UserRole, - default: UserRole.PARENT, - }) - role: UserRole; - - @Column({ default: 'pending' }) - status: string; - - @Column({ nullable: true }) - phone: string; - - @Column({ nullable: true }) - address: string; - - @Column({ nullable: true }) - photo_url: string; - - @Column({ default: false }) - consent_photo: boolean; - - @Column({ type: 'timestamp', nullable: true }) - consent_photo_at: Date; - - @Column({ default: false }) - must_change_password: boolean; - - @CreateDateColumn() - created_at: Date; - - @UpdateDateColumn() - updated_at: Date; - - @OneToOne(() => AssistanteMaternelle, a => a.user) - assistanteMaternelle?: AssistanteMaternelle; - - @OneToOne(() => Parents, p => p.user) - parent?: Parents; - -} - diff --git a/src/entities/users.entity.ts b/src/entities/users.entity.ts new file mode 100644 index 0000000..2968861 --- /dev/null +++ b/src/entities/users.entity.ts @@ -0,0 +1,109 @@ +import { + Entity, PrimaryGeneratedColumn, Column, + CreateDateColumn, UpdateDateColumn, + OneToOne, OneToMany +} from 'typeorm'; +import { AssistanteMaternelle } from './assistantes_maternelles.entity'; +import { Parents } from './parents.entity'; +import { Message } from './messages.entity'; + +// Enums alignés avec la BDD PostgreSQL +export enum RoleType { + PARENT = 'parent', + GESTIONNAIRE = 'gestionnaire', + SUPER_ADMIN = 'super_admin', + ASSISTANTE_MATERNELLE = 'assistante_maternelle', +} + +//Enum pour definir le genre +export enum GenreType { + H = 'H', + F = 'F', + AUTRE = 'Autre', +} + +//Enum pour definir le statut utilisateur +export enum StatutUtilisateurType { + EN_ATTENTE = 'en_attente', + ACTIF = 'actif', + SUSPENDU = 'suspendu', +} + +//Declaration de l'entite utilisateur +@Entity('utilisateurs') +export class Users { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true, name: 'courriel' }) + email: string; + + @Column({ name: 'mot_de_passe_hash' }) + password_hash: string; + + @Column({ name: 'prenom', nullable: true }) + first_name: string; + + @Column({ name: 'nom', nullable: true }) + last_name: string; + + @Column({ + type: 'enum', + enum: GenreType, + enumName: 'genre_type', // correspond à l'enum de la db psql + nullable: true, + name: 'genre' + }) + gender?: GenreType; + + @Column({ + type: 'enum', + enum: RoleType, + enumName: 'role_type', // correspond à l'enum de la db psql + name: 'role' + }) + role: RoleType; + + @Column({ + type: 'enum', + enum: StatutUtilisateurType, + enumName: 'statut_utilisateur_type', // correspond à l'enum de la db psql + default: StatutUtilisateurType.EN_ATTENTE, + name: 'statut' + }) + status: StatutUtilisateurType; + + @Column({ nullable: true, name: 'telephone' }) + phone?: string; + + @Column({ nullable: true, name: 'adresse' }) + address?: string; + + @Column({ nullable: true, name: 'photo_url' }) + photo_url?: string; + + @Column({ default: false, name: 'consentement_photo' }) + consent_photo: boolean; + + @Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' }) + consent_photo_at?: Date; + + @Column({ default: false, name: 'changement_mdp_obligatoire' }) + must_change_password: boolean; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + updated_at: Date; + + // Relations + @OneToOne(() => AssistanteMaternelle, a => a.user) + assistanteMaternelle?: AssistanteMaternelle; + + @OneToOne(() => Parents, p => p.user) + parent?: Parents; + + @OneToMany(() => Message, m => m.sender) + messages?: Message[]; +} diff --git a/src/entities/validations.entity.ts b/src/entities/validations.entity.ts new file mode 100644 index 0000000..d508a78 --- /dev/null +++ b/src/entities/validations.entity.ts @@ -0,0 +1,36 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; +import { Users } from "./users.entity"; + +export enum StatutValidationType { + EN_ATTENTE = 'en_attente', + VALIDE = 'valide', + REFUSE = 'refuse', +} + +@Entity('validations') +export class Validation { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user?: Users; + + @Column({ type: 'varchar', length: 50, name: 'type' }) + type: string; + + @Column({ + type: 'enum', + enum: StatutValidationType, + enumName: 'statut_validation_type', + name: 'statut', + default: StatutValidationType.EN_ATTENTE + }) + status: StatutValidationType; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + updated_at: Date; +} diff --git a/src/routes/auth/auth.controller.ts b/src/routes/auth/auth.controller.ts deleted file mode 100644 index b6fe521..0000000 --- a/src/routes/auth/auth.controller.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Body, Controller, Get, Post } from '@nestjs/common'; -import { AuthService } from './auth.service'; -import { LoginDto } from './dto/login.dto'; -import { RegisterDto } from './dto/register.dto'; - -@Controller('auth') -export class AuthController { - constructor(private readonly authService: AuthService) {} - - - //Route pour se connecter - @Post('login') - login(@Body() loginDto: LoginDto) { - return this.authService.login(loginDto); - } - - //Route pour s'inscrire - @Post('register') - register(@Body() registerDto: RegisterDto) { - return this.authService.register(registerDto); - } -} diff --git a/src/routes/auth/auth.module.ts b/src/routes/auth/auth.module.ts deleted file mode 100644 index b51ac54..0000000 --- a/src/routes/auth/auth.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; - -@Module({ - controllers: [AuthController], - providers: [AuthService] -}) -export class AuthModule {} diff --git a/src/routes/auth/auth.service.ts b/src/routes/auth/auth.service.ts deleted file mode 100644 index 0c62e51..0000000 --- a/src/routes/auth/auth.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { LoginDto } from './dto/login.dto'; -import { RegisterDto } from './dto/register.dto'; - -@Injectable() -export class AuthService { - - register(registerDto: RegisterDto) { - return { - message: `User registered successfully ${registerDto}`, - user: registerDto, - }; - } - - login(loginDto: LoginDto) { - return { - message: `Login successful ${loginDto.email}`, - user: loginDto, - }; - } -} diff --git a/src/routes/auth/dto/login.dto.ts b/src/routes/auth/dto/login.dto.ts deleted file mode 100644 index 804b28c..0000000 --- a/src/routes/auth/dto/login.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IsEmail, MinLength } from 'class-validator'; - -export class LoginDto { - @IsEmail() - email: string; - - @MinLength(8) - password: string; -} diff --git a/src/routes/auth/dto/register.dto.ts b/src/routes/auth/dto/register.dto.ts deleted file mode 100644 index ab94e78..0000000 --- a/src/routes/auth/dto/register.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IsEmail, IsNotEmpty, MaxLength, MinLength } from "class-validator"; - -export class RegisterDto { - - @IsEmail() - email: string; - - @IsNotEmpty() - username: string; - - @MinLength(8) - password: string; -} - - diff --git a/src/routes/parents/dto/create_parents.dto.ts b/src/routes/parents/dto/create_parents.dto.ts new file mode 100644 index 0000000..b049891 --- /dev/null +++ b/src/routes/parents/dto/create_parents.dto.ts @@ -0,0 +1,10 @@ +import { IsUUID, IsOptional } from 'class-validator'; + +export class CreateParentDto { + @IsUUID() + id_utilisateur: string; + + @IsOptional() + @IsUUID() + co_parent_id?: string; +} diff --git a/src/routes/auth/auth.controller.spec.ts b/src/routes/parents/parents.controller.spec.ts similarity index 51% rename from src/routes/auth/auth.controller.spec.ts rename to src/routes/parents/parents.controller.spec.ts index 27a31e6..6735376 100644 --- a/src/routes/auth/auth.controller.spec.ts +++ b/src/routes/parents/parents.controller.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { AuthController } from './auth.controller'; +import { ParentsController } from './parents.controller'; -describe('AuthController', () => { - let controller: AuthController; +describe('ParentsController', () => { + let controller: ParentsController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - controllers: [AuthController], + controllers: [ParentsController], }).compile(); - controller = module.get(AuthController); + controller = module.get(ParentsController); }); it('should be defined', () => { diff --git a/src/routes/parents/parents.controller.ts b/src/routes/parents/parents.controller.ts new file mode 100644 index 0000000..be17890 --- /dev/null +++ b/src/routes/parents/parents.controller.ts @@ -0,0 +1,19 @@ +import { Controller, Get } from '@nestjs/common'; +import { BaseController } from 'src/common/base.controller'; +import { Parents } from 'src/entities/parents.entity'; +import { ParentsService } from './parents.service'; +import { Roles } from 'src/common/decorators/roles.decorator'; +import { RoleType } from 'src/entities/users.entity'; + +@Controller('parents') +export class ParentsController extends BaseController { + constructor(private readonly parentsService: ParentsService) { + super(parentsService); + } + + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @Get() + override getAll(): Promise { + return this.parentsService.findAll(); + } +} diff --git a/src/routes/parents/parents.module.ts b/src/routes/parents/parents.module.ts new file mode 100644 index 0000000..258e9dc --- /dev/null +++ b/src/routes/parents/parents.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Parents } from 'src/entities/parents.entity'; +import { ParentsController } from './parents.controller'; +import { ParentsService } from './parents.service'; +import { Users } from 'src/entities/users.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Parents, Users])], + controllers: [ParentsController], + providers: [ParentsService], +}) +export class ParentsModule {} \ No newline at end of file diff --git a/src/routes/auth/auth.service.spec.ts b/src/routes/parents/parents.service.spec.ts similarity index 54% rename from src/routes/auth/auth.service.spec.ts rename to src/routes/parents/parents.service.spec.ts index 800ab66..83b23f2 100644 --- a/src/routes/auth/auth.service.spec.ts +++ b/src/routes/parents/parents.service.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { AuthService } from './auth.service'; +import { ParentsService } from './parents.service'; -describe('AuthService', () => { - let service: AuthService; +describe('ParentsService', () => { + let service: ParentsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [AuthService], + providers: [ParentsService], }).compile(); - service = module.get(AuthService); + service = module.get(ParentsService); }); it('should be defined', () => { diff --git a/src/routes/parents/parents.service.ts b/src/routes/parents/parents.service.ts new file mode 100644 index 0000000..9e61bea --- /dev/null +++ b/src/routes/parents/parents.service.ts @@ -0,0 +1,57 @@ +import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { BaseService } from 'src/common/base.service'; +import { Parents } from 'src/entities/parents.entity'; +import { RoleType, Users } from 'src/entities/users.entity'; +import { DeepPartial, Repository } from 'typeorm'; +import { CreateParentDto } from './dto/create_parents.dto'; + +@Injectable() +export class ParentsService extends BaseService { + constructor( + @InjectRepository(Parents) private readonly parentsRepository: Repository, + @InjectRepository(Users) private readonly usersRepository: Repository, + ) { + super(parentsRepository); + } + + override async create(data: DeepPartial): Promise { + const dto = data as CreateParentDto; + + const user = await this.usersRepository.findOneBy({ id: dto.id_utilisateur }); + if (!user) throw new NotFoundException('Utilisateur introuvable'); + if (user.role !== RoleType.PARENT) throw new BadRequestException('Acces reserve aux parents'); + + const exist = await this.parentsRepository.findOneBy({ user_id: dto.id_utilisateur }); + if (exist) throw new ConflictException('Ce parent existe deja'); + + let co_parent: Users | null = null; + if (dto.co_parent_id) { + co_parent = await this.usersRepository.findOneBy({ id: dto.co_parent_id }); + if (!co_parent) throw new NotFoundException('Co-parent introuvable'); + if (co_parent.role !== RoleType.PARENT) throw new BadRequestException('Acces reserve aux parents'); + } + const entity = this.parentsRepository.create({ + user_id: dto.id_utilisateur, + user, + co_parent: co_parent ?? undefined, + }) as DeepPartial; + return this.parentsRepository.save(entity) + } + + override async findAll(): Promise { + return this.parentsRepository.find({ + relations: ['user', 'co_parent', + 'parentChildren', 'dossiers'], + }); + } + + override async findOne(user_id: string): Promise { + const parent = await this.parentsRepository.findOne({ + where: { user_id }, + relations: ['user', 'co_parent', 'parentChildren', 'dossiers'], + }); + if (!parent) throw new NotFoundException('Parent introuvable'); + return parent; + } +} \ No newline at end of file