feat(loans): confirm declared material state changes
This commit is contained in:
@@ -4,7 +4,10 @@ import { StatutEmprunt } from '../models/enums';
|
||||
import { AppError } from '../errors/app-error';
|
||||
|
||||
export type EmpruntAvecMateriel = Prisma.EmpruntGetPayload<{
|
||||
include: { materiel: { include: { categorie: true } } };
|
||||
include: {
|
||||
materiel: { include: { categorie: true } };
|
||||
checklists: { include: { elements: true } };
|
||||
};
|
||||
}>;
|
||||
|
||||
export type EmpruntAvecChecklists = Prisma.EmpruntGetPayload<{
|
||||
@@ -12,6 +15,12 @@ export type EmpruntAvecChecklists = Prisma.EmpruntGetPayload<{
|
||||
}>;
|
||||
|
||||
const STATUTS_EN_COURS: StatutEmprunt[] = ['EN_COURS', 'EN_RETARD'];
|
||||
const STATUTS_BLOQUANTS: StatutEmprunt[] = [
|
||||
'EN_ATTENTE_VALIDATION_DEPART',
|
||||
'EN_COURS',
|
||||
'EN_RETARD',
|
||||
'EN_ATTENTE_VALIDATION_RETOUR',
|
||||
];
|
||||
|
||||
export function findEnCoursParUtilisateur(utilisateurId: number): Promise<EmpruntAvecMateriel[]> {
|
||||
return prisma.emprunt.findMany({
|
||||
@@ -19,7 +28,10 @@ export function findEnCoursParUtilisateur(utilisateurId: number): Promise<Emprun
|
||||
utilisateurId,
|
||||
statut: { in: STATUTS_EN_COURS },
|
||||
},
|
||||
include: { materiel: { include: { categorie: true } } },
|
||||
include: {
|
||||
materiel: { include: { categorie: true } },
|
||||
checklists: { include: { elements: true } },
|
||||
},
|
||||
orderBy: { dateRetourPrevue: 'asc' },
|
||||
});
|
||||
}
|
||||
@@ -31,6 +43,71 @@ export function findByIdAvecChecklists(id: number): Promise<EmpruntAvecChecklist
|
||||
});
|
||||
}
|
||||
|
||||
export interface SignalerTentativeRetourData {
|
||||
empruntId: number;
|
||||
utilisateurId: number;
|
||||
materielId: number;
|
||||
campusId: number;
|
||||
sallePretId: number;
|
||||
posteEmpruntId: number;
|
||||
responsablesIds: number[];
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function signalerTentativeRetour(data: SignalerTentativeRetourData): Promise<boolean> {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const emprunt = await tx.emprunt.findFirst({
|
||||
where: {
|
||||
id: data.empruntId,
|
||||
utilisateurId: data.utilisateurId,
|
||||
statut: { in: STATUTS_EN_COURS },
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (!emprunt) {
|
||||
throw new AppError(409, 'Emprunt non restituable');
|
||||
}
|
||||
|
||||
const dejaSignalee = await tx.historique.findFirst({
|
||||
where: {
|
||||
empruntId: data.empruntId,
|
||||
action: 'TENTATIVE_MODIFICATION_RETOUR',
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (dejaSignalee) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.responsablesIds.length > 0) {
|
||||
await tx.notification.createMany({
|
||||
data: data.responsablesIds.map((responsableId) => ({
|
||||
utilisateurId: responsableId,
|
||||
titre: 'Tentative de modification au retour',
|
||||
message: data.description,
|
||||
type: 'TENTATIVE_MODIFICATION_RETOUR',
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
await tx.historique.create({
|
||||
data: {
|
||||
utilisateurId: data.utilisateurId,
|
||||
empruntId: data.empruntId,
|
||||
materielId: data.materielId,
|
||||
campusId: data.campusId,
|
||||
sallePretId: data.sallePretId,
|
||||
posteEmpruntId: data.posteEmpruntId,
|
||||
action: 'TENTATIVE_MODIFICATION_RETOUR',
|
||||
description: data.description,
|
||||
dateAction: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
interface ChecklistElementData {
|
||||
accessoireId?: number;
|
||||
nomElement: string;
|
||||
@@ -42,6 +119,7 @@ interface ChecklistElementData {
|
||||
export interface CreerEmpruntData {
|
||||
utilisateurId: number;
|
||||
materielId: number;
|
||||
categorieId: number;
|
||||
campusId: number;
|
||||
sallePretId: number;
|
||||
posteEmpruntId: number;
|
||||
@@ -49,59 +127,122 @@ export interface CreerEmpruntData {
|
||||
modeIdentification: string;
|
||||
commentaireDepart?: string;
|
||||
elements: ChecklistElementData[];
|
||||
statutEmprunt: StatutEmprunt;
|
||||
statutMateriel: string;
|
||||
responsablesIds: number[];
|
||||
descriptionEcart?: string;
|
||||
}
|
||||
|
||||
/* Création atomique : réservation du matériel (DISPONIBLE -> EMPRUNTE), emprunt,
|
||||
checklist de départ et ses éléments. Si une étape échoue, tout est annulé. */
|
||||
export function creerEmpruntComplet(data: CreerEmpruntData): Promise<EmpruntAvecMateriel> {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
// Réservation atomique : empêche un double emprunt du même matériel (RG07).
|
||||
const reservation = await tx.materiel.updateMany({
|
||||
where: { id: data.materielId, statut: 'DISPONIBLE' },
|
||||
data: { statut: 'EMPRUNTE' },
|
||||
});
|
||||
if (reservation.count === 0) {
|
||||
throw new AppError(409, 'Materiel non disponible');
|
||||
}
|
||||
|
||||
const emprunt = await tx.emprunt.create({
|
||||
data: {
|
||||
utilisateurId: data.utilisateurId,
|
||||
materielId: data.materielId,
|
||||
campusId: data.campusId,
|
||||
sallePretId: data.sallePretId,
|
||||
posteEmpruntId: data.posteEmpruntId,
|
||||
dateEmprunt: new Date(),
|
||||
dateRetourPrevue: data.dateRetourPrevue,
|
||||
statut: 'EN_COURS',
|
||||
modeIdentificationEmprunt: data.modeIdentification,
|
||||
commentaireDepart: data.commentaireDepart ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.checklist.create({
|
||||
data: {
|
||||
empruntId: emprunt.id,
|
||||
utilisateurId: data.utilisateurId,
|
||||
type: 'DEPART',
|
||||
dateVerification: new Date(),
|
||||
elements: {
|
||||
create: data.elements.map((element) => ({
|
||||
accessoireId: element.accessoireId ?? null,
|
||||
nomElement: element.nomElement,
|
||||
etat: element.etat,
|
||||
quantiteConstatee: element.quantiteConstatee,
|
||||
commentaire: element.commentaire ?? null,
|
||||
})),
|
||||
return prisma.$transaction(
|
||||
async (tx) => {
|
||||
const empruntBloquant = await tx.emprunt.findFirst({
|
||||
where: {
|
||||
utilisateurId: data.utilisateurId,
|
||||
statut: { in: STATUTS_BLOQUANTS },
|
||||
materiel: { categorieId: data.categorieId },
|
||||
},
|
||||
},
|
||||
});
|
||||
select: { id: true },
|
||||
});
|
||||
if (empruntBloquant) {
|
||||
throw new AppError(
|
||||
409,
|
||||
'Un materiel de cette categorie est deja emprunte ou en attente de confirmation',
|
||||
);
|
||||
}
|
||||
|
||||
return tx.emprunt.findUniqueOrThrow({
|
||||
where: { id: emprunt.id },
|
||||
include: { materiel: { include: { categorie: true } } },
|
||||
});
|
||||
});
|
||||
const reservation = await tx.materiel.updateMany({
|
||||
where: { id: data.materielId, statut: 'DISPONIBLE' },
|
||||
data: { statut: data.statutMateriel },
|
||||
});
|
||||
if (reservation.count === 0) {
|
||||
throw new AppError(409, 'Materiel non disponible');
|
||||
}
|
||||
|
||||
const maintenant = new Date();
|
||||
const emprunt = await tx.emprunt.create({
|
||||
data: {
|
||||
utilisateurId: data.utilisateurId,
|
||||
materielId: data.materielId,
|
||||
campusId: data.campusId,
|
||||
sallePretId: data.sallePretId,
|
||||
posteEmpruntId: data.posteEmpruntId,
|
||||
dateEmprunt: maintenant,
|
||||
dateRetourPrevue: data.dateRetourPrevue,
|
||||
statut: data.statutEmprunt,
|
||||
modeIdentificationEmprunt: data.modeIdentification,
|
||||
commentaireDepart: data.commentaireDepart ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.checklist.create({
|
||||
data: {
|
||||
empruntId: emprunt.id,
|
||||
utilisateurId: data.utilisateurId,
|
||||
type: 'DEPART',
|
||||
dateVerification: maintenant,
|
||||
elements: {
|
||||
create: data.elements.map((element) => ({
|
||||
accessoireId: element.accessoireId ?? null,
|
||||
nomElement: element.nomElement,
|
||||
etat: element.etat,
|
||||
quantiteConstatee: element.quantiteConstatee,
|
||||
commentaire: element.commentaire ?? null,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (data.descriptionEcart) {
|
||||
if (data.responsablesIds.length > 0) {
|
||||
await tx.notification.createMany({
|
||||
data: data.responsablesIds.map((responsableId) => ({
|
||||
utilisateurId: responsableId,
|
||||
titre: 'Ecart signale au depart',
|
||||
message: data.descriptionEcart as string,
|
||||
type: 'ECART_DEPART',
|
||||
})),
|
||||
});
|
||||
}
|
||||
await tx.historique.create({
|
||||
data: {
|
||||
utilisateurId: data.utilisateurId,
|
||||
empruntId: emprunt.id,
|
||||
materielId: data.materielId,
|
||||
campusId: data.campusId,
|
||||
sallePretId: data.sallePretId,
|
||||
posteEmpruntId: data.posteEmpruntId,
|
||||
action: 'ECART_DEPART_SIGNALE',
|
||||
description: data.descriptionEcart,
|
||||
dateAction: maintenant,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await tx.historique.create({
|
||||
data: {
|
||||
utilisateurId: data.utilisateurId,
|
||||
empruntId: emprunt.id,
|
||||
materielId: data.materielId,
|
||||
campusId: data.campusId,
|
||||
sallePretId: data.sallePretId,
|
||||
posteEmpruntId: data.posteEmpruntId,
|
||||
action: 'EMPRUNT_AUTOMATIQUE',
|
||||
description: `Emprunt #${emprunt.id} active avec une checklist de depart inchangee`,
|
||||
dateAction: maintenant,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return tx.emprunt.findUniqueOrThrow({
|
||||
where: { id: emprunt.id },
|
||||
include: {
|
||||
materiel: { include: { categorie: true } },
|
||||
checklists: { include: { elements: true } },
|
||||
},
|
||||
});
|
||||
},
|
||||
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
|
||||
);
|
||||
}
|
||||
|
||||
export interface RestituerEmpruntData {
|
||||
@@ -113,19 +254,34 @@ export interface RestituerEmpruntData {
|
||||
modeIdentification: string;
|
||||
commentaireRetour?: string;
|
||||
elements: ChecklistElementData[];
|
||||
anomalie?: {
|
||||
type: string;
|
||||
description: string;
|
||||
responsablesIds: number[];
|
||||
};
|
||||
campusId: number;
|
||||
sallePretId: number;
|
||||
posteEmpruntId: number;
|
||||
responsablesIds: number[];
|
||||
descriptionEcart?: string;
|
||||
}
|
||||
|
||||
/* Restitution atomique : checklist de retour, mise à jour de l'emprunt et du
|
||||
matériel, et — si non conforme — création de l'anomalie et des notifications. */
|
||||
export function restituerEmpruntComplet(data: RestituerEmpruntData): Promise<EmpruntAvecMateriel> {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const maintenant = new Date();
|
||||
|
||||
const transition = await tx.emprunt.updateMany({
|
||||
where: {
|
||||
id: data.empruntId,
|
||||
utilisateurId: data.utilisateurId,
|
||||
statut: { in: STATUTS_EN_COURS },
|
||||
},
|
||||
data: {
|
||||
statut: data.statutEmprunt,
|
||||
...(data.statutEmprunt === 'CLOTURE' ? { dateRetourReelle: maintenant } : {}),
|
||||
modeIdentificationRetour: data.modeIdentification,
|
||||
commentaireRetour: data.commentaireRetour ?? null,
|
||||
},
|
||||
});
|
||||
if (transition.count === 0) {
|
||||
throw new AppError(409, 'Emprunt non restituable');
|
||||
}
|
||||
|
||||
await tx.checklist.create({
|
||||
data: {
|
||||
empruntId: data.empruntId,
|
||||
@@ -144,52 +300,64 @@ export function restituerEmpruntComplet(data: RestituerEmpruntData): Promise<Emp
|
||||
},
|
||||
});
|
||||
|
||||
await tx.emprunt.update({
|
||||
where: { id: data.empruntId },
|
||||
data: {
|
||||
statut: data.statutEmprunt,
|
||||
dateRetourReelle: maintenant,
|
||||
modeIdentificationRetour: data.modeIdentification,
|
||||
commentaireRetour: data.commentaireRetour ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.materiel.update({
|
||||
where: { id: data.materielId },
|
||||
data: { statut: data.statutMateriel },
|
||||
});
|
||||
|
||||
if (data.anomalie) {
|
||||
const infoAnomalie = data.anomalie;
|
||||
const anomalie = await tx.anomalie.create({
|
||||
data: {
|
||||
if (data.descriptionEcart) {
|
||||
const tentativeDejaSignalee = await tx.historique.findFirst({
|
||||
where: {
|
||||
empruntId: data.empruntId,
|
||||
materielId: data.materielId,
|
||||
utilisateurId: data.utilisateurId,
|
||||
type: infoAnomalie.type,
|
||||
description: infoAnomalie.description,
|
||||
statut: 'DETECTEE',
|
||||
detecteeAutomatiquement: true,
|
||||
dateDetection: maintenant,
|
||||
action: 'TENTATIVE_MODIFICATION_RETOUR',
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (infoAnomalie.responsablesIds.length > 0) {
|
||||
if (!tentativeDejaSignalee && data.responsablesIds.length > 0) {
|
||||
await tx.notification.createMany({
|
||||
data: infoAnomalie.responsablesIds.map((responsableId) => ({
|
||||
data: data.responsablesIds.map((responsableId) => ({
|
||||
utilisateurId: responsableId,
|
||||
anomalieId: anomalie.id,
|
||||
titre: 'Nouvelle anomalie detectee',
|
||||
message: infoAnomalie.description,
|
||||
type: 'ANOMALIE',
|
||||
titre: 'Changement signale au retour',
|
||||
message: data.descriptionEcart as string,
|
||||
type: 'ECART_RETOUR',
|
||||
})),
|
||||
});
|
||||
}
|
||||
await tx.historique.create({
|
||||
data: {
|
||||
utilisateurId: data.utilisateurId,
|
||||
empruntId: data.empruntId,
|
||||
materielId: data.materielId,
|
||||
campusId: data.campusId,
|
||||
sallePretId: data.sallePretId,
|
||||
posteEmpruntId: data.posteEmpruntId,
|
||||
action: 'ECART_RETOUR_SIGNALE',
|
||||
description: data.descriptionEcart,
|
||||
dateAction: maintenant,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await tx.historique.create({
|
||||
data: {
|
||||
utilisateurId: data.utilisateurId,
|
||||
empruntId: data.empruntId,
|
||||
materielId: data.materielId,
|
||||
campusId: data.campusId,
|
||||
sallePretId: data.sallePretId,
|
||||
posteEmpruntId: data.posteEmpruntId,
|
||||
action: 'RESTITUTION_AUTOMATIQUE',
|
||||
description: `Emprunt #${data.empruntId} cloture avec une checklist de retour inchangee`,
|
||||
dateAction: maintenant,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return tx.emprunt.findUniqueOrThrow({
|
||||
where: { id: data.empruntId },
|
||||
include: { materiel: { include: { categorie: true } } },
|
||||
include: {
|
||||
materiel: { include: { categorie: true } },
|
||||
checklists: { include: { elements: true } },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user