feat: implement equipment and container loading rollback functionality with corresponding backend cloud functions
This commit is contained in:
@@ -1,60 +1,22 @@
|
||||
const {onRequest} = require("firebase-functions/v2/https");
|
||||
const {onCall} = require("firebase-functions/v2/https");
|
||||
const admin = require("firebase-admin");
|
||||
const nodemailer = require("nodemailer");
|
||||
const logger = require("firebase-functions/logger");
|
||||
const {getSmtpConfig, EMAIL_CONFIG} = require("./utils/emailConfig");
|
||||
const {renderTemplate, getEmailSubject, getAlertTitle, prepareTemplateData, checkAlertPreference} = require("./utils/emailTemplates");
|
||||
const auth = require("./utils/auth");
|
||||
|
||||
// Configuration CORS
|
||||
const setCorsHeaders = (res, req) => {
|
||||
// Utiliser l'origin de la requête pour permettre les credentials
|
||||
const origin = req.headers.origin || "*";
|
||||
|
||||
res.set("Access-Control-Allow-Origin", origin);
|
||||
|
||||
// N'autoriser les credentials que si on a un origin spécifique (pas '*')
|
||||
if (origin !== "*") {
|
||||
res.set("Access-Control-Allow-Credentials", "true");
|
||||
}
|
||||
|
||||
res.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
res.set("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, Origin, X-Requested-With");
|
||||
res.set("Access-Control-Max-Age", "3600");
|
||||
};
|
||||
|
||||
const withCors = (handler) => {
|
||||
return async (req, res) => {
|
||||
setCorsHeaders(res, req);
|
||||
// Gérer les requêtes preflight OPTIONS immédiatement
|
||||
if (req.method === "OPTIONS") {
|
||||
res.status(204).send("");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await handler(req, res);
|
||||
} catch (error) {
|
||||
logger.error("Unhandled error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Crée une alerte et envoie les notifications
|
||||
* Gère tout le processus côté backend de A à Z
|
||||
*/
|
||||
exports.createAlert = onRequest({
|
||||
cors: false,
|
||||
invoker: "public",
|
||||
region: "europe-west9",
|
||||
}, withCors(async (req, res) => {
|
||||
const handler = async (request) => {
|
||||
try {
|
||||
const {auth, data} = request;
|
||||
|
||||
// Vérifier l'authentification
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const data = req.body.data || req.body;
|
||||
if (!auth) {
|
||||
throw new Error("L'utilisateur doit être authentifié");
|
||||
}
|
||||
|
||||
|
||||
const {
|
||||
@@ -96,7 +58,7 @@ exports.createAlert = onRequest({
|
||||
metadata: metadata || {},
|
||||
assignedTo: userIds,
|
||||
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
createdBy: decodedToken.uid,
|
||||
createdBy: auth.uid,
|
||||
isRead: false,
|
||||
emailSent: false,
|
||||
status: "ACTIVE",
|
||||
@@ -117,17 +79,17 @@ exports.createAlert = onRequest({
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
return {
|
||||
success: true,
|
||||
alertId: alertRef.id,
|
||||
usersNotified: userIds.length,
|
||||
emailsSent: Object.values(emailResults).filter((v) => v).length,
|
||||
});
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("[createAlert] Erreur:", error);
|
||||
res.status(500).json({error: `Erreur lors de la création de l'alerte: ${error.message}`});
|
||||
throw error;
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Détermine les utilisateurs à notifier selon le type d'alerte
|
||||
@@ -189,7 +151,7 @@ async function determineTargetUsers(alertType, severity, eventId) {
|
||||
*/
|
||||
async function sendAlertEmails(alertId, alertData, userIds) {
|
||||
const results = {};
|
||||
const transporter = nodemailer.createTransporter(getSmtpConfig());
|
||||
const transporter = nodemailer.createTransport(getSmtpConfig());
|
||||
|
||||
// Envoyer les emails en parallèle (batch de 5)
|
||||
const batches = [];
|
||||
@@ -269,3 +231,10 @@ async function sendSingleEmail(transporter, alertId, alertData, userId) {
|
||||
}
|
||||
}
|
||||
|
||||
exports.createAlert = onCall({
|
||||
cors: true,
|
||||
region: "europe-west9",
|
||||
}, handler);
|
||||
|
||||
exports.handler = handler;
|
||||
|
||||
|
||||
+101
-19
@@ -190,14 +190,6 @@ exports.getEventWithDetails = onRequest(httpOptions, withCors((req, res) => {
|
||||
return require("./src/events").getEventWithDetails(req, res);
|
||||
}));
|
||||
|
||||
exports.validateEquipmentPreparation = onRequest(httpOptions, withCors((req, res) => {
|
||||
return require("./src/events").validateEquipmentPreparation(req, res);
|
||||
}));
|
||||
|
||||
exports.validateAllPreparation = onRequest(httpOptions, withCors((req, res) => {
|
||||
return require("./src/events").validateAllPreparation(req, res);
|
||||
}));
|
||||
|
||||
exports.validateEquipmentLoading = onRequest(httpOptions, withCors((req, res) => {
|
||||
return require("./src/events").validateEquipmentLoading(req, res);
|
||||
}));
|
||||
@@ -214,14 +206,6 @@ exports.validateAllUnloading = onRequest(httpOptions, withCors((req, res) => {
|
||||
return require("./src/events").validateAllUnloading(req, res);
|
||||
}));
|
||||
|
||||
exports.validateEquipmentReturn = onRequest(httpOptions, withCors((req, res) => {
|
||||
return require("./src/events").validateEquipmentReturn(req, res);
|
||||
}));
|
||||
|
||||
exports.validateAllReturn = onRequest(httpOptions, withCors((req, res) => {
|
||||
return require("./src/events").validateAllReturn(req, res);
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// MAINTENANCES
|
||||
// ============================================================================
|
||||
@@ -379,15 +363,19 @@ exports.aiEquipmentProposal = onRequest(aiHttpOptions, withCors((req, res) => {
|
||||
// CALLABLE EMAIL & VALIDATION (LEGACY LAZY WRAPPERS)
|
||||
// ============================================================================
|
||||
exports.sendAlertEmail = onCall({region: "europe-west9", cors: true}, (request) => {
|
||||
return require("./sendAlertEmail").sendAlertEmail(request);
|
||||
return require("./sendAlertEmail").handler(request);
|
||||
});
|
||||
|
||||
exports.createAlert = onCall({region: "europe-west9", cors: true}, (request) => {
|
||||
return require("./createAlert").createAlert(request);
|
||||
return require("./createAlert").handler(request);
|
||||
});
|
||||
|
||||
exports.processEquipmentValidation = onCall({region: "europe-west9", cors: true}, (request) => {
|
||||
return require("./processEquipmentValidation").processEquipmentValidation(request);
|
||||
return require("./processEquipmentValidation").handler(request);
|
||||
});
|
||||
|
||||
exports.rollbackEventStep = onCall({region: "europe-west9", cors: true}, (request) => {
|
||||
return require("./rollbackEventStep").handler(request);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
@@ -515,3 +503,97 @@ exports.onAlertCreated = onDocumentCreated({
|
||||
logger.error("[onAlertCreated] Erreur:", error);
|
||||
}
|
||||
});
|
||||
|
||||
exports.onEventReturnCompleted = onDocumentUpdated({
|
||||
document: "events/{eventId}",
|
||||
region: "europe-west9",
|
||||
}, async (event) => {
|
||||
const before = event.data.before.data();
|
||||
const after = event.data.after.data();
|
||||
const eventId = event.params.eventId;
|
||||
|
||||
try {
|
||||
const beforeReturnStatus = (before.returnStatus || "").toString().toUpperCase();
|
||||
const afterReturnStatus = (after.returnStatus || "").toString().toUpperCase();
|
||||
|
||||
if (afterReturnStatus === "COMPLETED" && beforeReturnStatus !== "COMPLETED") {
|
||||
logger.info(`[onEventReturnCompleted] Event ${eventId} returnStatus completed. Resetting assigned equipments...`);
|
||||
|
||||
const eventRef = db.collection("events").doc(eventId);
|
||||
|
||||
await db.runTransaction(async (transaction) => {
|
||||
const currentEventDoc = await transaction.get(eventRef);
|
||||
if (!currentEventDoc.exists) {
|
||||
logger.warn(`[onEventReturnCompleted] Event doc ${eventId} not found during transaction.`);
|
||||
return;
|
||||
}
|
||||
const currentEventData = currentEventDoc.data();
|
||||
if (currentEventData.stocksRestored === true) {
|
||||
logger.info(`[onEventReturnCompleted] Stocks already restored for event ${eventId}, skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const assignedEquipment = currentEventData.assignedEquipment || [];
|
||||
if (assignedEquipment.length === 0) {
|
||||
logger.info(`[onEventReturnCompleted] No assigned equipment for event ${eventId}.`);
|
||||
transaction.update(eventRef, {
|
||||
stocksRestored: true,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all unique equipment docs in the transaction
|
||||
const equipmentIds = Array.from(new Set(assignedEquipment.map((eq) => eq.equipmentId).filter(Boolean)));
|
||||
const equipmentDocsMap = {};
|
||||
|
||||
for (const eqId of equipmentIds) {
|
||||
const eqRef = db.collection("equipments").doc(eqId);
|
||||
const eqDoc = await transaction.get(eqRef);
|
||||
if (eqDoc.exists) {
|
||||
equipmentDocsMap[eqId] = eqDoc.data();
|
||||
}
|
||||
}
|
||||
|
||||
// Update equipment statuses and quantities
|
||||
for (const eq of assignedEquipment) {
|
||||
const eqId = eq.equipmentId;
|
||||
const equipmentData = equipmentDocsMap[eqId];
|
||||
if (!equipmentData) continue;
|
||||
|
||||
const hasQuantity = equipmentData.hasQuantity === true ||
|
||||
equipmentData.category === "CABLE" ||
|
||||
equipmentData.category === "CONSUMABLE";
|
||||
|
||||
const eqRef = db.collection("equipments").doc(eqId);
|
||||
if (!hasQuantity) {
|
||||
// Non-consumable: reset to AVAILABLE
|
||||
transaction.update(eqRef, {
|
||||
status: "AVAILABLE",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
logger.info(`[onEventReturnCompleted] Set status to AVAILABLE for equipment ${eqId}`);
|
||||
} else if (hasQuantity && eq.quantityAtReturn !== undefined && eq.quantityAtReturn !== null) {
|
||||
// Consumable: increment availableQuantity
|
||||
const currentAvailable = Number(equipmentData.availableQuantity) || 0;
|
||||
const returnedQty = Number(eq.quantityAtReturn) || 0;
|
||||
transaction.update(eqRef, {
|
||||
availableQuantity: currentAvailable + returnedQty,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
logger.info(`[onEventReturnCompleted] Restored ${returnedQty} items for consumable ${eqId} (new available: ${currentAvailable + returnedQty})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark event as stocksRestored
|
||||
transaction.update(eventRef, {
|
||||
stocksRestored: true,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
});
|
||||
logger.info(`[onEventReturnCompleted] Transaction completed successfully for event ${eventId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[onEventReturnCompleted] Error resetting equipment statuses for event ${eventId}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,10 +8,7 @@ const {getSmtpConfig, EMAIL_CONFIG} = require("./utils/emailConfig");
|
||||
* Appelée par le client lors du chargement/déchargement
|
||||
* Crée automatiquement les alertes nécessaires
|
||||
*/
|
||||
exports.processEquipmentValidation = onCall({
|
||||
cors: true,
|
||||
region: "europe-west9",
|
||||
}, async (request) => {
|
||||
const handler = async (request) => {
|
||||
try {
|
||||
// L'authentification est automatique avec onCall
|
||||
const {auth, data} = request;
|
||||
@@ -140,9 +137,8 @@ exports.processEquipmentValidation = onCall({
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Mettre à jour les équipements de l'événement
|
||||
// 3. Mettre à jour les équipements de l'événement (uniquement lastValidation, assignedEquipment est déjà mis à jour par le client)
|
||||
await eventRef.update({
|
||||
equipment: equipmentList,
|
||||
lastValidation: {
|
||||
type: validationType,
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||
@@ -461,3 +457,10 @@ function parseFirestoreDate(value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
exports.processEquipmentValidation = onCall({
|
||||
cors: true,
|
||||
region: "europe-west9",
|
||||
}, handler);
|
||||
|
||||
exports.handler = handler;
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
const {onCall} = require("firebase-functions/v2/https");
|
||||
const admin = require("firebase-admin");
|
||||
const logger = require("firebase-functions/logger");
|
||||
|
||||
/**
|
||||
* Reverts the validation progress of an event to a target step.
|
||||
* Resets subsequent step statuses and validation flags of assigned equipment.
|
||||
* If rolling back from a completed return, it decrements the consumable stock quantities
|
||||
* that were restored during return validation.
|
||||
*/
|
||||
const handler = async (request) => {
|
||||
try {
|
||||
const {auth, data} = request;
|
||||
if (!auth) {
|
||||
throw new Error("L'utilisateur doit être authentifié");
|
||||
}
|
||||
|
||||
const {eventId, targetStep} = data;
|
||||
if (!eventId || !targetStep) {
|
||||
throw new Error("eventId et targetStep sont requis");
|
||||
}
|
||||
|
||||
const db = admin.firestore();
|
||||
const eventRef = db.collection("events").doc(eventId);
|
||||
|
||||
await db.runTransaction(async (transaction) => {
|
||||
const eventDoc = await transaction.get(eventRef);
|
||||
if (!eventDoc.exists) {
|
||||
throw new Error("Événement introuvable");
|
||||
}
|
||||
|
||||
const event = eventDoc.data();
|
||||
const assignedEquipment = event.assignedEquipment || [];
|
||||
|
||||
// Si le retour était complété et qu'on revient en arrière, on doit annuler la restauration des stocks
|
||||
const shouldRevertStocks = event.stocksRestored === true;
|
||||
|
||||
if (shouldRevertStocks) {
|
||||
// Charger tous les équipements uniques de l'événement pour ajuster leur stock
|
||||
const equipmentIds = Array.from(new Set(assignedEquipment.map((eq) => eq.equipmentId).filter(Boolean)));
|
||||
const equipmentDocsMap = {};
|
||||
|
||||
for (const eqId of equipmentIds) {
|
||||
const eqRef = db.collection("equipments").doc(eqId);
|
||||
const eqDoc = await transaction.get(eqRef);
|
||||
if (eqDoc.exists) {
|
||||
equipmentDocsMap[eqId] = eqDoc.data();
|
||||
}
|
||||
}
|
||||
|
||||
for (const eq of assignedEquipment) {
|
||||
const eqId = eq.equipmentId;
|
||||
const equipmentData = equipmentDocsMap[eqId];
|
||||
if (!equipmentData) continue;
|
||||
|
||||
const hasQuantity = equipmentData.hasQuantity === true ||
|
||||
equipmentData.category === "CABLE" ||
|
||||
equipmentData.category === "CONSUMABLE";
|
||||
|
||||
if (hasQuantity) {
|
||||
// C'est un consommable, on doit déduire la quantité qui avait été restaurée
|
||||
const qtyAtRet = Number(eq.quantityAtReturn) || 0;
|
||||
if (qtyAtRet > 0) {
|
||||
const eqRef = db.collection("equipments").doc(eqId);
|
||||
const currentAvailable = Number(equipmentData.availableQuantity) || 0;
|
||||
// S'assurer de ne pas descendre en dessous de 0 (ou autoriser le négatif si stock virtuel)
|
||||
const newAvailable = Math.max(0, currentAvailable - qtyAtRet);
|
||||
transaction.update(eqRef, {
|
||||
availableQuantity: newAvailable,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
logger.info(`[rollbackEventStep] Annulé la restauration de ${qtyAtRet} pour ${eqId}. Ancien stock: ${currentAvailable}, Nouveau stock: ${newAvailable}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Préparer les nouvelles valeurs des étapes
|
||||
let prepStatus = event.preparationStatus;
|
||||
let loadStatus = event.loadingStatus;
|
||||
let unloadStatus = event.unloadingStatus;
|
||||
let retStatus = event.returnStatus;
|
||||
|
||||
if (targetStep === 'PREPARATION') {
|
||||
prepStatus = 'IN_PROGRESS';
|
||||
loadStatus = 'NOT_STARTED';
|
||||
unloadStatus = 'NOT_STARTED';
|
||||
retStatus = 'NOT_STARTED';
|
||||
} else if (targetStep === 'LOADING') {
|
||||
loadStatus = 'IN_PROGRESS';
|
||||
unloadStatus = 'NOT_STARTED';
|
||||
retStatus = 'NOT_STARTED';
|
||||
} else if (targetStep === 'UNLOADING') {
|
||||
unloadStatus = 'IN_PROGRESS';
|
||||
retStatus = 'NOT_STARTED';
|
||||
} else if (targetStep === 'RETURN') {
|
||||
retStatus = 'IN_PROGRESS';
|
||||
} else {
|
||||
throw new Error("targetStep invalide. Doit être PREPARATION, LOADING, UNLOADING ou RETURN");
|
||||
}
|
||||
|
||||
// Nettoyer les champs de validation des équipements pour les étapes annulées
|
||||
const updatedEquipment = assignedEquipment.map((eq) => {
|
||||
let isPrepared = eq.isPrepared;
|
||||
let isMissingAtPreparation = eq.isMissingAtPreparation;
|
||||
let quantityAtPreparation = eq.quantityAtPreparation;
|
||||
|
||||
let isLoaded = eq.isLoaded;
|
||||
let isMissingAtLoading = eq.isMissingAtLoading;
|
||||
let quantityAtLoading = eq.quantityAtLoading;
|
||||
|
||||
let isUnloaded = eq.isUnloaded;
|
||||
let isMissingAtUnloading = eq.isMissingAtUnloading;
|
||||
let quantityAtUnloading = eq.quantityAtUnloading;
|
||||
|
||||
let isReturned = eq.isReturned;
|
||||
let isMissingAtReturn = eq.isMissingAtReturn;
|
||||
let quantityAtReturn = eq.quantityAtReturn;
|
||||
|
||||
if (targetStep === 'PREPARATION') {
|
||||
isLoaded = false;
|
||||
isMissingAtLoading = false;
|
||||
quantityAtLoading = null;
|
||||
isUnloaded = false;
|
||||
isMissingAtUnloading = false;
|
||||
quantityAtUnloading = null;
|
||||
isReturned = false;
|
||||
isMissingAtReturn = false;
|
||||
quantityAtReturn = null;
|
||||
} else if (targetStep === 'LOADING') {
|
||||
isUnloaded = false;
|
||||
isMissingAtUnloading = false;
|
||||
quantityAtUnloading = null;
|
||||
isReturned = false;
|
||||
isMissingAtReturn = false;
|
||||
quantityAtReturn = null;
|
||||
} else if (targetStep === 'UNLOADING') {
|
||||
isReturned = false;
|
||||
isMissingAtReturn = false;
|
||||
quantityAtReturn = null;
|
||||
}
|
||||
|
||||
return {
|
||||
...eq,
|
||||
isPrepared,
|
||||
isMissingAtPreparation,
|
||||
quantityAtPreparation,
|
||||
isLoaded,
|
||||
isMissingAtLoading,
|
||||
quantityAtLoading,
|
||||
isUnloaded,
|
||||
isMissingAtUnloading,
|
||||
quantityAtUnloading,
|
||||
isReturned,
|
||||
isMissingAtReturn,
|
||||
quantityAtReturn,
|
||||
};
|
||||
});
|
||||
|
||||
// Mettre à jour le document de l'événement
|
||||
transaction.update(eventRef, {
|
||||
preparationStatus: prepStatus,
|
||||
loadingStatus: loadStatus,
|
||||
unloadingStatus: unloadStatus,
|
||||
returnStatus: retStatus,
|
||||
assignedEquipment: updatedEquipment,
|
||||
stocksRestored: false,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
});
|
||||
|
||||
logger.info(`[rollbackEventStep] Événement ${eventId} réinitialisé avec succès à l'étape ${targetStep}`);
|
||||
return {success: true};
|
||||
} catch (error) {
|
||||
logger.error("[rollbackEventStep] Erreur:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
exports.rollbackEventStep = onCall({
|
||||
cors: true,
|
||||
region: "europe-west9",
|
||||
}, handler);
|
||||
|
||||
exports.handler = handler;
|
||||
@@ -10,10 +10,7 @@ const {getSmtpConfig, EMAIL_CONFIG} = require("./utils/emailConfig");
|
||||
* Envoie un email d'alerte à un utilisateur
|
||||
* Appelé par le client Dart via callable function
|
||||
*/
|
||||
exports.sendAlertEmail = onCall({
|
||||
region: "europe-west9",
|
||||
cors: true,
|
||||
}, async (request) => {
|
||||
const handler = async (request) => {
|
||||
// Vérifier l'authentification
|
||||
if (!request.auth) {
|
||||
throw new Error("L'utilisateur doit être authentifié");
|
||||
@@ -75,7 +72,7 @@ exports.sendAlertEmail = onCall({
|
||||
);
|
||||
|
||||
// Configurer le transporteur SMTP
|
||||
const transporter = nodemailer.createTransporter(getSmtpConfig());
|
||||
const transporter = nodemailer.createTransport(getSmtpConfig());
|
||||
|
||||
// Envoyer l'email
|
||||
const info = await transporter.sendMail({
|
||||
@@ -263,3 +260,10 @@ async function renderTemplate(templateName, data) {
|
||||
}
|
||||
}
|
||||
|
||||
exports.sendAlertEmail = onCall({
|
||||
region: "europe-west9",
|
||||
cors: true,
|
||||
}, handler);
|
||||
|
||||
exports.handler = handler;
|
||||
|
||||
|
||||
@@ -470,7 +470,7 @@ exports.findAlternativeEquipment = async (req, res) => {
|
||||
const alternatives = [];
|
||||
equipmentsSnapshot.docs.forEach((doc) => {
|
||||
const data = doc.data();
|
||||
if (!conflictingEquipmentIds.has(doc.id) && data.status === "available") {
|
||||
if (!conflictingEquipmentIds.has(doc.id) && data.status === "AVAILABLE") {
|
||||
alternatives.push({
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(data, ["purchaseDate", "nextMaintenanceDate", "lastMaintenanceDate", "createdAt", "updatedAt"]),
|
||||
@@ -560,15 +560,15 @@ exports.calculateEquipmentStatuses = async (req, res) => {
|
||||
let calculatedStatus = equipmentData.status;
|
||||
|
||||
// Si l'équipement est perdu ou HS, garder ce statut
|
||||
if (equipmentData.status === "lost" || equipmentData.status === "outOfService") {
|
||||
if (equipmentData.status === "LOST" || equipmentData.status === "OUT_OF_SERVICE") {
|
||||
calculatedStatus = equipmentData.status;
|
||||
} else if (equipmentIdsInUse.has(equipmentId)) {
|
||||
calculatedStatus = "inUse";
|
||||
} else if (equipmentData.status === "maintenance" ||
|
||||
equipmentData.status === "rented") {
|
||||
calculatedStatus = "IN_USE";
|
||||
} else if (equipmentData.status === "MAINTENANCE" ||
|
||||
equipmentData.status === "RENTED") {
|
||||
calculatedStatus = equipmentData.status;
|
||||
} else {
|
||||
calculatedStatus = "available";
|
||||
calculatedStatus = "AVAILABLE";
|
||||
}
|
||||
|
||||
statuses[equipmentId] = calculatedStatus;
|
||||
|
||||
@@ -651,121 +651,7 @@ exports.getEventWithDetails = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: Mettre à jour le statut d'un équipement
|
||||
async function updateEquipmentStatus(equipmentId, status) {
|
||||
try {
|
||||
const doc = await db.collection("equipments").doc(equipmentId).get();
|
||||
if (!doc.exists) {
|
||||
logger.warn(`Equipment ${equipmentId} does not exist, skipping status update`);
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("equipments").doc(equipmentId).update({
|
||||
status: status,
|
||||
updatedAt: admin.firestore.Timestamp.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error updating equipment status for ${equipmentId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Valider un équipement individuel en préparation
|
||||
exports.validateEquipmentPreparation = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, "manage_events");
|
||||
if (!canManage) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_events permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {eventId, equipmentId} = req.body.data;
|
||||
if (!eventId || !equipmentId) {
|
||||
res.status(400).json({error: "eventId and equipmentId are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const eventDoc = await db.collection("events").doc(eventId).get();
|
||||
if (!eventDoc.exists) {
|
||||
res.status(404).json({error: "Event not found"});
|
||||
return;
|
||||
}
|
||||
|
||||
const eventData = eventDoc.data();
|
||||
const assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
// Mettre à jour le statut de l'équipement
|
||||
const updatedEquipment = assignedEquipment.map((eq) => {
|
||||
if (eq.equipmentId === equipmentId) {
|
||||
return {...eq, isPrepared: true};
|
||||
}
|
||||
return eq;
|
||||
});
|
||||
|
||||
// Vérifier si tous sont préparés
|
||||
const allPrepared = updatedEquipment.every((eq) => eq.isPrepared);
|
||||
|
||||
const updateData = {
|
||||
assignedEquipment: updatedEquipment,
|
||||
preparationStatus: allPrepared ? "completed" : "inProgress",
|
||||
};
|
||||
|
||||
await db.collection("events").doc(eventId).update(updateData);
|
||||
|
||||
res.status(200).json({success: true, allPrepared});
|
||||
} catch (error) {
|
||||
logger.error("Error validating equipment preparation:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Valider tous les équipements en préparation
|
||||
exports.validateAllPreparation = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, "manage_events");
|
||||
if (!canManage) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_events permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {eventId} = req.body.data;
|
||||
if (!eventId) {
|
||||
res.status(400).json({error: "eventId is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const eventDoc = await db.collection("events").doc(eventId).get();
|
||||
if (!eventDoc.exists) {
|
||||
res.status(404).json({error: "Event not found"});
|
||||
return;
|
||||
}
|
||||
|
||||
const eventData = eventDoc.data();
|
||||
const assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
// Marquer tous comme préparés
|
||||
const updatedEquipment = assignedEquipment.map((eq) => ({
|
||||
...eq,
|
||||
isPrepared: true,
|
||||
}));
|
||||
|
||||
await db.collection("events").doc(eventId).update({
|
||||
assignedEquipment: updatedEquipment,
|
||||
preparationStatus: "completed",
|
||||
});
|
||||
|
||||
// Mettre à jour le statut des équipements à "inUse"
|
||||
for (const equipment of assignedEquipment) {
|
||||
await updateEquipmentStatus(equipment.equipmentId, "inUse");
|
||||
}
|
||||
|
||||
res.status(200).json({success: true});
|
||||
} catch (error) {
|
||||
logger.error("Error validating all preparation:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Valider un équipement individuel pour le chargement
|
||||
exports.validateEquipmentLoading = async (req, res) => {
|
||||
@@ -947,136 +833,4 @@ exports.validateAllUnloading = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Valider un équipement individuel pour le retour
|
||||
exports.validateEquipmentReturn = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, "manage_events");
|
||||
if (!canManage) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_events permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {eventId, equipmentId, returnedQuantity} = req.body.data;
|
||||
if (!eventId || !equipmentId) {
|
||||
res.status(400).json({error: "eventId and equipmentId are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const eventDoc = await db.collection("events").doc(eventId).get();
|
||||
if (!eventDoc.exists) {
|
||||
res.status(404).json({error: "Event not found"});
|
||||
return;
|
||||
}
|
||||
|
||||
const eventData = eventDoc.data();
|
||||
const assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
const updatedEquipment = assignedEquipment.map((eq) => {
|
||||
if (eq.equipmentId === equipmentId) {
|
||||
return {
|
||||
...eq,
|
||||
isReturned: true,
|
||||
returnedQuantity: returnedQuantity !== undefined ? returnedQuantity : eq.returnedQuantity,
|
||||
};
|
||||
}
|
||||
return eq;
|
||||
});
|
||||
|
||||
const allReturned = updatedEquipment.every((eq) => eq.isReturned);
|
||||
|
||||
const updateData = {
|
||||
assignedEquipment: updatedEquipment,
|
||||
returnStatus: allReturned ? "completed" : "inProgress",
|
||||
};
|
||||
|
||||
await db.collection("events").doc(eventId).update(updateData);
|
||||
|
||||
// Mettre à jour le stock si c'est un consommable
|
||||
if (returnedQuantity !== undefined) {
|
||||
const equipmentDoc = await db.collection("equipments").doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
const equipmentData = equipmentDoc.data();
|
||||
if (equipmentData.hasQuantity) {
|
||||
const currentAvailable = equipmentData.availableQuantity || 0;
|
||||
await db.collection("equipments").doc(equipmentId).update({
|
||||
availableQuantity: currentAvailable + returnedQuantity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({success: true, allReturned});
|
||||
} catch (error) {
|
||||
logger.error("Error validating equipment return:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Valider tous les retours
|
||||
exports.validateAllReturn = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, "manage_events");
|
||||
if (!canManage) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_events permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {eventId, returnedQuantities} = req.body.data;
|
||||
if (!eventId) {
|
||||
res.status(400).json({error: "eventId is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const eventDoc = await db.collection("events").doc(eventId).get();
|
||||
if (!eventDoc.exists) {
|
||||
res.status(404).json({error: "Event not found"});
|
||||
return;
|
||||
}
|
||||
|
||||
const eventData = eventDoc.data();
|
||||
const assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
const updatedEquipment = assignedEquipment.map((eq) => {
|
||||
const returnedQty = returnedQuantities?.[eq.equipmentId] || eq.returnedQuantity || eq.quantity;
|
||||
return {
|
||||
...eq,
|
||||
isReturned: true,
|
||||
returnedQuantity: returnedQty,
|
||||
};
|
||||
});
|
||||
|
||||
await db.collection("events").doc(eventId).update({
|
||||
assignedEquipment: updatedEquipment,
|
||||
returnStatus: "completed",
|
||||
});
|
||||
|
||||
// Mettre à jour le statut des équipements à "available" et gérer les stocks
|
||||
for (const equipment of updatedEquipment) {
|
||||
const equipmentDoc = await db.collection("equipments").doc(equipment.equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
const equipmentData = equipmentDoc.data();
|
||||
|
||||
// Mettre à jour le statut uniquement pour les équipements non quantifiables
|
||||
if (!equipmentData.hasQuantity) {
|
||||
await updateEquipmentStatus(equipment.equipmentId, "available");
|
||||
}
|
||||
|
||||
// Restaurer le stock pour les consommables
|
||||
if (equipmentData.hasQuantity && equipment.returnedQuantity) {
|
||||
const currentAvailable = equipmentData.availableQuantity || 0;
|
||||
await db.collection("equipments").doc(equipment.equipmentId).update({
|
||||
availableQuantity: currentAvailable + equipment.returnedQuantity,
|
||||
updatedAt: admin.firestore.Timestamp.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({success: true});
|
||||
} catch (error) {
|
||||
logger.error("Error validating all return:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user