feat(backend): add responsable historique endpoint
This commit is contained in:
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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-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