feat: implement equipment and container loading rollback functionality with corresponding backend cloud functions
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
Toutes les modifications notables de ce projet seront documentées dans ce fichier.
|
Toutes les modifications notables de ce projet seront documentées dans ce fichier.
|
||||||
|
|
||||||
|
## 27/05/2026
|
||||||
|
Ajout de la fonction retour en arriere pour la validation du chargement des equipements et des conteneurs.
|
||||||
|
|
||||||
## 26/05/2026
|
## 26/05/2026
|
||||||
Optimisation des perfomance de l'application, amélioration de la gestion des données et refonte visuelle de la page de gestion des équipements.
|
Optimisation des perfomance de l'application, amélioration de la gestion des données et refonte visuelle de la page de gestion des équipements.
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +1,22 @@
|
|||||||
const {onRequest} = require("firebase-functions/v2/https");
|
const {onCall} = require("firebase-functions/v2/https");
|
||||||
const admin = require("firebase-admin");
|
const admin = require("firebase-admin");
|
||||||
const nodemailer = require("nodemailer");
|
const nodemailer = require("nodemailer");
|
||||||
const logger = require("firebase-functions/logger");
|
const logger = require("firebase-functions/logger");
|
||||||
const {getSmtpConfig, EMAIL_CONFIG} = require("./utils/emailConfig");
|
const {getSmtpConfig, EMAIL_CONFIG} = require("./utils/emailConfig");
|
||||||
const {renderTemplate, getEmailSubject, getAlertTitle, prepareTemplateData, checkAlertPreference} = require("./utils/emailTemplates");
|
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
|
* Crée une alerte et envoie les notifications
|
||||||
* Gère tout le processus côté backend de A à Z
|
* Gère tout le processus côté backend de A à Z
|
||||||
*/
|
*/
|
||||||
exports.createAlert = onRequest({
|
const handler = async (request) => {
|
||||||
cors: false,
|
|
||||||
invoker: "public",
|
|
||||||
region: "europe-west9",
|
|
||||||
}, withCors(async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
|
const {auth, data} = request;
|
||||||
|
|
||||||
// Vérifier l'authentification
|
// Vérifier l'authentification
|
||||||
const decodedToken = await auth.authenticateUser(req);
|
if (!auth) {
|
||||||
const data = req.body.data || req.body;
|
throw new Error("L'utilisateur doit être authentifié");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -96,7 +58,7 @@ exports.createAlert = onRequest({
|
|||||||
metadata: metadata || {},
|
metadata: metadata || {},
|
||||||
assignedTo: userIds,
|
assignedTo: userIds,
|
||||||
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
createdBy: decodedToken.uid,
|
createdBy: auth.uid,
|
||||||
isRead: false,
|
isRead: false,
|
||||||
emailSent: false,
|
emailSent: false,
|
||||||
status: "ACTIVE",
|
status: "ACTIVE",
|
||||||
@@ -117,17 +79,17 @@ exports.createAlert = onRequest({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
alertId: alertRef.id,
|
alertId: alertRef.id,
|
||||||
usersNotified: userIds.length,
|
usersNotified: userIds.length,
|
||||||
emailsSent: Object.values(emailResults).filter((v) => v).length,
|
emailsSent: Object.values(emailResults).filter((v) => v).length,
|
||||||
});
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[createAlert] Erreur:", 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
|
* 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) {
|
async function sendAlertEmails(alertId, alertData, userIds) {
|
||||||
const results = {};
|
const results = {};
|
||||||
const transporter = nodemailer.createTransporter(getSmtpConfig());
|
const transporter = nodemailer.createTransport(getSmtpConfig());
|
||||||
|
|
||||||
// Envoyer les emails en parallèle (batch de 5)
|
// Envoyer les emails en parallèle (batch de 5)
|
||||||
const batches = [];
|
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);
|
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) => {
|
exports.validateEquipmentLoading = onRequest(httpOptions, withCors((req, res) => {
|
||||||
return require("./src/events").validateEquipmentLoading(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);
|
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
|
// MAINTENANCES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -379,15 +363,19 @@ exports.aiEquipmentProposal = onRequest(aiHttpOptions, withCors((req, res) => {
|
|||||||
// CALLABLE EMAIL & VALIDATION (LEGACY LAZY WRAPPERS)
|
// CALLABLE EMAIL & VALIDATION (LEGACY LAZY WRAPPERS)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
exports.sendAlertEmail = onCall({region: "europe-west9", cors: true}, (request) => {
|
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) => {
|
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) => {
|
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);
|
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
|
* Appelée par le client lors du chargement/déchargement
|
||||||
* Crée automatiquement les alertes nécessaires
|
* Crée automatiquement les alertes nécessaires
|
||||||
*/
|
*/
|
||||||
exports.processEquipmentValidation = onCall({
|
const handler = async (request) => {
|
||||||
cors: true,
|
|
||||||
region: "europe-west9",
|
|
||||||
}, async (request) => {
|
|
||||||
try {
|
try {
|
||||||
// L'authentification est automatique avec onCall
|
// L'authentification est automatique avec onCall
|
||||||
const {auth, data} = request;
|
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({
|
await eventRef.update({
|
||||||
equipment: equipmentList,
|
|
||||||
lastValidation: {
|
lastValidation: {
|
||||||
type: validationType,
|
type: validationType,
|
||||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
@@ -461,3 +457,10 @@ function parseFirestoreDate(value) {
|
|||||||
return null;
|
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
|
* Envoie un email d'alerte à un utilisateur
|
||||||
* Appelé par le client Dart via callable function
|
* Appelé par le client Dart via callable function
|
||||||
*/
|
*/
|
||||||
exports.sendAlertEmail = onCall({
|
const handler = async (request) => {
|
||||||
region: "europe-west9",
|
|
||||||
cors: true,
|
|
||||||
}, async (request) => {
|
|
||||||
// Vérifier l'authentification
|
// Vérifier l'authentification
|
||||||
if (!request.auth) {
|
if (!request.auth) {
|
||||||
throw new Error("L'utilisateur doit être authentifié");
|
throw new Error("L'utilisateur doit être authentifié");
|
||||||
@@ -75,7 +72,7 @@ exports.sendAlertEmail = onCall({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Configurer le transporteur SMTP
|
// Configurer le transporteur SMTP
|
||||||
const transporter = nodemailer.createTransporter(getSmtpConfig());
|
const transporter = nodemailer.createTransport(getSmtpConfig());
|
||||||
|
|
||||||
// Envoyer l'email
|
// Envoyer l'email
|
||||||
const info = await transporter.sendMail({
|
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 = [];
|
const alternatives = [];
|
||||||
equipmentsSnapshot.docs.forEach((doc) => {
|
equipmentsSnapshot.docs.forEach((doc) => {
|
||||||
const data = doc.data();
|
const data = doc.data();
|
||||||
if (!conflictingEquipmentIds.has(doc.id) && data.status === "available") {
|
if (!conflictingEquipmentIds.has(doc.id) && data.status === "AVAILABLE") {
|
||||||
alternatives.push({
|
alternatives.push({
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
...helpers.serializeTimestamps(data, ["purchaseDate", "nextMaintenanceDate", "lastMaintenanceDate", "createdAt", "updatedAt"]),
|
...helpers.serializeTimestamps(data, ["purchaseDate", "nextMaintenanceDate", "lastMaintenanceDate", "createdAt", "updatedAt"]),
|
||||||
@@ -560,15 +560,15 @@ exports.calculateEquipmentStatuses = async (req, res) => {
|
|||||||
let calculatedStatus = equipmentData.status;
|
let calculatedStatus = equipmentData.status;
|
||||||
|
|
||||||
// Si l'équipement est perdu ou HS, garder ce statut
|
// 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;
|
calculatedStatus = equipmentData.status;
|
||||||
} else if (equipmentIdsInUse.has(equipmentId)) {
|
} else if (equipmentIdsInUse.has(equipmentId)) {
|
||||||
calculatedStatus = "inUse";
|
calculatedStatus = "IN_USE";
|
||||||
} else if (equipmentData.status === "maintenance" ||
|
} else if (equipmentData.status === "MAINTENANCE" ||
|
||||||
equipmentData.status === "rented") {
|
equipmentData.status === "RENTED") {
|
||||||
calculatedStatus = equipmentData.status;
|
calculatedStatus = equipmentData.status;
|
||||||
} else {
|
} else {
|
||||||
calculatedStatus = "available";
|
calculatedStatus = "AVAILABLE";
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses[equipmentId] = calculatedStatus;
|
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
|
// Valider un équipement individuel pour le chargement
|
||||||
exports.validateEquipmentLoading = async (req, res) => {
|
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});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// Configuration de la version de l'application
|
/// Configuration de la version de l'application
|
||||||
class AppVersion {
|
class AppVersion {
|
||||||
static const String version = '1.2.3';
|
static const String version = '1.2.4';
|
||||||
|
|
||||||
/// Retourne la version complète de l'application
|
/// Retourne la version complète de l'application
|
||||||
static String get fullVersion => 'v$version';
|
static String get fullVersion => 'v$version';
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import 'package:em2rp/services/equipment_status_calculator.dart';
|
|
||||||
import 'package:em2rp/services/api_service.dart';
|
|
||||||
|
|
||||||
class EventPreparationService {
|
class EventPreparationService {
|
||||||
final ApiService _apiService = apiService;
|
|
||||||
|
|
||||||
/// Retourne true si l'équipement était absent du flux événementiel.
|
/// Retourne true si l'équipement était absent du flux événementiel.
|
||||||
///
|
///
|
||||||
@@ -42,35 +38,7 @@ class EventPreparationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === PRÉPARATION ===
|
|
||||||
|
|
||||||
/// Valider un équipement individuel en préparation
|
|
||||||
Future<void> validateEquipmentPreparation(String eventId, String equipmentId) async {
|
|
||||||
try {
|
|
||||||
await _apiService.call('validateEquipmentPreparation', {
|
|
||||||
'eventId': eventId,
|
|
||||||
'equipmentId': equipmentId,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
print('Error validating equipment preparation: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Valider tous les équipements en préparation
|
|
||||||
Future<void> validateAllPreparation(String eventId) async {
|
|
||||||
try {
|
|
||||||
await _apiService.call('validateAllPreparation', {
|
|
||||||
'eventId': eventId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Invalider le cache des statuts d'équipement
|
|
||||||
EquipmentStatusCalculator.invalidateGlobalCache();
|
|
||||||
} catch (e) {
|
|
||||||
print('Error validating all preparation: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ces méthodes ne sont plus utilisées et devraient être remplacées par des Cloud Functions
|
// Ces méthodes ne sont plus utilisées et devraient être remplacées par des Cloud Functions
|
||||||
// si nécessaire dans le futur
|
// si nécessaire dans le futur
|
||||||
@@ -85,46 +53,8 @@ class EventPreparationService {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// === RETOUR ===
|
|
||||||
|
|
||||||
|
|
||||||
/// Valider le retour d'un équipement individuel
|
|
||||||
Future<void> validateEquipmentReturn(
|
|
||||||
String eventId,
|
|
||||||
String equipmentId, {
|
|
||||||
int? returnedQuantity,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
await _apiService.call('validateEquipmentReturn', {
|
|
||||||
'eventId': eventId,
|
|
||||||
'equipmentId': equipmentId,
|
|
||||||
if (returnedQuantity != null) 'returnedQuantity': returnedQuantity,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
print('Error validating equipment return: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Valider tous les retours
|
|
||||||
Future<void> validateAllReturn(
|
|
||||||
String eventId, [
|
|
||||||
Map<String, int>? returnedQuantities,
|
|
||||||
]) async {
|
|
||||||
try {
|
|
||||||
await _apiService.call('validateAllReturn', {
|
|
||||||
'eventId': eventId,
|
|
||||||
if (returnedQuantities != null) 'returnedQuantities': returnedQuantities,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Invalider le cache des statuts d'équipement
|
|
||||||
EquipmentStatusCalculator.invalidateGlobalCache();
|
|
||||||
} catch (e) {
|
|
||||||
print('Error validating all return: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@Deprecated('Use Cloud Functions instead')
|
@Deprecated('Use Cloud Functions instead')
|
||||||
Future<void> completeReturnWithMissing(
|
Future<void> completeReturnWithMissing(
|
||||||
|
|||||||
@@ -88,22 +88,22 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
|
|
||||||
// Logique stricte : on avance étape par étape
|
// Logique stricte : on avance étape par étape
|
||||||
// 1. Préparation dépôt
|
// 1. Préparation dépôt
|
||||||
if (prep != PreparationStatus.completed) {
|
if (prep != PreparationStatus.completed && prep != PreparationStatus.completedWithMissing) {
|
||||||
return PreparationStep.preparation;
|
return PreparationStep.preparation;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Chargement aller (après préparation complète)
|
// 2. Chargement aller (après préparation complète)
|
||||||
if (loading != LoadingStatus.completed) {
|
if (loading != LoadingStatus.completed && loading != LoadingStatus.completedWithMissing) {
|
||||||
return PreparationStep.loadingOutbound;
|
return PreparationStep.loadingOutbound;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Chargement retour (après chargement aller complet)
|
// 3. Chargement retour (après chargement aller complet)
|
||||||
if (unloading != UnloadingStatus.completed) {
|
if (unloading != UnloadingStatus.completed && unloading != UnloadingStatus.completedWithMissing) {
|
||||||
return PreparationStep.unloadingReturn;
|
return PreparationStep.unloadingReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Retour dépôt (après déchargement complet)
|
// 4. Retour dépôt (après déchargement complet)
|
||||||
if (returnStatus != ReturnStatus.completed) {
|
if (returnStatus != ReturnStatus.completed && returnStatus != ReturnStatus.completedWithMissing) {
|
||||||
return PreparationStep.return_;
|
return PreparationStep.return_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (_isCurrentStepCompleted()) {
|
if (_isCurrentStepCompleted()) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
const SnackBar(
|
||||||
content: Text('Cette étape est déjà terminée'),
|
content: Text('Cette étape est déjà terminée'),
|
||||||
backgroundColor: Colors.orange,
|
backgroundColor: Colors.orange,
|
||||||
),
|
),
|
||||||
@@ -141,6 +141,17 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_isPreviousStepCompleted()) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('L\'étape précédente n\'est pas terminée. Impossible d\'accéder à cette étape.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Charger les équipements après le premier frame pour éviter setState pendant build
|
// Charger les équipements après le premier frame pour éviter setState pendant build
|
||||||
_loadEquipmentAndContainers();
|
_loadEquipmentAndContainers();
|
||||||
});
|
});
|
||||||
@@ -150,13 +161,34 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
bool _isCurrentStepCompleted() {
|
bool _isCurrentStepCompleted() {
|
||||||
switch (_currentStep) {
|
switch (_currentStep) {
|
||||||
case PreparationStep.preparation:
|
case PreparationStep.preparation:
|
||||||
return (_currentEvent.preparationStatus ?? PreparationStatus.notStarted) == PreparationStatus.completed;
|
final status = _currentEvent.preparationStatus ?? PreparationStatus.notStarted;
|
||||||
|
return status == PreparationStatus.completed || status == PreparationStatus.completedWithMissing;
|
||||||
case PreparationStep.loadingOutbound:
|
case PreparationStep.loadingOutbound:
|
||||||
return (_currentEvent.loadingStatus ?? LoadingStatus.notStarted) == LoadingStatus.completed;
|
final status = _currentEvent.loadingStatus ?? LoadingStatus.notStarted;
|
||||||
|
return status == LoadingStatus.completed || status == LoadingStatus.completedWithMissing;
|
||||||
case PreparationStep.unloadingReturn:
|
case PreparationStep.unloadingReturn:
|
||||||
return (_currentEvent.unloadingStatus ?? UnloadingStatus.notStarted) == UnloadingStatus.completed;
|
final status = _currentEvent.unloadingStatus ?? UnloadingStatus.notStarted;
|
||||||
|
return status == UnloadingStatus.completed || status == UnloadingStatus.completedWithMissing;
|
||||||
case PreparationStep.return_:
|
case PreparationStep.return_:
|
||||||
return (_currentEvent.returnStatus ?? ReturnStatus.notStarted) == ReturnStatus.completed;
|
final status = _currentEvent.returnStatus ?? ReturnStatus.notStarted;
|
||||||
|
return status == ReturnStatus.completed || status == ReturnStatus.completedWithMissing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifie si l'étape précédente est bien complétée
|
||||||
|
bool _isPreviousStepCompleted() {
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
return true; // Première étape, toujours OK
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
final prep = _currentEvent.preparationStatus ?? PreparationStatus.notStarted;
|
||||||
|
return prep == PreparationStatus.completed || prep == PreparationStatus.completedWithMissing;
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
final loading = _currentEvent.loadingStatus ?? LoadingStatus.notStarted;
|
||||||
|
return loading == LoadingStatus.completed || loading == LoadingStatus.completedWithMissing;
|
||||||
|
case PreparationStep.return_:
|
||||||
|
final unloading = _currentEvent.unloadingStatus ?? UnloadingStatus.notStarted;
|
||||||
|
return unloading == UnloadingStatus.completed || unloading == UnloadingStatus.completedWithMissing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,10 +271,15 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_quantitiesAtPreparation[eq.equipmentId] = eq.quantityAtPreparation ?? eq.quantity;
|
||||||
|
_quantitiesAtLoading[eq.equipmentId] = eq.quantityAtLoading ?? eq.quantityAtPreparation ?? eq.quantity;
|
||||||
|
_quantitiesAtUnloading[eq.equipmentId] = eq.quantityAtUnloading ?? eq.quantityAtLoading ?? eq.quantityAtPreparation ?? eq.quantity;
|
||||||
|
_quantitiesAtReturn[eq.equipmentId] = eq.quantityAtReturn ?? eq.quantityAtUnloading ?? eq.quantityAtLoading ?? eq.quantityAtPreparation ?? eq.quantity;
|
||||||
|
|
||||||
if ((_currentStep == PreparationStep.return_ ||
|
if ((_currentStep == PreparationStep.return_ ||
|
||||||
_currentStep == PreparationStep.unloadingReturn) &&
|
_currentStep == PreparationStep.unloadingReturn) &&
|
||||||
(equipmentItem?.hasQuantity ?? false)) {
|
(equipmentItem?.hasQuantity ?? false)) {
|
||||||
_returnedQuantities[eq.equipmentId] = eq.quantityAtReturn ?? eq.quantity;
|
_returnedQuantities[eq.equipmentId] = eq.quantityAtReturn ?? eq.quantityAtUnloading ?? eq.quantityAtLoading ?? eq.quantityAtPreparation ?? eq.quantity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,9 +455,8 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
returnStatus: updateData['returnStatus'],
|
returnStatus: updateData['returnStatus'],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mettre à jour les statuts des équipements si nécessaire
|
// Mettre à jour les statuts des équipements si nécessaire (uniquement pour la préparation, le retour étant géré par le trigger Firestore Cloud Function)
|
||||||
if (_currentStep == PreparationStep.preparation ||
|
if (_currentStep == PreparationStep.preparation) {
|
||||||
(_currentStep == PreparationStep.unloadingReturn && _loadSimultaneously)) {
|
|
||||||
await _updateEquipmentStatuses(updatedEquipment);
|
await _updateEquipmentStatuses(updatedEquipment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,6 +541,8 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateEquipmentStatuses(List<EventEquipment> equipment) async {
|
Future<void> _updateEquipmentStatuses(List<EventEquipment> equipment) async {
|
||||||
|
final List<String> failedUpdates = [];
|
||||||
|
|
||||||
for (var eq in equipment) {
|
for (var eq in equipment) {
|
||||||
try {
|
try {
|
||||||
final equipmentData = _equipmentCache[eq.equipmentId];
|
final equipmentData = _equipmentCache[eq.equipmentId];
|
||||||
@@ -513,7 +551,9 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
// Déterminer le nouveau statut
|
// Déterminer le nouveau statut
|
||||||
EquipmentStatus newStatus;
|
EquipmentStatus newStatus;
|
||||||
if (eq.isReturned) {
|
if (eq.isReturned) {
|
||||||
newStatus = EquipmentStatus.available;
|
// Note : Le retour est géré par le trigger Firestore Cloud Function en tâche de fond.
|
||||||
|
// On évite les conflits d'écritures client/serveur et les double-restaurations de stock.
|
||||||
|
continue;
|
||||||
} else if (eq.isPrepared || eq.isLoaded) {
|
} else if (eq.isPrepared || eq.isLoaded) {
|
||||||
newStatus = EquipmentStatus.inUse;
|
newStatus = EquipmentStatus.inUse;
|
||||||
} else {
|
} else {
|
||||||
@@ -527,19 +567,22 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
status: equipmentStatusToString(newStatus),
|
status: equipmentStatusToString(newStatus),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gérer les stocks pour les consommables
|
|
||||||
if (equipmentData.hasQuantity && eq.isReturned && eq.quantityAtReturn != null) {
|
|
||||||
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
|
||||||
await _dataService.updateEquipmentStatusOnly(
|
|
||||||
equipmentId: eq.equipmentId,
|
|
||||||
availableQuantity: currentAvailable + eq.quantityAtReturn!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Erreur silencieuse pour ne pas bloquer le processus
|
DebugLog.error('[EventPreparationPage] Échec de la mise à jour du statut pour l\'équipement ${eq.equipmentId}', e);
|
||||||
|
failedUpdates.add(eq.equipmentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (failedUpdates.isNotEmpty && mounted) {
|
||||||
|
final names = failedUpdates.map((id) => _equipmentCache[id]?.name ?? id).join(', ');
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Attention : Échec de mise à jour du statut en base pour : $names. Le matériel a tout de même été validé pour l\'événement.'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
duration: const Duration(seconds: 6),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getSuccessMessage() {
|
String _getSuccessMessage() {
|
||||||
@@ -895,26 +938,28 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
_quantitiesAtReturn.addAll(quantities);
|
_quantitiesAtReturn.addAll(quantities);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtenir la quantité requise selon l'étape (nouvelle logique)
|
// Mettre à jour `_currentEvent.assignedEquipment` pour que l'UI se reconstruise avec les bonnes valeurs
|
||||||
int _getTargetQuantity(EventEquipment eventEquipment) {
|
final updatedList = _currentEvent.assignedEquipment.map((eq) {
|
||||||
|
final qty = quantities[eq.equipmentId];
|
||||||
|
if (qty != null) {
|
||||||
switch (_currentStep) {
|
switch (_currentStep) {
|
||||||
case PreparationStep.preparation:
|
case PreparationStep.preparation:
|
||||||
return eventEquipment.quantity; // Quantité initiale
|
return eq.copyWith(quantityAtPreparation: qty);
|
||||||
case PreparationStep.loadingOutbound:
|
case PreparationStep.loadingOutbound:
|
||||||
return eventEquipment.quantityAtPreparation ?? eventEquipment.quantity;
|
return eq.copyWith(quantityAtLoading: qty);
|
||||||
case PreparationStep.unloadingReturn:
|
case PreparationStep.unloadingReturn:
|
||||||
return eventEquipment.quantityAtLoading ??
|
return eq.copyWith(quantityAtUnloading: qty);
|
||||||
eventEquipment.quantityAtPreparation ??
|
|
||||||
eventEquipment.quantity;
|
|
||||||
case PreparationStep.return_:
|
case PreparationStep.return_:
|
||||||
return eventEquipment.quantityAtUnloading ??
|
return eq.copyWith(quantityAtReturn: qty);
|
||||||
eventEquipment.quantityAtLoading ??
|
|
||||||
eventEquipment.quantityAtPreparation ??
|
|
||||||
eventEquipment.quantity;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return eq;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
_currentEvent = _currentEvent.copyWith(assignedEquipment: updatedList);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Afficher un message de succès
|
/// Afficher un message de succès
|
||||||
void _showSuccessFeedback(String message) {
|
void _showSuccessFeedback(String message) {
|
||||||
@@ -1020,20 +1065,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
/// Mettre à jour la quantité d'un équipement à l'étape actuelle
|
/// Mettre à jour la quantité d'un équipement à l'étape actuelle
|
||||||
void _updateEquipmentQuantity(String equipmentId, int newQuantity) {
|
void _updateEquipmentQuantity(String equipmentId, int newQuantity) {
|
||||||
setState(() {
|
setState(() {
|
||||||
switch (_currentStep) {
|
_updateQuantitiesMap({equipmentId: newQuantity});
|
||||||
case PreparationStep.preparation:
|
|
||||||
_quantitiesAtPreparation[equipmentId] = newQuantity;
|
|
||||||
break;
|
|
||||||
case PreparationStep.loadingOutbound:
|
|
||||||
_quantitiesAtLoading[equipmentId] = newQuantity;
|
|
||||||
break;
|
|
||||||
case PreparationStep.unloadingReturn:
|
|
||||||
_quantitiesAtUnloading[equipmentId] = newQuantity;
|
|
||||||
break;
|
|
||||||
case PreparationStep.return_:
|
|
||||||
_quantitiesAtReturn[equipmentId] = newQuantity;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+162
-6
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:em2rp/providers/event_provider.dart';
|
import 'package:em2rp/providers/event_provider.dart';
|
||||||
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
import 'package:em2rp/views/event_preparation_page.dart';
|
import 'package:em2rp/views/event_preparation_page.dart';
|
||||||
|
import 'package:em2rp/services/api_service.dart';
|
||||||
import 'package:em2rp/utils/colors.dart';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
|
||||||
/// Boutons de préparation et retour d'événement
|
/// Boutons de préparation et retour d'événement
|
||||||
@@ -52,16 +54,16 @@ class _EventPreparationButtonsState extends State<EventPreparationButtons> {
|
|||||||
IconData buttonIcon;
|
IconData buttonIcon;
|
||||||
bool isCompleted = false;
|
bool isCompleted = false;
|
||||||
|
|
||||||
if (prep != PreparationStatus.completed) {
|
if (prep != PreparationStatus.completed && prep != PreparationStatus.completedWithMissing) {
|
||||||
buttonText = 'Préparation dépôt';
|
buttonText = 'Préparation dépôt';
|
||||||
buttonIcon = Icons.inventory_2;
|
buttonIcon = Icons.inventory_2;
|
||||||
} else if (loading != LoadingStatus.completed) {
|
} else if (loading != LoadingStatus.completed && loading != LoadingStatus.completedWithMissing) {
|
||||||
buttonText = 'Chargement aller';
|
buttonText = 'Chargement aller';
|
||||||
buttonIcon = Icons.local_shipping;
|
buttonIcon = Icons.local_shipping;
|
||||||
} else if (unloading != UnloadingStatus.completed) {
|
} else if (unloading != UnloadingStatus.completed && unloading != UnloadingStatus.completedWithMissing) {
|
||||||
buttonText = 'Chargement retour';
|
buttonText = 'Chargement retour';
|
||||||
buttonIcon = Icons.unarchive;
|
buttonIcon = Icons.unarchive;
|
||||||
} else if (returnStatus != ReturnStatus.completed) {
|
} else if (returnStatus != ReturnStatus.completed && returnStatus != ReturnStatus.completedWithMissing) {
|
||||||
buttonText = 'Retour dépôt';
|
buttonText = 'Retour dépôt';
|
||||||
buttonIcon = Icons.assignment_return;
|
buttonIcon = Icons.assignment_return;
|
||||||
} else {
|
} else {
|
||||||
@@ -131,9 +133,9 @@ class _EventPreparationButtonsState extends State<EventPreparationButtons> {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: Colors.green, width: 1),
|
border: Border.all(color: Colors.green, width: 1),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: const Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: const [
|
children: [
|
||||||
Icon(Icons.check_circle, color: Colors.green, size: 20),
|
Icon(Icons.check_circle, color: Colors.green, size: 20),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
@@ -147,9 +149,163 @@ class _EventPreparationButtonsState extends State<EventPreparationButtons> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Bouton de retour en arrière si au moins une étape est commencée/validée
|
||||||
|
if (prep != PreparationStatus.notStarted) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () => _showRollbackDialog(context, event),
|
||||||
|
icon: const Icon(Icons.undo, size: 18),
|
||||||
|
label: const Text('Revenir à une étape précédente'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.orange[800],
|
||||||
|
side: BorderSide(color: Colors.orange[300]!),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showRollbackDialog(BuildContext context, EventModel event) async {
|
||||||
|
final prep = event.preparationStatus ?? PreparationStatus.notStarted;
|
||||||
|
final loading = event.loadingStatus ?? LoadingStatus.notStarted;
|
||||||
|
final unloading = event.unloadingStatus ?? UnloadingStatus.notStarted;
|
||||||
|
final returnStatus = event.returnStatus ?? ReturnStatus.notStarted;
|
||||||
|
|
||||||
|
final List<Map<String, String>> steps = [];
|
||||||
|
|
||||||
|
if (prep == PreparationStatus.completed || prep == PreparationStatus.completedWithMissing) {
|
||||||
|
steps.add({'key': 'PREPARATION', 'label': 'Préparation dépôt'});
|
||||||
|
}
|
||||||
|
if (loading == LoadingStatus.completed || loading == LoadingStatus.completedWithMissing) {
|
||||||
|
steps.add({'key': 'LOADING', 'label': 'Chargement aller'});
|
||||||
|
}
|
||||||
|
if (unloading == UnloadingStatus.completed || unloading == UnloadingStatus.completedWithMissing) {
|
||||||
|
steps.add({'key': 'UNLOADING', 'label': 'Chargement retour'});
|
||||||
|
}
|
||||||
|
if (returnStatus == ReturnStatus.completed || returnStatus == ReturnStatus.completedWithMissing) {
|
||||||
|
steps.add({'key': 'RETURN', 'label': 'Retour dépôt'});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (steps.isEmpty) return;
|
||||||
|
|
||||||
|
final String? selectedStep = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.undo, color: Colors.orange),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Revenir en arrière'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Sélectionnez l\'étape à laquelle vous souhaitez revenir :'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...steps.map((step) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.arrow_back, color: AppColors.rouge),
|
||||||
|
title: Text(step['label']!),
|
||||||
|
onTap: () => Navigator.of(context).pop(step['key']),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedStep != null && context.mounted) {
|
||||||
|
final confirm = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Confirmer le retour en arrière'),
|
||||||
|
content: const Text(
|
||||||
|
'Êtes-vous sûr de vouloir revenir à cette étape ?\n\n'
|
||||||
|
'Toutes les validations des étapes ultérieures seront effacées, '
|
||||||
|
'et si le retour était terminé, les stocks restaurés seront annulés.'
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.rouge,
|
||||||
|
),
|
||||||
|
child: const Text('Confirmer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirm == true && context.mounted) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final apiService = FirebaseFunctionsApiService();
|
||||||
|
await apiService.call('rollbackEventStep', {
|
||||||
|
'eventId': event.id,
|
||||||
|
'targetStep': selectedStep,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||||
|
final userProvider = Provider.of<LocalUserProvider>(context, listen: false);
|
||||||
|
if (userProvider.currentUser != null) {
|
||||||
|
await eventProvider.refreshEvents(userProvider.currentUser!.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.of(context).pop(); // Fermer le loader
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Retour en arrière effectué avec succès'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop(); // Fermer le loader
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur : $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ enum ChecklistStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Widget pour afficher un équipement dans une checklist de préparation/retour
|
/// Widget pour afficher un équipement dans une checklist de préparation/retour
|
||||||
class EquipmentChecklistItem extends StatelessWidget {
|
class EquipmentChecklistItem extends StatefulWidget {
|
||||||
final EquipmentModel equipment;
|
final EquipmentModel equipment;
|
||||||
final EventEquipment eventEquipment;
|
final EventEquipment eventEquipment;
|
||||||
final ChecklistStep step;
|
final ChecklistStep step;
|
||||||
@@ -34,92 +34,120 @@ class EquipmentChecklistItem extends StatelessWidget {
|
|||||||
this.wasMissingBefore = false,
|
this.wasMissingBefore = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Retourne la quantité actuelle selon l'étape
|
@override
|
||||||
|
State<EquipmentChecklistItem> createState() => _EquipmentChecklistItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EquipmentChecklistItemState extends State<EquipmentChecklistItem> {
|
||||||
|
late TextEditingController _quantityController;
|
||||||
|
|
||||||
int _getCurrentQuantity() {
|
int _getCurrentQuantity() {
|
||||||
switch (step) {
|
switch (widget.step) {
|
||||||
case ChecklistStep.preparation:
|
case ChecklistStep.preparation:
|
||||||
return eventEquipment.quantityAtPreparation ?? eventEquipment.quantity;
|
return widget.eventEquipment.quantityAtPreparation ?? widget.eventEquipment.quantity;
|
||||||
case ChecklistStep.loading:
|
case ChecklistStep.loading:
|
||||||
return eventEquipment.quantityAtLoading ?? eventEquipment.quantityAtPreparation ?? eventEquipment.quantity;
|
return widget.eventEquipment.quantityAtLoading ?? widget.eventEquipment.quantityAtPreparation ?? widget.eventEquipment.quantity;
|
||||||
case ChecklistStep.unloading:
|
case ChecklistStep.unloading:
|
||||||
return eventEquipment.quantityAtUnloading ?? eventEquipment.quantityAtLoading ?? eventEquipment.quantity;
|
return widget.eventEquipment.quantityAtUnloading ?? widget.eventEquipment.quantityAtLoading ?? widget.eventEquipment.quantityAtPreparation ?? widget.eventEquipment.quantity;
|
||||||
case ChecklistStep.return_:
|
case ChecklistStep.return_:
|
||||||
return eventEquipment.quantityAtReturn ?? eventEquipment.quantityAtUnloading ?? eventEquipment.quantity;
|
return widget.eventEquipment.quantityAtReturn ?? widget.eventEquipment.quantityAtUnloading ?? widget.eventEquipment.quantityAtLoading ?? widget.eventEquipment.quantityAtPreparation ?? widget.eventEquipment.quantity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_quantityController = TextEditingController(text: _getCurrentQuantity().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant EquipmentChecklistItem oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
final currentQty = _getCurrentQuantity();
|
||||||
|
final controllerQty = int.tryParse(_quantityController.text);
|
||||||
|
if (controllerQty != currentQty) {
|
||||||
|
_quantityController.text = currentQty.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_quantityController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final hasQuantity = equipment.hasQuantity;
|
final hasQuantity = widget.equipment.hasQuantity;
|
||||||
|
|
||||||
// Déterminer la quantité actuelle selon l'étape
|
// Déterminer la quantité actuelle selon l'étape
|
||||||
final int currentQuantity = _getCurrentQuantity();
|
final int currentQuantity = _getCurrentQuantity();
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: isChild ? 32.0 : 0.0, // Indentation pour les enfants
|
left: widget.isChild ? 32.0 : 0.0, // Indentation pour les enfants
|
||||||
top: 4.0,
|
top: 4.0,
|
||||||
bottom: 4.0,
|
bottom: 4.0,
|
||||||
),
|
),
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
elevation: isChild ? 0 : 1, // Pas d'élévation pour les enfants
|
elevation: widget.isChild ? 0 : 1, // Pas d'élévation pour les enfants
|
||||||
color: wasMissingBefore
|
color: widget.wasMissingBefore
|
||||||
? Colors.orange.shade50
|
? Colors.orange.shade50
|
||||||
: (isChild ? Colors.grey.shade50 : Colors.white),
|
: (widget.isChild ? Colors.grey.shade50 : Colors.white),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: wasMissingBefore
|
color: widget.wasMissingBefore
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
: (isValidated ? Colors.green : Colors.grey.shade300),
|
: (widget.isValidated ? Colors.green : Colors.grey.shade300),
|
||||||
width: (isValidated || wasMissingBefore) ? 2 : 1,
|
width: (widget.isValidated || widget.wasMissingBefore) ? 2 : 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: isChild, // Plus compact pour les enfants
|
dense: widget.isChild, // Plus compact pour les enfants
|
||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: EdgeInsets.symmetric(
|
||||||
horizontal: isChild ? 8.0 : 16.0,
|
horizontal: widget.isChild ? 8.0 : 16.0,
|
||||||
vertical: isChild ? 4.0 : 8.0,
|
vertical: widget.isChild ? 4.0 : 8.0,
|
||||||
),
|
),
|
||||||
leading: Container(
|
leading: Container(
|
||||||
width: isChild ? 32 : 40,
|
width: widget.isChild ? 32 : 40,
|
||||||
height: isChild ? 32 : 40,
|
height: widget.isChild ? 32 : 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: wasMissingBefore
|
color: widget.wasMissingBefore
|
||||||
? Colors.orange.shade100
|
? Colors.orange.shade100
|
||||||
: (isValidated ? Colors.green.shade100 : Colors.grey.shade100),
|
: (widget.isValidated ? Colors.green.shade100 : Colors.grey.shade100),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
wasMissingBefore
|
widget.wasMissingBefore
|
||||||
? Icons.warning
|
? Icons.warning
|
||||||
: (isValidated ? Icons.check_circle : Icons.radio_button_unchecked),
|
: (widget.isValidated ? Icons.check_circle : Icons.radio_button_unchecked),
|
||||||
color: wasMissingBefore
|
color: widget.wasMissingBefore
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
: (isValidated ? Colors.green : Colors.grey),
|
: (widget.isValidated ? Colors.green : Colors.grey),
|
||||||
size: isChild ? 18 : 24,
|
size: widget.isChild ? 18 : 24,
|
||||||
),
|
),
|
||||||
onPressed: onToggle,
|
onPressed: widget.onToggle,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
equipment.name,
|
widget.equipment.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: isChild ? FontWeight.w500 : FontWeight.w600,
|
fontWeight: widget.isChild ? FontWeight.w500 : FontWeight.w600,
|
||||||
fontSize: isChild ? 13 : 15,
|
fontSize: widget.isChild ? 13 : 15,
|
||||||
decoration: isValidated ? TextDecoration.lineThrough : null,
|
decoration: widget.isValidated ? TextDecoration.lineThrough : null,
|
||||||
color: isValidated ? Colors.grey : null,
|
color: widget.isValidated ? Colors.grey : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (equipment.model != null)
|
if (widget.equipment.model != null)
|
||||||
Text(
|
Text(
|
||||||
equipment.model!,
|
widget.equipment.model!,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey.shade600,
|
color: Colors.grey.shade600,
|
||||||
@@ -127,12 +155,12 @@ class EquipmentChecklistItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Indicateur si manquant à l'étape précédente
|
// Indicateur si manquant à l'étape précédente
|
||||||
if (wasMissingBefore)
|
if (widget.wasMissingBefore)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 4.0),
|
padding: const EdgeInsets.only(top: 4.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.warning_amber, size: 14, color: Colors.orange),
|
const Icon(Icons.warning_amber, size: 14, color: Colors.orange),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'Était manquant à l\'étape précédente',
|
'Était manquant à l\'étape précédente',
|
||||||
@@ -151,7 +179,7 @@ class EquipmentChecklistItem extends StatelessWidget {
|
|||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
const Text(
|
||||||
'Quantité : ',
|
'Quantité : ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -159,11 +187,11 @@ class EquipmentChecklistItem extends StatelessWidget {
|
|||||||
color: AppColors.bleuFonce,
|
color: AppColors.bleuFonce,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (onQuantityChanged != null)
|
if (widget.onQuantityChanged != null)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 60,
|
width: 60,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
initialValue: currentQuantity.toString(),
|
controller: _quantityController,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -175,14 +203,14 @@ class EquipmentChecklistItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
final qty = int.tryParse(value) ?? currentQuantity;
|
final qty = int.tryParse(value) ?? currentQuantity;
|
||||||
onQuantityChanged!(qty);
|
widget.onQuantityChanged!(qty);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Text(
|
Text(
|
||||||
currentQuantity.toString(),
|
currentQuantity.toString(),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.bleuFonce,
|
color: AppColors.bleuFonce,
|
||||||
@@ -193,16 +221,16 @@ class EquipmentChecklistItem extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: isValidated
|
trailing: widget.isValidated
|
||||||
? Container(
|
? Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.green.shade100,
|
color: Colors.green.shade100,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: const Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: const [
|
children: [
|
||||||
Icon(Icons.check, size: 16, color: Colors.green),
|
Icon(Icons.check, size: 16, color: Colors.green),
|
||||||
SizedBox(width: 4),
|
SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"updateUrl": "https://app.em2events.fr",
|
"updateUrl": "https://app.em2events.fr",
|
||||||
"forceUpdate": true,
|
"forceUpdate": true,
|
||||||
"releaseNotes": "Optimisation des perfomance de l'application, amélioration de la gestion des données et refonte visuelle de la page de gestion des équipements.",
|
"releaseNotes": "Ajout de la fonction retour en arriere pour la validation du chargement des equipements et des conteneurs.",
|
||||||
"timestamp": "2026-05-26T13:34:16.390Z"
|
"timestamp": "2026-05-27T20:00:59.431Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user