From a3795b41a0b9ee2b073376570f0d4d9d18bd61ee Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Wed, 15 Jul 2026 11:04:09 +0200 Subject: [PATCH] feat(backend): add responsable dashboard endpoint --- .../src/controllers/responsable.controller.ts | 14 ++++ eme-backend/src/dtos/responsable.dto.ts | 83 +++++++++++++++++++ .../repositories/responsable.repository.ts | 71 ++++++++++++++++ eme-backend/src/routes/index.ts | 2 + eme-backend/src/routes/responsable.routes.ts | 6 ++ .../src/services/responsable.service.ts | 45 ++++++++++ review.md | 10 ++- 7 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 eme-backend/src/controllers/responsable.controller.ts create mode 100644 eme-backend/src/dtos/responsable.dto.ts create mode 100644 eme-backend/src/repositories/responsable.repository.ts create mode 100644 eme-backend/src/routes/responsable.routes.ts create mode 100644 eme-backend/src/services/responsable.service.ts diff --git a/eme-backend/src/controllers/responsable.controller.ts b/eme-backend/src/controllers/responsable.controller.ts new file mode 100644 index 0000000..87ad7d5 --- /dev/null +++ b/eme-backend/src/controllers/responsable.controller.ts @@ -0,0 +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'; + +export async function getDashboard(req: Request, res: Response): Promise { + const user = req.user; + if (!user) { + throw new AppError(401, 'Authentification requise'); + } + + const dashboard = await getDashboardResponsable(user.id, user.roleCode, user.campusId); + res.json({ data: toDashboardResponsableResponse(dashboard) }); +} diff --git a/eme-backend/src/dtos/responsable.dto.ts b/eme-backend/src/dtos/responsable.dto.ts new file mode 100644 index 0000000..6ae3a3d --- /dev/null +++ b/eme-backend/src/dtos/responsable.dto.ts @@ -0,0 +1,83 @@ +import { ActiviteResponsable, StatutCount } from '../repositories/responsable.repository'; +import { DashboardResponsableData } from '../services/responsable.service'; + +export interface RepartitionStatutResponse { + total: number; + parStatut: Record; +} + +export interface ActiviteResponsableResponse { + id: number; + action: string; + description: string; + dateAction: Date; + utilisateur: { + id: number; + nom: string; + prenom: string; + }; + materiel: { + id: number; + nom: string; + reference: string; + } | null; + empruntId: number | null; +} + +export interface DashboardResponsableResponse { + kpis: { + materiels: RepartitionStatutResponse; + emprunts: RepartitionStatutResponse; + anomalies: RepartitionStatutResponse; + notificationsNonLues: number; + }; + activiteRecente: ActiviteResponsableResponse[]; +} + +function toRepartition(rows: StatutCount[]): RepartitionStatutResponse { + const parStatut: Record = {}; + let total = 0; + + for (const row of rows) { + parStatut[row.statut] = row.count; + total += row.count; + } + + return { total, parStatut }; +} + +function toActiviteResponse(activite: ActiviteResponsable): ActiviteResponsableResponse { + return { + id: activite.id, + action: activite.action, + description: activite.description, + dateAction: activite.dateAction, + utilisateur: { + id: activite.utilisateur.id, + nom: activite.utilisateur.nom, + prenom: activite.utilisateur.prenom, + }, + materiel: activite.materiel + ? { + id: activite.materiel.id, + nom: activite.materiel.nom, + reference: activite.materiel.reference, + } + : null, + empruntId: activite.empruntId, + }; +} + +export function toDashboardResponsableResponse( + data: DashboardResponsableData, +): DashboardResponsableResponse { + return { + kpis: { + materiels: toRepartition(data.materiels), + emprunts: toRepartition(data.emprunts), + anomalies: toRepartition(data.anomalies), + notificationsNonLues: data.notificationsNonLues, + }, + activiteRecente: data.activiteRecente.map(toActiviteResponse), + }; +} diff --git a/eme-backend/src/repositories/responsable.repository.ts b/eme-backend/src/repositories/responsable.repository.ts new file mode 100644 index 0000000..af9b2c6 --- /dev/null +++ b/eme-backend/src/repositories/responsable.repository.ts @@ -0,0 +1,71 @@ +import { Prisma } from '@prisma/client'; +import { prisma } from '../db/prisma'; + +export interface StatutCount { + statut: string; + count: number; +} + +export type ActiviteResponsable = Prisma.HistoriqueGetPayload<{ + include: { + utilisateur: true; + materiel: true; + emprunt: true; + }; +}>; + +function mapStatutCounts(rows: Array<{ statut: string; _count: { _all: number } }>): StatutCount[] { + return rows.map((row) => ({ statut: row.statut, count: row._count._all })); +} + +export async function countMaterielsParStatut(campusId: number): Promise { + const rows = await prisma.materiel.groupBy({ + by: ['statut'], + where: { campusId, actif: true }, + _count: { _all: true }, + }); + + return mapStatutCounts(rows); +} + +export async function countEmpruntsParStatut(campusId: number): Promise { + const rows = await prisma.emprunt.groupBy({ + by: ['statut'], + where: { campusId }, + _count: { _all: true }, + }); + + return mapStatutCounts(rows); +} + +export async function countAnomaliesParStatut(campusId: number): Promise { + const rows = await prisma.anomalie.groupBy({ + by: ['statut'], + where: { emprunt: { campusId } }, + _count: { _all: true }, + }); + + return mapStatutCounts(rows); +} + +export function countNotificationsNonLues(utilisateurId: number): Promise { + return prisma.notification.count({ + where: { utilisateurId, lu: false }, + }); +} + +export function findActiviteRecente( + campusId: number, + limite: number, +): Promise { + return prisma.historique.findMany({ + where: { campusId }, + include: { + utilisateur: true, + materiel: true, + emprunt: true, + }, + orderBy: { dateAction: 'desc' }, + take: limite, + }); +} diff --git a/eme-backend/src/routes/index.ts b/eme-backend/src/routes/index.ts index c956ac6..a6740e0 100644 --- a/eme-backend/src/routes/index.ts +++ b/eme-backend/src/routes/index.ts @@ -3,6 +3,7 @@ import { currentUser } from '../middlewares/current-user'; import { authRoutes } from './auth.routes'; import { materielRoutes } from './materiel.routes'; import { empruntRoutes, mesEmpruntsRoutes } from './emprunt.routes'; +import { responsableRoutes } from './responsable.routes'; export const apiRouter: Router = Router(); @@ -13,3 +14,4 @@ apiRouter.use('/auth', authRoutes); apiRouter.use('/materiels', currentUser, materielRoutes); apiRouter.use('/emprunts', currentUser, empruntRoutes); apiRouter.use('/mes-emprunts', currentUser, mesEmpruntsRoutes); +apiRouter.use('/responsable', currentUser, responsableRoutes); diff --git a/eme-backend/src/routes/responsable.routes.ts b/eme-backend/src/routes/responsable.routes.ts new file mode 100644 index 0000000..f7e19b1 --- /dev/null +++ b/eme-backend/src/routes/responsable.routes.ts @@ -0,0 +1,6 @@ +import { Router } from 'express'; +import { getDashboard } from '../controllers/responsable.controller'; + +export const responsableRoutes: Router = Router(); + +responsableRoutes.get('/dashboard', getDashboard); diff --git a/eme-backend/src/services/responsable.service.ts b/eme-backend/src/services/responsable.service.ts new file mode 100644 index 0000000..bd761c2 --- /dev/null +++ b/eme-backend/src/services/responsable.service.ts @@ -0,0 +1,45 @@ +import { AppError } from '../errors/app-error'; +import { + countAnomaliesParStatut, + countEmpruntsParStatut, + countMaterielsParStatut, + countNotificationsNonLues, + findActiviteRecente, + StatutCount, + ActiviteResponsable, +} from '../repositories/responsable.repository'; + +export interface DashboardResponsableData { + materiels: StatutCount[]; + emprunts: StatutCount[]; + anomalies: StatutCount[]; + notificationsNonLues: number; + activiteRecente: ActiviteResponsable[]; +} + +export async function getDashboardResponsable( + utilisateurId: number, + roleCode: string, + campusId: number, +): Promise { + if (roleCode !== 'RESPONSABLE') { + throw new AppError(403, 'Acces reserve au responsable materiel'); + } + + const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] = + await Promise.all([ + countMaterielsParStatut(campusId), + countEmpruntsParStatut(campusId), + countAnomaliesParStatut(campusId), + countNotificationsNonLues(utilisateurId), + findActiviteRecente(campusId, 10), + ]); + + return { + materiels, + emprunts, + anomalies, + notificationsNonLues, + activiteRecente, + }; +} diff --git a/review.md b/review.md index 8fccfb9..871c4f1 100644 --- a/review.md +++ b/review.md @@ -324,6 +324,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Dette technique reformatée en tableau avec statut et impact V1. - Clarification des étapes frontend statiques historiques : elles sont conservées pour mémoire mais remplacées par les branchements API ultérieurs. +### Étape 34 — API responsable : dashboard +- Branche dédiée `feat/responsable-dashboard-api` créée depuis `develop`. +- Endpoint `GET /api/responsable/dashboard` ajouté, protégé par rôle `RESPONSABLE`. +- Périmètre campus appliqué via `user.campusId` (RG27). +- KPIs exposés : matériels par statut, emprunts par statut, anomalies par statut, notifications non lues. +- 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. + --- -*Dernière mise à jour : 2026-07-09 — État courant du projet et journal de décisions clarifiés.* +*Dernière mise à jour : 2026-07-15 — Premier endpoint responsable ajouté et testé.*