From bee21b9fd8e63b98c2f61fec157dcacfb9b720e9 Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Fri, 19 Jun 2026 10:39:27 +0200 Subject: [PATCH] feat(backend): add GET /api/materiels catalogue endpoint --- eme-backend/src/app.ts | 3 ++ .../src/controllers/materiel.controller.ts | 29 +++++++++++++++ .../src/repositories/materiel.repository.ts | 37 +++++++++++++++++++ eme-backend/src/routes/index.ts | 8 ++++ eme-backend/src/routes/materiel.routes.ts | 6 +++ eme-backend/src/services/materiel.service.ts | 14 +++++++ review.md | 25 ++++++++----- 7 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 eme-backend/src/controllers/materiel.controller.ts create mode 100644 eme-backend/src/repositories/materiel.repository.ts create mode 100644 eme-backend/src/routes/index.ts create mode 100644 eme-backend/src/routes/materiel.routes.ts create mode 100644 eme-backend/src/services/materiel.service.ts diff --git a/eme-backend/src/app.ts b/eme-backend/src/app.ts index 7d4452c..17fff57 100644 --- a/eme-backend/src/app.ts +++ b/eme-backend/src/app.ts @@ -6,6 +6,7 @@ import { logger } from './utils/logger'; import { requestLogger } from './middlewares/request-logger'; import { notFoundHandler } from './middlewares/not-found'; import { errorHandler } from './middlewares/error-handler'; +import { apiRouter } from './routes'; const app = express(); @@ -17,6 +18,8 @@ app.get('/health', (_req, res) => { res.json({ status: 'ok', app: 'EME API' }); }); +app.use('/api', apiRouter); + /* notFoundHandler et errorHandler doivent rester après toutes les routes : le premier capture les URL non gérées, le second clôt la chaîne Express. */ app.use(notFoundHandler); diff --git a/eme-backend/src/controllers/materiel.controller.ts b/eme-backend/src/controllers/materiel.controller.ts new file mode 100644 index 0000000..ff50bd6 --- /dev/null +++ b/eme-backend/src/controllers/materiel.controller.ts @@ -0,0 +1,29 @@ +import { Request, Response } from 'express'; +import { AppError } from '../errors/app-error'; +import { listerCatalogue } from '../services/materiel.service'; + +function parseCategorieId(value: unknown): number | undefined { + if (value === undefined) { + return undefined; + } + + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new AppError(400, 'categorieId invalide'); + } + + return parsed; +} + +export async function getCatalogue(req: Request, res: Response): Promise { + const user = req.user; + if (!user) { + throw new AppError(401, 'Authentification requise'); + } + + const recherche = typeof req.query.q === 'string' ? req.query.q : undefined; + const categorieId = parseCategorieId(req.query.categorieId); + + const materiels = await listerCatalogue(user.campusId, { categorieId, recherche }); + res.json({ data: materiels }); +} diff --git a/eme-backend/src/repositories/materiel.repository.ts b/eme-backend/src/repositories/materiel.repository.ts new file mode 100644 index 0000000..3f33807 --- /dev/null +++ b/eme-backend/src/repositories/materiel.repository.ts @@ -0,0 +1,37 @@ +import { Prisma } from '@prisma/client'; +import { prisma } from '../db/prisma'; + +export type MaterielAvecCategorie = Prisma.MaterielGetPayload<{ include: { categorie: true } }>; + +export interface CatalogueFiltres { + categorieId?: number; + recherche?: string; +} + +export function findDisponiblesParCampus( + campusId: number, + filtres: CatalogueFiltres, +): Promise { + const { categorieId, recherche } = filtres; + + return prisma.materiel.findMany({ + where: { + campusId, + actif: true, + statut: 'DISPONIBLE', + ...(categorieId !== undefined ? { categorieId } : {}), + ...(recherche + ? { + OR: [ + { nom: { contains: recherche } }, + { marque: { contains: recherche } }, + { modele: { contains: recherche } }, + { reference: { contains: recherche } }, + ], + } + : {}), + }, + include: { categorie: true }, + orderBy: { nom: 'asc' }, + }); +} diff --git a/eme-backend/src/routes/index.ts b/eme-backend/src/routes/index.ts new file mode 100644 index 0000000..3d9e8ca --- /dev/null +++ b/eme-backend/src/routes/index.ts @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import { currentUser } from '../middlewares/current-user'; +import { materielRoutes } from './materiel.routes'; + +export const apiRouter: Router = Router(); + +apiRouter.use(currentUser); +apiRouter.use('/materiels', materielRoutes); diff --git a/eme-backend/src/routes/materiel.routes.ts b/eme-backend/src/routes/materiel.routes.ts new file mode 100644 index 0000000..385f897 --- /dev/null +++ b/eme-backend/src/routes/materiel.routes.ts @@ -0,0 +1,6 @@ +import { Router } from 'express'; +import { getCatalogue } from '../controllers/materiel.controller'; + +export const materielRoutes: Router = Router(); + +materielRoutes.get('/', getCatalogue); diff --git a/eme-backend/src/services/materiel.service.ts b/eme-backend/src/services/materiel.service.ts new file mode 100644 index 0000000..d23e09b --- /dev/null +++ b/eme-backend/src/services/materiel.service.ts @@ -0,0 +1,14 @@ +import { + findDisponiblesParCampus, + CatalogueFiltres, + MaterielAvecCategorie, +} from '../repositories/materiel.repository'; + +/* RG10 : après identification, seuls les matériels disponibles du campus de + l'étudiant sont affichés. */ +export function listerCatalogue( + campusId: number, + filtres: CatalogueFiltres, +): Promise { + return findDisponiblesParCampus(campusId, filtres); +} diff --git a/review.md b/review.md index 1d86175..c2fdde7 100644 --- a/review.md +++ b/review.md @@ -45,26 +45,26 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Defaults + timestamps auto ajoutés sur Campus, Utilisateur, CategorieMateriel (Role n'a pas de timestamps -> non modifié). - `@unique` sur `Role.code`, `Utilisateur.microsoftId`, `Utilisateur.email`. - `Utilisateur.classe` -> nullable : un `RESPONSABLE` n'a pas de classe (seuls les étudiants en ont). -- ERD mis à jour en miroir (`UK`, `nullable`). +- Ajout dans l'ERD des annotations `UK` (sur `code`, `microsoft_id`, `email`) et `nullable` (sur `classe`). - Choix laissé de côté : tailles `@db.NVarChar(n)` (colonnes restées en `NVARCHAR(1000)` par défaut). À reconsidérer plus tard. ### Étape 3 — Bloc A : Infrastructure (SallePret, PosteEmprunt) — commit `d026b75` -- `SallePret` rattachée à `Campus` ; `PosteEmprunt` rattaché à `SallePret`. Conforme à l'ERD. +- `SallePret` rattachée à `Campus` (FK `campus_id`) ; `PosteEmprunt` rattaché à `SallePret` (FK `salle_pret_id`). ### Étape 4 — Bloc B : CarteEtudiante — commit `dfa73cd` - Rattachée à `Utilisateur`. - Dette notée : `qr_code` devrait être `@unique` (sert à l'identification, RG02) — non posé pour rester fidèle à l'ERD. ### Étape 5 — Bloc C : Matériel (Materiel, Accessoire, MaterielAccessoire) — commit `0774ee3` -- Table de liaison `MaterielAccessoire` (sans timestamps, conforme ERD). +- Table de liaison `MaterielAccessoire` entre `Materiel` et `Accessoire`, sans colonnes `created_at`/`updated_at`. - Dette notée : `@@unique([materielId, accessoireId])` et `Materiel.statut @default("DISPONIBLE")`. ### Étape 6 — Bloc D : Transactions (Emprunt, Checklist, ChecklistElement) — commit `6908a2b` - Champs de retour/optionnels nullable : `dateRetourReelle`, `modeIdentificationRetour`, `commentaireDepart`, `commentaireRetour`, `Checklist.commentaire`, `ChecklistElement.commentaire`. - Pourquoi : RG12 crée l'emprunt au départ ; les infos de retour n'existent pas encore. En `NOT NULL`, la création serait impossible. -- `ChecklistElement.accessoireId` nullable : conforme au diagramme de classes (`Accessoire "0..1"`), un élément peut être un libellé libre sans accessoire catalogué. +- `ChecklistElement.accessoireId` rendu nullable : un élément de checklist peut être un libellé libre, sans accessoire catalogué associé (cardinalité 0..1). - Les 9 FK en `NoAction` : traçabilité + évitement des cascade paths. -- ERD mis à jour en miroir (champs annotés `nullable`). +- Annotation `nullable` ajoutée dans l'ERD sur les champs de retour/commentaires d'`Emprunt` et `accessoire_id`/`commentaire` de checklist. ### Étape 7 — Stratégie Git : branche `develop` - Création de `develop` depuis `master`. Désormais tous les commits vont sur `develop` ; `master` figé au Bloc C. @@ -75,8 +75,8 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Double relation `Anomalie` vers `Utilisateur` nommée : `@relation("AnomalieEtudiant")` (étudiant concerné) et `@relation("AnomalieResponsable")` (responsable qui traite). - Defaults : `Anomalie.detecteeAutomatiquement @default(true)`, `Notification.lu @default(false)`, `Notification.dateCreation @default(now())`. - Les 12 FK en `NoAction` (traçabilité). -- `Notification` et `Historique` sans `updatedAt` (conforme ERD). -- ERD mis à jour en miroir. +- `Notification` et `Historique` créées sans colonne `updatedAt` (seulement `date_creation`/`created_at`). +- Annotation `nullable` ajoutée dans l'ERD sur `traitee_par_id`, `observation`, `date_resolution`, `anomalie_id` et les 5 FK optionnelles d'`Historique`. ### Étape 9 — Seeds de référence — commit `2c3a5db` - Fichier `prisma/seed.ts` autonome (adapter MSSQL, lecture directe des variables DB), lancé via `npm run seed` (script ajouté au `package.json`). @@ -108,7 +108,7 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y ### Étape 12 — Block 1 : ESLint + Prettier - ESLint 10 (flat config `eslint.config.mjs`) + typescript-eslint 8 ; Prettier 3 (`.prettierrc.json`). -- Règles du `CLAUDE.md` encodées : `no-explicit-any`, `no-non-null-assertion`, `explicit-module-boundary-types`, `no-unused-vars` (ignore le préfixe `_`), `prefer-const`. `eslint-config-prettier` désactive les règles en conflit avec Prettier. +- Règles de qualité strictes encodées : `no-explicit-any`, `no-non-null-assertion`, `explicit-module-boundary-types`, `no-unused-vars` (ignore le préfixe `_`), `prefer-const`. `eslint-config-prettier` désactive les règles en conflit avec Prettier. - Prettier : quotes simples, point-virgule, 100 colonnes, 2 espaces, trailing commas. - Scripts : `lint`, `lint:fix`, `format`, `format:check`. - État : lint vert sur tout le code existant ; formatage appliqué (3 fichiers) ; build OK. @@ -118,6 +118,13 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Correctif : `tsconfig.json` élargi à `src` + `prisma` en `noEmit` (IDE et `tsc --noEmit`) ; nouveau `tsconfig.build.json` dédié à la compilation `src` -> `dist` (`npm run build`). - Bénéfice : les scripts Prisma sont désormais type-checkés (angle mort comblé). +### Étape 14 — Block 4 : architecture en couches + 1er endpoint (catalogue matériel) +- Mise en place de l'architecture en couches : `routes/` -> `controllers/` -> `services/` -> `repositories/` (un dossier par couche). +- Auth simulée temporaire : middleware `current-user` qui résout l'utilisateur via l'en-tête `x-user-email` et pose `req.user` (typé via `types/authenticated-user.ts` + augmentation `Express.Request`). À remplacer par la validation JWT au Block 3. +- Endpoint `GET /api/materiels` (catalogue) : applique RG10 (matériels `DISPONIBLE` du campus de l'étudiant), filtres `categorieId` et `q` (recherche nom/marque/modèle/référence). +- Format de réponse standardisé : `{ "data": ... }` (cohérent avec `{ "error": ... }`). +- Vérifié au runtime : 401 sans en-tête / utilisateur inconnu, 200 avec les disponibles du campus, filtres OK, 400 sur `categorieId` invalide. + --- ## Dette technique en attente @@ -132,4 +139,4 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y --- -*Dernière mise à jour : 2026-06-15 — Tooling backend finalisé (ESLint, Prettier, tsconfig split).* +*Dernière mise à jour : 2026-06-18 — 1er endpoint (catalogue matériel) et architecture en couches en place.*