feat(backend): add responsable historique endpoint
This commit is contained in:
@@ -3,6 +3,7 @@ import { AppError } from '../errors/app-error';
|
|||||||
import {
|
import {
|
||||||
getDashboardResponsable,
|
getDashboardResponsable,
|
||||||
listerAnomaliesResponsable,
|
listerAnomaliesResponsable,
|
||||||
|
listerHistoriqueResponsable,
|
||||||
listerNotificationsResponsable,
|
listerNotificationsResponsable,
|
||||||
listerMaterielsResponsable,
|
listerMaterielsResponsable,
|
||||||
listerEmpruntsResponsable,
|
listerEmpruntsResponsable,
|
||||||
@@ -11,6 +12,8 @@ import {
|
|||||||
parseCategorieId,
|
parseCategorieId,
|
||||||
parseLu,
|
parseLu,
|
||||||
parseNotificationId,
|
parseNotificationId,
|
||||||
|
parseDateQuery,
|
||||||
|
parsePositiveIntQuery,
|
||||||
parseStatutAnomalie,
|
parseStatutAnomalie,
|
||||||
parseStatutMateriel,
|
parseStatutMateriel,
|
||||||
parseStatutEmprunt,
|
parseStatutEmprunt,
|
||||||
@@ -19,10 +22,31 @@ import {
|
|||||||
toAnomalieResponsableResponse,
|
toAnomalieResponsableResponse,
|
||||||
toDashboardResponsableResponse,
|
toDashboardResponsableResponse,
|
||||||
toEmpruntResponsableResponse,
|
toEmpruntResponsableResponse,
|
||||||
|
toHistoriqueResponsableResponse,
|
||||||
toMaterielResponsableResponse,
|
toMaterielResponsableResponse,
|
||||||
toNotificationResponsableResponse,
|
toNotificationResponsableResponse,
|
||||||
} from '../dtos/responsable.dto';
|
} 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> {
|
export async function getDashboard(req: Request, res: Response): Promise<void> {
|
||||||
const user = req.user;
|
const user = req.user;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -113,3 +137,53 @@ export async function patchNotificationsLues(req: Request, res: Response): Promi
|
|||||||
const count = await marquerToutesNotificationsResponsableLues(user.id, user.roleCode);
|
const count = await marquerToutesNotificationsResponsableLues(user.id, user.roleCode);
|
||||||
res.json({ data: { count } });
|
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'));
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
ActiviteResponsable,
|
ActiviteResponsable,
|
||||||
AnomalieResponsable,
|
AnomalieResponsable,
|
||||||
EmpruntResponsable,
|
EmpruntResponsable,
|
||||||
|
HistoriqueResponsable,
|
||||||
MaterielResponsable,
|
MaterielResponsable,
|
||||||
NotificationResponsable,
|
NotificationResponsable,
|
||||||
StatutCount,
|
StatutCount,
|
||||||
@@ -146,6 +147,33 @@ export interface NotificationResponsableResponse {
|
|||||||
} | null;
|
} | 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 {
|
function toRepartition(rows: StatutCount[]): RepartitionStatutResponse {
|
||||||
const parStatut: Record<string, number> = {};
|
const parStatut: Record<string, number> = {};
|
||||||
let total = 0;
|
let total = 0;
|
||||||
@@ -318,3 +346,40 @@ export function toNotificationResponsableResponse(
|
|||||||
: null,
|
: 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 {
|
export interface EmpruntResponsableFiltres {
|
||||||
statut?: string;
|
statut?: string;
|
||||||
}
|
}
|
||||||
@@ -67,6 +77,15 @@ export interface NotificationResponsableFiltres {
|
|||||||
lu?: boolean;
|
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[] {
|
function mapStatutCounts(rows: Array<{ statut: string; _count: { _all: number } }>): StatutCount[] {
|
||||||
return rows.map((row) => ({ statut: row.statut, count: row._count._all }));
|
return rows.map((row) => ({ statut: row.statut, count: row._count._all }));
|
||||||
}
|
}
|
||||||
@@ -239,3 +258,34 @@ export async function marquerNotificationsResponsableLues(utilisateurId: number)
|
|||||||
|
|
||||||
return result.count;
|
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,
|
getAnomalies,
|
||||||
getDashboard,
|
getDashboard,
|
||||||
getEmprunts,
|
getEmprunts,
|
||||||
|
getHistorique,
|
||||||
|
getHistoriqueCsv,
|
||||||
getMateriels,
|
getMateriels,
|
||||||
getNotifications,
|
getNotifications,
|
||||||
patchNotificationLue,
|
patchNotificationLue,
|
||||||
@@ -18,3 +20,5 @@ responsableRoutes.get('/anomalies', getAnomalies);
|
|||||||
responsableRoutes.get('/notifications', getNotifications);
|
responsableRoutes.get('/notifications', getNotifications);
|
||||||
responsableRoutes.patch('/notifications/lu-toutes', patchNotificationsLues);
|
responsableRoutes.patch('/notifications/lu-toutes', patchNotificationsLues);
|
||||||
responsableRoutes.patch('/notifications/:id/lu', patchNotificationLue);
|
responsableRoutes.patch('/notifications/:id/lu', patchNotificationLue);
|
||||||
|
responsableRoutes.get('/historique', getHistorique);
|
||||||
|
responsableRoutes.get('/historique/export.csv', getHistoriqueCsv);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
findActiviteRecente,
|
findActiviteRecente,
|
||||||
findAnomaliesResponsable,
|
findAnomaliesResponsable,
|
||||||
findEmpruntsResponsable,
|
findEmpruntsResponsable,
|
||||||
|
findHistoriqueResponsable,
|
||||||
findMaterielsResponsable,
|
findMaterielsResponsable,
|
||||||
findNotificationsResponsable,
|
findNotificationsResponsable,
|
||||||
marquerNotificationLue,
|
marquerNotificationLue,
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
ActiviteResponsable,
|
ActiviteResponsable,
|
||||||
AnomalieResponsable,
|
AnomalieResponsable,
|
||||||
EmpruntResponsable,
|
EmpruntResponsable,
|
||||||
|
HistoriqueResponsable,
|
||||||
MaterielResponsable,
|
MaterielResponsable,
|
||||||
NotificationResponsable,
|
NotificationResponsable,
|
||||||
} from '../repositories/responsable.repository';
|
} from '../repositories/responsable.repository';
|
||||||
@@ -54,6 +56,15 @@ export interface ListerNotificationsResponsableFiltres {
|
|||||||
lu?: boolean;
|
lu?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ListerHistoriqueResponsableFiltres {
|
||||||
|
action?: string;
|
||||||
|
utilisateurId?: number;
|
||||||
|
materielId?: number;
|
||||||
|
empruntId?: number;
|
||||||
|
dateDebut?: Date;
|
||||||
|
dateFin?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
function verifierResponsable(roleCode: string): void {
|
function verifierResponsable(roleCode: string): void {
|
||||||
if (roleCode !== 'RESPONSABLE') {
|
if (roleCode !== 'RESPONSABLE') {
|
||||||
throw new AppError(403, 'Acces reserve au responsable materiel');
|
throw new AppError(403, 'Acces reserve au responsable materiel');
|
||||||
@@ -142,6 +153,38 @@ export function parseNotificationId(value: unknown): number {
|
|||||||
return parsed;
|
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 {
|
export function parseLu(value: unknown): boolean | undefined {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -212,3 +255,12 @@ export function marquerToutesNotificationsResponsableLues(
|
|||||||
verifierResponsable(roleCode);
|
verifierResponsable(roleCode);
|
||||||
return marquerNotificationsResponsableLues(utilisateurId);
|
return marquerNotificationsResponsableLues(utilisateurId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listerHistoriqueResponsable(
|
||||||
|
roleCode: string,
|
||||||
|
campusId: number,
|
||||||
|
filtres: ListerHistoriqueResponsableFiltres,
|
||||||
|
): Promise<HistoriqueResponsable[]> {
|
||||||
|
verifierResponsable(roleCode);
|
||||||
|
return findHistoriqueResponsable(campusId, filtres);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
- 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.
|
- 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-15 — Dashboard, consultations responsable et notifications ajoutés côté API.*
|
*Dernière mise à jour : 2026-07-16 — Bloc API responsable complété côté consultation, notifications et historique/export.*
|
||||||
|
|||||||
Reference in New Issue
Block a user