diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 0124bfe..0197cd2 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -16,6 +16,7 @@ import { AllExceptionsFilter } from './common/filters/all_exceptions.filters'; 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'; @Module({ imports: [ @@ -53,6 +54,7 @@ import { DocumentsLegauxModule } from './modules/documents-legaux'; AuthModule, AppConfigModule, DocumentsLegauxModule, + RelaisModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/config/typeorm.config.ts b/backend/src/config/typeorm.config.ts new file mode 100644 index 0000000..f431eb2 --- /dev/null +++ b/backend/src/config/typeorm.config.ts @@ -0,0 +1,15 @@ +import { DataSource } from 'typeorm'; +import { config } from 'dotenv'; + +config(); + +export default new DataSource({ + type: 'postgres', + host: process.env.DATABASE_HOST, + port: parseInt(process.env.DATABASE_PORT || '5432', 10), + username: process.env.DATABASE_USERNAME, + password: process.env.DATABASE_PASSWORD, + database: process.env.DATABASE_NAME, + entities: ['src/**/*.entity.ts'], + migrations: ['src/migrations/*.ts'], +}); diff --git a/backend/src/entities/relais.entity.ts b/backend/src/entities/relais.entity.ts new file mode 100644 index 0000000..9b492da --- /dev/null +++ b/backend/src/entities/relais.entity.ts @@ -0,0 +1,35 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; +import { Users } from './users.entity'; + +@Entity('relais', { schema: 'public' }) +export class Relais { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'nom' }) + nom: string; + + @Column({ name: 'adresse' }) + adresse: string; + + @Column({ type: 'jsonb', name: 'horaires_ouverture', nullable: true }) + horaires_ouverture?: any; + + @Column({ name: 'ligne_fixe', nullable: true }) + ligne_fixe?: string; + + @Column({ default: true, name: 'actif' }) + actif: boolean; + + @Column({ type: 'text', name: 'notes', nullable: true }) + notes?: string; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + cree_le: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + modifie_le: Date; + + @OneToMany(() => Users, user => user.relais) + gestionnaires: Users[]; +} diff --git a/backend/src/entities/users.entity.ts b/backend/src/entities/users.entity.ts index 25db178..3f74dc5 100644 --- a/backend/src/entities/users.entity.ts +++ b/backend/src/entities/users.entity.ts @@ -1,11 +1,12 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, - OneToOne, OneToMany + OneToOne, OneToMany, ManyToOne, JoinColumn } from 'typeorm'; import { AssistanteMaternelle } from './assistantes_maternelles.entity'; import { Parents } from './parents.entity'; import { Message } from './messages.entity'; +import { Relais } from './relais.entity'; // Enums alignés avec la BDD PostgreSQL export enum RoleType { @@ -147,4 +148,11 @@ export class Users { @OneToMany(() => Parents, parent => parent.co_parent) co_parent_in?: Parents[]; + + @Column({ nullable: true, name: 'relais_id' }) + relaisId?: string; + + @ManyToOne(() => Relais, relais => relais.gestionnaires, { nullable: true }) + @JoinColumn({ name: 'relais_id' }) + relais?: Relais; } diff --git a/backend/src/routes/relais/dto/create-relais.dto.ts b/backend/src/routes/relais/dto/create-relais.dto.ts new file mode 100644 index 0000000..b5cf737 --- /dev/null +++ b/backend/src/routes/relais/dto/create-relais.dto.ts @@ -0,0 +1,34 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsObject } from 'class-validator'; + +export class CreateRelaisDto { + @ApiProperty({ example: 'Relais Petite Enfance Centre' }) + @IsString() + @IsNotEmpty() + nom: string; + + @ApiProperty({ example: '12 rue de la Mairie, 75000 Paris' }) + @IsString() + @IsNotEmpty() + adresse: string; + + @ApiProperty({ example: { lundi: '09:00-17:00' }, required: false }) + @IsOptional() + @IsObject() + horaires_ouverture?: any; + + @ApiProperty({ example: '0123456789', required: false }) + @IsOptional() + @IsString() + ligne_fixe?: string; + + @ApiProperty({ default: true, required: false }) + @IsOptional() + @IsBoolean() + actif?: boolean; + + @ApiProperty({ example: 'Notes internes...', required: false }) + @IsOptional() + @IsString() + notes?: string; +} diff --git a/backend/src/routes/relais/dto/update-relais.dto.ts b/backend/src/routes/relais/dto/update-relais.dto.ts new file mode 100644 index 0000000..f7c0b9b --- /dev/null +++ b/backend/src/routes/relais/dto/update-relais.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateRelaisDto } from './create-relais.dto'; + +export class UpdateRelaisDto extends PartialType(CreateRelaisDto) {} diff --git a/backend/src/routes/relais/relais.controller.ts b/backend/src/routes/relais/relais.controller.ts new file mode 100644 index 0000000..5d30871 --- /dev/null +++ b/backend/src/routes/relais/relais.controller.ts @@ -0,0 +1,57 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common'; +import { RelaisService } from './relais.service'; +import { CreateRelaisDto } from './dto/create-relais.dto'; +import { UpdateRelaisDto } from './dto/update-relais.dto'; +import { ApiBearerAuth, ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { AuthGuard } from 'src/common/guards/auth.guard'; +import { RolesGuard } from 'src/common/guards/roles.guard'; +import { Roles } from 'src/common/decorators/roles.decorator'; +import { RoleType } from 'src/entities/users.entity'; + +@ApiTags('Relais') +@ApiBearerAuth('access-token') +@UseGuards(AuthGuard, RolesGuard) +@Controller('relais') +export class RelaisController { + constructor(private readonly relaisService: RelaisService) {} + + @Post() + @Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR) + @ApiOperation({ summary: 'Créer un relais' }) + @ApiResponse({ status: 201, description: 'Le relais a été créé.' }) + create(@Body() createRelaisDto: CreateRelaisDto) { + return this.relaisService.create(createRelaisDto); + } + + @Get() + @Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR) + @ApiOperation({ summary: 'Lister tous les relais' }) + @ApiResponse({ status: 200, description: 'Liste des relais.' }) + findAll() { + return this.relaisService.findAll(); + } + + @Get(':id') + @Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR) + @ApiOperation({ summary: 'Récupérer un relais par ID' }) + @ApiResponse({ status: 200, description: 'Le relais trouvé.' }) + findOne(@Param('id') id: string) { + return this.relaisService.findOne(id); + } + + @Patch(':id') + @Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR) + @ApiOperation({ summary: 'Mettre à jour un relais' }) + @ApiResponse({ status: 200, description: 'Le relais a été mis à jour.' }) + update(@Param('id') id: string, @Body() updateRelaisDto: UpdateRelaisDto) { + return this.relaisService.update(id, updateRelaisDto); + } + + @Delete(':id') + @Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR) + @ApiOperation({ summary: 'Supprimer un relais' }) + @ApiResponse({ status: 200, description: 'Le relais a été supprimé.' }) + remove(@Param('id') id: string) { + return this.relaisService.remove(id); + } +} diff --git a/backend/src/routes/relais/relais.module.ts b/backend/src/routes/relais/relais.module.ts new file mode 100644 index 0000000..393eb1d --- /dev/null +++ b/backend/src/routes/relais/relais.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RelaisService } from './relais.service'; +import { RelaisController } from './relais.controller'; +import { Relais } from 'src/entities/relais.entity'; +import { AuthModule } from 'src/routes/auth/auth.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Relais]), + AuthModule, + ], + controllers: [RelaisController], + providers: [RelaisService], + exports: [RelaisService], +}) +export class RelaisModule {} diff --git a/backend/src/routes/relais/relais.service.ts b/backend/src/routes/relais/relais.service.ts new file mode 100644 index 0000000..b2fb022 --- /dev/null +++ b/backend/src/routes/relais/relais.service.ts @@ -0,0 +1,42 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Relais } from 'src/entities/relais.entity'; +import { CreateRelaisDto } from './dto/create-relais.dto'; +import { UpdateRelaisDto } from './dto/update-relais.dto'; + +@Injectable() +export class RelaisService { + constructor( + @InjectRepository(Relais) + private readonly relaisRepository: Repository, + ) {} + + create(createRelaisDto: CreateRelaisDto) { + const relais = this.relaisRepository.create(createRelaisDto); + return this.relaisRepository.save(relais); + } + + findAll() { + return this.relaisRepository.find({ order: { nom: 'ASC' } }); + } + + async findOne(id: string) { + const relais = await this.relaisRepository.findOne({ where: { id } }); + if (!relais) { + throw new NotFoundException(`Relais #${id} not found`); + } + return relais; + } + + async update(id: string, updateRelaisDto: UpdateRelaisDto) { + const relais = await this.findOne(id); + Object.assign(relais, updateRelaisDto); + return this.relaisRepository.save(relais); + } + + async remove(id: string) { + const relais = await this.findOne(id); + return this.relaisRepository.remove(relais); + } +} diff --git a/backend/src/routes/user/dto/create_gestionnaire.dto.ts b/backend/src/routes/user/dto/create_gestionnaire.dto.ts index fceea23..26f36ab 100644 --- a/backend/src/routes/user/dto/create_gestionnaire.dto.ts +++ b/backend/src/routes/user/dto/create_gestionnaire.dto.ts @@ -1,4 +1,10 @@ -import { OmitType } from "@nestjs/swagger"; +import { ApiProperty, OmitType } from "@nestjs/swagger"; import { CreateUserDto } from "./create_user.dto"; +import { IsOptional, IsUUID } from "class-validator"; -export class CreateGestionnaireDto extends OmitType(CreateUserDto, ['role'] as const) {} +export class CreateGestionnaireDto extends OmitType(CreateUserDto, ['role'] as const) { + @ApiProperty({ required: false, description: 'ID du relais de rattachement' }) + @IsOptional() + @IsUUID() + relaisId?: string; +} diff --git a/backend/src/routes/user/gestionnaires/gestionnaires.service.ts b/backend/src/routes/user/gestionnaires/gestionnaires.service.ts index 4e1406e..590730d 100644 --- a/backend/src/routes/user/gestionnaires/gestionnaires.service.ts +++ b/backend/src/routes/user/gestionnaires/gestionnaires.service.ts @@ -41,19 +41,24 @@ export class GestionnairesService { : undefined, changement_mdp_obligatoire: dto.changement_mdp_obligatoire ?? false, role: RoleType.GESTIONNAIRE, + relaisId: dto.relaisId, }); return this.gestionnaireRepository.save(entity); } // Liste des gestionnaires async findAll(): Promise { - return this.gestionnaireRepository.find({ where: { role: RoleType.GESTIONNAIRE } }); + return this.gestionnaireRepository.find({ + where: { role: RoleType.GESTIONNAIRE }, + relations: ['relais'], + }); } // Récupérer un gestionnaire par ID async findOne(id: string): Promise { const gestionnaire = await this.gestionnaireRepository.findOne({ where: { id, role: RoleType.GESTIONNAIRE }, + relations: ['relais'], }); if (!gestionnaire) throw new NotFoundException('Gestionnaire introuvable'); return gestionnaire; diff --git a/database/BDD.sql b/database/BDD.sql index 991ce3a..6a26917 100644 --- a/database/BDD.sql +++ b/database/BDD.sql @@ -331,13 +331,29 @@ CREATE INDEX idx_acceptations_utilisateur ON acceptations_documents(id_utilisate CREATE INDEX idx_acceptations_document ON acceptations_documents(id_document); -- ========================================================== --- Modification Table : utilisateurs (ajout colonnes documents) +-- Table : relais +-- ========================================================== +CREATE TABLE relais ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nom VARCHAR(255) NOT NULL, + adresse TEXT NOT NULL, + horaires_ouverture JSONB, + ligne_fixe VARCHAR(20), + actif BOOLEAN DEFAULT true, + notes TEXT, + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Modification Table : utilisateurs (ajout colonnes documents et relais) -- ========================================================== ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS cgu_version_acceptee INTEGER, ADD COLUMN IF NOT EXISTS cgu_acceptee_le TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS privacy_version_acceptee INTEGER, - ADD COLUMN IF NOT EXISTS privacy_acceptee_le TIMESTAMPTZ; + ADD COLUMN IF NOT EXISTS privacy_acceptee_le TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS relais_id UUID REFERENCES relais(id) ON DELETE SET NULL; -- ========================================================== -- Seed : Documents légaux génériques v1 diff --git a/docs/23_LISTE-TICKETS.md b/docs/23_LISTE-TICKETS.md index 0d39b55..ad962d3 100644 --- a/docs/23_LISTE-TICKETS.md +++ b/docs/23_LISTE-TICKETS.md @@ -1,7 +1,7 @@ # 🎫 Liste Complète des Tickets - Projet P'titsPas -**Version** : 1.4 -**Date** : 9 Février 2026 +**Version** : 1.5 +**Date** : 17 Février 2026 **Auteur** : Équipe PtitsPas **Estimation totale** : ~184h @@ -28,7 +28,11 @@ | 15 | [Frontend] Écran Paramètres (accès permanent) | Ouvert | | 16 | [Doc] Documentation configuration on-premise | Ouvert | | 17–88 | (voir sections ci‑dessous ; #82, #78, #79, #81, #83 ; #86, #87, #88 fermés en doublon) | — | -| 92 | [Frontend] Dashboard Admin - Données réelles et branchement API | Ouvert | +| 91 | [Frontend] Inscription AM – Branchement soumission formulaire à l'API | Ouvert | +| 92 | [Frontend] Dashboard Admin - Données réelles et branchement API | ✅ Terminé | +| 93 | [Frontend] Panneau Admin - Homogeneiser la presentation des onglets | Ouvert | +| 94 | [Backend] Relais - modele, API CRUD et liaison gestionnaire | Ouvert | +| 95 | [Frontend] Admin - gestion des relais et rattachement gestionnaire | Ouvert | *Gitea #1 et #2 = anciens tickets de test (fermés). Liste complète : https://git.ptits-pas.fr/jmartin/petitspas/issues* @@ -641,6 +645,21 @@ Enregistrer les acceptations de documents légaux lors de l'inscription (traçab --- +### Ticket #94 : [Backend] Relais - Modèle, API CRUD et liaison gestionnaire +**Estimation** : 4h +**Labels** : `backend`, `p2`, `admin` + +**Description** : +Le back-office admin doit gérer des Relais avec des données réelles en base, et permettre une liaison simple avec les gestionnaires. + +**Tâches** : +- [ ] Créer le modèle `Relais` (nom, adresse, horaires, téléphone, actif, notes) +- [ ] Exposer les endpoints admin CRUD pour les relais (`GET`, `POST`, `PATCH`, `DELETE`) +- [ ] Ajouter la liaison : un gestionnaire peut être rattaché à un relais principal (`relais_id` dans `users` ?) +- [ ] Validations (champs requis, format horaires) + +--- + ## 🟢 PRIORITÉ 3 : Frontend - Interfaces ### Ticket #35 : [Frontend] Écran Création Gestionnaire @@ -894,9 +913,10 @@ Créer l'écran de gestion des documents légaux (CGU/Privacy) pour l'admin. --- -### Ticket #92 : [Frontend] Dashboard Admin - Données réelles et branchement API +### Ticket #92 : [Frontend] Dashboard Admin - Données réelles et branchement API ✅ **Estimation** : 8h **Labels** : `frontend`, `p3`, `admin` +**Statut** : ✅ TERMINÉ (Fermé le 2026-02-17) **Description** : Le dashboard admin (onglets Gestionnaires | Parents | Assistantes maternelles | Administrateurs) affiche actuellement des données en dur (mock). Remplacer par des appels API pour afficher les vrais utilisateurs et permettre les actions de gestion (voir, modifier, valider/refuser). Référence : [90_AUDIT.md](./90_AUDIT.md). @@ -1018,6 +1038,51 @@ Adapter l'écran de choix Parent/AM pour une meilleure expérience mobile et coh --- +### Ticket #91 : [Frontend] Inscription AM – Branchement soumission formulaire à l'API +**Estimation** : 3h +**Labels** : `frontend`, `p3`, `auth`, `cdc` + +**Description** : +Branchement du formulaire d'inscription AM (étape 4) à l'endpoint d'inscription. + +**Tâches** : +- [ ] Construire le body (DTO) à partir de `AmRegistrationData` +- [ ] Appel HTTP `POST /api/v1/auth/register/am` +- [ ] Gestion réponse (201 : succès + redirection ; 4xx : erreur) +- [ ] Conversion photo en base64 si nécessaire + +--- + +### Ticket #93 : [Frontend] Panneau Admin - Homogénéisation des onglets +**Estimation** : 4h +**Labels** : `frontend`, `p3`, `admin`, `ux` + +**Description** : +Uniformiser l'UI/UX des 4 onglets du dashboard admin (Gestionnaires, Parents, AM, Admins). + +**Tâches** : +- [ ] Standardiser le header de liste (Recherche, Filtres, Bouton Action) +- [ ] Standardiser les cartes utilisateurs (`ListTile` uniforme) +- [ ] Standardiser les états (Loading, Erreur, Vide) +- [ ] Factoriser les composants partagés + +--- + +### Ticket #95 : [Frontend] Admin - Gestion des Relais et rattachement gestionnaire +**Estimation** : 5h +**Labels** : `frontend`, `p3`, `admin` + +**Description** : +Interface de gestion des Relais dans le dashboard admin et rattachement des gestionnaires. + +**Tâches** : +- [ ] Section Relais avec 2 sous-onglets : Paramètres techniques / Paramètres territoriaux +- [ ] Liste, Création, Édition, Activation/Désactivation des relais +- [ ] Champs UI : nom, adresse, horaires, téléphone, statut, notes +- [ ] Onglet Gestionnaires : Ajout contrôle de rattachement au relais principal + +--- + ## 🔵 PRIORITÉ 4 : Tests & Documentation ### Ticket #52 : [Tests] Tests unitaires Backend @@ -1235,28 +1300,29 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit ## 📊 Résumé final -**Total** : 65 tickets -**Estimation** : ~184h de développement +**Total** : 69 tickets +**Estimation** : ~200h de développement ### Par priorité - **P0 (Bloquant BDD)** : 7 tickets (~5h) - **P1 (Bloquant Config)** : 7 tickets (~22h) -- **P2 (Backend)** : 18 tickets (~50h) -- **P3 (Frontend)** : 22 tickets (~71h) ← +1 mobile RegisterChoice +- **P2 (Backend)** : 19 tickets (~54h) +- **P3 (Frontend)** : 25 tickets (~83h) - **P4 (Tests/Doc)** : 4 tickets (~24h) - **Critiques** : 6 tickets (~13h) - **Juridique** : 1 ticket (~8h) ### Par domaine - **BDD** : 7 tickets -- **Backend** : 23 tickets -- **Frontend** : 22 tickets ← +1 mobile RegisterChoice +- **Backend** : 24 tickets +- **Frontend** : 25 tickets - **Tests** : 3 tickets - **Documentation** : 5 tickets - **Infra** : 2 tickets - **Juridique** : 1 ticket ### Modifications par rapport à la version initiale +- ✅ **v1.5** : Ajout tickets #91, #93, #94, #95. Ticket #92 terminé. - ✅ **v1.4** : Numéros de section du doc = numéros Gitea (Ticket #n = issue #n). Tableau et sections renumérotés. Doublons #86, #87, #88 fermés sur Gitea (#86→#12, #87→#14, #88→#15) ; tickets sources #12, #14, #15 mis à jour (doc + body Gitea). - ✅ **Concept v1.3** : Configuration initiale = un seul panneau Paramètres (3 sections) dans le dashboard ; plus de page dédiée « Setup Wizard » ; navigation bloquée jusqu’à sauvegarde au premier déploiement. Tickets #10, #12, #13 alignés. - ❌ **Supprimé** : Tickets "Renvoyer email validation" (backend + frontend) - Pas prioritaire