From c76f546f2a2e7bffbbcb12b8c3c6bab5b1144f3e Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Wed, 15 Jul 2026 12:35:16 +0200 Subject: [PATCH] feat(backend): add responsable stock endpoint --- .../src/controllers/responsable.controller.ts | 22 +++++++++ eme-backend/src/dtos/responsable.dto.ts | 45 +++++++++++++++++++ .../repositories/responsable.repository.ts | 42 +++++++++++++++++ eme-backend/src/routes/responsable.routes.ts | 3 +- .../src/services/responsable.service.ts | 42 ++++++++++++++++- review.md | 11 ++++- 6 files changed, 162 insertions(+), 3 deletions(-) diff --git a/eme-backend/src/controllers/responsable.controller.ts b/eme-backend/src/controllers/responsable.controller.ts index 9d18cca..c6aaf76 100644 --- a/eme-backend/src/controllers/responsable.controller.ts +++ b/eme-backend/src/controllers/responsable.controller.ts @@ -2,12 +2,16 @@ import { Request, Response } from 'express'; import { AppError } from '../errors/app-error'; import { getDashboardResponsable, + listerMaterielsResponsable, listerEmpruntsResponsable, + parseCategorieId, + parseStatutMateriel, parseStatutEmprunt, } from '../services/responsable.service'; import { toDashboardResponsableResponse, toEmpruntResponsableResponse, + toMaterielResponsableResponse, } from '../dtos/responsable.dto'; export async function getDashboard(req: Request, res: Response): Promise { @@ -30,3 +34,21 @@ export async function getEmprunts(req: Request, res: Response): Promise { const emprunts = await listerEmpruntsResponsable(user.roleCode, user.campusId, { statut }); res.json({ data: emprunts.map(toEmpruntResponsableResponse) }); } + +export async function getMateriels(req: Request, res: Response): Promise { + const user = req.user; + if (!user) { + throw new AppError(401, 'Authentification requise'); + } + + const statut = parseStatutMateriel(req.query.statut); + const categorieId = parseCategorieId(req.query.categorieId); + const recherche = typeof req.query.q === 'string' ? req.query.q : undefined; + + const materiels = await listerMaterielsResponsable(user.roleCode, user.campusId, { + statut, + categorieId, + recherche, + }); + res.json({ data: materiels.map(toMaterielResponsableResponse) }); +} diff --git a/eme-backend/src/dtos/responsable.dto.ts b/eme-backend/src/dtos/responsable.dto.ts index c672895..b8e9b39 100644 --- a/eme-backend/src/dtos/responsable.dto.ts +++ b/eme-backend/src/dtos/responsable.dto.ts @@ -1,6 +1,7 @@ import { ActiviteResponsable, EmpruntResponsable, + MaterielResponsable, StatutCount, } from '../repositories/responsable.repository'; import { DashboardResponsableData } from '../services/responsable.service'; @@ -63,6 +64,26 @@ export interface EmpruntResponsableResponse { }; } +export interface MaterielResponsableResponse { + id: number; + nom: string; + marque: string; + modele: string; + reference: string; + statut: string; + etatGeneral: string; + categorie: { + id: number; + nom: string; + }; + accessoires: Array<{ + id: number; + nom: string; + quantiteAttendue: number; + obligatoire: boolean; + }>; +} + function toRepartition(rows: StatutCount[]): RepartitionStatutResponse { const parStatut: Record = {}; let total = 0; @@ -139,3 +160,27 @@ export function toEmpruntResponsableResponse( }, }; } + +export function toMaterielResponsableResponse( + materiel: MaterielResponsable, +): MaterielResponsableResponse { + return { + id: materiel.id, + nom: materiel.nom, + marque: materiel.marque, + modele: materiel.modele, + reference: materiel.reference, + statut: materiel.statut, + etatGeneral: materiel.etatGeneral, + categorie: { + id: materiel.categorie.id, + nom: materiel.categorie.nom, + }, + accessoires: materiel.accessoires.map((liaison) => ({ + id: liaison.accessoire.id, + nom: liaison.accessoire.nom, + quantiteAttendue: liaison.quantiteAttendue, + obligatoire: liaison.obligatoire, + })), + }; +} diff --git a/eme-backend/src/repositories/responsable.repository.ts b/eme-backend/src/repositories/responsable.repository.ts index a19df62..b12c06e 100644 --- a/eme-backend/src/repositories/responsable.repository.ts +++ b/eme-backend/src/repositories/responsable.repository.ts @@ -21,10 +21,23 @@ export type EmpruntResponsable = Prisma.EmpruntGetPayload<{ }; }>; +export type MaterielResponsable = Prisma.MaterielGetPayload<{ + include: { + categorie: true; + accessoires: { include: { accessoire: true } }; + }; +}>; + export interface EmpruntResponsableFiltres { statut?: string; } +export interface MaterielResponsableFiltres { + statut?: string; + categorieId?: number; + recherche?: string; +} + function mapStatutCounts(rows: Array<{ statut: string; _count: { _all: number } }>): StatutCount[] { return rows.map((row) => ({ statut: row.statut, count: row._count._all })); } @@ -97,3 +110,32 @@ export function findEmpruntsResponsable( orderBy: { dateEmprunt: 'desc' }, }); } + +export function findMaterielsResponsable( + campusId: number, + filtres: MaterielResponsableFiltres, +): Promise { + return prisma.materiel.findMany({ + where: { + campusId, + actif: true, + ...(filtres.statut ? { statut: filtres.statut } : {}), + ...(filtres.categorieId !== undefined ? { categorieId: filtres.categorieId } : {}), + ...(filtres.recherche + ? { + OR: [ + { nom: { contains: filtres.recherche } }, + { marque: { contains: filtres.recherche } }, + { modele: { contains: filtres.recherche } }, + { reference: { contains: filtres.recherche } }, + ], + } + : {}), + }, + include: { + categorie: true, + accessoires: { include: { accessoire: true } }, + }, + orderBy: { nom: 'asc' }, + }); +} diff --git a/eme-backend/src/routes/responsable.routes.ts b/eme-backend/src/routes/responsable.routes.ts index 74b6b50..a122977 100644 --- a/eme-backend/src/routes/responsable.routes.ts +++ b/eme-backend/src/routes/responsable.routes.ts @@ -1,7 +1,8 @@ import { Router } from 'express'; -import { getDashboard, getEmprunts } from '../controllers/responsable.controller'; +import { getDashboard, getEmprunts, getMateriels } from '../controllers/responsable.controller'; export const responsableRoutes: Router = Router(); responsableRoutes.get('/dashboard', getDashboard); responsableRoutes.get('/emprunts', getEmprunts); +responsableRoutes.get('/materiels', getMateriels); diff --git a/eme-backend/src/services/responsable.service.ts b/eme-backend/src/services/responsable.service.ts index a367ac6..e0753d7 100644 --- a/eme-backend/src/services/responsable.service.ts +++ b/eme-backend/src/services/responsable.service.ts @@ -6,11 +6,13 @@ import { countNotificationsNonLues, findActiviteRecente, findEmpruntsResponsable, + findMaterielsResponsable, StatutCount, ActiviteResponsable, EmpruntResponsable, + MaterielResponsable, } from '../repositories/responsable.repository'; -import { STATUT_EMPRUNT, StatutEmprunt } from '../models/enums'; +import { STATUT_EMPRUNT, STATUT_MATERIEL, StatutEmprunt, StatutMateriel } from '../models/enums'; export interface DashboardResponsableData { materiels: StatutCount[]; @@ -24,6 +26,12 @@ export interface ListerEmpruntsResponsableFiltres { statut?: StatutEmprunt; } +export interface ListerMaterielsResponsableFiltres { + statut?: StatutMateriel; + categorieId?: number; + recherche?: string; +} + function verifierResponsable(roleCode: string): void { if (roleCode !== 'RESPONSABLE') { throw new AppError(403, 'Acces reserve au responsable materiel'); @@ -65,6 +73,29 @@ export function parseStatutEmprunt(value: unknown): StatutEmprunt | undefined { return value as StatutEmprunt; } +export function parseStatutMateriel(value: unknown): StatutMateriel | undefined { + if (value === undefined) { + return undefined; + } + if (typeof value !== 'string' || !(STATUT_MATERIEL as readonly string[]).includes(value)) { + throw new AppError(400, 'statut invalide'); + } + return value as StatutMateriel; +} + +export 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 function listerEmpruntsResponsable( roleCode: string, campusId: number, @@ -73,3 +104,12 @@ export function listerEmpruntsResponsable( verifierResponsable(roleCode); return findEmpruntsResponsable(campusId, filtres); } + +export function listerMaterielsResponsable( + roleCode: string, + campusId: number, + filtres: ListerMaterielsResponsableFiltres, +): Promise { + verifierResponsable(roleCode); + return findMaterielsResponsable(campusId, filtres); +} diff --git a/review.md b/review.md index dcc9121..fa1404f 100644 --- a/review.md +++ b/review.md @@ -340,6 +340,15 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Réponse enrichie avec l'étudiant, le matériel, la catégorie, les dates et le statut. - Vérifié au runtime : Karim obtient les emprunts du campus, `statut=EN_COURS` filtre correctement, statut invalide renvoie 400, Lucas obtient 403. +### Étape 36 — API responsable : consultation du stock +- Branche dédiée `feat/responsable-stock-api` créée après merge de la consultation des emprunts responsable dans `develop`. +- Endpoint `GET /api/responsable/materiels` ajouté, protégé par rôle `RESPONSABLE`. +- Filtrage par campus du responsable appliqué systématiquement (RG27/RG29). +- Filtres optionnels ajoutés : `statut`, `categorieId`, `q` (nom, marque, modèle, référence). +- Contrairement au catalogue étudiant, cet endpoint retourne tout le stock actif du campus, quel que soit le statut. +- Réponse enrichie avec catégorie et accessoires attendus. +- Vérifié au runtime : Karim obtient tout le stock du campus, `statut=DISPONIBLE` et `q=MacBook` filtrent correctement, statut invalide renvoie 400, Lucas obtient 403. + --- -*Dernière mise à jour : 2026-07-15 — Dashboard et consultation des emprunts responsable ajoutés et testés.* +*Dernière mise à jour : 2026-07-15 — Dashboard, consultation des emprunts et consultation du stock responsable ajoutés et testés.*