From 1ce023de710210fa809b5aeb3ed4ff8ae4c8fc89 Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Wed, 15 Jul 2026 15:30:24 +0200 Subject: [PATCH] feat(backend): add responsable anomalies endpoint --- .../src/controllers/responsable.controller.ts | 19 ++++ eme-backend/src/dtos/responsable.dto.ts | 87 +++++++++++++++++++ .../repositories/responsable.repository.ts | 34 ++++++++ eme-backend/src/routes/responsable.routes.ts | 8 +- .../src/services/responsable.service.ts | 42 ++++++++- review.md | 10 ++- 6 files changed, 194 insertions(+), 6 deletions(-) diff --git a/eme-backend/src/controllers/responsable.controller.ts b/eme-backend/src/controllers/responsable.controller.ts index c6aaf76..9d2ae2c 100644 --- a/eme-backend/src/controllers/responsable.controller.ts +++ b/eme-backend/src/controllers/responsable.controller.ts @@ -2,13 +2,16 @@ import { Request, Response } from 'express'; import { AppError } from '../errors/app-error'; import { getDashboardResponsable, + listerAnomaliesResponsable, listerMaterielsResponsable, listerEmpruntsResponsable, parseCategorieId, + parseStatutAnomalie, parseStatutMateriel, parseStatutEmprunt, } from '../services/responsable.service'; import { + toAnomalieResponsableResponse, toDashboardResponsableResponse, toEmpruntResponsableResponse, toMaterielResponsableResponse, @@ -52,3 +55,19 @@ export async function getMateriels(req: Request, res: Response): Promise { }); res.json({ data: materiels.map(toMaterielResponsableResponse) }); } + +export async function getAnomalies(req: Request, res: Response): Promise { + const user = req.user; + if (!user) { + throw new AppError(401, 'Authentification requise'); + } + + const statut = parseStatutAnomalie(req.query.statut); + const type = typeof req.query.type === 'string' ? req.query.type : undefined; + + const anomalies = await listerAnomaliesResponsable(user.roleCode, user.campusId, { + statut, + type, + }); + res.json({ data: anomalies.map(toAnomalieResponsableResponse) }); +} diff --git a/eme-backend/src/dtos/responsable.dto.ts b/eme-backend/src/dtos/responsable.dto.ts index b8e9b39..18dcfa7 100644 --- a/eme-backend/src/dtos/responsable.dto.ts +++ b/eme-backend/src/dtos/responsable.dto.ts @@ -1,5 +1,6 @@ import { ActiviteResponsable, + AnomalieResponsable, EmpruntResponsable, MaterielResponsable, StatutCount, @@ -84,6 +85,46 @@ export interface MaterielResponsableResponse { }>; } +export interface AnomalieResponsableResponse { + id: number; + type: string; + description: string; + statut: string; + detecteeAutomatiquement: boolean; + observation: string | null; + dateDetection: Date; + dateResolution: Date | null; + emprunt: { + 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; + }; + }; + traitePar: { + id: number; + nom: string; + prenom: string; + } | null; +} + function toRepartition(rows: StatutCount[]): RepartitionStatutResponse { const parStatut: Record = {}; let total = 0; @@ -184,3 +225,49 @@ export function toMaterielResponsableResponse( })), }; } + +export function toAnomalieResponsableResponse( + anomalie: AnomalieResponsable, +): AnomalieResponsableResponse { + return { + id: anomalie.id, + type: anomalie.type, + description: anomalie.description, + statut: anomalie.statut, + detecteeAutomatiquement: anomalie.detecteeAutomatiquement, + observation: anomalie.observation, + dateDetection: anomalie.dateDetection, + dateResolution: anomalie.dateResolution, + emprunt: { + id: anomalie.emprunt.id, + statut: anomalie.emprunt.statut, + dateEmprunt: anomalie.emprunt.dateEmprunt, + dateRetourPrevue: anomalie.emprunt.dateRetourPrevue, + dateRetourReelle: anomalie.emprunt.dateRetourReelle, + }, + etudiant: { + id: anomalie.etudiant.id, + nom: anomalie.etudiant.nom, + prenom: anomalie.etudiant.prenom, + email: anomalie.etudiant.email, + classe: anomalie.etudiant.classe, + }, + materiel: { + id: anomalie.materiel.id, + nom: anomalie.materiel.nom, + reference: anomalie.materiel.reference, + statut: anomalie.materiel.statut, + categorie: { + id: anomalie.materiel.categorie.id, + nom: anomalie.materiel.categorie.nom, + }, + }, + traitePar: anomalie.traitePar + ? { + id: anomalie.traitePar.id, + nom: anomalie.traitePar.nom, + prenom: anomalie.traitePar.prenom, + } + : null, + }; +} diff --git a/eme-backend/src/repositories/responsable.repository.ts b/eme-backend/src/repositories/responsable.repository.ts index b12c06e..9bdf16b 100644 --- a/eme-backend/src/repositories/responsable.repository.ts +++ b/eme-backend/src/repositories/responsable.repository.ts @@ -28,6 +28,15 @@ export type MaterielResponsable = Prisma.MaterielGetPayload<{ }; }>; +export type AnomalieResponsable = Prisma.AnomalieGetPayload<{ + include: { + emprunt: true; + etudiant: true; + materiel: { include: { categorie: true } }; + traitePar: true; + }; +}>; + export interface EmpruntResponsableFiltres { statut?: string; } @@ -38,6 +47,11 @@ export interface MaterielResponsableFiltres { recherche?: string; } +export interface AnomalieResponsableFiltres { + statut?: string; + type?: string; +} + function mapStatutCounts(rows: Array<{ statut: string; _count: { _all: number } }>): StatutCount[] { return rows.map((row) => ({ statut: row.statut, count: row._count._all })); } @@ -139,3 +153,23 @@ export function findMaterielsResponsable( orderBy: { nom: 'asc' }, }); } + +export function findAnomaliesResponsable( + campusId: number, + filtres: AnomalieResponsableFiltres, +): Promise { + return prisma.anomalie.findMany({ + where: { + emprunt: { campusId }, + ...(filtres.statut ? { statut: filtres.statut } : {}), + ...(filtres.type ? { type: filtres.type } : {}), + }, + include: { + emprunt: true, + etudiant: true, + materiel: { include: { categorie: true } }, + traitePar: true, + }, + orderBy: { dateDetection: 'desc' }, + }); +} diff --git a/eme-backend/src/routes/responsable.routes.ts b/eme-backend/src/routes/responsable.routes.ts index a122977..b534c26 100644 --- a/eme-backend/src/routes/responsable.routes.ts +++ b/eme-backend/src/routes/responsable.routes.ts @@ -1,8 +1,14 @@ import { Router } from 'express'; -import { getDashboard, getEmprunts, getMateriels } from '../controllers/responsable.controller'; +import { + getAnomalies, + getDashboard, + getEmprunts, + getMateriels, +} from '../controllers/responsable.controller'; export const responsableRoutes: Router = Router(); responsableRoutes.get('/dashboard', getDashboard); responsableRoutes.get('/emprunts', getEmprunts); responsableRoutes.get('/materiels', getMateriels); +responsableRoutes.get('/anomalies', getAnomalies); diff --git a/eme-backend/src/services/responsable.service.ts b/eme-backend/src/services/responsable.service.ts index e0753d7..9b3b290 100644 --- a/eme-backend/src/services/responsable.service.ts +++ b/eme-backend/src/services/responsable.service.ts @@ -5,14 +5,23 @@ import { countMaterielsParStatut, countNotificationsNonLues, findActiviteRecente, + findAnomaliesResponsable, findEmpruntsResponsable, findMaterielsResponsable, StatutCount, ActiviteResponsable, + AnomalieResponsable, EmpruntResponsable, MaterielResponsable, } from '../repositories/responsable.repository'; -import { STATUT_EMPRUNT, STATUT_MATERIEL, StatutEmprunt, StatutMateriel } from '../models/enums'; +import { + STATUT_ANOMALIE, + STATUT_EMPRUNT, + STATUT_MATERIEL, + StatutAnomalie, + StatutEmprunt, + StatutMateriel, +} from '../models/enums'; export interface DashboardResponsableData { materiels: StatutCount[]; @@ -32,6 +41,11 @@ export interface ListerMaterielsResponsableFiltres { recherche?: string; } +export interface ListerAnomaliesResponsableFiltres { + statut?: StatutAnomalie; + type?: string; +} + function verifierResponsable(roleCode: string): void { if (roleCode !== 'RESPONSABLE') { throw new AppError(403, 'Acces reserve au responsable materiel'); @@ -45,14 +59,15 @@ export async function getDashboardResponsable( ): Promise { verifierResponsable(roleCode); - const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] = - await Promise.all([ + const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] = await Promise.all( + [ countMaterielsParStatut(campusId), countEmpruntsParStatut(campusId), countAnomaliesParStatut(campusId), countNotificationsNonLues(utilisateurId), findActiviteRecente(campusId, 10), - ]); + ], + ); return { materiels, @@ -83,6 +98,16 @@ export function parseStatutMateriel(value: unknown): StatutMateriel | undefined return value as StatutMateriel; } +export function parseStatutAnomalie(value: unknown): StatutAnomalie | undefined { + if (value === undefined) { + return undefined; + } + if (typeof value !== 'string' || !(STATUT_ANOMALIE as readonly string[]).includes(value)) { + throw new AppError(400, 'statut invalide'); + } + return value as StatutAnomalie; +} + export function parseCategorieId(value: unknown): number | undefined { if (value === undefined) { return undefined; @@ -113,3 +138,12 @@ export function listerMaterielsResponsable( verifierResponsable(roleCode); return findMaterielsResponsable(campusId, filtres); } + +export function listerAnomaliesResponsable( + roleCode: string, + campusId: number, + filtres: ListerAnomaliesResponsableFiltres, +): Promise { + verifierResponsable(roleCode); + return findAnomaliesResponsable(campusId, filtres); +} diff --git a/review.md b/review.md index fa1404f..89af463 100644 --- a/review.md +++ b/review.md @@ -349,6 +349,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - 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. +### Étape 37 — API responsable : consultation des anomalies +- Branche dédiée `feat/responsable-anomalies-api` créée après merge de la consultation du stock responsable dans `develop`. +- Endpoint `GET /api/responsable/anomalies` ajouté, protégé par rôle `RESPONSABLE`. +- Filtrage par campus du responsable appliqué systématiquement via l'emprunt lié à l'anomalie (RG27). +- Filtres optionnels ajoutés : `statut` (`DETECTEE`, `EN_COURS_TRAITEMENT`, `RESOLUE`, `CLOTUREE`) et `type`. +- Réponse enrichie avec anomalie, emprunt, étudiant concerné, matériel, catégorie et responsable de traitement si renseigné. +- Limite volontaire : consultation uniquement ; le cycle de traitement/résolution reste à développer. + --- -*Dernière mise à jour : 2026-07-15 — Dashboard, consultation des emprunts et consultation du stock responsable ajoutés et testés.* +*Dernière mise à jour : 2026-07-15 — Dashboard, consultation des emprunts, du stock et des anomalies responsable ajoutés côté API.*