From f7f61a8ebdf99c1043da02dfc5da4bef421cfdb2 Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Wed, 15 Jul 2026 11:47:36 +0200 Subject: [PATCH] feat(backend): add responsable emprunts endpoint --- .../src/controllers/responsable.controller.ts | 22 ++++++- eme-backend/src/dtos/responsable.dto.ts | 60 ++++++++++++++++++- .../repositories/responsable.repository.ts | 28 +++++++++ eme-backend/src/routes/responsable.routes.ts | 3 +- .../src/services/responsable.service.ts | 36 ++++++++++- review.md | 10 +++- 6 files changed, 151 insertions(+), 8 deletions(-) diff --git a/eme-backend/src/controllers/responsable.controller.ts b/eme-backend/src/controllers/responsable.controller.ts index 87ad7d5..9d18cca 100644 --- a/eme-backend/src/controllers/responsable.controller.ts +++ b/eme-backend/src/controllers/responsable.controller.ts @@ -1,7 +1,14 @@ import { Request, Response } from 'express'; import { AppError } from '../errors/app-error'; -import { getDashboardResponsable } from '../services/responsable.service'; -import { toDashboardResponsableResponse } from '../dtos/responsable.dto'; +import { + getDashboardResponsable, + listerEmpruntsResponsable, + parseStatutEmprunt, +} from '../services/responsable.service'; +import { + toDashboardResponsableResponse, + toEmpruntResponsableResponse, +} from '../dtos/responsable.dto'; export async function getDashboard(req: Request, res: Response): Promise { const user = req.user; @@ -12,3 +19,14 @@ export async function getDashboard(req: Request, res: Response): Promise { const dashboard = await getDashboardResponsable(user.id, user.roleCode, user.campusId); res.json({ data: toDashboardResponsableResponse(dashboard) }); } + +export async function getEmprunts(req: Request, res: Response): Promise { + const user = req.user; + if (!user) { + throw new AppError(401, 'Authentification requise'); + } + + const statut = parseStatutEmprunt(req.query.statut); + const emprunts = await listerEmpruntsResponsable(user.roleCode, user.campusId, { statut }); + res.json({ data: emprunts.map(toEmpruntResponsableResponse) }); +} diff --git a/eme-backend/src/dtos/responsable.dto.ts b/eme-backend/src/dtos/responsable.dto.ts index 6ae3a3d..c672895 100644 --- a/eme-backend/src/dtos/responsable.dto.ts +++ b/eme-backend/src/dtos/responsable.dto.ts @@ -1,4 +1,8 @@ -import { ActiviteResponsable, StatutCount } from '../repositories/responsable.repository'; +import { + ActiviteResponsable, + EmpruntResponsable, + StatutCount, +} from '../repositories/responsable.repository'; import { DashboardResponsableData } from '../services/responsable.service'; export interface RepartitionStatutResponse { @@ -34,6 +38,31 @@ export interface DashboardResponsableResponse { activiteRecente: ActiviteResponsableResponse[]; } +export interface EmpruntResponsableResponse { + id: number; + statut: string; + dateEmprunt: Date; + dateRetourPrevue: Date; + dateRetourReelle: Date | null; + etudiant: { + id: number; + nom: string; + prenom: string; + email: string; + classe: string | null; + }; + materiel: { + id: number; + nom: string; + reference: string; + statut: string; + categorie: { + id: number; + nom: string; + }; + }; +} + function toRepartition(rows: StatutCount[]): RepartitionStatutResponse { const parStatut: Record = {}; let total = 0; @@ -81,3 +110,32 @@ export function toDashboardResponsableResponse( activiteRecente: data.activiteRecente.map(toActiviteResponse), }; } + +export function toEmpruntResponsableResponse( + emprunt: EmpruntResponsable, +): EmpruntResponsableResponse { + return { + id: emprunt.id, + statut: emprunt.statut, + dateEmprunt: emprunt.dateEmprunt, + dateRetourPrevue: emprunt.dateRetourPrevue, + dateRetourReelle: emprunt.dateRetourReelle, + etudiant: { + id: emprunt.utilisateur.id, + nom: emprunt.utilisateur.nom, + prenom: emprunt.utilisateur.prenom, + email: emprunt.utilisateur.email, + classe: emprunt.utilisateur.classe, + }, + materiel: { + id: emprunt.materiel.id, + nom: emprunt.materiel.nom, + reference: emprunt.materiel.reference, + statut: emprunt.materiel.statut, + categorie: { + id: emprunt.materiel.categorie.id, + nom: emprunt.materiel.categorie.nom, + }, + }, + }; +} diff --git a/eme-backend/src/repositories/responsable.repository.ts b/eme-backend/src/repositories/responsable.repository.ts index af9b2c6..a19df62 100644 --- a/eme-backend/src/repositories/responsable.repository.ts +++ b/eme-backend/src/repositories/responsable.repository.ts @@ -14,6 +14,17 @@ export type ActiviteResponsable = Prisma.HistoriqueGetPayload<{ }; }>; +export type EmpruntResponsable = Prisma.EmpruntGetPayload<{ + include: { + utilisateur: true; + materiel: { include: { categorie: true } }; + }; +}>; + +export interface EmpruntResponsableFiltres { + statut?: string; +} + function mapStatutCounts(rows: Array<{ statut: string; _count: { _all: number } }>): StatutCount[] { return rows.map((row) => ({ statut: row.statut, count: row._count._all })); } @@ -69,3 +80,20 @@ export function findActiviteRecente( take: limite, }); } + +export function findEmpruntsResponsable( + campusId: number, + filtres: EmpruntResponsableFiltres, +): Promise { + return prisma.emprunt.findMany({ + where: { + campusId, + ...(filtres.statut ? { statut: filtres.statut } : {}), + }, + include: { + utilisateur: true, + materiel: { include: { categorie: true } }, + }, + orderBy: { dateEmprunt: 'desc' }, + }); +} diff --git a/eme-backend/src/routes/responsable.routes.ts b/eme-backend/src/routes/responsable.routes.ts index f7e19b1..74b6b50 100644 --- a/eme-backend/src/routes/responsable.routes.ts +++ b/eme-backend/src/routes/responsable.routes.ts @@ -1,6 +1,7 @@ import { Router } from 'express'; -import { getDashboard } from '../controllers/responsable.controller'; +import { getDashboard, getEmprunts } from '../controllers/responsable.controller'; export const responsableRoutes: Router = Router(); responsableRoutes.get('/dashboard', getDashboard); +responsableRoutes.get('/emprunts', getEmprunts); diff --git a/eme-backend/src/services/responsable.service.ts b/eme-backend/src/services/responsable.service.ts index bd761c2..a367ac6 100644 --- a/eme-backend/src/services/responsable.service.ts +++ b/eme-backend/src/services/responsable.service.ts @@ -5,9 +5,12 @@ import { countMaterielsParStatut, countNotificationsNonLues, findActiviteRecente, + findEmpruntsResponsable, StatutCount, ActiviteResponsable, + EmpruntResponsable, } from '../repositories/responsable.repository'; +import { STATUT_EMPRUNT, StatutEmprunt } from '../models/enums'; export interface DashboardResponsableData { materiels: StatutCount[]; @@ -17,14 +20,22 @@ export interface DashboardResponsableData { activiteRecente: ActiviteResponsable[]; } +export interface ListerEmpruntsResponsableFiltres { + statut?: StatutEmprunt; +} + +function verifierResponsable(roleCode: string): void { + if (roleCode !== 'RESPONSABLE') { + throw new AppError(403, 'Acces reserve au responsable materiel'); + } +} + export async function getDashboardResponsable( utilisateurId: number, roleCode: string, campusId: number, ): Promise { - if (roleCode !== 'RESPONSABLE') { - throw new AppError(403, 'Acces reserve au responsable materiel'); - } + verifierResponsable(roleCode); const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] = await Promise.all([ @@ -43,3 +54,22 @@ export async function getDashboardResponsable( activiteRecente, }; } + +export function parseStatutEmprunt(value: unknown): StatutEmprunt | undefined { + if (value === undefined) { + return undefined; + } + if (typeof value !== 'string' || !(STATUT_EMPRUNT as readonly string[]).includes(value)) { + throw new AppError(400, 'statut invalide'); + } + return value as StatutEmprunt; +} + +export function listerEmpruntsResponsable( + roleCode: string, + campusId: number, + filtres: ListerEmpruntsResponsableFiltres, +): Promise { + verifierResponsable(roleCode); + return findEmpruntsResponsable(campusId, filtres); +} diff --git a/review.md b/review.md index 871c4f1..dcc9121 100644 --- a/review.md +++ b/review.md @@ -332,6 +332,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Activité récente exposée depuis l'historique, avec utilisateur, matériel et emprunt associé. - Vérifié au runtime : Karim (`RESPONSABLE`) obtient 200 avec KPIs ; Lucas (`ETUDIANT`) obtient 403. +### Étape 35 — API responsable : consultation des emprunts +- Branche dédiée `feat/responsable-emprunts-api` créée après merge du dashboard responsable dans `develop`. +- Endpoint `GET /api/responsable/emprunts` ajouté, protégé par rôle `RESPONSABLE`. +- Filtrage par campus du responsable appliqué systématiquement (RG27). +- Filtre optionnel `statut` ajouté pour RG28 : `EN_COURS`, `EN_RETARD`, `CLOTURE`, `RETOUR_NON_CONFORME`, `ANNULE`. +- 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. + --- -*Dernière mise à jour : 2026-07-15 — Premier endpoint responsable ajouté et testé.* +*Dernière mise à jour : 2026-07-15 — Dashboard et consultation des emprunts responsable ajoutés et testés.*