From faff06e4dfc15bbac204af768eaf42e55cb9336c Mon Sep 17 00:00:00 2001 From: ElPoyo Date: Wed, 27 May 2026 22:04:46 +0200 Subject: [PATCH] feat: implement equipment and container loading rollback functionality with corresponding backend cloud functions --- em2rp/CHANGELOG.md | 3 + em2rp/functions/createAlert.js | 71 ++--- em2rp/functions/index.js | 120 +++++++-- em2rp/functions/processEquipmentValidation.js | 15 +- em2rp/functions/rollbackEventStep.js | 185 +++++++++++++ em2rp/functions/sendAlertEmail.js | 14 +- em2rp/functions/src/availability.js | 12 +- em2rp/functions/src/events.js | 246 ------------------ em2rp/lib/config/app_version.dart | 2 +- .../services/event_preparation_service.dart | 70 ----- em2rp/lib/views/event_preparation_page.dart | 144 ++++++---- .../event_preparation_buttons.dart | 168 +++++++++++- .../equipment/equipment_checklist_item.dart | 118 +++++---- em2rp/temp.txt | Bin 0 -> 27864 bytes em2rp/web/version.json | 6 +- 15 files changed, 660 insertions(+), 514 deletions(-) create mode 100644 em2rp/functions/rollbackEventStep.js create mode 100644 em2rp/temp.txt diff --git a/em2rp/CHANGELOG.md b/em2rp/CHANGELOG.md index d7d02ef..70fcb1c 100644 --- a/em2rp/CHANGELOG.md +++ b/em2rp/CHANGELOG.md @@ -2,6 +2,9 @@ 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 Optimisation des perfomance de l'application, amélioration de la gestion des données et refonte visuelle de la page de gestion des équipements. diff --git a/em2rp/functions/createAlert.js b/em2rp/functions/createAlert.js index fcbd052..0d672c6 100644 --- a/em2rp/functions/createAlert.js +++ b/em2rp/functions/createAlert.js @@ -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; + diff --git a/em2rp/functions/index.js b/em2rp/functions/index.js index 1901213..996b1e3 100644 --- a/em2rp/functions/index.js +++ b/em2rp/functions/index.js @@ -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); + } +}); diff --git a/em2rp/functions/processEquipmentValidation.js b/em2rp/functions/processEquipmentValidation.js index 21acbc8..65383cd 100644 --- a/em2rp/functions/processEquipmentValidation.js +++ b/em2rp/functions/processEquipmentValidation.js @@ -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; + diff --git a/em2rp/functions/rollbackEventStep.js b/em2rp/functions/rollbackEventStep.js new file mode 100644 index 0000000..4add319 --- /dev/null +++ b/em2rp/functions/rollbackEventStep.js @@ -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; diff --git a/em2rp/functions/sendAlertEmail.js b/em2rp/functions/sendAlertEmail.js index 3521f7c..5269f95 100644 --- a/em2rp/functions/sendAlertEmail.js +++ b/em2rp/functions/sendAlertEmail.js @@ -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; + diff --git a/em2rp/functions/src/availability.js b/em2rp/functions/src/availability.js index ccbf9f5..d76bfaa 100644 --- a/em2rp/functions/src/availability.js +++ b/em2rp/functions/src/availability.js @@ -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; diff --git a/em2rp/functions/src/events.js b/em2rp/functions/src/events.js index fc00cb7..37d07e3 100644 --- a/em2rp/functions/src/events.js +++ b/em2rp/functions/src/events.js @@ -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}); - } -}; diff --git a/em2rp/lib/config/app_version.dart b/em2rp/lib/config/app_version.dart index 7b9044f..08e8a69 100644 --- a/em2rp/lib/config/app_version.dart +++ b/em2rp/lib/config/app_version.dart @@ -1,6 +1,6 @@ /// Configuration de la version de l'application class AppVersion { - static const String version = '1.2.3'; + static const String version = '1.2.4'; /// Retourne la version complète de l'application static String get fullVersion => 'v$version'; diff --git a/em2rp/lib/services/event_preparation_service.dart b/em2rp/lib/services/event_preparation_service.dart index 479bb11..a7756cd 100644 --- a/em2rp/lib/services/event_preparation_service.dart +++ b/em2rp/lib/services/event_preparation_service.dart @@ -1,8 +1,4 @@ -import 'package:em2rp/services/equipment_status_calculator.dart'; -import 'package:em2rp/services/api_service.dart'; - class EventPreparationService { - final ApiService _apiService = apiService; /// 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 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 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 // si nécessaire dans le futur @@ -85,46 +53,8 @@ class EventPreparationService { } */ - // === RETOUR === - /// Valider le retour d'un équipement individuel - Future 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 validateAllReturn( - String eventId, [ - Map? 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') Future completeReturnWithMissing( diff --git a/em2rp/lib/views/event_preparation_page.dart b/em2rp/lib/views/event_preparation_page.dart index 7fd467b..f3477fb 100644 --- a/em2rp/lib/views/event_preparation_page.dart +++ b/em2rp/lib/views/event_preparation_page.dart @@ -88,22 +88,22 @@ class _EventPreparationPageState extends State with Single // Logique stricte : on avance étape par étape // 1. Préparation dépôt - if (prep != PreparationStatus.completed) { + if (prep != PreparationStatus.completed && prep != PreparationStatus.completedWithMissing) { return PreparationStep.preparation; } // 2. Chargement aller (après préparation complète) - if (loading != LoadingStatus.completed) { + if (loading != LoadingStatus.completed && loading != LoadingStatus.completedWithMissing) { return PreparationStep.loadingOutbound; } // 3. Chargement retour (après chargement aller complet) - if (unloading != UnloadingStatus.completed) { + if (unloading != UnloadingStatus.completed && unloading != UnloadingStatus.completedWithMissing) { return PreparationStep.unloadingReturn; } // 4. Retour dépôt (après déchargement complet) - if (returnStatus != ReturnStatus.completed) { + if (returnStatus != ReturnStatus.completed && returnStatus != ReturnStatus.completedWithMissing) { return PreparationStep.return_; } @@ -132,7 +132,7 @@ class _EventPreparationPageState extends State with Single WidgetsBinding.instance.addPostFrameCallback((_) { if (_isCurrentStepCompleted()) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( + const SnackBar( content: Text('Cette étape est déjà terminée'), backgroundColor: Colors.orange, ), @@ -141,6 +141,17 @@ class _EventPreparationPageState extends State with Single 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 _loadEquipmentAndContainers(); }); @@ -150,13 +161,34 @@ class _EventPreparationPageState extends State with Single bool _isCurrentStepCompleted() { switch (_currentStep) { 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: - return (_currentEvent.loadingStatus ?? LoadingStatus.notStarted) == LoadingStatus.completed; + final status = _currentEvent.loadingStatus ?? LoadingStatus.notStarted; + return status == LoadingStatus.completed || status == LoadingStatus.completedWithMissing; 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_: - 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 with Single 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_ || _currentStep == PreparationStep.unloadingReturn) && (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 with Single returnStatus: updateData['returnStatus'], ); - // Mettre à jour les statuts des équipements si nécessaire - if (_currentStep == PreparationStep.preparation || - (_currentStep == PreparationStep.unloadingReturn && _loadSimultaneously)) { + // 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) { await _updateEquipmentStatuses(updatedEquipment); } @@ -505,6 +541,8 @@ class _EventPreparationPageState extends State with Single } Future _updateEquipmentStatuses(List equipment) async { + final List failedUpdates = []; + for (var eq in equipment) { try { final equipmentData = _equipmentCache[eq.equipmentId]; @@ -513,7 +551,9 @@ class _EventPreparationPageState extends State with Single // Déterminer le nouveau statut EquipmentStatus newStatus; 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) { newStatus = EquipmentStatus.inUse; } else { @@ -527,19 +567,22 @@ class _EventPreparationPageState extends State with Single 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) { - // 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() { @@ -895,26 +938,28 @@ class _EventPreparationPageState extends State with Single _quantitiesAtReturn.addAll(quantities); break; } + + // Mettre à jour `_currentEvent.assignedEquipment` pour que l'UI se reconstruise avec les bonnes valeurs + final updatedList = _currentEvent.assignedEquipment.map((eq) { + final qty = quantities[eq.equipmentId]; + if (qty != null) { + switch (_currentStep) { + case PreparationStep.preparation: + return eq.copyWith(quantityAtPreparation: qty); + case PreparationStep.loadingOutbound: + return eq.copyWith(quantityAtLoading: qty); + case PreparationStep.unloadingReturn: + return eq.copyWith(quantityAtUnloading: qty); + case PreparationStep.return_: + return eq.copyWith(quantityAtReturn: qty); + } + } + return eq; + }).toList(); + + _currentEvent = _currentEvent.copyWith(assignedEquipment: updatedList); } - /// Obtenir la quantité requise selon l'étape (nouvelle logique) - int _getTargetQuantity(EventEquipment eventEquipment) { - switch (_currentStep) { - case PreparationStep.preparation: - return eventEquipment.quantity; // Quantité initiale - case PreparationStep.loadingOutbound: - return eventEquipment.quantityAtPreparation ?? eventEquipment.quantity; - case PreparationStep.unloadingReturn: - return eventEquipment.quantityAtLoading ?? - eventEquipment.quantityAtPreparation ?? - eventEquipment.quantity; - case PreparationStep.return_: - return eventEquipment.quantityAtUnloading ?? - eventEquipment.quantityAtLoading ?? - eventEquipment.quantityAtPreparation ?? - eventEquipment.quantity; - } - } /// Afficher un message de succès void _showSuccessFeedback(String message) { @@ -1020,20 +1065,7 @@ class _EventPreparationPageState extends State with Single /// Mettre à jour la quantité d'un équipement à l'étape actuelle void _updateEquipmentQuantity(String equipmentId, int newQuantity) { setState(() { - switch (_currentStep) { - 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; - } + _updateQuantitiesMap({equipmentId: newQuantity}); }); } diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart index 08dbbb8..dee3e8a 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:em2rp/models/event_model.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/services/api_service.dart'; import 'package:em2rp/utils/colors.dart'; /// Boutons de préparation et retour d'événement @@ -52,16 +54,16 @@ class _EventPreparationButtonsState extends State { IconData buttonIcon; bool isCompleted = false; - if (prep != PreparationStatus.completed) { + if (prep != PreparationStatus.completed && prep != PreparationStatus.completedWithMissing) { buttonText = 'Préparation dépôt'; buttonIcon = Icons.inventory_2; - } else if (loading != LoadingStatus.completed) { + } else if (loading != LoadingStatus.completed && loading != LoadingStatus.completedWithMissing) { buttonText = 'Chargement aller'; buttonIcon = Icons.local_shipping; - } else if (unloading != UnloadingStatus.completed) { + } else if (unloading != UnloadingStatus.completed && unloading != UnloadingStatus.completedWithMissing) { buttonText = 'Chargement retour'; buttonIcon = Icons.unarchive; - } else if (returnStatus != ReturnStatus.completed) { + } else if (returnStatus != ReturnStatus.completed && returnStatus != ReturnStatus.completedWithMissing) { buttonText = 'Retour dépôt'; buttonIcon = Icons.assignment_return; } else { @@ -131,9 +133,9 @@ class _EventPreparationButtonsState extends State { borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.green, width: 1), ), - child: Row( + child: const Row( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Icon(Icons.check_circle, color: Colors.green, size: 20), SizedBox(width: 8), Text( @@ -147,9 +149,163 @@ class _EventPreparationButtonsState extends State { ), ), ), + + // 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 _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> 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( + 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( + 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(context, listen: false); + final userProvider = Provider.of(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, + ), + ); + } + } + } + } + } } diff --git a/em2rp/lib/views/widgets/equipment/equipment_checklist_item.dart b/em2rp/lib/views/widgets/equipment/equipment_checklist_item.dart index 7d27207..6cb02d2 100644 --- a/em2rp/lib/views/widgets/equipment/equipment_checklist_item.dart +++ b/em2rp/lib/views/widgets/equipment/equipment_checklist_item.dart @@ -12,7 +12,7 @@ enum ChecklistStep { } /// Widget pour afficher un équipement dans une checklist de préparation/retour -class EquipmentChecklistItem extends StatelessWidget { +class EquipmentChecklistItem extends StatefulWidget { final EquipmentModel equipment; final EventEquipment eventEquipment; final ChecklistStep step; @@ -34,92 +34,120 @@ class EquipmentChecklistItem extends StatelessWidget { this.wasMissingBefore = false, }); - /// Retourne la quantité actuelle selon l'étape + @override + State createState() => _EquipmentChecklistItemState(); +} + +class _EquipmentChecklistItemState extends State { + late TextEditingController _quantityController; + int _getCurrentQuantity() { - switch (step) { + switch (widget.step) { case ChecklistStep.preparation: - return eventEquipment.quantityAtPreparation ?? eventEquipment.quantity; + return widget.eventEquipment.quantityAtPreparation ?? widget.eventEquipment.quantity; case ChecklistStep.loading: - return eventEquipment.quantityAtLoading ?? eventEquipment.quantityAtPreparation ?? eventEquipment.quantity; + return widget.eventEquipment.quantityAtLoading ?? widget.eventEquipment.quantityAtPreparation ?? widget.eventEquipment.quantity; 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_: - 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 Widget build(BuildContext context) { - final hasQuantity = equipment.hasQuantity; + final hasQuantity = widget.equipment.hasQuantity; // Déterminer la quantité actuelle selon l'étape final int currentQuantity = _getCurrentQuantity(); return Padding( 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, bottom: 4.0, ), child: Card( margin: EdgeInsets.zero, - elevation: isChild ? 0 : 1, // Pas d'élévation pour les enfants - color: wasMissingBefore + elevation: widget.isChild ? 0 : 1, // Pas d'élévation pour les enfants + color: widget.wasMissingBefore ? Colors.orange.shade50 - : (isChild ? Colors.grey.shade50 : Colors.white), + : (widget.isChild ? Colors.grey.shade50 : Colors.white), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide( - color: wasMissingBefore + color: widget.wasMissingBefore ? Colors.orange - : (isValidated ? Colors.green : Colors.grey.shade300), - width: (isValidated || wasMissingBefore) ? 2 : 1, + : (widget.isValidated ? Colors.green : Colors.grey.shade300), + width: (widget.isValidated || widget.wasMissingBefore) ? 2 : 1, ), ), child: ListTile( - dense: isChild, // Plus compact pour les enfants + dense: widget.isChild, // Plus compact pour les enfants contentPadding: EdgeInsets.symmetric( - horizontal: isChild ? 8.0 : 16.0, - vertical: isChild ? 4.0 : 8.0, + horizontal: widget.isChild ? 8.0 : 16.0, + vertical: widget.isChild ? 4.0 : 8.0, ), leading: Container( - width: isChild ? 32 : 40, - height: isChild ? 32 : 40, + width: widget.isChild ? 32 : 40, + height: widget.isChild ? 32 : 40, decoration: BoxDecoration( - color: wasMissingBefore + color: widget.wasMissingBefore ? Colors.orange.shade100 - : (isValidated ? Colors.green.shade100 : Colors.grey.shade100), + : (widget.isValidated ? Colors.green.shade100 : Colors.grey.shade100), borderRadius: BorderRadius.circular(8), ), child: IconButton( icon: Icon( - wasMissingBefore + widget.wasMissingBefore ? Icons.warning - : (isValidated ? Icons.check_circle : Icons.radio_button_unchecked), - color: wasMissingBefore + : (widget.isValidated ? Icons.check_circle : Icons.radio_button_unchecked), + color: widget.wasMissingBefore ? Colors.orange - : (isValidated ? Colors.green : Colors.grey), - size: isChild ? 18 : 24, + : (widget.isValidated ? Colors.green : Colors.grey), + size: widget.isChild ? 18 : 24, ), - onPressed: onToggle, + onPressed: widget.onToggle, padding: EdgeInsets.zero, ), ), title: Text( - equipment.name, + widget.equipment.name, style: TextStyle( - fontWeight: isChild ? FontWeight.w500 : FontWeight.w600, - fontSize: isChild ? 13 : 15, - decoration: isValidated ? TextDecoration.lineThrough : null, - color: isValidated ? Colors.grey : null, + fontWeight: widget.isChild ? FontWeight.w500 : FontWeight.w600, + fontSize: widget.isChild ? 13 : 15, + decoration: widget.isValidated ? TextDecoration.lineThrough : null, + color: widget.isValidated ? Colors.grey : null, ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (equipment.model != null) + if (widget.equipment.model != null) Text( - equipment.model!, + widget.equipment.model!, style: TextStyle( fontSize: 12, color: Colors.grey.shade600, @@ -127,12 +155,12 @@ class EquipmentChecklistItem extends StatelessWidget { ), // Indicateur si manquant à l'étape précédente - if (wasMissingBefore) + if (widget.wasMissingBefore) Padding( padding: const EdgeInsets.only(top: 4.0), child: Row( children: [ - Icon(Icons.warning_amber, size: 14, color: Colors.orange), + const Icon(Icons.warning_amber, size: 14, color: Colors.orange), const SizedBox(width: 4), Text( 'Était manquant à l\'étape précédente', @@ -151,7 +179,7 @@ class EquipmentChecklistItem extends StatelessWidget { const SizedBox(height: 6), Row( children: [ - Text( + const Text( 'Quantité : ', style: TextStyle( fontSize: 12, @@ -159,11 +187,11 @@ class EquipmentChecklistItem extends StatelessWidget { color: AppColors.bleuFonce, ), ), - if (onQuantityChanged != null) + if (widget.onQuantityChanged != null) SizedBox( width: 60, child: TextFormField( - initialValue: currentQuantity.toString(), + controller: _quantityController, keyboardType: TextInputType.number, style: const TextStyle(fontSize: 12), decoration: InputDecoration( @@ -175,14 +203,14 @@ class EquipmentChecklistItem extends StatelessWidget { ), onChanged: (value) { final qty = int.tryParse(value) ?? currentQuantity; - onQuantityChanged!(qty); + widget.onQuantityChanged!(qty); }, ), ) else Text( currentQuantity.toString(), - style: TextStyle( + style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.bleuFonce, @@ -193,16 +221,16 @@ class EquipmentChecklistItem extends StatelessWidget { ], ], ), - trailing: isValidated + trailing: widget.isValidated ? Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(12), ), - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Icon(Icons.check, size: 16, color: Colors.green), SizedBox(width: 4), Text( diff --git a/em2rp/temp.txt b/em2rp/temp.txt new file mode 100644 index 0000000000000000000000000000000000000000..038cd29a649c53125f62fae861376c1172d60538 GIT binary patch literal 27864 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 literal 0 HcmV?d00001 diff --git a/em2rp/web/version.json b/em2rp/web/version.json index b81bffc..fb0b3b2 100644 --- a/em2rp/web/version.json +++ b/em2rp/web/version.json @@ -1,7 +1,7 @@ { - "version": "1.2.3", + "version": "1.2.4", "updateUrl": "https://app.em2events.fr", "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.", - "timestamp": "2026-05-26T13:34:16.390Z" + "releaseNotes": "Ajout de la fonction retour en arriere pour la validation du chargement des equipements et des conteneurs.", + "timestamp": "2026-05-27T20:00:59.431Z" } \ No newline at end of file