merge: responsable notifications api
This commit is contained in:
@@ -3,9 +3,14 @@ import { AppError } from '../errors/app-error';
|
|||||||
import {
|
import {
|
||||||
getDashboardResponsable,
|
getDashboardResponsable,
|
||||||
listerAnomaliesResponsable,
|
listerAnomaliesResponsable,
|
||||||
|
listerNotificationsResponsable,
|
||||||
listerMaterielsResponsable,
|
listerMaterielsResponsable,
|
||||||
listerEmpruntsResponsable,
|
listerEmpruntsResponsable,
|
||||||
|
marquerNotificationResponsableLue,
|
||||||
|
marquerToutesNotificationsResponsableLues,
|
||||||
parseCategorieId,
|
parseCategorieId,
|
||||||
|
parseLu,
|
||||||
|
parseNotificationId,
|
||||||
parseStatutAnomalie,
|
parseStatutAnomalie,
|
||||||
parseStatutMateriel,
|
parseStatutMateriel,
|
||||||
parseStatutEmprunt,
|
parseStatutEmprunt,
|
||||||
@@ -15,6 +20,7 @@ import {
|
|||||||
toDashboardResponsableResponse,
|
toDashboardResponsableResponse,
|
||||||
toEmpruntResponsableResponse,
|
toEmpruntResponsableResponse,
|
||||||
toMaterielResponsableResponse,
|
toMaterielResponsableResponse,
|
||||||
|
toNotificationResponsableResponse,
|
||||||
} from '../dtos/responsable.dto';
|
} from '../dtos/responsable.dto';
|
||||||
|
|
||||||
export async function getDashboard(req: Request, res: Response): Promise<void> {
|
export async function getDashboard(req: Request, res: Response): Promise<void> {
|
||||||
@@ -71,3 +77,39 @@ export async function getAnomalies(req: Request, res: Response): Promise<void> {
|
|||||||
});
|
});
|
||||||
res.json({ data: anomalies.map(toAnomalieResponsableResponse) });
|
res.json({ data: anomalies.map(toAnomalieResponsableResponse) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getNotifications(req: Request, res: Response): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
const user = req.user;
|
||||||
|
if (!user) {
|
||||||
|
throw new AppError(401, 'Authentification requise');
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = await marquerToutesNotificationsResponsableLues(user.id, user.roleCode);
|
||||||
|
res.json({ data: { count } });
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
AnomalieResponsable,
|
AnomalieResponsable,
|
||||||
EmpruntResponsable,
|
EmpruntResponsable,
|
||||||
MaterielResponsable,
|
MaterielResponsable,
|
||||||
|
NotificationResponsable,
|
||||||
StatutCount,
|
StatutCount,
|
||||||
} from '../repositories/responsable.repository';
|
} from '../repositories/responsable.repository';
|
||||||
import { DashboardResponsableData } from '../services/responsable.service';
|
import { DashboardResponsableData } from '../services/responsable.service';
|
||||||
@@ -125,6 +126,26 @@ export interface AnomalieResponsableResponse {
|
|||||||
} | null;
|
} | 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 {
|
function toRepartition(rows: StatutCount[]): RepartitionStatutResponse {
|
||||||
const parStatut: Record<string, number> = {};
|
const parStatut: Record<string, number> = {};
|
||||||
let total = 0;
|
let total = 0;
|
||||||
@@ -271,3 +292,29 @@ export function toAnomalieResponsableResponse(
|
|||||||
: null,
|
: 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,17 @@ export type AnomalieResponsable = Prisma.AnomalieGetPayload<{
|
|||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type NotificationResponsable = Prisma.NotificationGetPayload<{
|
||||||
|
include: {
|
||||||
|
anomalie: {
|
||||||
|
include: {
|
||||||
|
emprunt: true;
|
||||||
|
materiel: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
export interface EmpruntResponsableFiltres {
|
export interface EmpruntResponsableFiltres {
|
||||||
statut?: string;
|
statut?: string;
|
||||||
}
|
}
|
||||||
@@ -52,6 +63,10 @@ export interface AnomalieResponsableFiltres {
|
|||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NotificationResponsableFiltres {
|
||||||
|
lu?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
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 }));
|
||||||
}
|
}
|
||||||
@@ -173,3 +188,54 @@ export function findAnomaliesResponsable(
|
|||||||
orderBy: { dateDetection: 'desc' },
|
orderBy: { dateDetection: 'desc' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findNotificationsResponsable(
|
||||||
|
utilisateurId: number,
|
||||||
|
filtres: NotificationResponsableFiltres,
|
||||||
|
): Promise<NotificationResponsable[]> {
|
||||||
|
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<NotificationResponsable> {
|
||||||
|
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<number> {
|
||||||
|
const result = await prisma.notification.updateMany({
|
||||||
|
where: { utilisateurId, lu: false },
|
||||||
|
data: { lu: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.count;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import {
|
|||||||
getDashboard,
|
getDashboard,
|
||||||
getEmprunts,
|
getEmprunts,
|
||||||
getMateriels,
|
getMateriels,
|
||||||
|
getNotifications,
|
||||||
|
patchNotificationLue,
|
||||||
|
patchNotificationsLues,
|
||||||
} from '../controllers/responsable.controller';
|
} from '../controllers/responsable.controller';
|
||||||
|
|
||||||
export const responsableRoutes: Router = Router();
|
export const responsableRoutes: Router = Router();
|
||||||
@@ -12,3 +15,6 @@ responsableRoutes.get('/dashboard', getDashboard);
|
|||||||
responsableRoutes.get('/emprunts', getEmprunts);
|
responsableRoutes.get('/emprunts', getEmprunts);
|
||||||
responsableRoutes.get('/materiels', getMateriels);
|
responsableRoutes.get('/materiels', getMateriels);
|
||||||
responsableRoutes.get('/anomalies', getAnomalies);
|
responsableRoutes.get('/anomalies', getAnomalies);
|
||||||
|
responsableRoutes.get('/notifications', getNotifications);
|
||||||
|
responsableRoutes.patch('/notifications/lu-toutes', patchNotificationsLues);
|
||||||
|
responsableRoutes.patch('/notifications/:id/lu', patchNotificationLue);
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ import {
|
|||||||
findAnomaliesResponsable,
|
findAnomaliesResponsable,
|
||||||
findEmpruntsResponsable,
|
findEmpruntsResponsable,
|
||||||
findMaterielsResponsable,
|
findMaterielsResponsable,
|
||||||
|
findNotificationsResponsable,
|
||||||
|
marquerNotificationLue,
|
||||||
|
marquerNotificationsResponsableLues,
|
||||||
StatutCount,
|
StatutCount,
|
||||||
ActiviteResponsable,
|
ActiviteResponsable,
|
||||||
AnomalieResponsable,
|
AnomalieResponsable,
|
||||||
EmpruntResponsable,
|
EmpruntResponsable,
|
||||||
MaterielResponsable,
|
MaterielResponsable,
|
||||||
|
NotificationResponsable,
|
||||||
} from '../repositories/responsable.repository';
|
} from '../repositories/responsable.repository';
|
||||||
import {
|
import {
|
||||||
STATUT_ANOMALIE,
|
STATUT_ANOMALIE,
|
||||||
@@ -46,6 +50,10 @@ export interface ListerAnomaliesResponsableFiltres {
|
|||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ListerNotificationsResponsableFiltres {
|
||||||
|
lu?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
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');
|
||||||
@@ -121,6 +129,32 @@ export function parseCategorieId(value: unknown): number | undefined {
|
|||||||
return parsed;
|
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(
|
export function listerEmpruntsResponsable(
|
||||||
roleCode: string,
|
roleCode: string,
|
||||||
campusId: number,
|
campusId: number,
|
||||||
@@ -147,3 +181,34 @@ export function listerAnomaliesResponsable(
|
|||||||
verifierResponsable(roleCode);
|
verifierResponsable(roleCode);
|
||||||
return findAnomaliesResponsable(campusId, filtres);
|
return findAnomaliesResponsable(campusId, filtres);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listerNotificationsResponsable(
|
||||||
|
utilisateurId: number,
|
||||||
|
roleCode: string,
|
||||||
|
filtres: ListerNotificationsResponsableFiltres,
|
||||||
|
): Promise<NotificationResponsable[]> {
|
||||||
|
verifierResponsable(roleCode);
|
||||||
|
return findNotificationsResponsable(utilisateurId, filtres);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function marquerNotificationResponsableLue(
|
||||||
|
utilisateurId: number,
|
||||||
|
roleCode: string,
|
||||||
|
notificationId: number,
|
||||||
|
): Promise<NotificationResponsable> {
|
||||||
|
verifierResponsable(roleCode);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await marquerNotificationLue(utilisateurId, notificationId);
|
||||||
|
} catch {
|
||||||
|
throw new AppError(404, 'Notification introuvable');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function marquerToutesNotificationsResponsableLues(
|
||||||
|
utilisateurId: number,
|
||||||
|
roleCode: string,
|
||||||
|
): Promise<number> {
|
||||||
|
verifierResponsable(roleCode);
|
||||||
|
return marquerNotificationsResponsableLues(utilisateurId);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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é.
|
- 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.
|
- 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.*
|
||||||
|
|||||||
Reference in New Issue
Block a user