feat(backend): add responsable historique endpoint

This commit is contained in:
SaidSoighiri94
2026-07-16 16:45:41 +02:00
parent 832aa51e23
commit 92e25356bf
6 changed files with 254 additions and 1 deletions
@@ -3,6 +3,7 @@ import { AppError } from '../errors/app-error';
import {
getDashboardResponsable,
listerAnomaliesResponsable,
listerHistoriqueResponsable,
listerNotificationsResponsable,
listerMaterielsResponsable,
listerEmpruntsResponsable,
@@ -11,6 +12,8 @@ import {
parseCategorieId,
parseLu,
parseNotificationId,
parseDateQuery,
parsePositiveIntQuery,
parseStatutAnomalie,
parseStatutMateriel,
parseStatutEmprunt,
@@ -19,10 +22,31 @@ import {
toAnomalieResponsableResponse,
toDashboardResponsableResponse,
toEmpruntResponsableResponse,
toHistoriqueResponsableResponse,
toMaterielResponsableResponse,
toNotificationResponsableResponse,
} from '../dtos/responsable.dto';
function getHistoriqueFiltres(req: Request) {
const action = typeof req.query.action === 'string' ? req.query.action : undefined;
return {
action,
utilisateurId: parsePositiveIntQuery(req.query.utilisateurId, 'utilisateurId'),
materielId: parsePositiveIntQuery(req.query.materielId, 'materielId'),
empruntId: parsePositiveIntQuery(req.query.empruntId, 'empruntId'),
dateDebut: parseDateQuery(req.query.dateDebut, 'dateDebut'),
dateFin: parseDateQuery(req.query.dateFin, 'dateFin'),
};
}
function escapeCsv(value: string | number | Date | null): string {
if (value === null) {
return '';
}
const raw = value instanceof Date ? value.toISOString() : String(value);
return `"${raw.replace(/"/g, '""')}"`;
}
export async function getDashboard(req: Request, res: Response): Promise<void> {
const user = req.user;
if (!user) {
@@ -113,3 +137,53 @@ export async function patchNotificationsLues(req: Request, res: Response): Promi
const count = await marquerToutesNotificationsResponsableLues(user.id, user.roleCode);
res.json({ data: { count } });
}
export async function getHistorique(req: Request, res: Response): Promise<void> {
const user = req.user;
if (!user) {
throw new AppError(401, 'Authentification requise');
}
const historique = await listerHistoriqueResponsable(
user.roleCode,
user.campusId,
getHistoriqueFiltres(req),
);
res.json({ data: historique.map(toHistoriqueResponsableResponse) });
}
export async function getHistoriqueCsv(req: Request, res: Response): Promise<void> {
const user = req.user;
if (!user) {
throw new AppError(401, 'Authentification requise');
}
const historique = await listerHistoriqueResponsable(
user.roleCode,
user.campusId,
getHistoriqueFiltres(req),
);
const lignes = [
['id', 'dateAction', 'action', 'description', 'utilisateur', 'email', 'materiel', 'empruntId']
.map(escapeCsv)
.join(','),
...historique.map((item) =>
[
item.id,
item.dateAction,
item.action,
item.description,
`${item.utilisateur.prenom} ${item.utilisateur.nom}`,
item.utilisateur.email,
item.materiel ? `${item.materiel.nom} (${item.materiel.reference})` : null,
item.empruntId,
]
.map(escapeCsv)
.join(','),
),
];
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename="historique-responsable.csv"');
res.send(lignes.join('\n'));
}
+65
View File
@@ -2,6 +2,7 @@ import {
ActiviteResponsable,
AnomalieResponsable,
EmpruntResponsable,
HistoriqueResponsable,
MaterielResponsable,
NotificationResponsable,
StatutCount,
@@ -146,6 +147,33 @@ export interface NotificationResponsableResponse {
} | null;
}
export interface HistoriqueResponsableResponse {
id: number;
action: string;
description: string;
dateAction: Date;
utilisateur: {
id: number;
nom: string;
prenom: string;
email: string;
};
materiel: {
id: number;
nom: string;
reference: string;
} | null;
empruntId: number | null;
sallePret: {
id: number;
nom: string;
} | null;
posteEmprunt: {
id: number;
nom: string;
} | null;
}
function toRepartition(rows: StatutCount[]): RepartitionStatutResponse {
const parStatut: Record<string, number> = {};
let total = 0;
@@ -318,3 +346,40 @@ export function toNotificationResponsableResponse(
: null,
};
}
export function toHistoriqueResponsableResponse(
historique: HistoriqueResponsable,
): HistoriqueResponsableResponse {
return {
id: historique.id,
action: historique.action,
description: historique.description,
dateAction: historique.dateAction,
utilisateur: {
id: historique.utilisateur.id,
nom: historique.utilisateur.nom,
prenom: historique.utilisateur.prenom,
email: historique.utilisateur.email,
},
materiel: historique.materiel
? {
id: historique.materiel.id,
nom: historique.materiel.nom,
reference: historique.materiel.reference,
}
: null,
empruntId: historique.empruntId,
sallePret: historique.sallePret
? {
id: historique.sallePret.id,
nom: historique.sallePret.nom,
}
: null,
posteEmprunt: historique.posteEmprunt
? {
id: historique.posteEmprunt.id,
nom: historique.posteEmprunt.nom,
}
: null,
};
}
@@ -48,6 +48,16 @@ export type NotificationResponsable = Prisma.NotificationGetPayload<{
};
}>;
export type HistoriqueResponsable = Prisma.HistoriqueGetPayload<{
include: {
utilisateur: true;
materiel: true;
emprunt: true;
sallePret: true;
posteEmprunt: true;
};
}>;
export interface EmpruntResponsableFiltres {
statut?: string;
}
@@ -67,6 +77,15 @@ export interface NotificationResponsableFiltres {
lu?: boolean;
}
export interface HistoriqueResponsableFiltres {
action?: string;
utilisateurId?: number;
materielId?: number;
empruntId?: number;
dateDebut?: Date;
dateFin?: Date;
}
function mapStatutCounts(rows: Array<{ statut: string; _count: { _all: number } }>): StatutCount[] {
return rows.map((row) => ({ statut: row.statut, count: row._count._all }));
}
@@ -239,3 +258,34 @@ export async function marquerNotificationsResponsableLues(utilisateurId: number)
return result.count;
}
export function findHistoriqueResponsable(
campusId: number,
filtres: HistoriqueResponsableFiltres,
): Promise<HistoriqueResponsable[]> {
return prisma.historique.findMany({
where: {
campusId,
...(filtres.action ? { action: filtres.action } : {}),
...(filtres.utilisateurId !== undefined ? { utilisateurId: filtres.utilisateurId } : {}),
...(filtres.materielId !== undefined ? { materielId: filtres.materielId } : {}),
...(filtres.empruntId !== undefined ? { empruntId: filtres.empruntId } : {}),
...(filtres.dateDebut || filtres.dateFin
? {
dateAction: {
...(filtres.dateDebut ? { gte: filtres.dateDebut } : {}),
...(filtres.dateFin ? { lte: filtres.dateFin } : {}),
},
}
: {}),
},
include: {
utilisateur: true,
materiel: true,
emprunt: true,
sallePret: true,
posteEmprunt: true,
},
orderBy: { dateAction: 'desc' },
});
}
@@ -3,6 +3,8 @@ import {
getAnomalies,
getDashboard,
getEmprunts,
getHistorique,
getHistoriqueCsv,
getMateriels,
getNotifications,
patchNotificationLue,
@@ -18,3 +20,5 @@ responsableRoutes.get('/anomalies', getAnomalies);
responsableRoutes.get('/notifications', getNotifications);
responsableRoutes.patch('/notifications/lu-toutes', patchNotificationsLues);
responsableRoutes.patch('/notifications/:id/lu', patchNotificationLue);
responsableRoutes.get('/historique', getHistorique);
responsableRoutes.get('/historique/export.csv', getHistoriqueCsv);
@@ -7,6 +7,7 @@ import {
findActiviteRecente,
findAnomaliesResponsable,
findEmpruntsResponsable,
findHistoriqueResponsable,
findMaterielsResponsable,
findNotificationsResponsable,
marquerNotificationLue,
@@ -15,6 +16,7 @@ import {
ActiviteResponsable,
AnomalieResponsable,
EmpruntResponsable,
HistoriqueResponsable,
MaterielResponsable,
NotificationResponsable,
} from '../repositories/responsable.repository';
@@ -54,6 +56,15 @@ export interface ListerNotificationsResponsableFiltres {
lu?: boolean;
}
export interface ListerHistoriqueResponsableFiltres {
action?: string;
utilisateurId?: number;
materielId?: number;
empruntId?: number;
dateDebut?: Date;
dateFin?: Date;
}
function verifierResponsable(roleCode: string): void {
if (roleCode !== 'RESPONSABLE') {
throw new AppError(403, 'Acces reserve au responsable materiel');
@@ -142,6 +153,38 @@ export function parseNotificationId(value: unknown): number {
return parsed;
}
export function parsePositiveIntQuery(value: unknown, champ: string): number | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== 'string') {
throw new AppError(400, `${champ} invalide`);
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new AppError(400, `${champ} invalide`);
}
return parsed;
}
export function parseDateQuery(value: unknown, champ: string): Date | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== 'string') {
throw new AppError(400, `${champ} invalide`);
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
throw new AppError(400, `${champ} invalide`);
}
return parsed;
}
export function parseLu(value: unknown): boolean | undefined {
if (value === undefined) {
return undefined;
@@ -212,3 +255,12 @@ export function marquerToutesNotificationsResponsableLues(
verifierResponsable(roleCode);
return marquerNotificationsResponsableLues(utilisateurId);
}
export function listerHistoriqueResponsable(
roleCode: string,
campusId: number,
filtres: ListerHistoriqueResponsableFiltres,
): Promise<HistoriqueResponsable[]> {
verifierResponsable(roleCode);
return findHistoriqueResponsable(campusId, filtres);
}
+9 -1
View File
@@ -365,6 +365,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
- Endpoint `PATCH /api/responsable/notifications/lu-toutes` ajouté pour marquer toutes les notifications non lues du responsable connecté comme lues.
- Réponse enrichie avec l'anomalie liée et le matériel concerné lorsque la notification pointe vers une anomalie.
### Étape 39 — API responsable : historique et export
- Branche dédiée `feat/responsable-historique-api` créée après merge des notifications responsable dans `develop`.
- Endpoint `GET /api/responsable/historique` ajouté, protégé par rôle `RESPONSABLE`.
- Filtrage par campus du responsable appliqué systématiquement (RG27).
- Filtres optionnels ajoutés : `action`, `utilisateurId`, `materielId`, `empruntId`, `dateDebut`, `dateFin`.
- Endpoint `GET /api/responsable/historique/export.csv` ajouté avec les mêmes filtres.
- Export CSV généré côté API avec les colonnes principales : date, action, description, utilisateur, email, matériel, emprunt.
---
*Dernière mise à jour : 2026-07-15Dashboard, consultations responsable et notifications ajoutés côté API.*
*Dernière mise à jour : 2026-07-16Bloc API responsable complété côté consultation, notifications et historique/export.*