714 lines
26 KiB
JavaScript
714 lines
26 KiB
JavaScript
/**
|
|
* EM2RP Cloud Functions
|
|
* Architecture backend sécurisée et modulaire avec dynamic imports (lazy loading)
|
|
*/
|
|
|
|
const path = require("path");
|
|
require("dotenv").config({path: path.join(__dirname, ".env.local")});
|
|
require("dotenv").config({path: path.join(__dirname, ".env")});
|
|
|
|
const {onRequest, onCall} = require("firebase-functions/v2/https");
|
|
const {onSchedule} = require("firebase-functions/v2/scheduler");
|
|
const {onDocumentCreated, onDocumentUpdated} = require("firebase-functions/v2/firestore");
|
|
const logger = require("firebase-functions/logger");
|
|
const admin = require("firebase-admin");
|
|
|
|
// Initialisation sécurisée de Firebase Admin au démarrage
|
|
if (!admin.apps.length) {
|
|
admin.initializeApp();
|
|
}
|
|
const db = admin.firestore();
|
|
|
|
// Configuration commune pour toutes les fonctions HTTP
|
|
const httpOptions = {
|
|
cors: false,
|
|
invoker: "public",
|
|
region: "europe-west9",
|
|
};
|
|
|
|
// Options dédiées pour les traitements IA potentiellement longs.
|
|
const aiHttpOptions = {
|
|
...httpOptions,
|
|
timeoutSeconds: 300,
|
|
memory: "1GiB",
|
|
};
|
|
|
|
// Options HTTP spécifiques pour TTS avec CORS activé
|
|
const ttsHttpOptions = {
|
|
cors: true,
|
|
invoker: "public",
|
|
region: "europe-west9",
|
|
};
|
|
|
|
// CORS Middleware
|
|
const setCorsHeaders = (res, req) => {
|
|
const origin = req.headers.origin || "*";
|
|
res.set("Access-Control-Allow-Origin", origin);
|
|
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);
|
|
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});
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
// ============================================================================
|
|
// STORAGE
|
|
// ============================================================================
|
|
exports.moveEventFileV2 = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/storage").moveEventFileV2(req, res);
|
|
}));
|
|
|
|
// ============================================================================
|
|
// EQUIPMENT
|
|
// ============================================================================
|
|
exports.createEquipment = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/equipments").createEquipment(req, res);
|
|
}));
|
|
|
|
exports.updateEquipment = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/equipments").updateEquipment(req, res);
|
|
}));
|
|
|
|
exports.deleteEquipment = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/equipments").deleteEquipment(req, res);
|
|
}));
|
|
|
|
exports.getEquipment = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/equipments").getEquipment(req, res);
|
|
}));
|
|
|
|
exports.getEquipmentsByIds = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/equipments").getEquipmentsByIds(req, res);
|
|
}));
|
|
|
|
exports.updateEquipmentStatusOnly = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/equipments").updateEquipmentStatusOnly(req, res);
|
|
}));
|
|
|
|
exports.updateEquipmentStatus = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/equipments").updateEquipmentStatus(req, res);
|
|
}));
|
|
|
|
exports.getEquipmentsPaginated = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/equipments").getEquipmentsPaginated(req, res);
|
|
}));
|
|
|
|
exports.quickSearch = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/equipments").quickSearch(req, res);
|
|
}));
|
|
|
|
// ============================================================================
|
|
// CONTAINERS
|
|
// ============================================================================
|
|
exports.createContainer = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/containers").createContainer(req, res);
|
|
}));
|
|
|
|
exports.updateContainer = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/containers").updateContainer(req, res);
|
|
}));
|
|
|
|
exports.deleteContainer = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/containers").deleteContainer(req, res);
|
|
}));
|
|
|
|
exports.getContainersByEquipment = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/containers").getContainersByEquipment(req, res);
|
|
}));
|
|
|
|
exports.getContainersByIds = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/containers").getContainersByIds(req, res);
|
|
}));
|
|
|
|
exports.addEquipmentToContainer = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/containers").addEquipmentToContainer(req, res);
|
|
}));
|
|
|
|
exports.removeEquipmentFromContainer = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/containers").removeEquipmentFromContainer(req, res);
|
|
}));
|
|
|
|
exports.getContainersPaginated = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/containers").getContainersPaginated(req, res);
|
|
}));
|
|
|
|
// ============================================================================
|
|
// EVENTS
|
|
// ============================================================================
|
|
exports.createEvent = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").createEvent(req, res);
|
|
}));
|
|
|
|
exports.updateEvent = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").updateEvent(req, res);
|
|
}));
|
|
|
|
exports.deleteEvent = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").deleteEvent(req, res);
|
|
}));
|
|
|
|
exports.updateEventEquipment = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").updateEventEquipment(req, res);
|
|
}));
|
|
|
|
exports.getEventsByEventType = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").getEventsByEventType(req, res);
|
|
}));
|
|
|
|
exports.getEvents = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").getEvents(req, res);
|
|
}));
|
|
|
|
exports.getEventsByMonth = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").getEventsByMonth(req, res);
|
|
}));
|
|
|
|
exports.searchEvents = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").searchEvents(req, res);
|
|
}));
|
|
|
|
exports.getEventWithDetails = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").getEventWithDetails(req, res);
|
|
}));
|
|
|
|
exports.validateEquipmentLoading = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").validateEquipmentLoading(req, res);
|
|
}));
|
|
|
|
exports.validateAllLoading = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").validateAllLoading(req, res);
|
|
}));
|
|
|
|
exports.validateEquipmentUnloading = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").validateEquipmentUnloading(req, res);
|
|
}));
|
|
|
|
exports.validateAllUnloading = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/events").validateAllUnloading(req, res);
|
|
}));
|
|
|
|
// ============================================================================
|
|
// MAINTENANCES
|
|
// ============================================================================
|
|
exports.createMaintenance = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/maintenances").createMaintenance(req, res);
|
|
}));
|
|
|
|
exports.updateMaintenance = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/maintenances").updateMaintenance(req, res);
|
|
}));
|
|
|
|
exports.getMaintenances = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/maintenances").getMaintenances(req, res);
|
|
}));
|
|
|
|
exports.deleteMaintenance = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/maintenances").deleteMaintenance(req, res);
|
|
}));
|
|
|
|
exports.checkUpcomingMaintenances = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/maintenances").checkUpcomingMaintenances(req, res);
|
|
}));
|
|
|
|
exports.completeMaintenance = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/maintenances").completeMaintenance(req, res);
|
|
}));
|
|
|
|
// ============================================================================
|
|
// OPTIONS & EVENT TYPES
|
|
// ============================================================================
|
|
exports.createOption = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/options").createOption(req, res);
|
|
}));
|
|
|
|
exports.updateOption = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/options").updateOption(req, res);
|
|
}));
|
|
|
|
exports.deleteOption = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/options").deleteOption(req, res);
|
|
}));
|
|
|
|
exports.getOptions = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/options").getOptions(req, res);
|
|
}));
|
|
|
|
exports.getEventTypes = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/options").getEventTypes(req, res);
|
|
}));
|
|
|
|
exports.createEventType = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/options").createEventType(req, res);
|
|
}));
|
|
|
|
exports.updateEventType = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/options").updateEventType(req, res);
|
|
}));
|
|
|
|
exports.deleteEventType = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/options").deleteEventType(req, res);
|
|
}));
|
|
|
|
// ============================================================================
|
|
// USERS
|
|
// ============================================================================
|
|
exports.createUser = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/users").createUser(req, res);
|
|
}));
|
|
|
|
exports.createUserWithInvite = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/users").createUserWithInvite(req, res);
|
|
}));
|
|
|
|
exports.updateUser = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/users").updateUser(req, res);
|
|
}));
|
|
|
|
exports.deleteUser = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/users").deleteUser(req, res);
|
|
}));
|
|
|
|
exports.getUsers = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/users").getUsers(req, res);
|
|
}));
|
|
|
|
exports.getUser = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/users").getUser(req, res);
|
|
}));
|
|
|
|
exports.getCurrentUser = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/users").getCurrentUser(req, res);
|
|
}));
|
|
|
|
exports.getRoles = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/users").getRoles(req, res);
|
|
}));
|
|
|
|
// ============================================================================
|
|
// ALERTS
|
|
// ============================================================================
|
|
exports.getAlerts = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/alerts").getAlerts(req, res);
|
|
}));
|
|
|
|
exports.markAlertAsRead = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/alerts").markAlertAsRead(req, res);
|
|
}));
|
|
|
|
exports.deleteAlert = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/alerts").deleteAlert(req, res);
|
|
}));
|
|
|
|
// ============================================================================
|
|
// AVAILABILITY
|
|
// ============================================================================
|
|
exports.checkEquipmentAvailability = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/availability").checkEquipmentAvailability(req, res);
|
|
}));
|
|
|
|
exports.checkContainerAvailability = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/availability").checkContainerAvailability(req, res);
|
|
}));
|
|
|
|
exports.getConflictingEquipmentIds = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/availability").getConflictingEquipmentIds(req, res);
|
|
}));
|
|
|
|
exports.findAlternativeEquipment = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/availability").findAlternativeEquipment(req, res);
|
|
}));
|
|
|
|
exports.calculateEquipmentStatuses = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/availability").calculateEquipmentStatuses(req, res);
|
|
}));
|
|
|
|
exports.getActiveEvents = onRequest(httpOptions, withCors((req, res) => {
|
|
return require("./src/availability").getActiveEvents(req, res);
|
|
}));
|
|
|
|
// ============================================================================
|
|
// TEXT-TO-SPEECH
|
|
// ============================================================================
|
|
exports.generateTTSV2 = onRequest(ttsHttpOptions, (req, res) => {
|
|
return require("./src/tts").generateTTSV2(req, res);
|
|
});
|
|
|
|
// ============================================================================
|
|
// AI
|
|
// ============================================================================
|
|
exports.aiEquipmentProposal = onRequest(aiHttpOptions, withCors((req, res) => {
|
|
return require("./aiEquipmentProposal").handleAiEquipmentProposal(req, res);
|
|
}));
|
|
|
|
// ============================================================================
|
|
// CALLABLE EMAIL & VALIDATION (LEGACY LAZY WRAPPERS)
|
|
// ============================================================================
|
|
exports.sendAlertEmail = onCall({region: "europe-west9", cors: true}, (request) => {
|
|
return require("./sendAlertEmail").handler(request);
|
|
});
|
|
|
|
exports.createAlert = onCall({region: "europe-west9", cors: true}, (request) => {
|
|
return require("./createAlert").handler(request);
|
|
});
|
|
|
|
exports.processEquipmentValidation = onCall({region: "europe-west9", cors: true}, (request) => {
|
|
return require("./processEquipmentValidation").handler(request);
|
|
});
|
|
|
|
exports.rollbackEventStep = onCall({region: "europe-west9", cors: true}, (request) => {
|
|
return require("./rollbackEventStep").handler(request);
|
|
});
|
|
|
|
// ============================================================================
|
|
// SCHEDULED FUNCTIONS
|
|
// ============================================================================
|
|
exports.sendDailyDigest = onSchedule({
|
|
schedule: "0 8 * * *",
|
|
timeZone: "Europe/Paris",
|
|
region: "europe-west9",
|
|
retryCount: 2,
|
|
memory: "512MiB",
|
|
}, async (context) => {
|
|
logger.info("[Scheduler] Démarrage sendDailyDigest");
|
|
try {
|
|
const {sendDailyDigest} = require("./sendDailyDigest");
|
|
await sendDailyDigest();
|
|
logger.info("[Scheduler] sendDailyDigest terminé avec succès");
|
|
} catch (error) {
|
|
logger.error("[Scheduler] Erreur sendDailyDigest:", error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// FIRESTORE TRIGGERS
|
|
// ============================================================================
|
|
|
|
exports.onEventCreated = onDocumentCreated({
|
|
document: "events/{eventId}",
|
|
region: "europe-west9",
|
|
}, async (event) => {
|
|
logger.info(`[onEventCreated] Événement créé: ${event.params.eventId}`);
|
|
try {
|
|
const eventData = event.data.data();
|
|
const eventId = event.params.eventId;
|
|
|
|
await db.collection("alerts").add({
|
|
type: "EVENT_CREATED",
|
|
severity: "INFO",
|
|
message: `Nouvel événement créé : "${eventData.name}" le ${new Date(eventData.startDate?.toDate ? eventData.startDate.toDate() : eventData.startDate).toLocaleDateString("fr-FR")}`,
|
|
eventId: eventId,
|
|
eventName: eventData.name,
|
|
eventDate: eventData.startDate,
|
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
isRead: false,
|
|
metadata: {
|
|
eventId: eventId,
|
|
eventName: eventData.name,
|
|
eventDate: eventData.startDate,
|
|
},
|
|
assignedTo: [],
|
|
});
|
|
logger.info(`[onEventCreated] Alerte créée pour événement ${eventId}`);
|
|
} catch (error) {
|
|
logger.error("[onEventCreated] Erreur:", error);
|
|
}
|
|
});
|
|
|
|
exports.onEventUpdated = 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 workforceBefore = before.workforce || [];
|
|
const workforceAfter = after.workforce || [];
|
|
|
|
const newMembers = workforceAfter.filter((afterMember) => {
|
|
return !workforceBefore.some((beforeMember) =>
|
|
beforeMember.userId === afterMember.userId,
|
|
);
|
|
});
|
|
|
|
if (newMembers.length > 0) {
|
|
logger.info(`[onEventUpdated] ${newMembers.length} nouveaux membres ajoutés à ${eventId}`);
|
|
for (const member of newMembers) {
|
|
await db.collection("alerts").add({
|
|
type: "WORKFORCE_ADDED",
|
|
severity: "INFO",
|
|
message: `Vous avez été ajouté(e) à l'événement "${after.name}" le ${new Date(after.startDate?.toDate ? after.startDate.toDate() : after.startDate).toLocaleDateString("fr-FR")}`,
|
|
eventId: eventId,
|
|
eventName: after.name,
|
|
eventDate: after.startDate,
|
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
isRead: false,
|
|
metadata: {
|
|
eventId: eventId,
|
|
eventName: after.name,
|
|
eventDate: after.startDate,
|
|
},
|
|
assignedTo: [member.userId],
|
|
});
|
|
logger.info(`[onEventUpdated] Alerte créée pour ${member.userId}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error("[onEventUpdated] Erreur:", error);
|
|
}
|
|
});
|
|
|
|
exports.onAlertCreated = onDocumentCreated({
|
|
document: "alerts/{alertId}",
|
|
region: "europe-west9",
|
|
}, async (event) => {
|
|
const alertId = event.params.alertId;
|
|
const alertData = event.data.data();
|
|
|
|
logger.info(`[onAlertCreated] Nouvelle alerte: ${alertId} (${alertData.severity})`);
|
|
|
|
try {
|
|
if (alertData.severity === "CRITICAL" && !alertData.emailSent) {
|
|
const userIds = alertData.assignedTo || [];
|
|
if (userIds.length > 0) {
|
|
logger.info(`[onAlertCreated] Envoi email immédiat à ${userIds.length} utilisateurs`);
|
|
await db.collection("alerts").doc(alertId).update({
|
|
pendingEmailSend: true,
|
|
});
|
|
logger.info(`[onAlertCreated] Alerte marquée pour envoi email`);
|
|
}
|
|
}
|
|
} catch (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);
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// SEARCH - Recherche unifiée avec autocomplétion
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Recherche rapide d'équipements et containers pour l'autocomplétion
|
|
* Retourne un nombre limité de résultats pour des performances optimales
|
|
*/
|
|
exports.quickSearch = onRequest(httpOptions, withCors(async (req, res) => {
|
|
try {
|
|
const decodedToken = await auth.authenticateUser(req);
|
|
|
|
// Vérifier les permissions
|
|
const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment');
|
|
|
|
if (!canView) {
|
|
res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' });
|
|
return;
|
|
}
|
|
|
|
const params = req.method === 'GET' ? req.query : (req.body?.data || {});
|
|
const searchQuery = params.query?.toLowerCase() || '';
|
|
const limit = Math.min(parseInt(params.limit) || 10, 50);
|
|
const includeEquipments = params.includeEquipments !== 'false';
|
|
const includeContainers = params.includeContainers !== 'false';
|
|
|
|
if (!searchQuery || searchQuery.length < 2) {
|
|
res.status(200).json({ results: [] });
|
|
return;
|
|
}
|
|
|
|
const results = [];
|
|
|
|
// Rechercher dans les équipements
|
|
if (includeEquipments) {
|
|
const equipmentSnapshot = await db.collection('equipments')
|
|
.orderBy('id')
|
|
.limit(limit * 2) // Récupérer plus pour filtrer ensuite
|
|
.get();
|
|
|
|
equipmentSnapshot.docs.forEach(doc => {
|
|
const data = doc.data();
|
|
const searchableText = [
|
|
data.name || '',
|
|
doc.id || '',
|
|
data.model || '',
|
|
data.brand || ''
|
|
].join(' ').toLowerCase();
|
|
|
|
if (searchableText.includes(searchQuery)) {
|
|
results.push({
|
|
type: 'equipment',
|
|
id: doc.id,
|
|
name: data.name,
|
|
category: data.category,
|
|
model: data.model,
|
|
brand: data.brand
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Rechercher dans les containers
|
|
if (includeContainers) {
|
|
const containerSnapshot = await db.collection('containers')
|
|
.orderBy('id')
|
|
.limit(limit * 2)
|
|
.get();
|
|
|
|
containerSnapshot.docs.forEach(doc => {
|
|
const data = doc.data();
|
|
const searchableText = [
|
|
data.name || '',
|
|
doc.id || ''
|
|
].join(' ').toLowerCase();
|
|
|
|
if (searchableText.includes(searchQuery)) {
|
|
results.push({
|
|
type: 'container',
|
|
id: doc.id,
|
|
name: data.name,
|
|
containerType: data.type
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Limiter et trier les résultats
|
|
const limitedResults = results
|
|
.sort((a, b) => {
|
|
// Prioriser les correspondances exactes au début
|
|
const aStarts = a.id.toLowerCase().startsWith(searchQuery);
|
|
const bStarts = b.id.toLowerCase().startsWith(searchQuery);
|
|
if (aStarts && !bStarts) return -1;
|
|
if (!aStarts && bStarts) return 1;
|
|
return 0;
|
|
})
|
|
.slice(0, limit);
|
|
|
|
res.status(200).json({ results: limitedResults });
|
|
|
|
} catch (error) {
|
|
logger.error("Error in quick search:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
// ============================================================================
|
|
// GOOGLE MAPS & TRAVEL (Proxies CORS + Calcul d'itinéraires)
|
|
// ============================================================================
|
|
const travel = require('./src/travel');
|
|
exports.googleMapsAutocomplete = onRequest(httpOptions, withCors(travel.googleMapsAutocomplete));
|
|
exports.googleMapsComputeRoute = onRequest(httpOptions, withCors(travel.googleMapsComputeRoute));
|