feat(backend): add responsable dashboard endpoint

This commit is contained in:
SaidSoighiri94
2026-07-15 11:04:09 +02:00
parent e2165b9826
commit a3795b41a0
7 changed files with 230 additions and 1 deletions
@@ -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<void> {
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) });
}
+83
View File
@@ -0,0 +1,83 @@
import { ActiviteResponsable, StatutCount } from '../repositories/responsable.repository';
import { DashboardResponsableData } from '../services/responsable.service';
export interface RepartitionStatutResponse {
total: number;
parStatut: Record<string, number>;
}
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<string, number> = {};
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),
};
}
@@ -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<StatutCount[]> {
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<StatutCount[]> {
const rows = await prisma.emprunt.groupBy({
by: ['statut'],
where: { campusId },
_count: { _all: true },
});
return mapStatutCounts(rows);
}
export async function countAnomaliesParStatut(campusId: number): Promise<StatutCount[]> {
const rows = await prisma.anomalie.groupBy({
by: ['statut'],
where: { emprunt: { campusId } },
_count: { _all: true },
});
return mapStatutCounts(rows);
}
export function countNotificationsNonLues(utilisateurId: number): Promise<number> {
return prisma.notification.count({
where: { utilisateurId, lu: false },
});
}
export function findActiviteRecente(
campusId: number,
limite: number,
): Promise<ActiviteResponsable[]> {
return prisma.historique.findMany({
where: { campusId },
include: {
utilisateur: true,
materiel: true,
emprunt: true,
},
orderBy: { dateAction: 'desc' },
take: limite,
});
}
+2
View File
@@ -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);
@@ -0,0 +1,6 @@
import { Router } from 'express';
import { getDashboard } from '../controllers/responsable.controller';
export const responsableRoutes: Router = Router();
responsableRoutes.get('/dashboard', getDashboard);
@@ -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<DashboardResponsableData> {
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,
};
}
+9 -1
View File
@@ -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-15Premier endpoint responsable ajouté et testé.*