587 lines
15 KiB
TypeScript
587 lines
15 KiB
TypeScript
import { AppError } from '../errors/app-error';
|
|
import {
|
|
changerStatutAnomalieResponsable,
|
|
countAnomaliesParStatut,
|
|
countEmpruntsParStatut,
|
|
countMaterielsParStatut,
|
|
countNotificationsNonLues,
|
|
deciderEcartResponsable,
|
|
findActiviteRecente,
|
|
findEcartResponsableById,
|
|
findEcartsResponsable,
|
|
findAnomalieResponsableById,
|
|
findAnomaliesResponsable,
|
|
findEmpruntsResponsable,
|
|
findHistoriqueResponsable,
|
|
findMaterielsResponsable,
|
|
findNotificationsResponsable,
|
|
marquerNotificationLue,
|
|
marquerNotificationsResponsableLues,
|
|
StatutCount,
|
|
ActiviteResponsable,
|
|
AnomalieResponsable,
|
|
EmpruntResponsable,
|
|
HistoriqueResponsable,
|
|
MaterielResponsable,
|
|
NotificationResponsable,
|
|
EcartResponsable,
|
|
} from '../repositories/responsable.repository';
|
|
import {
|
|
STATUT_ANOMALIE,
|
|
STATUT_EMPRUNT,
|
|
STATUT_MATERIEL,
|
|
StatutAnomalie,
|
|
StatutEmprunt,
|
|
StatutMateriel,
|
|
} from '../models/enums';
|
|
|
|
export interface DashboardResponsableData {
|
|
materiels: StatutCount[];
|
|
emprunts: StatutCount[];
|
|
anomalies: StatutCount[];
|
|
notificationsNonLues: number;
|
|
activiteRecente: ActiviteResponsable[];
|
|
}
|
|
|
|
export interface ListerEmpruntsResponsableFiltres {
|
|
statut?: StatutEmprunt;
|
|
annee?: number;
|
|
recherche?: string;
|
|
tri?: TriEmpruntResponsable;
|
|
ordre?: OrdreTri;
|
|
}
|
|
|
|
export type TriEmpruntResponsable =
|
|
| 'dateEmprunt'
|
|
| 'dateRetourPrevue'
|
|
| 'dateRetourReelle'
|
|
| 'materiel'
|
|
| 'etudiant'
|
|
| 'statut';
|
|
|
|
export type OrdreTri = 'asc' | 'desc';
|
|
|
|
export interface ListerMaterielsResponsableFiltres {
|
|
statut?: StatutMateriel;
|
|
categorieId?: number;
|
|
recherche?: string;
|
|
tri?: TriMaterielResponsable;
|
|
ordre?: OrdreTri;
|
|
}
|
|
|
|
export type TriMaterielResponsable =
|
|
| 'nom'
|
|
| 'reference'
|
|
| 'categorie'
|
|
| 'marque'
|
|
| 'etatGeneral'
|
|
| 'statut';
|
|
|
|
export interface ListerAnomaliesResponsableFiltres {
|
|
statut?: StatutAnomalie;
|
|
type?: string;
|
|
recherche?: string;
|
|
tri?: TriAnomalieResponsable;
|
|
ordre?: OrdreTri;
|
|
}
|
|
|
|
export type TriAnomalieResponsable =
|
|
| 'dateDetection'
|
|
| 'type'
|
|
| 'materiel'
|
|
| 'etudiant'
|
|
| 'statut';
|
|
|
|
export interface ListerNotificationsResponsableFiltres {
|
|
lu?: boolean;
|
|
}
|
|
|
|
export interface ListerHistoriqueResponsableFiltres {
|
|
action?: string;
|
|
utilisateurId?: number;
|
|
materielId?: number;
|
|
empruntId?: number;
|
|
dateDebut?: Date;
|
|
dateFin?: Date;
|
|
recherche?: string;
|
|
tri?: TriHistoriqueResponsable;
|
|
ordre?: OrdreTri;
|
|
}
|
|
|
|
export type TriHistoriqueResponsable =
|
|
| 'dateAction'
|
|
| 'action'
|
|
| 'utilisateur'
|
|
| 'materiel'
|
|
| 'emprunt';
|
|
|
|
export interface ChangerStatutAnomalieResponsableRequest {
|
|
statut: StatutAnomalie;
|
|
observation?: string;
|
|
}
|
|
|
|
export interface DeciderEcartResponsableRequest {
|
|
decision: 'CONFIRMER' | 'REFUSER';
|
|
observation?: string;
|
|
statutMateriel?: StatutMateriel;
|
|
}
|
|
|
|
const STATUTS_MATERIEL_RETOUR_CONFIRMES: readonly StatutMateriel[] = [
|
|
'DISPONIBLE',
|
|
'NON_CONFORME',
|
|
'DETERIORE',
|
|
'MAINTENANCE',
|
|
'INDISPONIBLE',
|
|
];
|
|
|
|
const TRANSITIONS_ANOMALIE: Record<StatutAnomalie, StatutAnomalie[]> = {
|
|
DETECTEE: ['EN_COURS_TRAITEMENT'],
|
|
EN_COURS_TRAITEMENT: ['RESOLUE'],
|
|
RESOLUE: ['CLOTUREE'],
|
|
CLOTUREE: [],
|
|
};
|
|
|
|
function verifierResponsable(roleCode: string): void {
|
|
if (roleCode !== 'RESPONSABLE') {
|
|
throw new AppError(403, 'Acces reserve au responsable materiel');
|
|
}
|
|
}
|
|
|
|
export async function getDashboardResponsable(
|
|
utilisateurId: number,
|
|
roleCode: string,
|
|
campusId: number,
|
|
): Promise<DashboardResponsableData> {
|
|
verifierResponsable(roleCode);
|
|
|
|
const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] = await Promise.all(
|
|
[
|
|
countMaterielsParStatut(campusId),
|
|
countEmpruntsParStatut(campusId),
|
|
countAnomaliesParStatut(campusId),
|
|
countNotificationsNonLues(utilisateurId),
|
|
findActiviteRecente(campusId, 10),
|
|
],
|
|
);
|
|
|
|
return {
|
|
materiels,
|
|
emprunts,
|
|
anomalies,
|
|
notificationsNonLues,
|
|
activiteRecente,
|
|
};
|
|
}
|
|
|
|
export function parseStatutEmprunt(value: unknown): StatutEmprunt | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
if (typeof value !== 'string' || !(STATUT_EMPRUNT as readonly string[]).includes(value)) {
|
|
throw new AppError(400, 'statut invalide');
|
|
}
|
|
return value as StatutEmprunt;
|
|
}
|
|
|
|
export function parseAnneeEmprunt(value: unknown): number | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
if (typeof value !== 'string') {
|
|
throw new AppError(400, 'annee invalide');
|
|
}
|
|
|
|
const annee = Number(value);
|
|
if (!Number.isInteger(annee) || annee < 2000 || annee > 2100) {
|
|
throw new AppError(400, 'annee invalide');
|
|
}
|
|
return annee;
|
|
}
|
|
|
|
export function parseTriEmprunt(value: unknown): TriEmpruntResponsable | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
const valeurs: readonly TriEmpruntResponsable[] = [
|
|
'dateEmprunt',
|
|
'dateRetourPrevue',
|
|
'dateRetourReelle',
|
|
'materiel',
|
|
'etudiant',
|
|
'statut',
|
|
];
|
|
if (typeof value !== 'string' || !valeurs.includes(value as TriEmpruntResponsable)) {
|
|
throw new AppError(400, 'tri invalide');
|
|
}
|
|
return value as TriEmpruntResponsable;
|
|
}
|
|
|
|
export function parseOrdreTri(value: unknown): OrdreTri | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
if (value !== 'asc' && value !== 'desc') {
|
|
throw new AppError(400, 'ordre invalide');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function parseStatutMateriel(value: unknown): StatutMateriel | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
if (typeof value !== 'string' || !(STATUT_MATERIEL as readonly string[]).includes(value)) {
|
|
throw new AppError(400, 'statut invalide');
|
|
}
|
|
return value as StatutMateriel;
|
|
}
|
|
|
|
export function parseTriMateriel(value: unknown): TriMaterielResponsable | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
const valeurs: readonly TriMaterielResponsable[] = [
|
|
'nom',
|
|
'reference',
|
|
'categorie',
|
|
'marque',
|
|
'etatGeneral',
|
|
'statut',
|
|
];
|
|
if (typeof value !== 'string' || !valeurs.includes(value as TriMaterielResponsable)) {
|
|
throw new AppError(400, 'tri invalide');
|
|
}
|
|
return value as TriMaterielResponsable;
|
|
}
|
|
|
|
export function parseStatutAnomalie(value: unknown): StatutAnomalie | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
if (typeof value !== 'string' || !(STATUT_ANOMALIE as readonly string[]).includes(value)) {
|
|
throw new AppError(400, 'statut invalide');
|
|
}
|
|
return value as StatutAnomalie;
|
|
}
|
|
|
|
export function parseTriAnomalie(value: unknown): TriAnomalieResponsable | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
const valeurs: readonly TriAnomalieResponsable[] = [
|
|
'dateDetection',
|
|
'type',
|
|
'materiel',
|
|
'etudiant',
|
|
'statut',
|
|
];
|
|
if (typeof value !== 'string' || !valeurs.includes(value as TriAnomalieResponsable)) {
|
|
throw new AppError(400, 'tri invalide');
|
|
}
|
|
return value as TriAnomalieResponsable;
|
|
}
|
|
|
|
export function parseCategorieId(value: unknown): number | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
const parsed = Number(value);
|
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
throw new AppError(400, 'categorieId invalide');
|
|
}
|
|
|
|
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 parseAnomalieId(value: unknown): number {
|
|
if (typeof value !== 'string') {
|
|
throw new AppError(400, 'anomalieId invalide');
|
|
}
|
|
|
|
const parsed = Number(value);
|
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
throw new AppError(400, 'anomalieId invalide');
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
export function parseEmpruntId(value: unknown): number {
|
|
if (typeof value !== 'string') {
|
|
throw new AppError(400, 'empruntId invalide');
|
|
}
|
|
|
|
const parsed = Number(value);
|
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
throw new AppError(400, 'empruntId invalide');
|
|
}
|
|
|
|
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 parseTriHistorique(value: unknown): TriHistoriqueResponsable | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
const valeurs: readonly TriHistoriqueResponsable[] = [
|
|
'dateAction',
|
|
'action',
|
|
'utilisateur',
|
|
'materiel',
|
|
'emprunt',
|
|
];
|
|
if (typeof value !== 'string' || !valeurs.includes(value as TriHistoriqueResponsable)) {
|
|
throw new AppError(400, 'tri invalide');
|
|
}
|
|
return value as TriHistoriqueResponsable;
|
|
}
|
|
|
|
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,
|
|
filtres: ListerEmpruntsResponsableFiltres,
|
|
): Promise<EmpruntResponsable[]> {
|
|
verifierResponsable(roleCode);
|
|
return findEmpruntsResponsable(campusId, filtres);
|
|
}
|
|
|
|
export function listerEcartsResponsable(
|
|
roleCode: string,
|
|
campusId: number,
|
|
): Promise<EcartResponsable[]> {
|
|
verifierResponsable(roleCode);
|
|
return findEcartsResponsable(campusId);
|
|
}
|
|
|
|
function decrireChecklist(ecart: EcartResponsable, type: 'DEPART' | 'RETOUR'): string {
|
|
const checklist = ecart.checklists.find((item) => item.type === type);
|
|
if (!checklist) {
|
|
throw new AppError(409, `Checklist de ${type.toLocaleLowerCase()} introuvable`);
|
|
}
|
|
|
|
return checklist.elements
|
|
.map((element) => `${element.nomElement}=${element.etat}/${element.quantiteConstatee}`)
|
|
.join(', ');
|
|
}
|
|
|
|
export async function deciderEcartResponsableService(
|
|
responsableId: number,
|
|
roleCode: string,
|
|
campusId: number,
|
|
empruntId: number,
|
|
request: DeciderEcartResponsableRequest,
|
|
): Promise<EcartResponsable> {
|
|
verifierResponsable(roleCode);
|
|
|
|
const ecart = await findEcartResponsableById(campusId, empruntId);
|
|
if (!ecart) {
|
|
throw new AppError(404, 'Ecart en attente introuvable');
|
|
}
|
|
|
|
const typeEcart = ecart.statut === 'EN_ATTENTE_VALIDATION_DEPART' ? 'DEPART' : 'RETOUR';
|
|
if (
|
|
typeEcart === 'RETOUR' &&
|
|
request.decision === 'CONFIRMER' &&
|
|
(!request.statutMateriel || !STATUTS_MATERIEL_RETOUR_CONFIRMES.includes(request.statutMateriel))
|
|
) {
|
|
throw new AppError(400, 'statutMateriel final requis pour confirmer le retour');
|
|
}
|
|
|
|
const description =
|
|
`Ecart ${typeEcart.toLocaleLowerCase()} ${request.decision.toLocaleLowerCase()} ` +
|
|
`pour l'emprunt #${ecart.id} : ${decrireChecklist(ecart, typeEcart)}.`;
|
|
|
|
let elementsDepartCorriges: Array<{ id: number; quantiteConstatee: number }> | undefined;
|
|
if (typeEcart === 'DEPART' && request.decision === 'REFUSER') {
|
|
const checklistDepart = ecart.checklists.find((checklist) => checklist.type === 'DEPART');
|
|
if (!checklistDepart) {
|
|
throw new AppError(409, 'Checklist de depart introuvable');
|
|
}
|
|
elementsDepartCorriges = checklistDepart.elements.map((element) => {
|
|
const accessoire = ecart.materiel.accessoires.find(
|
|
(liaison) => liaison.accessoireId === element.accessoireId,
|
|
);
|
|
return {
|
|
id: element.id,
|
|
quantiteConstatee: accessoire?.quantiteAttendue ?? 1,
|
|
};
|
|
});
|
|
}
|
|
|
|
try {
|
|
return await deciderEcartResponsable({
|
|
empruntId,
|
|
campusId,
|
|
responsableId,
|
|
statutAttendu: ecart.statut,
|
|
statutEmpruntFinal:
|
|
typeEcart === 'DEPART'
|
|
? 'EN_COURS'
|
|
: request.decision === 'CONFIRMER'
|
|
? 'RETOUR_NON_CONFORME'
|
|
: 'CLOTURE',
|
|
statutMaterielFinal:
|
|
typeEcart === 'DEPART'
|
|
? 'EMPRUNTE'
|
|
: request.decision === 'CONFIRMER'
|
|
? (request.statutMateriel as StatutMateriel)
|
|
: 'DISPONIBLE',
|
|
typeEcart,
|
|
decision: request.decision,
|
|
description,
|
|
observation: request.observation,
|
|
elementsDepartCorriges,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message === 'ECART_DEJA_TRAITE') {
|
|
throw new AppError(409, 'Cet ecart a deja ete traite');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function listerMaterielsResponsable(
|
|
roleCode: string,
|
|
campusId: number,
|
|
filtres: ListerMaterielsResponsableFiltres,
|
|
): Promise<MaterielResponsable[]> {
|
|
verifierResponsable(roleCode);
|
|
return findMaterielsResponsable(campusId, filtres);
|
|
}
|
|
|
|
export function listerAnomaliesResponsable(
|
|
roleCode: string,
|
|
campusId: number,
|
|
filtres: ListerAnomaliesResponsableFiltres,
|
|
): Promise<AnomalieResponsable[]> {
|
|
verifierResponsable(roleCode);
|
|
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);
|
|
}
|
|
|
|
export function listerHistoriqueResponsable(
|
|
roleCode: string,
|
|
campusId: number,
|
|
filtres: ListerHistoriqueResponsableFiltres,
|
|
): Promise<HistoriqueResponsable[]> {
|
|
verifierResponsable(roleCode);
|
|
return findHistoriqueResponsable(campusId, filtres);
|
|
}
|
|
|
|
export async function changerStatutAnomalieResponsableService(
|
|
responsableId: number,
|
|
roleCode: string,
|
|
campusId: number,
|
|
anomalieId: number,
|
|
data: ChangerStatutAnomalieResponsableRequest,
|
|
): Promise<AnomalieResponsable> {
|
|
verifierResponsable(roleCode);
|
|
|
|
const anomalie = await findAnomalieResponsableById(campusId, anomalieId);
|
|
if (!anomalie) {
|
|
throw new AppError(404, 'Anomalie introuvable');
|
|
}
|
|
|
|
const statutActuel = anomalie.statut as StatutAnomalie;
|
|
if (!TRANSITIONS_ANOMALIE[statutActuel]?.includes(data.statut)) {
|
|
throw new AppError(400, `Transition invalide depuis ${anomalie.statut} vers ${data.statut}`);
|
|
}
|
|
|
|
return changerStatutAnomalieResponsable({
|
|
anomalieId,
|
|
campusId,
|
|
responsableId,
|
|
statut: data.statut,
|
|
observation: data.observation,
|
|
});
|
|
}
|