feat: implement comprehensive Firebase Functions backend for equipment management and migrate core repository services
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
const admin = require("firebase-admin");
|
||||
const db = admin.firestore();
|
||||
const logger = require("firebase-functions/logger");
|
||||
const auth = require("../utils/auth");
|
||||
const helpers = require("../utils/helpers");
|
||||
|
||||
// Récupère toutes les alertes (filtrées et limitées)
|
||||
exports.getAlerts = async (req, res) => {
|
||||
try {
|
||||
await auth.authenticateUser(req);
|
||||
|
||||
const snapshot = await db.collection("alerts")
|
||||
.orderBy("createdAt", "desc")
|
||||
.limit(100)
|
||||
.get();
|
||||
|
||||
const alerts = snapshot.docs.map((doc) => {
|
||||
const data = doc.data();
|
||||
return {
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(data, ["createdAt"]),
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json({alerts});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching alerts:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Marquer une alerte comme lue
|
||||
exports.markAlertAsRead = async (req, res) => {
|
||||
try {
|
||||
await auth.authenticateUser(req);
|
||||
|
||||
const alertId = req.body.data?.alertId;
|
||||
if (!alertId) {
|
||||
res.status(400).json({error: "alertId is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("alerts").doc(alertId).update({
|
||||
isRead: true,
|
||||
});
|
||||
|
||||
res.status(200).json({success: true});
|
||||
} catch (error) {
|
||||
logger.error("Error marking alert as read:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Supprimer une alerte
|
||||
exports.deleteAlert = async (req, res) => {
|
||||
try {
|
||||
await auth.authenticateUser(req);
|
||||
|
||||
const alertId = req.body.data?.alertId;
|
||||
if (!alertId) {
|
||||
res.status(400).json({error: "alertId is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("alerts").doc(alertId).delete();
|
||||
|
||||
res.status(200).json({success: true});
|
||||
} catch (error) {
|
||||
logger.error("Error deleting alert:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,628 @@
|
||||
const admin = require("firebase-admin");
|
||||
const db = admin.firestore();
|
||||
const logger = require("firebase-functions/logger");
|
||||
const auth = require("../utils/auth");
|
||||
const helpers = require("../utils/helpers");
|
||||
|
||||
// Vérifie si un équipement est disponible pour une plage de dates
|
||||
exports.checkEquipmentAvailability = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "view_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires view_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {equipmentId, startDate, endDate, excludeEventId} = req.body.data;
|
||||
|
||||
if (!equipmentId || !startDate || !endDate) {
|
||||
res.status(400).json({error: "equipmentId, startDate, and endDate are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Checking availability for equipment ${equipmentId} from ${startDate} to ${endDate}, excluding event: ${excludeEventId}`);
|
||||
|
||||
const startTimestamp = admin.firestore.Timestamp.fromDate(new Date(startDate));
|
||||
const endTimestamp = admin.firestore.Timestamp.fromDate(new Date(endDate));
|
||||
|
||||
const eventsSnapshot = await db.collection("events")
|
||||
.where("status", "!=", "CANCELLED")
|
||||
.get();
|
||||
|
||||
logger.info(`Found ${eventsSnapshot.docs.length} events to check`);
|
||||
|
||||
const conflicts = [];
|
||||
|
||||
for (const eventDoc of eventsSnapshot.docs) {
|
||||
const event = eventDoc.data();
|
||||
|
||||
if (excludeEventId && eventDoc.id === excludeEventId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let eventStart; let eventEnd;
|
||||
if (event.StartDateTime) {
|
||||
eventStart = event.StartDateTime.toDate ? event.StartDateTime.toDate() : new Date(event.StartDateTime);
|
||||
}
|
||||
if (event.EndDateTime) {
|
||||
eventEnd = event.EndDateTime.toDate ? event.EndDateTime.toDate() : new Date(event.EndDateTime);
|
||||
}
|
||||
|
||||
if (!eventStart || !eventEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const assignedEquipment = event.assignedEquipment || [];
|
||||
const assignedContainers = event.assignedContainers || [];
|
||||
|
||||
const isEquipmentDirectlyAssigned = assignedEquipment.some((eq) => eq.equipmentId === equipmentId);
|
||||
|
||||
let isEquipmentInAssignedContainer = false;
|
||||
if (assignedContainers.length > 0) {
|
||||
logger.info(`Event ${eventDoc.id} has ${assignedContainers.length} assigned containers`);
|
||||
for (const containerId of assignedContainers) {
|
||||
const containerDoc = await db.collection("containers").doc(containerId).get();
|
||||
if (containerDoc.exists) {
|
||||
const containerData = containerDoc.data();
|
||||
const equipmentIds = containerData.equipmentIds || [];
|
||||
logger.info(`Container ${containerId} contains equipment IDs: ${equipmentIds.join(", ")}`);
|
||||
if (equipmentIds.includes(equipmentId)) {
|
||||
isEquipmentInAssignedContainer = true;
|
||||
logger.info(`Equipment ${equipmentId} found in container ${containerId} for event ${eventDoc.id}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isEquipmentDirectlyAssigned) {
|
||||
logger.info(`Equipment ${equipmentId} is directly assigned to event ${eventDoc.id}`);
|
||||
}
|
||||
|
||||
if (!isEquipmentDirectlyAssigned && !isEquipmentInAssignedContainer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const requestStart = startTimestamp.toDate();
|
||||
const requestEnd = endTimestamp.toDate();
|
||||
|
||||
const installationTime = event.InstallationTime || 0;
|
||||
const disassemblyTime = event.DisassemblyTime || 0;
|
||||
|
||||
const eventStartWithSetup = new Date(eventStart);
|
||||
eventStartWithSetup.setHours(eventStartWithSetup.getHours() - installationTime);
|
||||
|
||||
const eventEndWithTeardown = new Date(eventEnd);
|
||||
eventEndWithTeardown.setHours(eventEndWithTeardown.getHours() + disassemblyTime);
|
||||
|
||||
const hasOverlap = requestStart < eventEndWithTeardown && requestEnd > eventStartWithSetup;
|
||||
|
||||
if (hasOverlap) {
|
||||
const overlapStart = new Date(Math.max(requestStart, eventStartWithSetup));
|
||||
const overlapEnd = new Date(Math.min(requestEnd, eventEndWithTeardown));
|
||||
const overlapDays = Math.ceil((overlapEnd - overlapStart) / (1000 * 60 * 60 * 24));
|
||||
|
||||
logger.info(`Conflict detected: Equipment ${equipmentId} conflicts with event ${eventDoc.id} (${event.Name})`);
|
||||
|
||||
const eventData = helpers.serializeTimestamps(event);
|
||||
conflicts.push({
|
||||
eventId: eventDoc.id,
|
||||
eventName: event.Name,
|
||||
eventData: eventData,
|
||||
startDate: eventStart.toISOString(),
|
||||
endDate: eventEnd.toISOString(),
|
||||
overlapDays: overlapDays,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Total conflicts found: ${conflicts.length}`);
|
||||
|
||||
res.status(200).json({conflicts, available: conflicts.length === 0});
|
||||
} catch (error) {
|
||||
logger.error("Error checking equipment availability:", error);
|
||||
res.status(500).json({error: error.message || "Failed to check equipment availability"});
|
||||
}
|
||||
};
|
||||
|
||||
// Vérifie la disponibilité d'un container et de son contenu
|
||||
exports.checkContainerAvailability = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "view_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires view_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {containerId, startDate, endDate, excludeEventId} = req.body.data;
|
||||
|
||||
if (!containerId || !startDate || !endDate) {
|
||||
res.status(400).json({error: "containerId, startDate, and endDate are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const containerDoc = await db.collection("containers").doc(containerId).get();
|
||||
if (!containerDoc.exists) {
|
||||
throw new Error("Container not found");
|
||||
}
|
||||
|
||||
const containerData = containerDoc.data();
|
||||
const equipmentIds = containerData.equipmentIds || [];
|
||||
|
||||
const startTimestamp = admin.firestore.Timestamp.fromDate(new Date(startDate));
|
||||
const endTimestamp = admin.firestore.Timestamp.fromDate(new Date(endDate));
|
||||
|
||||
const eventsSnapshot = await db.collection("events")
|
||||
.where("status", "!=", "CANCELLED")
|
||||
.get();
|
||||
|
||||
const containerConflicts = [];
|
||||
const equipmentConflicts = {};
|
||||
|
||||
for (const eventDoc of eventsSnapshot.docs) {
|
||||
const event = eventDoc.data();
|
||||
|
||||
if (excludeEventId && eventDoc.id === excludeEventId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let eventStart; let eventEnd;
|
||||
if (event.StartDateTime) {
|
||||
eventStart = event.StartDateTime.toDate ? event.StartDateTime.toDate() : new Date(event.StartDateTime);
|
||||
}
|
||||
if (event.EndDateTime) {
|
||||
eventEnd = event.EndDateTime.toDate ? event.EndDateTime.toDate() : new Date(event.EndDateTime);
|
||||
}
|
||||
|
||||
if (!eventStart || !eventEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const assignedContainers = event.assignedContainers || [];
|
||||
const isContainerAssigned = assignedContainers.includes(containerId);
|
||||
|
||||
const assignedEquipment = event.assignedEquipment || [];
|
||||
const conflictingEquipmentIds = equipmentIds.filter((eqId) =>
|
||||
assignedEquipment.some((eq) => eq.equipmentId === eqId),
|
||||
);
|
||||
|
||||
if (!isContainerAssigned && conflictingEquipmentIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const requestStart = startTimestamp.toDate();
|
||||
const requestEnd = endTimestamp.toDate();
|
||||
|
||||
const installationTime = event.InstallationTime || 0;
|
||||
const disassemblyTime = event.DisassemblyTime || 0;
|
||||
|
||||
const eventStartWithSetup = new Date(eventStart);
|
||||
eventStartWithSetup.setHours(eventStartWithSetup.getHours() - installationTime);
|
||||
|
||||
const eventEndWithTeardown = new Date(eventEnd);
|
||||
eventEndWithTeardown.setHours(eventEndWithTeardown.getHours() + disassemblyTime);
|
||||
|
||||
const hasOverlap = requestStart < eventEndWithTeardown && requestEnd > eventStartWithSetup;
|
||||
|
||||
if (hasOverlap) {
|
||||
const overlapStart = new Date(Math.max(requestStart, eventStartWithSetup));
|
||||
const overlapEnd = new Date(Math.min(requestEnd, eventEndWithTeardown));
|
||||
const overlapDays = Math.ceil((overlapEnd - overlapStart) / (1000 * 60 * 60 * 24));
|
||||
|
||||
const conflictInfo = {
|
||||
eventId: eventDoc.id,
|
||||
eventName: event.Name,
|
||||
startDate: eventStart.toISOString(),
|
||||
endDate: eventEnd.toISOString(),
|
||||
overlapDays: overlapDays,
|
||||
};
|
||||
|
||||
if (isContainerAssigned) {
|
||||
containerConflicts.push(conflictInfo);
|
||||
}
|
||||
|
||||
conflictingEquipmentIds.forEach((eqId) => {
|
||||
if (!equipmentConflicts[eqId]) {
|
||||
equipmentConflicts[eqId] = [];
|
||||
}
|
||||
equipmentConflicts[eqId].push(conflictInfo);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const hasContainerConflict = containerConflicts.length > 0;
|
||||
const hasPartialConflict = Object.keys(equipmentConflicts).length > 0 && !hasContainerConflict;
|
||||
const conflictType = hasContainerConflict ? "complete" : (hasPartialConflict ? "partial" : "none");
|
||||
|
||||
res.status(200).json({
|
||||
conflictType,
|
||||
containerConflicts,
|
||||
equipmentConflicts,
|
||||
isAvailable: conflictType === "none",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error checking container availability:", error);
|
||||
res.status(500).json({error: error.message || "Failed to check container availability"});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupère tous les équipements et conteneurs en conflit pour une période donnée
|
||||
exports.getConflictingEquipmentIds = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "view_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires view_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {startDate, endDate, excludeEventId, installationTime = 0, disassemblyTime = 0} = req.body.data;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
res.status(400).json({error: "startDate and endDate are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Getting conflicting equipment IDs for period ${startDate} to ${endDate}`);
|
||||
|
||||
const requestStartDate = new Date(startDate);
|
||||
requestStartDate.setHours(requestStartDate.getHours() - installationTime);
|
||||
|
||||
const requestEndDate = new Date(endDate);
|
||||
requestEndDate.setHours(requestEndDate.getHours() + disassemblyTime);
|
||||
|
||||
const eventsSnapshot = await db.collection("events")
|
||||
.where("status", "!=", "CANCELLED")
|
||||
.get();
|
||||
|
||||
logger.info(`Found ${eventsSnapshot.docs.length} events to check`);
|
||||
|
||||
const equipmentsSnapshot = await db.collection("equipments").get();
|
||||
const equipmentsInfo = {};
|
||||
equipmentsSnapshot.docs.forEach((doc) => {
|
||||
const data = doc.data();
|
||||
equipmentsInfo[doc.id] = {
|
||||
category: data.category,
|
||||
totalQuantity: data.totalQuantity || 0,
|
||||
hasQuantity: data.category === "CABLE" || data.category === "CONSUMABLE",
|
||||
};
|
||||
});
|
||||
|
||||
const conflictingEquipmentIds = new Set();
|
||||
const conflictingContainerIds = new Set();
|
||||
const conflictDetails = {};
|
||||
const equipmentQuantities = {};
|
||||
|
||||
for (const eventDoc of eventsSnapshot.docs) {
|
||||
if (excludeEventId && eventDoc.id === excludeEventId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const event = eventDoc.data();
|
||||
|
||||
let eventStart; let eventEnd;
|
||||
if (event.StartDateTime) {
|
||||
eventStart = event.StartDateTime.toDate ? event.StartDateTime.toDate() : new Date(event.StartDateTime);
|
||||
}
|
||||
if (event.EndDateTime) {
|
||||
eventEnd = event.EndDateTime.toDate ? event.EndDateTime.toDate() : new Date(event.EndDateTime);
|
||||
}
|
||||
|
||||
if (!eventStart || !eventEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const eventInstallTime = event.InstallationTime || 0;
|
||||
const eventDisassemblyTime = event.DisassemblyTime || 0;
|
||||
|
||||
const eventStartWithSetup = new Date(eventStart);
|
||||
eventStartWithSetup.setHours(eventStartWithSetup.getHours() - eventInstallTime);
|
||||
|
||||
const eventEndWithTeardown = new Date(eventEnd);
|
||||
eventEndWithTeardown.setHours(eventEndWithTeardown.getHours() + eventDisassemblyTime);
|
||||
|
||||
const hasOverlap = requestStartDate < eventEndWithTeardown && requestEndDate > eventStartWithSetup;
|
||||
|
||||
if (!hasOverlap) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const assignedEquipment = event.assignedEquipment || [];
|
||||
const assignedContainers = event.assignedContainers || [];
|
||||
|
||||
const conflictInfo = {
|
||||
eventId: eventDoc.id,
|
||||
eventName: event.Name,
|
||||
startDate: eventStart.toISOString(),
|
||||
endDate: eventEnd.toISOString(),
|
||||
};
|
||||
|
||||
for (const eq of assignedEquipment) {
|
||||
const equipmentId = eq.equipmentId;
|
||||
const quantity = eq.quantity || 1;
|
||||
const equipInfo = equipmentsInfo[equipmentId];
|
||||
|
||||
if (equipInfo && equipInfo.hasQuantity) {
|
||||
if (!equipmentQuantities[equipmentId]) {
|
||||
equipmentQuantities[equipmentId] = {
|
||||
totalQuantity: equipInfo.totalQuantity,
|
||||
reservedQuantity: 0,
|
||||
availableQuantity: equipInfo.totalQuantity,
|
||||
reservations: [],
|
||||
};
|
||||
}
|
||||
|
||||
equipmentQuantities[equipmentId].reservedQuantity += quantity;
|
||||
equipmentQuantities[equipmentId].availableQuantity = equipInfo.totalQuantity - equipmentQuantities[equipmentId].reservedQuantity;
|
||||
equipmentQuantities[equipmentId].reservations.push({
|
||||
...conflictInfo,
|
||||
quantity: quantity,
|
||||
});
|
||||
|
||||
if (equipmentQuantities[equipmentId].availableQuantity <= 0) {
|
||||
conflictingEquipmentIds.add(equipmentId);
|
||||
}
|
||||
} else {
|
||||
conflictingEquipmentIds.add(equipmentId);
|
||||
}
|
||||
|
||||
if (!conflictDetails[equipmentId]) {
|
||||
conflictDetails[equipmentId] = [];
|
||||
}
|
||||
conflictDetails[equipmentId].push({
|
||||
...conflictInfo,
|
||||
quantity: quantity,
|
||||
});
|
||||
}
|
||||
|
||||
for (const containerId of assignedContainers) {
|
||||
conflictingContainerIds.add(containerId);
|
||||
|
||||
if (!conflictDetails[containerId]) {
|
||||
conflictDetails[containerId] = [];
|
||||
}
|
||||
conflictDetails[containerId].push(conflictInfo);
|
||||
|
||||
const containerDoc = await db.collection("containers").doc(containerId).get();
|
||||
if (containerDoc.exists) {
|
||||
const containerData = containerDoc.data();
|
||||
const equipmentIds = containerData.equipmentIds || [];
|
||||
|
||||
for (const equipmentId of equipmentIds) {
|
||||
conflictingEquipmentIds.add(equipmentId);
|
||||
|
||||
if (!conflictDetails[equipmentId]) {
|
||||
conflictDetails[equipmentId] = [];
|
||||
}
|
||||
conflictDetails[equipmentId].push({
|
||||
...conflictInfo,
|
||||
viaContainer: containerId,
|
||||
viaContainerName: containerData.name || "Conteneur inconnu",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Found ${conflictingEquipmentIds.size} conflicting equipment(s) and ${conflictingContainerIds.size} conflicting container(s)`);
|
||||
|
||||
res.status(200).json({
|
||||
conflictingEquipmentIds: Array.from(conflictingEquipmentIds),
|
||||
conflictingContainerIds: Array.from(conflictingContainerIds),
|
||||
conflictDetails: conflictDetails,
|
||||
equipmentQuantities: equipmentQuantities,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error getting conflicting equipment IDs:", error);
|
||||
res.status(500).json({error: error.message || "Failed to get conflicting equipment IDs"});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Trouver des alternatives (même modèle) disponibles pour une période donnée
|
||||
*/
|
||||
exports.findAlternativeEquipment = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "view_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires view_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {model, startDate, endDate} = req.body.data;
|
||||
|
||||
if (!model || !startDate || !endDate) {
|
||||
res.status(400).json({error: "model, startDate and endDate are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const start = admin.firestore.Timestamp.fromDate(new Date(startDate));
|
||||
const end = admin.firestore.Timestamp.fromDate(new Date(endDate));
|
||||
|
||||
// Récupérer tous les équipements du même modèle
|
||||
const equipmentsSnapshot = await db.collection("equipments")
|
||||
.where("model", "==", model)
|
||||
.get();
|
||||
|
||||
// Récupérer tous les événements qui chevauchent la période
|
||||
const eventsSnapshot = await db.collection("events")
|
||||
.where("StartDateTime", "<=", end)
|
||||
.where("EndDateTime", ">=", start)
|
||||
.where("status", "!=", "CANCELLED")
|
||||
.get();
|
||||
|
||||
// Créer un set des équipements en conflit
|
||||
const conflictingEquipmentIds = new Set();
|
||||
eventsSnapshot.docs.forEach((doc) => {
|
||||
const eventData = doc.data();
|
||||
const assignedEquipment = eventData.assignedEquipment || [];
|
||||
assignedEquipment.forEach((eq) => conflictingEquipmentIds.add(eq.equipmentId));
|
||||
});
|
||||
|
||||
// Filtrer les équipements disponibles
|
||||
const alternatives = [];
|
||||
equipmentsSnapshot.docs.forEach((doc) => {
|
||||
const data = doc.data();
|
||||
if (!conflictingEquipmentIds.has(doc.id) && data.status === "available") {
|
||||
alternatives.push({
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(data, ["purchaseDate", "nextMaintenanceDate", "lastMaintenanceDate", "createdAt", "updatedAt"]),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json({alternatives});
|
||||
} catch (error) {
|
||||
logger.error("Error finding alternative equipment:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculer le statut réel d'un ou plusieurs équipements basé sur les événements en cours
|
||||
*/
|
||||
exports.calculateEquipmentStatuses = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "view_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires view_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {equipmentIds} = req.body.data;
|
||||
|
||||
if (!equipmentIds || !Array.isArray(equipmentIds)) {
|
||||
res.status(400).json({error: "equipmentIds array is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer tous les événements en cours (préparation complétée mais pas encore retournés)
|
||||
const eventsSnapshot = await db.collection("events")
|
||||
.where("status", "!=", "CANCELLED")
|
||||
.get();
|
||||
|
||||
const equipmentIdsInUse = new Set();
|
||||
const containerIdsInUse = new Set();
|
||||
|
||||
eventsSnapshot.docs.forEach((doc) => {
|
||||
const event = doc.data();
|
||||
|
||||
const isPrepared = event.preparationStatus === "completed" ||
|
||||
event.preparationStatus === "completedWithMissing";
|
||||
const isReturned = event.returnStatus === "completed" ||
|
||||
event.returnStatus === "completedWithMissing";
|
||||
|
||||
if (isPrepared && !isReturned) {
|
||||
// Ajouter les équipements directs
|
||||
const assignedEquipment = event.assignedEquipment || [];
|
||||
assignedEquipment.forEach((eq) => equipmentIdsInUse.add(eq.equipmentId));
|
||||
|
||||
// Ajouter les conteneurs
|
||||
const assignedContainers = event.assignedContainers || [];
|
||||
assignedContainers.forEach((containerId) => containerIdsInUse.add(containerId));
|
||||
}
|
||||
});
|
||||
|
||||
// Récupérer les équipements dans les conteneurs en cours d'utilisation
|
||||
if (containerIdsInUse.size > 0) {
|
||||
const containersSnapshot = await db.collection("containers")
|
||||
.where(admin.firestore.FieldPath.documentId(), "in", Array.from(containerIdsInUse))
|
||||
.get();
|
||||
|
||||
containersSnapshot.docs.forEach((doc) => {
|
||||
const containerData = doc.data();
|
||||
const equipmentList = containerData.equipment || [];
|
||||
equipmentList.forEach((eq) => equipmentIdsInUse.add(eq.equipmentId));
|
||||
});
|
||||
}
|
||||
|
||||
// Récupérer les données des équipements demandés
|
||||
const statuses = {};
|
||||
|
||||
for (const equipmentId of equipmentIds) {
|
||||
const equipmentDoc = await db.collection("equipments").doc(equipmentId).get();
|
||||
|
||||
if (!equipmentDoc.exists) {
|
||||
statuses[equipmentId] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
const equipmentData = equipmentDoc.data();
|
||||
let calculatedStatus = equipmentData.status;
|
||||
|
||||
// Si l'équipement est perdu ou HS, garder ce statut
|
||||
if (equipmentData.status === "lost" || equipmentData.status === "outOfService") {
|
||||
calculatedStatus = equipmentData.status;
|
||||
} else if (equipmentIdsInUse.has(equipmentId)) {
|
||||
calculatedStatus = "inUse";
|
||||
} else if (equipmentData.status === "maintenance" ||
|
||||
equipmentData.status === "rented") {
|
||||
calculatedStatus = equipmentData.status;
|
||||
} else {
|
||||
calculatedStatus = "available";
|
||||
}
|
||||
|
||||
statuses[equipmentId] = calculatedStatus;
|
||||
}
|
||||
|
||||
res.status(200).json({statuses});
|
||||
} catch (error) {
|
||||
logger.error("Error calculating equipment statuses:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Récupérer tous les événements en cours (pour le calcul de statuts)
|
||||
*/
|
||||
exports.getActiveEvents = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "view_events");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires view_events permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les événements en cours (préparation complétée mais pas encore retournés)
|
||||
const eventsSnapshot = await db.collection("events")
|
||||
.where("status", "!=", "CANCELLED")
|
||||
.get();
|
||||
|
||||
const activeEvents = [];
|
||||
|
||||
eventsSnapshot.docs.forEach((doc) => {
|
||||
const event = doc.data();
|
||||
|
||||
const isPrepared = event.preparationStatus === "completed" ||
|
||||
event.preparationStatus === "completedWithMissing";
|
||||
const isReturned = event.returnStatus === "completed" ||
|
||||
event.returnStatus === "completedWithMissing";
|
||||
|
||||
if (isPrepared && !isReturned) {
|
||||
activeEvents.push({
|
||||
id: doc.id,
|
||||
assignedEquipment: event.assignedEquipment || [],
|
||||
assignedContainers: event.assignedContainers || [],
|
||||
preparationStatus: event.preparationStatus,
|
||||
returnStatus: event.returnStatus,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json({events: activeEvents});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching active events:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,504 @@
|
||||
const admin = require("firebase-admin");
|
||||
const db = admin.firestore();
|
||||
const logger = require("firebase-functions/logger");
|
||||
const auth = require("../utils/auth");
|
||||
const helpers = require("../utils/helpers");
|
||||
|
||||
// Créer un container
|
||||
exports.createContainer = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const containerData = req.body.data;
|
||||
const containerId = containerData.id;
|
||||
|
||||
if (!containerId) {
|
||||
res.status(400).json({error: "Container ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const existingDoc = await db.collection("containers").doc(containerId).get();
|
||||
if (existingDoc.exists) {
|
||||
res.status(409).json({error: "Container ID already exists"});
|
||||
return;
|
||||
}
|
||||
|
||||
const dataToSave = helpers.deserializeTimestamps(containerData, ["createdAt", "updatedAt"]);
|
||||
|
||||
await db.collection("containers").doc(containerId).set(dataToSave);
|
||||
|
||||
res.status(201).json({id: containerId, message: "Container created successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error creating container:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Mettre à jour un container
|
||||
exports.updateContainer = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {containerId, data} = req.body.data;
|
||||
|
||||
if (!containerId) {
|
||||
res.status(400).json({error: "Container ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
delete data.id;
|
||||
data.updatedAt = admin.firestore.Timestamp.now();
|
||||
|
||||
await db.collection("containers").doc(containerId).update(data);
|
||||
|
||||
res.status(200).json({message: "Container updated successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error updating container:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Supprimer un container
|
||||
exports.deleteContainer = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {containerId} = req.body.data;
|
||||
|
||||
if (!containerId) {
|
||||
res.status(400).json({error: "Container ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer le container pour obtenir les équipements
|
||||
const containerDoc = await db.collection("containers").doc(containerId).get();
|
||||
if (containerDoc.exists) {
|
||||
const containerData = containerDoc.data();
|
||||
const equipmentIds = containerData.equipmentIds || [];
|
||||
|
||||
// Retirer le container des parentBoxIds de chaque équipement
|
||||
for (const equipmentId of equipmentIds) {
|
||||
try {
|
||||
const equipmentDoc = await db.collection("equipments").doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
const equipmentData = equipmentDoc.data();
|
||||
const parentBoxIds = (equipmentData.parentBoxIds || []).filter((boxId) => boxId !== containerId);
|
||||
await db.collection("equipments").doc(equipmentId).update({
|
||||
parentBoxIds: parentBoxIds,
|
||||
updatedAt: admin.firestore.Timestamp.now(),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error updating equipment ${equipmentId} when deleting container:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db.collection("containers").doc(containerId).delete();
|
||||
|
||||
res.status(200).json({message: "Container deleted successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error deleting container:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupérer les containers contenant un équipement
|
||||
exports.getContainersByEquipment = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasViewAccess = await auth.hasPermission(decodedToken.uid, "view_equipment");
|
||||
const hasManageAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasViewAccess && !hasManageAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires view_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {equipmentId} = req.body.data || {};
|
||||
|
||||
if (!equipmentId) {
|
||||
res.status(400).json({error: "equipmentId is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = await db.collection("containers")
|
||||
.where("equipmentIds", "array-contains", equipmentId)
|
||||
.get();
|
||||
|
||||
const containers = [];
|
||||
snapshot.forEach((doc) => {
|
||||
let data = {id: doc.id, ...doc.data()};
|
||||
data = helpers.serializeTimestamps(data);
|
||||
data = helpers.serializeReferences(data);
|
||||
containers.push(data);
|
||||
});
|
||||
|
||||
res.status(200).json({containers});
|
||||
} catch (error) {
|
||||
logger.error("Error getting containers by equipment:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupérer plusieurs containers par leurs IDs
|
||||
exports.getContainersByIds = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasViewAccess = await auth.hasPermission(decodedToken.uid, "view_equipment");
|
||||
const hasManageAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasViewAccess && !hasManageAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires view_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {containerIds} = req.body.data || {};
|
||||
|
||||
if (!containerIds || !Array.isArray(containerIds) || containerIds.length === 0) {
|
||||
res.status(400).json({error: "containerIds array is required and must not be empty"});
|
||||
return;
|
||||
}
|
||||
|
||||
if (containerIds.length > 100) {
|
||||
res.status(400).json({error: "Maximum 100 container IDs per request"});
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = containerIds.map((id) => db.collection("containers").doc(id).get());
|
||||
const docs = await Promise.all(promises);
|
||||
|
||||
const containers = [];
|
||||
for (const doc of docs) {
|
||||
if (doc.exists) {
|
||||
let data = {id: doc.id, ...doc.data()};
|
||||
data = helpers.serializeTimestamps(data);
|
||||
data = helpers.serializeReferences(data);
|
||||
containers.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({containers});
|
||||
} catch (error) {
|
||||
logger.error("Error getting containers by IDs:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Ajouter un équipement à un container
|
||||
exports.addEquipmentToContainer = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {containerId, equipmentId, userId} = req.body.data;
|
||||
|
||||
if (!containerId || !equipmentId) {
|
||||
res.status(400).json({error: "containerId and equipmentId are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const containerDoc = await db.collection("containers").doc(containerId).get();
|
||||
if (!containerDoc.exists) {
|
||||
res.status(404).json({success: false, message: "Container non trouvé"});
|
||||
return;
|
||||
}
|
||||
|
||||
const containerData = containerDoc.data();
|
||||
const equipmentIds = containerData.equipmentIds || [];
|
||||
|
||||
if (equipmentIds.includes(equipmentId)) {
|
||||
res.status(400).json({success: false, message: "Cet équipement est déjà dans ce container"});
|
||||
return;
|
||||
}
|
||||
|
||||
const equipmentDoc = await db.collection("equipments").doc(equipmentId).get();
|
||||
if (!equipmentDoc.exists) {
|
||||
res.status(404).json({success: false, message: "Équipement non trouvé"});
|
||||
return;
|
||||
}
|
||||
|
||||
const equipmentData = equipmentDoc.data();
|
||||
const parentBoxIds = equipmentData.parentBoxIds || [];
|
||||
|
||||
const warnings = [];
|
||||
if (parentBoxIds.length > 0) {
|
||||
const otherContainersPromises = parentBoxIds.map((boxId) =>
|
||||
db.collection("containers").doc(boxId).get(),
|
||||
);
|
||||
const otherContainersDocs = await Promise.all(otherContainersPromises);
|
||||
const otherNames = otherContainersDocs
|
||||
.filter((doc) => doc.exists)
|
||||
.map((doc) => doc.data().name);
|
||||
|
||||
if (otherNames.length > 0) {
|
||||
warnings.push(`Attention : cet équipement est également dans les boites suivants : ${otherNames.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
await db.collection("containers").doc(containerId).update({
|
||||
equipmentIds: [...equipmentIds, equipmentId],
|
||||
updatedAt: admin.firestore.Timestamp.now(),
|
||||
});
|
||||
|
||||
await db.collection("equipments").doc(equipmentId).update({
|
||||
parentBoxIds: [...parentBoxIds, containerId],
|
||||
updatedAt: admin.firestore.Timestamp.now(),
|
||||
});
|
||||
|
||||
const history = containerData.history || [];
|
||||
const historyEntry = {
|
||||
timestamp: admin.firestore.Timestamp.now(),
|
||||
action: "equipment_added",
|
||||
equipmentId: equipmentId,
|
||||
newValue: equipmentId,
|
||||
userId: userId || decodedToken.uid,
|
||||
};
|
||||
|
||||
const updatedHistory = [...history, historyEntry].slice(-100);
|
||||
|
||||
await db.collection("containers").doc(containerId).update({
|
||||
history: updatedHistory,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "Équipement ajouté avec succès",
|
||||
warnings: warnings.length > 0 ? warnings[0] : null,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error adding equipment to container:", error);
|
||||
res.status(500).json({success: false, message: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Retirer un équipement d'un container
|
||||
exports.removeEquipmentFromContainer = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {containerId, equipmentId, userId} = req.body.data;
|
||||
|
||||
if (!containerId || !equipmentId) {
|
||||
res.status(400).json({error: "containerId and equipmentId are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const containerDoc = await db.collection("containers").doc(containerId).get();
|
||||
if (!containerDoc.exists) {
|
||||
res.status(404).json({error: "Container non trouvé"});
|
||||
return;
|
||||
}
|
||||
|
||||
const containerData = containerDoc.data();
|
||||
const equipmentIds = containerData.equipmentIds || [];
|
||||
|
||||
const updatedEquipmentIds = equipmentIds.filter((id) => id !== equipmentId);
|
||||
|
||||
await db.collection("containers").doc(containerId).update({
|
||||
equipmentIds: updatedEquipmentIds,
|
||||
updatedAt: admin.firestore.Timestamp.now(),
|
||||
});
|
||||
|
||||
const equipmentDoc = await db.collection("equipments").doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
const equipmentData = equipmentDoc.data();
|
||||
const parentBoxIds = equipmentData.parentBoxIds || [];
|
||||
const updatedParentBoxIds = parentBoxIds.filter((id) => id !== containerId);
|
||||
|
||||
await db.collection("equipments").doc(equipmentId).update({
|
||||
parentBoxIds: updatedParentBoxIds,
|
||||
updatedAt: admin.firestore.Timestamp.now(),
|
||||
});
|
||||
}
|
||||
|
||||
const history = containerData.history || [];
|
||||
const historyEntry = {
|
||||
timestamp: admin.firestore.Timestamp.now(),
|
||||
action: "equipment_removed",
|
||||
equipmentId: equipmentId,
|
||||
previousValue: equipmentId,
|
||||
userId: userId || decodedToken.uid,
|
||||
};
|
||||
|
||||
const updatedHistory = [...history, historyEntry].slice(-100);
|
||||
|
||||
await db.collection("containers").doc(containerId).update({
|
||||
history: updatedHistory,
|
||||
});
|
||||
|
||||
res.status(200).json({success: true});
|
||||
} catch (error) {
|
||||
logger.error("Error removing equipment from container:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupérer les containers avec pagination et filtrage côté serveur
|
||||
exports.getContainersPaginated = 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 equipment permissions"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les paramètres de la requête
|
||||
const params = req.method === "GET" ? req.query : (req.body?.data || {});
|
||||
const limit = Math.min(parseInt(params.limit) || 20, 100);
|
||||
const startAfterId = params.startAfter || null;
|
||||
const type = params.type ? params.type.toUpperCase() : null;
|
||||
const status = params.status ? params.status.toUpperCase() : null;
|
||||
const searchQuery = params.searchQuery?.toLowerCase() || null;
|
||||
const category = params.category ? params.category.toUpperCase() : null;
|
||||
const sortBy = params.sortBy || "id";
|
||||
const sortOrder = params.sortOrder === "desc" ? "desc" : "asc";
|
||||
|
||||
logger.info(`[getContainersPaginated] Params: limit=${limit}, startAfter=${startAfterId}, type=${type}, status=${status}, category=${category}, search=${searchQuery}`);
|
||||
|
||||
let query = db.collection("containers");
|
||||
|
||||
const queryLimit = (searchQuery || category) ? Math.min(limit * 10, 200) : limit;
|
||||
|
||||
if (type) {
|
||||
query = query.where("type", "==", type);
|
||||
}
|
||||
if (status) {
|
||||
query = query.where("status", "==", status);
|
||||
}
|
||||
|
||||
if (sortBy === "id") {
|
||||
query = query.orderBy(admin.firestore.FieldPath.documentId(), sortOrder);
|
||||
} else {
|
||||
query = query.orderBy(sortBy, sortOrder);
|
||||
}
|
||||
|
||||
if (startAfterId) {
|
||||
const startAfterDoc = await db.collection("containers").doc(startAfterId).get();
|
||||
if (startAfterDoc.exists) {
|
||||
query = query.startAfter(startAfterDoc);
|
||||
}
|
||||
}
|
||||
|
||||
query = query.limit(queryLimit + 1);
|
||||
|
||||
const snapshot = await query.get();
|
||||
|
||||
const rawDocCount = snapshot.docs.length;
|
||||
const hasMoreDocs = rawDocCount > queryLimit;
|
||||
const docsToProcess = hasMoreDocs ? snapshot.docs.slice(0, queryLimit) : snapshot.docs;
|
||||
|
||||
let containers = docsToProcess.map((doc) => {
|
||||
const data = doc.data();
|
||||
return {
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(data, ["createdAt", "updatedAt"]),
|
||||
};
|
||||
});
|
||||
|
||||
const allEquipmentIds = new Set();
|
||||
containers.forEach((c) => {
|
||||
if (c.equipmentIds && Array.isArray(c.equipmentIds)) {
|
||||
c.equipmentIds.forEach((id) => allEquipmentIds.add(id));
|
||||
}
|
||||
});
|
||||
|
||||
const equipmentMap = new Map();
|
||||
if (allEquipmentIds.size > 0) {
|
||||
const equipmentIdArray = Array.from(allEquipmentIds);
|
||||
const batchSize = 30;
|
||||
|
||||
for (let i = 0; i < equipmentIdArray.length; i += batchSize) {
|
||||
const batch = equipmentIdArray.slice(i, i + batchSize);
|
||||
const equipmentSnapshot = await db.collection("equipments")
|
||||
.where(admin.firestore.FieldPath.documentId(), "in", batch)
|
||||
.get();
|
||||
|
||||
equipmentSnapshot.docs.forEach((doc) => {
|
||||
const equipmentData = doc.data();
|
||||
equipmentMap.set(doc.id, {
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(equipmentData),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
containers = containers.map((container) => ({
|
||||
...container,
|
||||
equipment: (container.equipmentIds || [])
|
||||
.map((eqId) => equipmentMap.get(eqId))
|
||||
.filter((eq) => eq !== undefined),
|
||||
}));
|
||||
|
||||
if (category) {
|
||||
containers = containers.filter((c) => {
|
||||
return c.equipment.some((eq) => eq.category === category);
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery) {
|
||||
containers = containers.filter((c) => {
|
||||
const searchableText = [
|
||||
c.name || "",
|
||||
c.id || "",
|
||||
...(c.equipment || []).map((eq) => eq.name || ""),
|
||||
].join(" ").toLowerCase();
|
||||
return searchableText.includes(searchQuery);
|
||||
});
|
||||
}
|
||||
|
||||
const limitedContainers = containers.slice(0, limit);
|
||||
const lastVisible = limitedContainers.length > 0 ? limitedContainers[limitedContainers.length - 1].id : null;
|
||||
|
||||
const totalEquipmentCount = limitedContainers.reduce((sum, c) => sum + (c.equipment?.length || 0), 0);
|
||||
logger.info(`[getContainersPaginated] Returning ${limitedContainers.length} containers with ${totalEquipmentCount} total equipment(s)`);
|
||||
|
||||
limitedContainers.forEach((c) => {
|
||||
logger.info(` - Container ${c.id}: ${c.equipment?.length || 0} equipment(s)`);
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
containers: limitedContainers,
|
||||
hasMore: containers.length > limit || hasMoreDocs,
|
||||
lastVisible,
|
||||
total: limitedContainers.length,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching paginated containers:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,668 @@
|
||||
const admin = require("firebase-admin");
|
||||
const db = admin.firestore();
|
||||
const logger = require("firebase-functions/logger");
|
||||
const auth = require("../utils/auth");
|
||||
const helpers = require("../utils/helpers");
|
||||
|
||||
// Créer un équipement (admin ou manage_equipment)
|
||||
exports.createEquipment = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const equipmentData = req.body.data;
|
||||
const equipmentId = equipmentData.id;
|
||||
|
||||
if (!equipmentId) {
|
||||
res.status(400).json({error: "Equipment ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier unicité de l'ID
|
||||
const existingDoc = await db.collection("equipments").doc(equipmentId).get();
|
||||
if (existingDoc.exists) {
|
||||
res.status(409).json({error: "Equipment ID already exists"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Convertir les timestamps
|
||||
const dataToSave = helpers.deserializeTimestamps(equipmentData, [
|
||||
"createdAt", "updatedAt", "purchaseDate", "lastMaintenanceDate", "nextMaintenanceDate",
|
||||
]);
|
||||
|
||||
await db.collection("equipments").doc(equipmentId).set(dataToSave);
|
||||
|
||||
res.status(201).json({id: equipmentId, message: "Equipment created successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error creating equipment:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Mettre à jour un équipement
|
||||
exports.updateEquipment = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {equipmentId, data} = req.body.data;
|
||||
|
||||
if (!equipmentId) {
|
||||
res.status(400).json({error: "Equipment ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data || typeof data !== "object" || Object.keys(data).length === 0) {
|
||||
res.status(400).json({error: "Update data is required and must be a non-empty object"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Empêcher la modification de l'ID
|
||||
delete data.id;
|
||||
|
||||
// Ajouter updatedAt
|
||||
data.updatedAt = admin.firestore.Timestamp.now();
|
||||
|
||||
const dataToSave = helpers.deserializeTimestamps(data, [
|
||||
"purchaseDate", "lastMaintenanceDate", "nextMaintenanceDate",
|
||||
]);
|
||||
|
||||
await db.collection("equipments").doc(equipmentId).update(dataToSave);
|
||||
|
||||
res.status(200).json({message: "Equipment updated successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error updating equipment:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Supprimer un équipement
|
||||
exports.deleteEquipment = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {equipmentId, forceDelete = false} = req.body.data;
|
||||
|
||||
if (!equipmentId) {
|
||||
res.status(400).json({error: "Equipment ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier si l'équipement est utilisé dans des événements à venir
|
||||
const eventsSnapshot = await db.collection("events")
|
||||
.where("status", "!=", "CANCELLED")
|
||||
.get();
|
||||
|
||||
const now = new Date();
|
||||
const upcomingEvents = [];
|
||||
|
||||
for (const eventDoc of eventsSnapshot.docs) {
|
||||
const eventData = eventDoc.data();
|
||||
const assignedEquipment = eventData.assignedEquipment || [];
|
||||
|
||||
if (!assignedEquipment.some((eq) => eq.equipmentId === equipmentId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let eventStart = null;
|
||||
if (eventData.StartDateTime) {
|
||||
eventStart = eventData.StartDateTime.toDate ?
|
||||
eventData.StartDateTime.toDate() :
|
||||
new Date(eventData.StartDateTime);
|
||||
}
|
||||
|
||||
if (eventStart && eventStart > now) {
|
||||
upcomingEvents.push({
|
||||
eventId: eventDoc.id,
|
||||
eventName: eventData.Name || "",
|
||||
startDate: eventStart.toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (upcomingEvents.length > 0 && !forceDelete) {
|
||||
res.status(409).json({
|
||||
error: "FUTURE_EVENT_ASSIGNMENT: Cannot delete equipment because it is assigned to upcoming events",
|
||||
upcomingEvents,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("equipments").doc(equipmentId).delete();
|
||||
|
||||
res.status(200).json({message: "Equipment deleted successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error deleting equipment:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupérer un équipement par ID
|
||||
exports.getEquipment = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasViewAccess = await auth.hasPermission(decodedToken.uid, "view_equipment");
|
||||
const hasManageAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasViewAccess && !hasManageAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires view_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {equipmentId} = req.body.data || req.query;
|
||||
|
||||
if (!equipmentId) {
|
||||
res.status(400).json({error: "Equipment ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = await db.collection("equipments").doc(equipmentId).get();
|
||||
|
||||
if (!doc.exists) {
|
||||
res.status(404).json({error: "Equipment not found"});
|
||||
return;
|
||||
}
|
||||
|
||||
let data = {id: doc.id, ...doc.data()};
|
||||
data = helpers.serializeTimestamps(data);
|
||||
data = helpers.serializeReferences(data);
|
||||
|
||||
// Masquer les prix si pas de permission manage_equipment
|
||||
data = helpers.maskSensitiveFields(data, hasManageAccess);
|
||||
|
||||
res.status(200).json({equipment: data});
|
||||
} catch (error) {
|
||||
logger.error("Error getting equipment:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupérer plusieurs équipements par leurs IDs
|
||||
exports.getEquipmentsByIds = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasViewAccess = await auth.hasPermission(decodedToken.uid, "view_equipment");
|
||||
const hasManageAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasViewAccess && !hasManageAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires view_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {equipmentIds} = req.body.data || {};
|
||||
|
||||
if (!equipmentIds || !Array.isArray(equipmentIds) || equipmentIds.length === 0) {
|
||||
res.status(400).json({error: "equipmentIds array is required and must not be empty"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Limiter à 100 équipements max par requête
|
||||
if (equipmentIds.length > 100) {
|
||||
res.status(400).json({error: "Maximum 100 equipment IDs per request"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer tous les documents en parallèle
|
||||
const promises = equipmentIds.map((id) => db.collection("equipments").doc(id).get());
|
||||
const docs = await Promise.all(promises);
|
||||
|
||||
const equipments = [];
|
||||
for (const doc of docs) {
|
||||
if (doc.exists) {
|
||||
let data = {id: doc.id, ...doc.data()};
|
||||
data = helpers.serializeTimestamps(data);
|
||||
data = helpers.serializeReferences(data);
|
||||
// Masquer les prix si pas de permission manage_equipment
|
||||
data = helpers.maskSensitiveFields(data, hasManageAccess);
|
||||
equipments.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({equipments});
|
||||
} catch (error) {
|
||||
logger.error("Error getting equipments by IDs:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Mettre à jour uniquement le statut d'un équipement
|
||||
exports.updateEquipmentStatusOnly = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const {equipmentId, status, availableQuantity} = req.body.data;
|
||||
|
||||
if (!equipmentId) {
|
||||
res.status(400).json({error: "Equipment ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier les permissions
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = {updatedAt: admin.firestore.Timestamp.now()};
|
||||
if (status) updateData.status = status;
|
||||
if (availableQuantity !== undefined) updateData.availableQuantity = availableQuantity;
|
||||
|
||||
await db.collection("equipments").doc(equipmentId).update(updateData);
|
||||
|
||||
res.status(200).json({message: "Equipment status updated successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error updating equipment status:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Mettre à jour le statut de plusieurs équipements (pour préparation/retour)
|
||||
exports.updateEquipmentStatus = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const {eventId, updates} = req.body.data;
|
||||
|
||||
if (!eventId || !updates || !Array.isArray(updates)) {
|
||||
res.status(400).json({error: "Event ID and updates array are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est assigné à l'événement ou est admin
|
||||
const isAssigned = await auth.isAssignedToEvent(decodedToken.uid, eventId);
|
||||
const isAdminUser = await auth.isAdmin(decodedToken.uid);
|
||||
|
||||
if (!isAssigned && !isAdminUser) {
|
||||
res.status(403).json({error: "Forbidden: Not assigned to this event"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Batch update
|
||||
const batch = db.batch();
|
||||
|
||||
for (const update of updates) {
|
||||
const {equipmentId, status} = update;
|
||||
if (equipmentId && status) {
|
||||
const equipmentRef = db.collection("equipments").doc(equipmentId);
|
||||
batch.update(equipmentRef, {status});
|
||||
}
|
||||
}
|
||||
|
||||
await batch.commit();
|
||||
|
||||
res.status(200).json({message: "Equipment statuses updated successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error updating equipment statuses:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupère les équipements avec pagination et filtrage côté serveur
|
||||
exports.getEquipmentsPaginated = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
|
||||
// Vérifier les permissions
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
const canView = await auth.hasPermission(decodedToken.uid, "view_equipment");
|
||||
|
||||
if (!canManage && !canView) {
|
||||
res.status(403).json({error: "Forbidden: Requires equipment permissions"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les paramètres de la requête
|
||||
const params = req.method === "GET" ? req.query : (req.body?.data || {});
|
||||
const limit = Math.min(parseInt(params.limit) || 20, 100);
|
||||
const startAfterId = params.startAfter || null;
|
||||
const category = params.category ? params.category.toUpperCase() : null;
|
||||
const status = params.status ? params.status.toUpperCase() : null;
|
||||
const rawSearchQuery = typeof params.searchQuery === "string" ? params.searchQuery.trim() : "";
|
||||
const searchQuery = rawSearchQuery ? rawSearchQuery.toLowerCase() : null;
|
||||
const compactSearchQuery = searchQuery ? searchQuery.replace(/[\s_-]+/g, "") : null;
|
||||
const sortBy = params.sortBy || "id";
|
||||
const sortOrder = params.sortOrder === "desc" ? "desc" : "asc";
|
||||
|
||||
logger.info(`[getEquipmentsPaginated] Params: limit=${limit}, startAfter=${startAfterId}, category=${category}, status=${status}, search=${searchQuery}`);
|
||||
|
||||
// Fast-path pour une recherche d'ID exact
|
||||
if (searchQuery && !startAfterId) {
|
||||
const exactIdCandidates = Array.from(new Set([
|
||||
rawSearchQuery,
|
||||
rawSearchQuery.toUpperCase(),
|
||||
rawSearchQuery.toLowerCase(),
|
||||
].filter(Boolean)));
|
||||
|
||||
for (const candidateId of exactIdCandidates) {
|
||||
const exactDoc = await db.collection("equipments").doc(candidateId).get();
|
||||
if (!exactDoc.exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const exactData = exactDoc.data() || {};
|
||||
const matchesCategory = !category || exactData.category === category;
|
||||
const matchesStatus = !status || exactData.status === status;
|
||||
if (!matchesCategory || !matchesStatus) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!canManage) {
|
||||
delete exactData.purchasePrice;
|
||||
delete exactData.rentalPrice;
|
||||
}
|
||||
|
||||
const exactEquipment = {
|
||||
...helpers.serializeTimestamps(exactData, ["purchaseDate", "nextMaintenanceDate", "lastMaintenanceDate", "createdAt", "updatedAt"]),
|
||||
id: exactDoc.id,
|
||||
};
|
||||
|
||||
logger.info(`[getEquipmentsPaginated] Exact ID hit for ${exactDoc.id}`);
|
||||
res.status(200).json({
|
||||
equipments: [exactEquipment],
|
||||
hasMore: false,
|
||||
lastVisible: exactDoc.id,
|
||||
total: 1,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Compatibilité legacy
|
||||
for (const legacyId of exactIdCandidates) {
|
||||
let legacyIdQuery = db.collection("equipments").where("id", "==", legacyId);
|
||||
if (category) {
|
||||
legacyIdQuery = legacyIdQuery.where("category", "==", category);
|
||||
}
|
||||
if (status) {
|
||||
legacyIdQuery = legacyIdQuery.where("status", "==", status);
|
||||
}
|
||||
|
||||
const legacySnapshot = await legacyIdQuery.limit(1).get();
|
||||
if (legacySnapshot.empty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const exactDoc = legacySnapshot.docs[0];
|
||||
const exactData = exactDoc.data() || {};
|
||||
|
||||
if (!canManage) {
|
||||
delete exactData.purchasePrice;
|
||||
delete exactData.rentalPrice;
|
||||
}
|
||||
|
||||
const exactEquipment = {
|
||||
...helpers.serializeTimestamps(exactData, ["purchaseDate", "nextMaintenanceDate", "lastMaintenanceDate", "createdAt", "updatedAt"]),
|
||||
id: exactDoc.id,
|
||||
};
|
||||
|
||||
logger.info(`[getEquipmentsPaginated] Exact legacy ID hit for ${legacyId} -> ${exactDoc.id}`);
|
||||
res.status(200).json({
|
||||
equipments: [exactEquipment],
|
||||
hasMore: false,
|
||||
lastVisible: exactDoc.id,
|
||||
total: 1,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Construire la requête Firestore
|
||||
let query = db.collection("equipments");
|
||||
|
||||
if (category) {
|
||||
query = query.where("category", "==", category);
|
||||
}
|
||||
if (status) {
|
||||
query = query.where("status", "==", status);
|
||||
}
|
||||
|
||||
if (sortBy === "id") {
|
||||
query = query.orderBy(admin.firestore.FieldPath.documentId(), sortOrder);
|
||||
} else {
|
||||
query = query.orderBy(sortBy, sortOrder);
|
||||
}
|
||||
|
||||
if (startAfterId) {
|
||||
const startAfterDoc = await db.collection("equipments").doc(startAfterId).get();
|
||||
if (startAfterDoc.exists) {
|
||||
query = query.startAfter(startAfterDoc);
|
||||
}
|
||||
}
|
||||
|
||||
const timestampFields = ["purchaseDate", "nextMaintenanceDate", "lastMaintenanceDate", "createdAt", "updatedAt"];
|
||||
|
||||
const mapEquipmentDoc = (doc) => {
|
||||
const data = {...(doc.data() || {})};
|
||||
|
||||
if (!canManage) {
|
||||
delete data.purchasePrice;
|
||||
delete data.rentalPrice;
|
||||
}
|
||||
|
||||
const legacyId = typeof data.id === "string" ? data.id : "";
|
||||
|
||||
return {
|
||||
...helpers.serializeTimestamps(data, timestampFields),
|
||||
id: doc.id,
|
||||
_legacyId: legacyId,
|
||||
};
|
||||
};
|
||||
|
||||
const matchesSearchQuery = (equipment) => {
|
||||
const searchableText = [
|
||||
equipment.name || "",
|
||||
equipment.id || "",
|
||||
equipment._legacyId || "",
|
||||
equipment.model || "",
|
||||
equipment.brand || "",
|
||||
equipment.subCategory || "",
|
||||
].join(" ").toLowerCase();
|
||||
|
||||
if (searchableText.includes(searchQuery)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!compactSearchQuery) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const compactSearchableText = searchableText.replace(/[\s_-]+/g, "");
|
||||
return compactSearchableText.includes(compactSearchQuery);
|
||||
};
|
||||
|
||||
if (!searchQuery) {
|
||||
const snapshot = await query.limit(limit + 1).get();
|
||||
const rawDocCount = snapshot.docs.length;
|
||||
const hasMoreDocs = rawDocCount > limit;
|
||||
const docsToProcess = hasMoreDocs ? snapshot.docs.slice(0, limit) : snapshot.docs;
|
||||
|
||||
const limitedEquipments = docsToProcess
|
||||
.map(mapEquipmentDoc)
|
||||
.map(({_legacyId, ...equipment}) => equipment);
|
||||
const lastVisible = limitedEquipments.length > 0 ? limitedEquipments[limitedEquipments.length - 1].id : null;
|
||||
|
||||
logger.info(`[getEquipmentsPaginated] Firestore returned ${rawDocCount} docs, hasMore=${hasMoreDocs}`);
|
||||
logger.info(`[getEquipmentsPaginated] Returning ${limitedEquipments.length} equipments, hasMore=${hasMoreDocs}`);
|
||||
|
||||
res.status(200).json({
|
||||
equipments: limitedEquipments,
|
||||
hasMore: hasMoreDocs,
|
||||
lastVisible,
|
||||
total: limitedEquipments.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const searchBatchSize = Math.min(Math.max(limit * 10, limit), 200);
|
||||
const matchedEquipments = [];
|
||||
let scannedDocuments = 0;
|
||||
let searchQueryRef = query;
|
||||
let hasMoreMatches = false;
|
||||
let hasMoreDocsToScan = true;
|
||||
|
||||
while (hasMoreDocsToScan && !hasMoreMatches) {
|
||||
const snapshot = await searchQueryRef.limit(searchBatchSize).get();
|
||||
|
||||
if (snapshot.empty) {
|
||||
hasMoreDocsToScan = false;
|
||||
break;
|
||||
}
|
||||
|
||||
scannedDocuments += snapshot.docs.length;
|
||||
|
||||
for (const doc of snapshot.docs) {
|
||||
const equipment = mapEquipmentDoc(doc);
|
||||
if (!matchesSearchQuery(equipment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matchedEquipments.push(equipment);
|
||||
if (matchedEquipments.length > limit) {
|
||||
hasMoreMatches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMoreMatches) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (snapshot.docs.length < searchBatchSize) {
|
||||
hasMoreDocsToScan = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const lastDocInBatch = snapshot.docs[snapshot.docs.length - 1];
|
||||
searchQueryRef = query.startAfter(lastDocInBatch);
|
||||
}
|
||||
|
||||
const limitedEquipments = matchedEquipments
|
||||
.slice(0, limit)
|
||||
.map(({_legacyId, ...equipment}) => equipment);
|
||||
const lastVisible = limitedEquipments.length > 0 ? limitedEquipments[limitedEquipments.length - 1].id : null;
|
||||
|
||||
logger.info(`[getEquipmentsPaginated] Search scan read ${scannedDocuments} docs and found ${matchedEquipments.length} matches`);
|
||||
logger.info(`[getEquipmentsPaginated] Returning ${limitedEquipments.length} equipments, hasMore=${hasMoreMatches}`);
|
||||
|
||||
res.status(200).json({
|
||||
equipments: limitedEquipments,
|
||||
hasMore: hasMoreMatches,
|
||||
lastVisible,
|
||||
total: limitedEquipments.length,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching paginated equipments:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Recherche rapide d'équipements et containers pour l'autocomplétion
|
||||
exports.quickSearch = 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)
|
||||
.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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const limitedResults = results
|
||||
.sort((a, b) => {
|
||||
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});
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,328 @@
|
||||
const admin = require("firebase-admin");
|
||||
const db = admin.firestore();
|
||||
const logger = require("firebase-functions/logger");
|
||||
const auth = require("../utils/auth");
|
||||
const helpers = require("../utils/helpers");
|
||||
|
||||
// Créer une maintenance
|
||||
exports.createMaintenance = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_maintenances");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_maintenances permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const maintenanceData = req.body.data;
|
||||
|
||||
const dataToSave = helpers.deserializeTimestamps(maintenanceData, [
|
||||
"scheduledDate", "completedDate", "createdAt", "updatedAt",
|
||||
]);
|
||||
|
||||
const docRef = await db.collection("maintenances").add(dataToSave);
|
||||
const maintenanceId = docRef.id;
|
||||
|
||||
if (maintenanceData.equipmentIds && Array.isArray(maintenanceData.equipmentIds)) {
|
||||
for (const equipmentId of maintenanceData.equipmentIds) {
|
||||
try {
|
||||
const equipmentDoc = await db.collection("equipments").doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
const equipmentData = equipmentDoc.data();
|
||||
const maintenanceIds = equipmentData.maintenanceIds || [];
|
||||
if (!maintenanceIds.includes(maintenanceId)) {
|
||||
maintenanceIds.push(maintenanceId);
|
||||
await db.collection("equipments").doc(equipmentId).update({
|
||||
maintenanceIds: maintenanceIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (maintenanceData.scheduledDate) {
|
||||
const scheduledDate = maintenanceData.scheduledDate.toDate ?
|
||||
maintenanceData.scheduledDate.toDate() :
|
||||
new Date(maintenanceData.scheduledDate);
|
||||
const sevenDaysFromNow = new Date();
|
||||
sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7);
|
||||
|
||||
if (scheduledDate <= sevenDaysFromNow) {
|
||||
const existingAlerts = await db.collection("alerts")
|
||||
.where("equipmentId", "==", equipmentId)
|
||||
.where("type", "==", "maintenanceDue")
|
||||
.where("isRead", "==", false)
|
||||
.get();
|
||||
|
||||
let alertExists = false;
|
||||
for (const alertDoc of existingAlerts.docs) {
|
||||
const alertData = alertDoc.data();
|
||||
if (alertData.message && alertData.message.includes(maintenanceData.name || "")) {
|
||||
alertExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alertExists) {
|
||||
const equipmentName = equipmentDoc.exists ?
|
||||
(equipmentDoc.data().name || equipmentId) :
|
||||
equipmentId;
|
||||
|
||||
const daysUntil = Math.ceil((scheduledDate - new Date()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
await db.collection("alerts").add({
|
||||
type: "maintenanceDue",
|
||||
message: `Maintenance "${maintenanceData.name || "Sans nom"}" prévue dans ${daysUntil} jour(s) pour ${equipmentName}`,
|
||||
equipmentId: equipmentId,
|
||||
createdAt: admin.firestore.Timestamp.now(),
|
||||
isRead: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error updating equipment ${equipmentId} for maintenance:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({id: maintenanceId, message: "Maintenance created successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error creating maintenance:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Mettre à jour une maintenance
|
||||
exports.updateMaintenance = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_maintenances");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_maintenances permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {maintenanceId, data} = req.body.data;
|
||||
|
||||
if (!maintenanceId) {
|
||||
res.status(400).json({error: "Maintenance ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
delete data.id;
|
||||
data.updatedAt = admin.firestore.Timestamp.now();
|
||||
|
||||
const dataToSave = helpers.deserializeTimestamps(data, [
|
||||
"scheduledDate", "completedDate",
|
||||
]);
|
||||
|
||||
await db.collection("maintenances").doc(maintenanceId).update(dataToSave);
|
||||
|
||||
res.status(200).json({message: "Maintenance updated successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error updating maintenance:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupérer toutes les maintenances
|
||||
exports.getMaintenances = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const {equipmentId} = req.body.data || {};
|
||||
|
||||
const canView = await auth.hasPermission(decodedToken.uid, "view_equipment");
|
||||
|
||||
if (!canView) {
|
||||
res.status(403).json({error: "Forbidden: Requires equipment permissions"});
|
||||
return;
|
||||
}
|
||||
|
||||
let query = db.collection("maintenances");
|
||||
|
||||
if (equipmentId) {
|
||||
query = query.where("equipmentIds", "array-contains", equipmentId);
|
||||
}
|
||||
|
||||
const snapshot = await query.get();
|
||||
const maintenances = snapshot.docs.map((doc) => {
|
||||
const data = doc.data();
|
||||
return {
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(data, ["scheduledDate", "completedDate", "createdAt", "updatedAt"]),
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json({maintenances});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching maintenances:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Supprimer une maintenance
|
||||
exports.deleteMaintenance = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
if (!canManage) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const maintenanceId = req.body.data?.maintenanceId;
|
||||
if (!maintenanceId) {
|
||||
res.status(400).json({error: "maintenanceId is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const maintenanceDoc = await db.collection("maintenances").doc(maintenanceId).get();
|
||||
if (maintenanceDoc.exists) {
|
||||
const maintenance = maintenanceDoc.data();
|
||||
|
||||
if (maintenance.equipmentIds) {
|
||||
for (const equipmentId of maintenance.equipmentIds) {
|
||||
const equipmentDoc = await db.collection("equipments").doc(equipmentId).get();
|
||||
if (equipmentDoc.exists) {
|
||||
const equipmentData = equipmentDoc.data();
|
||||
const maintenanceIds = (equipmentData.maintenanceIds || []).filter((id) => id !== maintenanceId);
|
||||
await db.collection("equipments").doc(equipmentId).update({maintenanceIds});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db.collection("maintenances").doc(maintenanceId).delete();
|
||||
|
||||
res.status(200).json({success: true});
|
||||
} catch (error) {
|
||||
logger.error("Error deleting maintenance:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Vérifier les maintenances à venir et créer des alertes
|
||||
*/
|
||||
exports.checkUpcomingMaintenances = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const sevenDaysFromNow = new Date();
|
||||
sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7);
|
||||
|
||||
const now = admin.firestore.Timestamp.now();
|
||||
const sevenDaysTimestamp = admin.firestore.Timestamp.fromDate(sevenDaysFromNow);
|
||||
|
||||
// Récupérer les maintenances planifiées dans les 7 prochains jours
|
||||
const maintenancesSnapshot = await db.collection("maintenances")
|
||||
.where("scheduledDate", "<=", sevenDaysTimestamp)
|
||||
.where("scheduledDate", ">=", now)
|
||||
.get();
|
||||
|
||||
const alertsCreated = [];
|
||||
|
||||
for (const doc of maintenancesSnapshot.docs) {
|
||||
const maintenance = doc.data();
|
||||
|
||||
// Vérifier si une alerte existe déjà pour cette maintenance
|
||||
const existingAlertSnapshot = await db.collection("alerts")
|
||||
.where("type", "==", "MAINTENANCE_DUE")
|
||||
.where("relatedMaintenanceId", "==", doc.id)
|
||||
.get();
|
||||
|
||||
if (existingAlertSnapshot.empty) {
|
||||
// Créer une nouvelle alerte
|
||||
const alertData = {
|
||||
type: "MAINTENANCE_DUE",
|
||||
title: `Maintenance à venir`,
|
||||
message: `Une maintenance est prévue pour ${maintenance.equipmentIds?.length || 0} équipement(s)`,
|
||||
severity: "MEDIUM",
|
||||
isRead: false,
|
||||
relatedMaintenanceId: doc.id,
|
||||
createdAt: admin.firestore.Timestamp.now(),
|
||||
};
|
||||
|
||||
const alertRef = await db.collection("alerts").add(alertData);
|
||||
alertsCreated.push({id: alertRef.id, ...alertData});
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
alertsCreated: alertsCreated.length,
|
||||
alerts: alertsCreated,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error checking upcoming maintenances:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compléter une maintenance
|
||||
*/
|
||||
exports.completeMaintenance = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const hasAccess = await auth.hasPermission(decodedToken.uid, "manage_equipment");
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({error: "Forbidden: Requires manage_equipment permission"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {maintenanceId, performedBy, cost} = req.body.data;
|
||||
|
||||
if (!maintenanceId) {
|
||||
res.status(400).json({error: "maintenanceId is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const now = admin.firestore.Timestamp.now();
|
||||
const updateData = {
|
||||
completedDate: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
if (performedBy) {
|
||||
updateData.performedBy = performedBy;
|
||||
}
|
||||
|
||||
if (cost !== undefined && cost !== null) {
|
||||
updateData.cost = cost;
|
||||
}
|
||||
|
||||
// Mettre à jour la maintenance
|
||||
await db.collection("maintenances").doc(maintenanceId).update(updateData);
|
||||
|
||||
// Récupérer la maintenance pour mettre à jour les équipements
|
||||
const maintenanceDoc = await db.collection("maintenances").doc(maintenanceId).get();
|
||||
const maintenanceData = maintenanceDoc.data();
|
||||
|
||||
// Mettre à jour la date de dernière maintenance des équipements
|
||||
if (maintenanceData && maintenanceData.equipmentIds) {
|
||||
const updatePromises = maintenanceData.equipmentIds.map((equipmentId) =>
|
||||
db.collection("equipments").doc(equipmentId).update({
|
||||
lastMaintenanceDate: now,
|
||||
updatedAt: now,
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
}
|
||||
|
||||
res.status(200).json({success: true});
|
||||
} catch (error) {
|
||||
logger.error("Error completing maintenance:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,263 @@
|
||||
const admin = require("firebase-admin");
|
||||
const db = admin.firestore();
|
||||
const logger = require("firebase-functions/logger");
|
||||
const auth = require("../utils/auth");
|
||||
const helpers = require("../utils/helpers");
|
||||
|
||||
// Récupérer toutes les options (public pour utilisateurs authentifiés)
|
||||
exports.getOptions = async (req, res) => {
|
||||
try {
|
||||
await auth.authenticateUser(req);
|
||||
|
||||
const snapshot = await db.collection("options").get();
|
||||
const options = snapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(doc.data()),
|
||||
}));
|
||||
|
||||
res.status(200).json({options});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching options:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupérer tous les types d'événements (public pour utilisateurs authentifiés)
|
||||
exports.getEventTypes = async (req, res) => {
|
||||
try {
|
||||
await auth.authenticateUser(req);
|
||||
|
||||
const snapshot = await db.collection("eventTypes").get();
|
||||
const eventTypes = snapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(doc.data()),
|
||||
}));
|
||||
|
||||
res.status(200).json({eventTypes});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching event types:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Créer une option
|
||||
exports.createOption = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const isAdminUser = await auth.isAdmin(decodedToken.uid);
|
||||
|
||||
if (!isAdminUser) {
|
||||
res.status(403).json({error: "Forbidden: Admin access required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const optionData = req.body.data;
|
||||
const optionId = optionData.id;
|
||||
|
||||
if (!optionId) {
|
||||
res.status(400).json({error: "Option ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("options").doc(optionId).set(optionData);
|
||||
|
||||
res.status(201).json({id: optionId, message: "Option created successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error creating option:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Mettre à jour une option
|
||||
exports.updateOption = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const isAdminUser = await auth.isAdmin(decodedToken.uid);
|
||||
|
||||
if (!isAdminUser) {
|
||||
res.status(403).json({error: "Forbidden: Admin access required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {optionId, data} = req.body.data;
|
||||
|
||||
if (!optionId) {
|
||||
res.status(400).json({error: "Option ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
delete data.id;
|
||||
|
||||
await db.collection("options").doc(optionId).update(data);
|
||||
|
||||
res.status(200).json({message: "Option updated successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error updating option:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Supprimer une option
|
||||
exports.deleteOption = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const isAdminUser = await auth.isAdmin(decodedToken.uid);
|
||||
|
||||
if (!isAdminUser) {
|
||||
res.status(403).json({error: "Forbidden: Admin access required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {optionId} = req.body.data;
|
||||
|
||||
if (!optionId) {
|
||||
res.status(400).json({error: "Option ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("options").doc(optionId).delete();
|
||||
|
||||
res.status(200).json({message: "Option deleted successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error deleting option:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Créer un type d'événement
|
||||
exports.createEventType = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const isAdmin = await auth.hasPermission(decodedToken.uid, "edit_data");
|
||||
|
||||
if (!isAdmin) {
|
||||
res.status(403).json({error: "Forbidden: Admin permission required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {name, defaultPrice} = req.body.data;
|
||||
|
||||
if (!name || defaultPrice === undefined) {
|
||||
res.status(400).json({error: "Name and defaultPrice are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const existingSnapshot = await db.collection("eventTypes")
|
||||
.where("name", "==", name)
|
||||
.get();
|
||||
|
||||
if (!existingSnapshot.empty) {
|
||||
res.status(409).json({error: "Event type name already exists"});
|
||||
return;
|
||||
}
|
||||
|
||||
const eventTypeData = {
|
||||
name,
|
||||
defaultPrice,
|
||||
createdAt: admin.firestore.Timestamp.now(),
|
||||
};
|
||||
|
||||
const docRef = await db.collection("eventTypes").add(eventTypeData);
|
||||
|
||||
res.status(201).json({id: docRef.id, message: "Event type created successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error creating event type:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Mettre à jour un type d'événement
|
||||
exports.updateEventType = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const isAdmin = await auth.hasPermission(decodedToken.uid, "edit_data");
|
||||
|
||||
if (!isAdmin) {
|
||||
res.status(403).json({error: "Forbidden: Admin permission required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {eventTypeId, name, defaultPrice} = req.body.data;
|
||||
|
||||
if (!eventTypeId) {
|
||||
res.status(400).json({error: "Event type ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const docRef = db.collection("eventTypes").doc(eventTypeId);
|
||||
const doc = await docRef.get();
|
||||
|
||||
if (!doc.exists) {
|
||||
res.status(404).json({error: "Event type not found"});
|
||||
return;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
const existingSnapshot = await db.collection("eventTypes")
|
||||
.where("name", "==", name)
|
||||
.get();
|
||||
|
||||
const hasDuplicate = existingSnapshot.docs.some((d) => d.id !== eventTypeId);
|
||||
if (hasDuplicate) {
|
||||
res.status(409).json({error: "Event type name already exists"});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (name) updateData.name = name;
|
||||
if (defaultPrice !== undefined) updateData.defaultPrice = defaultPrice;
|
||||
|
||||
await docRef.update(updateData);
|
||||
|
||||
res.status(200).json({message: "Event type updated successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error updating event type:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Supprimer un type d'événement
|
||||
exports.deleteEventType = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const isAdmin = await auth.hasPermission(decodedToken.uid, "edit_data");
|
||||
|
||||
if (!isAdmin) {
|
||||
res.status(403).json({error: "Forbidden: Admin permission required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {eventTypeId} = req.body.data;
|
||||
|
||||
if (!eventTypeId) {
|
||||
res.status(400).json({error: "Event type ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const eventsSnapshot = await db.collection("events")
|
||||
.where("eventTypeId", "==", eventTypeId)
|
||||
.get();
|
||||
|
||||
const now = admin.firestore.Timestamp.now();
|
||||
const futureEvents = eventsSnapshot.docs.filter((doc) => {
|
||||
const startDate = doc.data().StartDateTime;
|
||||
return startDate && startDate > now;
|
||||
});
|
||||
|
||||
if (futureEvents.length > 0) {
|
||||
res.status(409).json({
|
||||
error: "Cannot delete event type with future events",
|
||||
futureEventsCount: futureEvents.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("eventTypes").doc(eventTypeId).delete();
|
||||
|
||||
res.status(200).json({message: "Event type deleted successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error deleting event type:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
const admin = require("firebase-admin");
|
||||
const {Storage} = require("@google-cloud/storage");
|
||||
const logger = require("firebase-functions/logger");
|
||||
const auth = require("../utils/auth");
|
||||
|
||||
const storage = new Storage();
|
||||
|
||||
exports.moveEventFileV2 = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const {sourcePath, destinationPath} = req.body.data || {};
|
||||
|
||||
if (!sourcePath || !destinationPath) {
|
||||
res.status(400).json({error: "Source and destination paths are required."});
|
||||
return;
|
||||
}
|
||||
|
||||
const bucketName = admin.storage().bucket().name;
|
||||
const bucket = storage.bucket(bucketName);
|
||||
|
||||
await bucket.file(sourcePath).copy(bucket.file(destinationPath));
|
||||
await bucket.file(sourcePath).delete();
|
||||
const [url] = await bucket.file(destinationPath).getSignedUrl({
|
||||
action: "read",
|
||||
expires: "03-01-2500",
|
||||
});
|
||||
|
||||
res.status(200).json({url});
|
||||
} catch (error) {
|
||||
logger.error("Error moving file:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
const admin = require("firebase-admin");
|
||||
const logger = require("firebase-functions/logger");
|
||||
const auth = require("../utils/auth");
|
||||
|
||||
exports.generateTTSV2 = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
|
||||
logger.info("[generateTTSV2] Request from user:", {
|
||||
uid: decodedToken.uid,
|
||||
email: decodedToken.email,
|
||||
});
|
||||
|
||||
const {text, voiceConfig} = req.body.data || {};
|
||||
|
||||
if (!text) {
|
||||
res.status(400).json({error: "Text parameter is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.length > 5000) {
|
||||
res.status(400).json({error: "Text too long (max 5000 characters)"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {Storage} = require("@google-cloud/storage");
|
||||
const storage = new Storage();
|
||||
const bucketName = admin.storage().bucket().name;
|
||||
const bucket = storage.bucket(bucketName);
|
||||
|
||||
const {generateTTS} = require("../generateTTS");
|
||||
const result = await generateTTS(text, storage, bucket, voiceConfig);
|
||||
|
||||
logger.info("[generateTTSV2] ✓ Success", {
|
||||
cached: result.cached,
|
||||
cacheKey: result.cacheKey,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
audioUrl: result.audioUrl,
|
||||
cached: result.cached,
|
||||
cacheKey: result.cacheKey,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("[generateTTSV2] ✗ Error:", {
|
||||
error: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
|
||||
if (error.code === "PERMISSION_DENIED") {
|
||||
res.status(403).json({error: "Permission denied. Check Google Cloud TTS API is enabled."});
|
||||
} else if (error.code === "QUOTA_EXCEEDED") {
|
||||
res.status(429).json({error: "TTS quota exceeded. Try again later."});
|
||||
} else {
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,336 @@
|
||||
const admin = require("firebase-admin");
|
||||
const db = admin.firestore();
|
||||
const logger = require("firebase-functions/logger");
|
||||
const auth = require("../utils/auth");
|
||||
const helpers = require("../utils/helpers");
|
||||
|
||||
// Créer un utilisateur
|
||||
exports.createUser = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const isAdminUser = await auth.isAdmin(decodedToken.uid);
|
||||
|
||||
if (!isAdminUser) {
|
||||
res.status(403).json({error: "Forbidden: Admin access required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const userData = req.body.data;
|
||||
const userId = userData.uid;
|
||||
|
||||
if (!userId) {
|
||||
res.status(400).json({error: "User ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("users").doc(userId).set(userData);
|
||||
|
||||
res.status(201).json({id: userId, message: "User created successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error creating user:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Créer un utilisateur avec invitation par email
|
||||
exports.createUserWithInvite = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const isAdminUser = await auth.isAdmin(decodedToken.uid);
|
||||
|
||||
if (!isAdminUser) {
|
||||
res.status(403).json({error: "Forbidden: Admin access required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {email, firstName, lastName, phoneNumber, roleId} = req.body.data;
|
||||
|
||||
if (!email || !firstName || !lastName || !roleId) {
|
||||
res.status(400).json({error: "email, firstName, lastName, and roleId are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const tempPassword = Math.random().toString(36).slice(-12) + "Aa1!";
|
||||
|
||||
let userRecord;
|
||||
try {
|
||||
userRecord = await admin.auth().createUser({
|
||||
email: email,
|
||||
password: tempPassword,
|
||||
emailVerified: false,
|
||||
displayName: `${firstName} ${lastName}`,
|
||||
});
|
||||
} catch (authError) {
|
||||
logger.error("Error creating user in Auth:", authError);
|
||||
res.status(500).json({error: `Failed to create user in Auth: ${authError.message}`});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await db.collection("users").doc(userRecord.uid).set({
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
email: email,
|
||||
phoneNumber: phoneNumber || "",
|
||||
profilePhotoUrl: "",
|
||||
role: db.collection("roles").doc(roleId),
|
||||
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
createdBy: decodedToken.uid,
|
||||
});
|
||||
} catch (firestoreError) {
|
||||
logger.error("Error creating user in Firestore:", firestoreError);
|
||||
try {
|
||||
await admin.auth().deleteUser(userRecord.uid);
|
||||
} catch (cleanupError) {
|
||||
logger.error("Error cleaning up Auth user:", cleanupError);
|
||||
}
|
||||
res.status(500).json({error: `Failed to create user in Firestore: ${firestoreError.message}`});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const axios = require("axios");
|
||||
const firebaseApiKey = "AIzaSyARQL4P-t5l-cNjQNP9cMokQrLJ8BorF0U";
|
||||
|
||||
await axios.post(
|
||||
`https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key=${firebaseApiKey}`,
|
||||
{
|
||||
requestType: "PASSWORD_RESET",
|
||||
email: email,
|
||||
},
|
||||
);
|
||||
logger.info(`Password reset email sent to ${email}`);
|
||||
} catch (emailError) {
|
||||
logger.warn(`Could not send password reset email to ${email}: ${emailError.message}`);
|
||||
}
|
||||
|
||||
logger.info(`User ${userRecord.uid} created by ${decodedToken.uid}`);
|
||||
res.status(201).json({
|
||||
id: userRecord.uid,
|
||||
message: "User created successfully. Password reset email sent.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error in createUserWithInvite:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Mettre à jour un utilisateur
|
||||
exports.updateUser = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const {userId, data} = req.body.data;
|
||||
|
||||
if (!userId) {
|
||||
res.status(400).json({error: "User ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Un utilisateur ne peut modifier que son propre compte, sauf s'il est admin
|
||||
const isAdminUser = await auth.isAdmin(decodedToken.uid);
|
||||
if (decodedToken.uid !== userId && !isAdminUser) {
|
||||
res.status(403).json({error: "Forbidden: Cannot update other user accounts"});
|
||||
return;
|
||||
}
|
||||
|
||||
// Empêcher les non-admins de modifier le rôle
|
||||
if (!isAdminUser && data.role) {
|
||||
delete data.role;
|
||||
}
|
||||
|
||||
// Si le rôle est fourni et est un string, le convertir en DocumentReference
|
||||
if (data.role && typeof data.role === "string") {
|
||||
data.role = db.collection("roles").doc(data.role);
|
||||
}
|
||||
|
||||
await db.collection("users").doc(userId).update(data);
|
||||
|
||||
res.status(200).json({message: "User updated successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error updating user:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Supprimer un utilisateur
|
||||
exports.deleteUser = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const isAdminUser = await auth.isAdmin(decodedToken.uid);
|
||||
|
||||
if (!isAdminUser) {
|
||||
res.status(403).json({error: "Forbidden: Admin access required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const {userId} = req.body.data;
|
||||
|
||||
if (!userId) {
|
||||
res.status(400).json({error: "User ID is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
if (decodedToken.uid === userId) {
|
||||
res.status(400).json({error: "Cannot delete your own account"});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("users").doc(userId).delete();
|
||||
|
||||
try {
|
||||
await admin.auth().deleteUser(userId);
|
||||
} catch (authError) {
|
||||
logger.warn(`Could not delete user from Auth: ${authError.message}`);
|
||||
}
|
||||
|
||||
res.status(200).json({message: "User deleted successfully"});
|
||||
} catch (error) {
|
||||
logger.error("Error deleting user:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupérer tous les utilisateurs (selon permissions)
|
||||
exports.getUsers = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
|
||||
const canViewAll = await auth.hasPermission(decodedToken.uid, "view_all_users");
|
||||
|
||||
if (!canViewAll) {
|
||||
const userDoc = await db.collection("users").doc(decodedToken.uid).get();
|
||||
|
||||
if (!userDoc.exists) {
|
||||
res.status(404).json({error: "User not found"});
|
||||
return;
|
||||
}
|
||||
|
||||
let userData = userDoc.data();
|
||||
userData = helpers.serializeTimestamps(userData);
|
||||
userData = helpers.serializeReferences(userData);
|
||||
|
||||
res.status(200).json({
|
||||
users: [{
|
||||
id: userDoc.id,
|
||||
...userData,
|
||||
}],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = await db.collection("users").get();
|
||||
const users = snapshot.docs.map((doc) => {
|
||||
let data = doc.data();
|
||||
data = helpers.serializeTimestamps(data);
|
||||
data = helpers.serializeReferences(data);
|
||||
return {
|
||||
id: doc.id,
|
||||
...data,
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json({users});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching users:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupère un utilisateur spécifique
|
||||
exports.getUser = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
|
||||
const {userId} = req.body.data || req.body || {};
|
||||
if (!userId) {
|
||||
res.status(400).json({error: "userId is required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const userDoc = await db.collection("users").doc(userId).get();
|
||||
if (!userDoc.exists) {
|
||||
res.status(404).json({error: "User not found"});
|
||||
return;
|
||||
}
|
||||
|
||||
const user = userDoc.data();
|
||||
|
||||
const userData = {
|
||||
id: userDoc.id,
|
||||
uid: user.uid || userDoc.id,
|
||||
email: user.email || "",
|
||||
firstName: user.firstName || "",
|
||||
lastName: user.lastName || "",
|
||||
phoneNumber: user.phoneNumber || "",
|
||||
profilePhotoUrl: user.profilePhotoUrl || "",
|
||||
};
|
||||
|
||||
if (user.role) {
|
||||
const roleDoc = await user.role.get();
|
||||
if (roleDoc.exists) {
|
||||
userData.role = {
|
||||
id: roleDoc.id,
|
||||
...roleDoc.data(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({user: userData});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching user:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupère l'utilisateur actuellement authentifié avec son rôle
|
||||
exports.getCurrentUser = async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
const userId = decodedToken.uid;
|
||||
|
||||
const userDoc = await db.collection("users").doc(userId).get();
|
||||
if (!userDoc.exists) {
|
||||
res.status(404).json({error: "User not found"});
|
||||
return;
|
||||
}
|
||||
|
||||
const userData = userDoc.data();
|
||||
|
||||
let roleData = null;
|
||||
if (userData.role) {
|
||||
const roleDoc = await userData.role.get();
|
||||
if (roleDoc.exists) {
|
||||
roleData = {id: roleDoc.id, ...roleDoc.data()};
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
user: {
|
||||
uid: userId,
|
||||
...helpers.serializeTimestamps(userData),
|
||||
role: roleData,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error getting current user:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
|
||||
// Récupère tous les rôles
|
||||
exports.getRoles = async (req, res) => {
|
||||
try {
|
||||
await auth.authenticateUser(req);
|
||||
|
||||
const snapshot = await db.collection("roles").get();
|
||||
const roles = snapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(doc.data()),
|
||||
}));
|
||||
|
||||
res.status(200).json({roles});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching roles:", error);
|
||||
res.status(500).json({error: error.message});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user