From d2d22456751ee836d76d06f0e9d71ae147d7908d Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Wed, 15 Jul 2026 15:50:08 +0200 Subject: [PATCH] feat(backend): add responsable notifications endpoints --- .../src/controllers/responsable.controller.ts | 42 ++++++++++++ eme-backend/src/dtos/responsable.dto.ts | 47 +++++++++++++ .../repositories/responsable.repository.ts | 66 +++++++++++++++++++ eme-backend/src/routes/responsable.routes.ts | 6 ++ .../src/services/responsable.service.ts | 65 ++++++++++++++++++ review.md | 10 ++- 6 files changed, 235 insertions(+), 1 deletion(-) diff --git a/eme-backend/src/controllers/responsable.controller.ts b/eme-backend/src/controllers/responsable.controller.ts index 9d2ae2c..469bf2f 100644 --- a/eme-backend/src/controllers/responsable.controller.ts +++ b/eme-backend/src/controllers/responsable.controller.ts @@ -3,9 +3,14 @@ import { AppError } from '../errors/app-error'; import { getDashboardResponsable, listerAnomaliesResponsable, + listerNotificationsResponsable, listerMaterielsResponsable, listerEmpruntsResponsable, + marquerNotificationResponsableLue, + marquerToutesNotificationsResponsableLues, parseCategorieId, + parseLu, + parseNotificationId, parseStatutAnomalie, parseStatutMateriel, parseStatutEmprunt, @@ -15,6 +20,7 @@ import { toDashboardResponsableResponse, toEmpruntResponsableResponse, toMaterielResponsableResponse, + toNotificationResponsableResponse, } from '../dtos/responsable.dto'; export async function getDashboard(req: Request, res: Response): Promise { @@ -71,3 +77,39 @@ export async function getAnomalies(req: Request, res: Response): Promise { }); res.json({ data: anomalies.map(toAnomalieResponsableResponse) }); } + +export async function getNotifications(req: Request, res: Response): Promise { + const user = req.user; + if (!user) { + throw new AppError(401, 'Authentification requise'); + } + + const lu = parseLu(req.query.lu); + const notifications = await listerNotificationsResponsable(user.id, user.roleCode, { lu }); + res.json({ data: notifications.map(toNotificationResponsableResponse) }); +} + +export async function patchNotificationLue(req: Request, res: Response): Promise { + const user = req.user; + if (!user) { + throw new AppError(401, 'Authentification requise'); + } + + const notificationId = parseNotificationId(req.params.id); + const notification = await marquerNotificationResponsableLue( + user.id, + user.roleCode, + notificationId, + ); + res.json({ data: toNotificationResponsableResponse(notification) }); +} + +export async function patchNotificationsLues(req: Request, res: Response): Promise { + const user = req.user; + if (!user) { + throw new AppError(401, 'Authentification requise'); + } + + const count = await marquerToutesNotificationsResponsableLues(user.id, user.roleCode); + res.json({ data: { count } }); +} diff --git a/eme-backend/src/dtos/responsable.dto.ts b/eme-backend/src/dtos/responsable.dto.ts index 18dcfa7..bb0935b 100644 --- a/eme-backend/src/dtos/responsable.dto.ts +++ b/eme-backend/src/dtos/responsable.dto.ts @@ -3,6 +3,7 @@ import { AnomalieResponsable, EmpruntResponsable, MaterielResponsable, + NotificationResponsable, StatutCount, } from '../repositories/responsable.repository'; import { DashboardResponsableData } from '../services/responsable.service'; @@ -125,6 +126,26 @@ export interface AnomalieResponsableResponse { } | null; } +export interface NotificationResponsableResponse { + id: number; + titre: string; + message: string; + type: string; + lu: boolean; + dateCreation: Date; + anomalie: { + id: number; + statut: string; + type: string; + empruntId: number; + materiel: { + id: number; + nom: string; + reference: string; + }; + } | null; +} + function toRepartition(rows: StatutCount[]): RepartitionStatutResponse { const parStatut: Record = {}; let total = 0; @@ -271,3 +292,29 @@ export function toAnomalieResponsableResponse( : null, }; } + +export function toNotificationResponsableResponse( + notification: NotificationResponsable, +): NotificationResponsableResponse { + return { + id: notification.id, + titre: notification.titre, + message: notification.message, + type: notification.type, + lu: notification.lu, + dateCreation: notification.dateCreation, + anomalie: notification.anomalie + ? { + id: notification.anomalie.id, + statut: notification.anomalie.statut, + type: notification.anomalie.type, + empruntId: notification.anomalie.emprunt.id, + materiel: { + id: notification.anomalie.materiel.id, + nom: notification.anomalie.materiel.nom, + reference: notification.anomalie.materiel.reference, + }, + } + : null, + }; +} diff --git a/eme-backend/src/repositories/responsable.repository.ts b/eme-backend/src/repositories/responsable.repository.ts index 9bdf16b..31bd613 100644 --- a/eme-backend/src/repositories/responsable.repository.ts +++ b/eme-backend/src/repositories/responsable.repository.ts @@ -37,6 +37,17 @@ export type AnomalieResponsable = Prisma.AnomalieGetPayload<{ }; }>; +export type NotificationResponsable = Prisma.NotificationGetPayload<{ + include: { + anomalie: { + include: { + emprunt: true; + materiel: true; + }; + }; + }; +}>; + export interface EmpruntResponsableFiltres { statut?: string; } @@ -52,6 +63,10 @@ export interface AnomalieResponsableFiltres { type?: string; } +export interface NotificationResponsableFiltres { + lu?: boolean; +} + function mapStatutCounts(rows: Array<{ statut: string; _count: { _all: number } }>): StatutCount[] { return rows.map((row) => ({ statut: row.statut, count: row._count._all })); } @@ -173,3 +188,54 @@ export function findAnomaliesResponsable( orderBy: { dateDetection: 'desc' }, }); } + +export function findNotificationsResponsable( + utilisateurId: number, + filtres: NotificationResponsableFiltres, +): Promise { + return prisma.notification.findMany({ + where: { + utilisateurId, + ...(filtres.lu !== undefined ? { lu: filtres.lu } : {}), + }, + include: { + anomalie: { + include: { + emprunt: true, + materiel: true, + }, + }, + }, + orderBy: { dateCreation: 'desc' }, + }); +} + +export function marquerNotificationLue( + utilisateurId: number, + notificationId: number, +): Promise { + return prisma.notification.update({ + where: { + id: notificationId, + utilisateurId, + }, + data: { lu: true }, + include: { + anomalie: { + include: { + emprunt: true, + materiel: true, + }, + }, + }, + }); +} + +export async function marquerNotificationsResponsableLues(utilisateurId: number): Promise { + const result = await prisma.notification.updateMany({ + where: { utilisateurId, lu: false }, + data: { lu: true }, + }); + + return result.count; +} diff --git a/eme-backend/src/routes/responsable.routes.ts b/eme-backend/src/routes/responsable.routes.ts index b534c26..94fe35a 100644 --- a/eme-backend/src/routes/responsable.routes.ts +++ b/eme-backend/src/routes/responsable.routes.ts @@ -4,6 +4,9 @@ import { getDashboard, getEmprunts, getMateriels, + getNotifications, + patchNotificationLue, + patchNotificationsLues, } from '../controllers/responsable.controller'; export const responsableRoutes: Router = Router(); @@ -12,3 +15,6 @@ responsableRoutes.get('/dashboard', getDashboard); responsableRoutes.get('/emprunts', getEmprunts); responsableRoutes.get('/materiels', getMateriels); responsableRoutes.get('/anomalies', getAnomalies); +responsableRoutes.get('/notifications', getNotifications); +responsableRoutes.patch('/notifications/lu-toutes', patchNotificationsLues); +responsableRoutes.patch('/notifications/:id/lu', patchNotificationLue); diff --git a/eme-backend/src/services/responsable.service.ts b/eme-backend/src/services/responsable.service.ts index 9b3b290..d6a1bbe 100644 --- a/eme-backend/src/services/responsable.service.ts +++ b/eme-backend/src/services/responsable.service.ts @@ -8,11 +8,15 @@ import { findAnomaliesResponsable, findEmpruntsResponsable, findMaterielsResponsable, + findNotificationsResponsable, + marquerNotificationLue, + marquerNotificationsResponsableLues, StatutCount, ActiviteResponsable, AnomalieResponsable, EmpruntResponsable, MaterielResponsable, + NotificationResponsable, } from '../repositories/responsable.repository'; import { STATUT_ANOMALIE, @@ -46,6 +50,10 @@ export interface ListerAnomaliesResponsableFiltres { type?: string; } +export interface ListerNotificationsResponsableFiltres { + lu?: boolean; +} + function verifierResponsable(roleCode: string): void { if (roleCode !== 'RESPONSABLE') { throw new AppError(403, 'Acces reserve au responsable materiel'); @@ -121,6 +129,32 @@ export function parseCategorieId(value: unknown): number | undefined { return parsed; } +export function parseNotificationId(value: unknown): number { + if (typeof value !== 'string') { + throw new AppError(400, 'notificationId invalide'); + } + + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new AppError(400, 'notificationId invalide'); + } + + return parsed; +} + +export function parseLu(value: unknown): boolean | undefined { + if (value === undefined) { + return undefined; + } + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + throw new AppError(400, 'lu invalide'); +} + export function listerEmpruntsResponsable( roleCode: string, campusId: number, @@ -147,3 +181,34 @@ export function listerAnomaliesResponsable( verifierResponsable(roleCode); return findAnomaliesResponsable(campusId, filtres); } + +export function listerNotificationsResponsable( + utilisateurId: number, + roleCode: string, + filtres: ListerNotificationsResponsableFiltres, +): Promise { + verifierResponsable(roleCode); + return findNotificationsResponsable(utilisateurId, filtres); +} + +export async function marquerNotificationResponsableLue( + utilisateurId: number, + roleCode: string, + notificationId: number, +): Promise { + verifierResponsable(roleCode); + + try { + return await marquerNotificationLue(utilisateurId, notificationId); + } catch { + throw new AppError(404, 'Notification introuvable'); + } +} + +export function marquerToutesNotificationsResponsableLues( + utilisateurId: number, + roleCode: string, +): Promise { + verifierResponsable(roleCode); + return marquerNotificationsResponsableLues(utilisateurId); +} diff --git a/review.md b/review.md index 89af463..6e1e578 100644 --- a/review.md +++ b/review.md @@ -357,6 +357,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Réponse enrichie avec anomalie, emprunt, étudiant concerné, matériel, catégorie et responsable de traitement si renseigné. - Limite volontaire : consultation uniquement ; le cycle de traitement/résolution reste à développer. +### Étape 38 — API responsable : notifications +- Branche dédiée `feat/responsable-notifications-api` créée après merge de la consultation des anomalies responsable dans `develop`. +- Endpoint `GET /api/responsable/notifications` ajouté, protégé par rôle `RESPONSABLE`. +- Filtre optionnel `lu=true|false` ajouté pour séparer les notifications lues et non lues. +- Endpoint `PATCH /api/responsable/notifications/:id/lu` ajouté pour marquer une notification du responsable connecté comme lue. +- 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. + --- -*Dernière mise à jour : 2026-07-15 — Dashboard, consultation des emprunts, du stock et des anomalies responsable ajoutés côté API.* +*Dernière mise à jour : 2026-07-15 — Dashboard, consultations responsable et notifications ajoutés côté API.*