merge: responsable dashboard api
This commit is contained in:
@@ -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) });
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { currentUser } from '../middlewares/current-user';
|
|||||||
import { authRoutes } from './auth.routes';
|
import { authRoutes } from './auth.routes';
|
||||||
import { materielRoutes } from './materiel.routes';
|
import { materielRoutes } from './materiel.routes';
|
||||||
import { empruntRoutes, mesEmpruntsRoutes } from './emprunt.routes';
|
import { empruntRoutes, mesEmpruntsRoutes } from './emprunt.routes';
|
||||||
|
import { responsableRoutes } from './responsable.routes';
|
||||||
|
|
||||||
export const apiRouter: Router = Router();
|
export const apiRouter: Router = Router();
|
||||||
|
|
||||||
@@ -13,3 +14,4 @@ apiRouter.use('/auth', authRoutes);
|
|||||||
apiRouter.use('/materiels', currentUser, materielRoutes);
|
apiRouter.use('/materiels', currentUser, materielRoutes);
|
||||||
apiRouter.use('/emprunts', currentUser, empruntRoutes);
|
apiRouter.use('/emprunts', currentUser, empruntRoutes);
|
||||||
apiRouter.use('/mes-emprunts', currentUser, mesEmpruntsRoutes);
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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.
|
- 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.
|
- 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é.*
|
||||||
|
|||||||
Reference in New Issue
Block a user