Files
EM2_ERP/em2rp/functions/index.js
ElPoyo 4e4573f57b feat: refactor de la gestion des utilisateurs et migration de la logique métier vers les Cloud Functions
Cette mise à jour majeure refactorise entièrement la gestion des utilisateurs pour la faire passer par des Cloud Functions sécurisées et migre une part importante de la logique métier (gestion des événements, maintenances, containers) du client vers le backend.

**Gestion des Utilisateurs (Backend & Frontend):**
- **Nouvelle fonction `createUserWithInvite` :**
    - Crée l'utilisateur dans Firebase Auth avec un mot de passe temporaire.
    - Crée le document utilisateur correspondant dans Firestore.
    - Envoie automatiquement un e-mail de réinitialisation de mot de passe (via l'API REST de Firebase et `axios`) pour que l'utilisateur définisse son propre mot de passe, améliorant la sécurité et l'expérience d'intégration.
- **Refactorisation de `updateUser` et `deleteUser` :**
    - Les anciennes fonctions `onCall` sont remplacées par des fonctions `onRequest` (HTTP) standards, alignées avec le reste de l'API.
    - La logique de suppression gère désormais la suppression dans Auth et Firestore.
- **Réinitialisation de Mot de Passe (UI) :**
    - Ajout d'un bouton "Réinitialiser le mot de passe" sur la carte utilisateur, permettant aux administrateurs d'envoyer un e-mail de réinitialisation à n'importe quel utilisateur.
- **Amélioration de l'UI :**
    - Boîte de dialogue de confirmation améliorée pour la suppression d'un utilisateur.
    - Notifications (Snackbars) pour les opérations de création, suppression et réinitialisation de mot de passe.

**Migration de la Logique Métier vers les Cloud Functions:**
- **Gestion de la Préparation d'Événements :**
    - Migration complète de la logique de validation des étapes (préparation, chargement, déchargement, retour) du client vers de nouvelles Cloud Functions (`validateEquipmentPreparation`, `validateAllLoading`, etc.).
    - Le backend gère désormais la mise à jour des statuts de l'événement (`inProgress`, `completed`) et des équipements (`inUse`, `available`).
    - Le code frontend (`EventPreparationService`) a été simplifié pour appeler ces nouvelles fonctions au lieu d'effectuer des écritures directes sur Firestore.
- **Création de Maintenance :**
    - La fonction `createMaintenance` gère maintenant la mise à jour des équipements associés (`maintenanceIds`) et la création d'alertes (`maintenanceDue`) si une maintenance est prévue prochainement. La logique client a été supprimée.
- **Suppression de Container :**
    - La fonction `deleteContainer` a été améliorée pour nettoyer automatiquement les références (`parentBoxIds`) dans tous les équipements contenus avant de supprimer le container.

**Refactorisation et Corrections (Backend & Frontend) :**
- **Fiabilisation des Appels API (Frontend) :**
    - Le `ApiService` a été renforcé pour convertir de manière plus robuste les données (notamment les `Map` de type `_JsonMap`) en JSON standard avant de les envoyer aux Cloud Functions, évitant ainsi des erreurs de sérialisation.
- **Correction des Références (Backend) :**
    - La fonction `updateUser` convertit correctement les `roleId` (string) en `DocumentReference` Firestore.
    - Sécurisation de la vérification de l'assignation d'un utilisateur à un événement (`workforce`) pour éviter les erreurs sur des références nulles.
- **Dépendance (Backend) :**
    - Ajout de la librairie `axios` pour effectuer des appels à l'API REST de Firebase.
2026-01-14 11:18:49 +01:00

2796 lines
93 KiB
JavaScript

/**
* EM2RP Cloud Functions
* Architecture backend sécurisée avec authentification et permissions
*/
const { onRequest, onCall } = require("firebase-functions/v2/https");
const logger = require("firebase-functions/logger");
const admin = require('firebase-admin');
const { Storage } = require('@google-cloud/storage');
// Utilitaires
const auth = require('./utils/auth');
const helpers = require('./utils/helpers');
// Initialisation
admin.initializeApp();
const storage = new Storage();
const db = admin.firestore();
// Configuration commune pour toutes les fonctions HTTP
const httpOptions = {
cors: true,
invoker: 'public', // Permet les invocations non authentifiées (l'auth est gérée par notre token Firebase)
// Version: 2.0 - Ajout de l'invoker public pour résoudre les problèmes CORS
};
// ============================================================================
// CORS Middleware
// ============================================================================
const setCorsHeaders = (res, req) => {
// Permettre toutes les origines en développement/production
const origin = req.headers.origin || req.headers.referer || '*';
res.set('Access-Control-Allow-Origin', origin);
res.set('Access-Control-Allow-Credentials', 'true');
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.set('Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Origin, X-Requested-With');
res.set('Access-Control-Max-Age', '3600');
};
// Wrapper pour les fonctions avec CORS
const withCors = (handler) => {
return async (req, res) => {
// Définir les headers CORS pour toutes les requêtes
setCorsHeaders(res, req);
// Gérer les requêtes preflight OPTIONS
if (req.method === 'OPTIONS') {
res.status(204).send('');
return;
}
// Exécuter le handler
try {
await handler(req, res);
} catch (error) {
logger.error("Unhandled error:", error);
if (!res.headersSent) {
res.status(500).json({ error: error.message });
}
}
};
};
// ============================================================================
// STORAGE - Move Event File
// ============================================================================
exports.moveEventFileV2 = onRequest(httpOptions, withCors(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 });
}
}));
// ============================================================================
// EQUIPMENT - CRUD
// ============================================================================
// Créer un équipement (admin ou manage_equipment)
exports.createEquipment = onRequest(httpOptions, withCors(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 = onRequest(httpOptions, withCors(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;
}
// 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 = onRequest(httpOptions, withCors(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 } = 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 actifs
const eventsSnapshot = await db.collection('events')
.where('status', '!=', 'CANCELLED')
.get();
for (const eventDoc of eventsSnapshot.docs) {
const eventData = eventDoc.data();
const assignedEquipment = eventData.assignedEquipment || [];
if (assignedEquipment.some(eq => eq.equipmentId === equipmentId)) {
res.status(409).json({
error: 'Cannot delete equipment: it is assigned to active events',
eventId: eventDoc.id
});
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 = onRequest(httpOptions, withCors(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 = onRequest(httpOptions, withCors(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 });
}
}));
// ============================================================================
// CONTAINERS - CRUD
// ============================================================================
// Créer un container
exports.createContainer = onRequest(httpOptions, withCors(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 = onRequest(httpOptions, withCors(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 = onRequest(httpOptions, withCors(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 = onRequest(httpOptions, withCors(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 = onRequest(httpOptions, withCors(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;
}
// Limiter à 100 conteneurs max par requête
if (containerIds.length > 100) {
res.status(400).json({ error: 'Maximum 100 container IDs per request' });
return;
}
// Récupérer tous les documents en parallèle
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 });
}
}));
// ============================================================================
// EVENTS - CRUD
// ============================================================================
// Créer un événement
exports.createEvent = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const hasAccess = await auth.hasPermission(decodedToken.uid, 'edit_event');
if (!hasAccess) {
res.status(403).json({ error: 'Forbidden: Requires edit_event permission' });
return;
}
const eventData = req.body.data;
const dataToSave = helpers.deserializeTimestamps(eventData, [
'StartDateTime', 'EndDateTime', 'createdAt', 'updatedAt'
]);
const docRef = await db.collection('events').add(dataToSave);
res.status(201).json({ id: docRef.id, message: 'Event created successfully' });
} catch (error) {
logger.error("Error creating event:", error);
res.status(500).json({ error: error.message });
}
}));
// Mettre à jour un événement
exports.updateEvent = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const hasAccess = await auth.hasPermission(decodedToken.uid, 'edit_event');
if (!hasAccess) {
res.status(403).json({ error: 'Forbidden: Requires edit_event permission' });
return;
}
const requestData = req.body.data;
logger.info(`Update event - requestData keys: ${Object.keys(requestData || {}).join(', ')}`);
const eventId = requestData.eventId;
logger.info(`Update event - eventId: ${eventId}`);
if (!eventId) {
logger.error('Event ID is missing from request');
res.status(400).json({ error: 'Event ID is required' });
return;
}
// Extraire eventId et préparer les données à sauvegarder
const { eventId: _, ...data } = requestData;
if (!data || Object.keys(data).length === 0) {
res.status(400).json({ error: 'No data to update' });
return;
}
delete data.id;
data.updatedAt = admin.firestore.Timestamp.now();
const dataToSave = helpers.deserializeTimestamps(data, [
'StartDateTime', 'EndDateTime'
]);
await db.collection('events').doc(eventId).update(dataToSave);
res.status(200).json({ message: 'Event updated successfully' });
} catch (error) {
logger.error("Error updating event:", error);
res.status(500).json({ error: error.message });
}
}));
// Supprimer un événement
exports.deleteEvent = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const hasAccess = await auth.hasPermission(decodedToken.uid, 'delete_event');
if (!hasAccess) {
res.status(403).json({ error: 'Forbidden: Requires delete_event permission' });
return;
}
const { eventId } = req.body.data;
if (!eventId) {
res.status(400).json({ error: 'Event ID is required' });
return;
}
await db.collection('events').doc(eventId).delete();
res.status(200).json({ message: 'Event deleted successfully' });
} catch (error) {
logger.error("Error deleting event:", error);
res.status(500).json({ error: error.message });
}
}));
// ============================================================================
// MAINTENANCES - CRUD
// ============================================================================
// Créer une maintenance
exports.createMaintenance = onRequest(httpOptions, withCors(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;
// Mettre à jour les équipements concernés
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,
});
}
}
// Si la maintenance est planifiée dans les 7 prochains jours, créer une alerte
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) {
// Vérifier si une alerte existe déjà
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 = onRequest(httpOptions, withCors(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 });
}
}));
// ============================================================================
// OPTIONS - CRUD
// ============================================================================
// Créer une option
exports.createOption = onRequest(httpOptions, withCors(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 = onRequest(httpOptions, withCors(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 = onRequest(httpOptions, withCors(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 });
}
}));
// ============================================================================
// USERS - CRUD
// ============================================================================
// Créer un utilisateur
exports.createUser = onRequest(httpOptions, withCors(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 = onRequest(httpOptions, withCors(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;
}
// Générer un mot de passe temporaire aléatoire
const tempPassword = Math.random().toString(36).slice(-12) + 'Aa1!';
// Créer l'utilisateur dans Firebase Auth
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;
}
// Créer le document utilisateur dans Firestore
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) {
// Si la création Firestore échoue, supprimer l'utilisateur Auth
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;
}
// Envoyer l'email de réinitialisation du mot de passe
// Utilisation de l'API REST de Firebase Auth pour déclencher l'envoi automatique
try {
const axios = require('axios');
const firebaseApiKey = 'AIzaSyARQL4P-t5l-cNjQNP9cMokQrLJ8BorF0U'; // Web API Key
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}`);
// Ne pas faire échouer la requête si l'email ne peut pas être envoyé
}
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 = onRequest(httpOptions, withCors(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;
}
// Vérifier si l'utilisateur met à jour son propre profil ou est admin
const isOwnProfile = decodedToken.uid === userId;
const isAdminUser = await auth.isAdmin(decodedToken.uid);
const hasEditPermission = await auth.hasPermission(decodedToken.uid, 'edit_user');
if (!isOwnProfile && !isAdminUser && !hasEditPermission) {
res.status(403).json({ error: 'Forbidden: Cannot edit other users' });
return;
}
// Si mise à jour propre profil, limiter les champs modifiables
if (isOwnProfile && !isAdminUser) {
const allowedFields = ['firstName', 'lastName', 'phoneNumber', 'profilePhotoUrl'];
const filteredData = {};
for (const field of allowedFields) {
if (data[field] !== undefined) {
filteredData[field] = data[field];
}
}
await db.collection('users').doc(userId).update(filteredData);
} else {
delete data.uid;
// Convertir le role string en DocumentReference si présent
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 = onRequest(httpOptions, withCors(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;
}
// Empêcher un admin de se supprimer lui-même
if (decodedToken.uid === userId) {
res.status(400).json({ error: 'Cannot delete your own account' });
return;
}
// Supprimer le document utilisateur dans Firestore
await db.collection('users').doc(userId).delete();
// Optionnel: Supprimer l'utilisateur de Firebase Auth
// Note: Cela nécessite le SDK Admin et des privilèges élevés
try {
await admin.auth().deleteUser(userId);
} catch (authError) {
logger.warn(`Could not delete user from Auth: ${authError.message}`);
// On continue même si la suppression Auth échoue
}
res.status(200).json({ message: 'User deleted successfully' });
} catch (error) {
logger.error("Error deleting user:", error);
res.status(500).json({ error: error.message });
}
}));
// ============================================================================
// EQUIPMENT STATUS - Batch Update
// ============================================================================
// Mettre à jour le statut de plusieurs équipements (pour préparation/retour)
exports.updateEquipmentStatus = onRequest(httpOptions, withCors(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 });
}
}));
// ============================================================================
// OPTIONS - Read (public pour utilisateurs authentifiés)
// ============================================================================
exports.getOptions = onRequest(httpOptions, withCors(async (req, res) => {
try {
await auth.authenticateUser(req); // Juste vérifier l'auth
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 });
}
}));
// ============================================================================
// EVENT TYPES - Read (public pour utilisateurs authentifiés)
// ============================================================================
exports.getEventTypes = onRequest(httpOptions, withCors(async (req, res) => {
try {
await auth.authenticateUser(req); // Juste vérifier l'auth
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 });
}
}));
// ============================================================================
// ROLES - Read (public pour utilisateurs authentifiés)
// ============================================================================
exports.getRoles = onRequest(httpOptions, withCors(async (req, res) => {
try {
await auth.authenticateUser(req); // Juste vérifier l'auth
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 });
}
}));
// ============================================================================
// EVENT EQUIPMENT - Update equipment status and quantities
// ============================================================================
exports.updateEventEquipment = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const { eventId, assignedEquipment, preparationStatus, loadingStatus, unloadingStatus, returnStatus } = req.body.data;
if (!eventId) {
res.status(400).json({ error: 'Event ID is required' });
return;
}
// Vérifier les permissions
const eventDoc = await db.collection('events').doc(eventId).get();
if (!eventDoc.exists) {
res.status(404).json({ error: 'Event not found' });
return;
}
const eventData = eventDoc.data();
const isAdminUser = await auth.hasPermission(decodedToken.uid, 'edit_event');
// Vérifier si l'utilisateur est assigné en vérifiant workforce de manière sécurisée
let isAssigned = false;
if (eventData.workforce && Array.isArray(eventData.workforce)) {
isAssigned = eventData.workforce.some(ref => {
if (!ref || !ref.path) return false;
return ref.path.endsWith(decodedToken.uid) || ref.path === `/users/${decodedToken.uid}`;
});
}
if (!isAssigned && !isAdminUser) {
res.status(403).json({ error: 'Forbidden: Not assigned to this event' });
return;
}
// Préparer les données à mettre à jour
const updateData = {};
if (assignedEquipment) {
// Convertir les timestamps dans assignedEquipment
updateData.assignedEquipment = assignedEquipment.map(eq =>
helpers.deserializeTimestamps(eq, [])
);
}
if (preparationStatus) updateData.preparationStatus = preparationStatus;
if (loadingStatus) updateData.loadingStatus = loadingStatus;
if (unloadingStatus) updateData.unloadingStatus = unloadingStatus;
if (returnStatus) updateData.returnStatus = returnStatus;
// Mettre à jour l'événement
await db.collection('events').doc(eventId).update(updateData);
res.status(200).json({ message: 'Event equipment updated successfully' });
} catch (error) {
logger.error("Error updating event equipment:", error);
res.status(500).json({ error: error.message });
}
}));
// ============================================================================
// EQUIPMENT STATUS - Update individual equipment status
// ============================================================================
exports.updateEquipmentStatusOnly = onRequest(httpOptions, withCors(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 });
}
}));
// ============================================================================
// EVENT TYPES - CRUD Operations
// ============================================================================
// Récupérer les événements utilisant un type d'événement
exports.getEventsByEventType = onRequest(httpOptions, withCors(async (req, res) => {
try {
await auth.authenticateUser(req);
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 events = eventsSnapshot.docs.map(doc => ({
id: doc.id,
name: doc.data().name,
startDateTime: doc.data().StartDateTime,
}));
res.status(200).json({ events });
} catch (error) {
logger.error("Error fetching events by type:", error);
res.status(500).json({ error: error.message });
}
}));
// Créer un type d'événement (admin uniquement)
exports.createEventType = onRequest(httpOptions, withCors(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;
}
// Vérifier l'unicité du nom
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 (admin uniquement)
exports.updateEventType = onRequest(httpOptions, withCors(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;
}
// Vérifier que le document existe
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;
}
// Vérifier l'unicité du nom (sauf pour le document actuel)
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 (admin uniquement)
exports.deleteEventType = onRequest(httpOptions, withCors(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;
}
// Vérifier qu'aucun événement futur n'utilise ce type
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 });
}
}));
// ============================================================================
// EVENTS - Read with permissions
// ============================================================================
exports.getEvents = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const { userId } = req.body.data || {};
// Vérifier si l'utilisateur peut voir tous les événements
const canViewAll = await auth.hasPermission(decodedToken.uid, 'view_all_events');
let eventsSnapshot;
if (canViewAll) {
// Admin : tous les événements
eventsSnapshot = await db.collection('events').get();
} else {
// Utilisateur normal : seulement ses événements assignés
const userRef = db.collection('users').doc(userId || decodedToken.uid);
eventsSnapshot = await db.collection('events')
.where('workforce', 'array-contains', userRef)
.get();
}
// Collecter tous les UIDs utilisateurs uniques
const userIdsSet = new Set();
eventsSnapshot.docs.forEach(doc => {
const data = doc.data();
if (data.workforce && Array.isArray(data.workforce)) {
data.workforce.forEach(userRef => {
if (userRef && userRef.id) {
userIdsSet.add(userRef.id);
} else if (typeof userRef === 'string' && userRef.startsWith('users/')) {
userIdsSet.add(userRef.split('/')[1]);
}
});
}
});
// Récupérer tous les utilisateurs en une seule fois
const usersMap = {};
if (userIdsSet.size > 0) {
const userIds = Array.from(userIdsSet);
// Récupérer par batch (Firestore limite à 10 par requête 'in')
const batchSize = 10;
for (let i = 0; i < userIds.length; i += batchSize) {
const batch = userIds.slice(i, i + batchSize);
const usersSnapshot = await db.collection('users')
.where(admin.firestore.FieldPath.documentId(), 'in', batch)
.get();
usersSnapshot.docs.forEach(userDoc => {
const userData = userDoc.data();
// Stocker uniquement les données publiques
usersMap[userDoc.id] = {
uid: userDoc.id,
firstName: userData.firstName || '',
lastName: userData.lastName || '',
email: userData.email || '',
phoneNumber: userData.phoneNumber || '',
profilePhotoUrl: userData.profilePhotoUrl || '',
};
});
}
}
// Sérialiser les événements avec workforce comme liste d'UIDs
const events = eventsSnapshot.docs.map(doc => {
const data = doc.data();
// Convertir workforce en liste d'UIDs
let workforceUids = [];
if (data.workforce && Array.isArray(data.workforce)) {
workforceUids = data.workforce.map(userRef => {
if (userRef && userRef.id) {
return userRef.id;
} else if (typeof userRef === 'string' && userRef.startsWith('users/')) {
return userRef.split('/')[1];
}
return null;
}).filter(uid => uid !== null);
}
return {
id: doc.id,
...helpers.serializeTimestamps(data),
workforce: workforceUids, // Liste d'UIDs au lieu de DocumentReference
};
});
// Retourner events + users map
res.status(200).json({
events,
users: usersMap // Map UID -> données utilisateur
});
} catch (error) {
logger.error("Error fetching events:", error);
res.status(500).json({ error: error.message });
}
}));
// ============================================================================
// EQUIPMENTS - Read with permissions
// ============================================================================
exports.getEquipments = onRequest(httpOptions, withCors(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;
}
const snapshot = await db.collection('equipments').get();
const equipments = snapshot.docs.map(doc => {
const data = doc.data();
// Masquer les prix si l'utilisateur n'a pas manage_equipment
if (!canManage) {
delete data.purchasePrice;
delete data.rentalPrice;
}
return {
id: doc.id,
...helpers.serializeTimestamps(data, ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt'])
};
});
res.status(200).json({ equipments });
} catch (error) {
logger.error("Error fetching equipments:", error);
res.status(500).json({ error: error.message });
}
}));
// ============================================================================
// CONTAINERS - Read with permissions
// ============================================================================
exports.getContainers = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
// Vérifier les permissions
const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment');
if (!canView) {
res.status(403).json({ error: 'Forbidden: Requires equipment permissions' });
return;
}
const snapshot = await db.collection('containers').get();
const containers = snapshot.docs.map(doc => {
const data = doc.data();
return {
id: doc.id,
...helpers.serializeTimestamps(data, ['createdAt', 'updatedAt'])
};
});
res.status(200).json({ containers });
} catch (error) {
logger.error("Error fetching containers:", error);
res.status(500).json({ error: error.message });
}
}));
// ============================================================================
// MAINTENANCES - Read with permissions
// ============================================================================
exports.getMaintenances = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const { equipmentId } = req.body.data || {};
// 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;
}
let query = db.collection('maintenances');
// Filtrer par équipement si spécifié
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 });
}
}));
// ============================================================================
// ALERTS - Read with permissions
// ============================================================================
exports.getAlerts = onRequest(httpOptions, withCors(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 });
}
}));
exports.markAlertAsRead = onRequest(httpOptions, withCors(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 });
}
}));
exports.deleteAlert = onRequest(httpOptions, withCors(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 });
}
}));
// ============================================================================
// USERS - Read with permissions
// ============================================================================
exports.getUsers = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
// Vérifier les permissions
const canViewAll = await auth.hasPermission(decodedToken.uid, 'view_all_users');
if (!canViewAll) {
// Si pas admin, ne retourner que l'utilisateur lui-même
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;
}
// Admin : tous les utilisateurs
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 });
}
}));
// ============================================================================
// USER - Récupération individuelle
// ============================================================================
/**
* Récupère un utilisateur spécifique par son ID
* Tout utilisateur authentifié peut accéder aux données publiques
*/
exports.getUser = onCall(async (request) => {
try {
await authenticateUser(request);
const db = getFirestore();
const { userId } = request.data;
if (!userId) {
throw new Error("userId is required");
}
const userDoc = await db.collection("users").doc(userId).get();
if (!userDoc.exists) {
throw new Error("User not found");
}
const user = userDoc.data();
// Données publiques accessibles à tous
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 || "",
};
// Inclure le rôle si disponible
if (user.role) {
const roleDoc = await user.role.get();
if (roleDoc.exists) {
userData.role = {
id: roleDoc.id,
...roleDoc.data(),
};
}
}
return { user: userData };
} catch (error) {
logger.error("Error fetching user:", error);
throw new Error(error.message || "Failed to fetch user");
}
});
// ============================================================================
// EQUIPMENT AVAILABILITY - Vérification de disponibilité
// ============================================================================
exports.checkEquipmentAvailability = onRequest(httpOptions, withCors(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;
}
// Gérer les dates qui peuvent être des Timestamps ou des objets Date
let eventStart, 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;
}
// Vérifier si l'équipement est assigné à cet événement (directement ou via une boîte)
const assignedEquipment = event.assignedEquipment || [];
const assignedContainers = event.assignedContainers || [];
// Vérifier si l'équipement est directement assigné
const isEquipmentDirectlyAssigned = assignedEquipment.some(eq => eq.equipmentId === equipmentId);
// Vérifier si l'équipement est dans une boîte assignée
let isEquipmentInAssignedContainer = false;
if (assignedContainers.length > 0) {
logger.info(`Event ${eventDoc.id} has ${assignedContainers.length} assigned containers`);
// Récupérer les conteneurs assignés et vérifier si l'équipement y est
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;
}
// Vérifier le chevauchement de dates
const requestStart = startTimestamp.toDate();
const requestEnd = endTimestamp.toDate();
// Inclure les temps d'installation et de démontage
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);
// Il y a conflit si les périodes se chevauchent
const hasOverlap = requestStart < eventEndWithTeardown && requestEnd > eventStartWithSetup;
if (hasOverlap) {
// Calculer les jours de chevauchement
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})`);
// Retourner les détails complets de l'événement
const eventData = helpers.serializeTimestamps(event);
conflicts.push({
eventId: eventDoc.id,
eventName: event.Name,
eventData: eventData, // Ajouter toutes les données de l'événement
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" });
}
}));
exports.checkContainerAvailability = onRequest(httpOptions, withCors(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;
}
// Récupérer le container et ses équipements
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;
}
// Gérer les dates
let eventStart, 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;
}
// Vérifier si le container est assigné
const assignedContainers = event.assignedContainers || [];
const isContainerAssigned = assignedContainers.includes(containerId);
// Vérifier si des équipements du container sont assignés
const assignedEquipment = event.assignedEquipment || [];
const conflictingEquipmentIds = equipmentIds.filter(eqId =>
assignedEquipment.some(eq => eq.equipmentId === eqId)
);
if (!isContainerAssigned && conflictingEquipmentIds.length === 0) {
continue;
}
// Vérifier le chevauchement de dates
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" });
}
}));
// ============================================================================
// AVAILABILITY - Optimized batch check
// ============================================================================
/**
* Récupère tous les équipements et conteneurs en conflit pour une période donnée
* Optimisé : une seule requête au lieu d'une par équipement
*/
exports.getConflictingEquipmentIds = onRequest(httpOptions, withCors(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}`);
// Calculer la période effective avec temps de montage/démontage
const requestStartDate = new Date(startDate);
requestStartDate.setHours(requestStartDate.getHours() - installationTime);
const requestEndDate = new Date(endDate);
requestEndDate.setHours(requestEndDate.getHours() + disassemblyTime);
// Récupérer tous les événements non annulés
const eventsSnapshot = await db.collection('events')
.where('status', '!=', 'CANCELLED')
.get();
logger.info(`Found ${eventsSnapshot.docs.length} events to check`);
// Récupérer tous les équipements pour savoir lesquels sont quantifiables
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'
};
});
// Maps pour stocker les conflits
const conflictingEquipmentIds = new Set();
const conflictingContainerIds = new Set();
const conflictDetails = {}; // { equipmentId/containerId: [{ eventId, eventName, startDate, endDate, quantity }] }
const equipmentQuantities = {}; // { equipmentId: { totalQuantity, reservedQuantity, availableQuantity, reservations: [...] } }
for (const eventDoc of eventsSnapshot.docs) {
// Exclure l'événement en cours d'édition
if (excludeEventId && eventDoc.id === excludeEventId) {
continue;
}
const event = eventDoc.data();
// Gérer les dates
let eventStart, 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;
}
// Ajouter temps de montage/démontage de cet événement
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);
// Vérifier le chevauchement de dates
const hasOverlap = requestStartDate < eventEndWithTeardown && requestEndDate > eventStartWithSetup;
if (!hasOverlap) {
continue;
}
// Il y a chevauchement ! Récupérer les équipements et conteneurs assignés
const assignedEquipment = event.assignedEquipment || [];
const assignedContainers = event.assignedContainers || [];
const conflictInfo = {
eventId: eventDoc.id,
eventName: event.Name,
startDate: eventStart.toISOString(),
endDate: eventEnd.toISOString(),
};
// Ajouter les équipements directement assignés
for (const eq of assignedEquipment) {
const equipmentId = eq.equipmentId;
const quantity = eq.quantity || 1;
const equipInfo = equipmentsInfo[equipmentId];
// Pour les équipements quantifiables, on ne les marque pas forcément comme "en conflit"
// On calcule juste les quantités réservées
if (equipInfo && equipInfo.hasQuantity) {
// Initialiser les infos de quantité si nécessaire
if (!equipmentQuantities[equipmentId]) {
equipmentQuantities[equipmentId] = {
totalQuantity: equipInfo.totalQuantity,
reservedQuantity: 0,
availableQuantity: equipInfo.totalQuantity,
reservations: []
};
}
// Ajouter la réservation
equipmentQuantities[equipmentId].reservedQuantity += quantity;
equipmentQuantities[equipmentId].availableQuantity = equipInfo.totalQuantity - equipmentQuantities[equipmentId].reservedQuantity;
equipmentQuantities[equipmentId].reservations.push({
...conflictInfo,
quantity: quantity
});
// Ne marquer comme "en conflit" que si la quantité totale est épuisée
if (equipmentQuantities[equipmentId].availableQuantity <= 0) {
conflictingEquipmentIds.add(equipmentId);
}
} else {
// Pour les équipements non quantifiables, comportement classique
conflictingEquipmentIds.add(equipmentId);
}
if (!conflictDetails[equipmentId]) {
conflictDetails[equipmentId] = [];
}
conflictDetails[equipmentId].push({
...conflictInfo,
quantity: quantity
});
}
// Ajouter les conteneurs assignés
for (const containerId of assignedContainers) {
conflictingContainerIds.add(containerId);
if (!conflictDetails[containerId]) {
conflictDetails[containerId] = [];
}
conflictDetails[containerId].push(conflictInfo);
// Récupérer les équipements dans ce conteneur
const containerDoc = await db.collection('containers').doc(containerId).get();
if (containerDoc.exists) {
const containerData = containerDoc.data();
const equipmentIds = containerData.equipmentIds || [];
// Marquer tous les équipements du conteneur comme en conflit
for (const equipmentId of equipmentIds) {
conflictingEquipmentIds.add(equipmentId);
if (!conflictDetails[equipmentId]) {
conflictDetails[equipmentId] = [];
}
// Ajouter une note indiquant que c'est via le conteneur
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, // NOUVEAU : Informations sur les quantités
});
} catch (error) {
logger.error("Error getting conflicting equipment IDs:", error);
res.status(500).json({ error: error.message || "Failed to get conflicting equipment IDs" });
}
}));
// ============================================================================
// USER - Get current authenticated user
// ============================================================================
exports.getCurrentUser = onRequest(httpOptions, withCors(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();
// Récupérer le rôle
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 });
}
}));
// ============================================================================
// MAINTENANCE - Delete
// ============================================================================
exports.deleteMaintenance = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
// Vérifier permission
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;
}
// Récupérer la maintenance pour connaître les équipements
const maintenanceDoc = await db.collection('maintenances').doc(maintenanceId).get();
if (maintenanceDoc.exists) {
const maintenance = maintenanceDoc.data();
// Retirer la maintenance des équipements
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 });
}
}));
// ============================================================================
// EVENT PREPARATION - Validation des étapes de préparation
// ============================================================================
// Helper: Mettre à jour le statut d'un équipement
async function updateEquipmentStatus(equipmentId, status) {
try {
const doc = await db.collection('equipments').doc(equipmentId).get();
if (!doc.exists) {
logger.warn(`Equipment ${equipmentId} does not exist, skipping status update`);
return;
}
await db.collection('equipments').doc(equipmentId).update({
status: status,
updatedAt: admin.firestore.Timestamp.now(),
});
} catch (error) {
logger.error(`Error updating equipment status for ${equipmentId}:`, error);
}
}
// Valider un équipement individuel en préparation
exports.validateEquipmentPreparation = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
if (!canManage) {
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
return;
}
const { eventId, equipmentId } = req.body.data;
if (!eventId || !equipmentId) {
res.status(400).json({ error: 'eventId and equipmentId are required' });
return;
}
const eventDoc = await db.collection('events').doc(eventId).get();
if (!eventDoc.exists) {
res.status(404).json({ error: 'Event not found' });
return;
}
const eventData = eventDoc.data();
const assignedEquipment = eventData.assignedEquipment || [];
// Mettre à jour le statut de l'équipement
const updatedEquipment = assignedEquipment.map(eq => {
if (eq.equipmentId === equipmentId) {
return { ...eq, isPrepared: true };
}
return eq;
});
// Vérifier si tous sont préparés
const allPrepared = updatedEquipment.every(eq => eq.isPrepared);
const updateData = {
assignedEquipment: updatedEquipment,
preparationStatus: allPrepared ? 'completed' : 'inProgress',
};
await db.collection('events').doc(eventId).update(updateData);
res.status(200).json({ success: true, allPrepared });
} catch (error) {
logger.error("Error validating equipment preparation:", error);
res.status(500).json({ error: error.message });
}
}));
// Valider tous les équipements en préparation
exports.validateAllPreparation = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
if (!canManage) {
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
return;
}
const { eventId } = req.body.data;
if (!eventId) {
res.status(400).json({ error: 'eventId is required' });
return;
}
const eventDoc = await db.collection('events').doc(eventId).get();
if (!eventDoc.exists) {
res.status(404).json({ error: 'Event not found' });
return;
}
const eventData = eventDoc.data();
const assignedEquipment = eventData.assignedEquipment || [];
// Marquer tous comme préparés
const updatedEquipment = assignedEquipment.map(eq => ({
...eq,
isPrepared: true,
}));
await db.collection('events').doc(eventId).update({
assignedEquipment: updatedEquipment,
preparationStatus: 'completed',
});
// Mettre à jour le statut des équipements à "inUse"
for (const equipment of assignedEquipment) {
await updateEquipmentStatus(equipment.equipmentId, 'inUse');
}
res.status(200).json({ success: true });
} catch (error) {
logger.error("Error validating all preparation:", error);
res.status(500).json({ error: error.message });
}
}));
// Valider un équipement individuel pour le chargement
exports.validateEquipmentLoading = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
if (!canManage) {
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
return;
}
const { eventId, equipmentId } = req.body.data;
if (!eventId || !equipmentId) {
res.status(400).json({ error: 'eventId and equipmentId are required' });
return;
}
const eventDoc = await db.collection('events').doc(eventId).get();
if (!eventDoc.exists) {
res.status(404).json({ error: 'Event not found' });
return;
}
const eventData = eventDoc.data();
const assignedEquipment = eventData.assignedEquipment || [];
const updatedEquipment = assignedEquipment.map(eq => {
if (eq.equipmentId === equipmentId) {
return { ...eq, isLoaded: true };
}
return eq;
});
const allLoaded = updatedEquipment.every(eq => eq.isLoaded);
const updateData = {
assignedEquipment: updatedEquipment,
loadingStatus: allLoaded ? 'completed' : 'inProgress',
};
await db.collection('events').doc(eventId).update(updateData);
res.status(200).json({ success: true, allLoaded });
} catch (error) {
logger.error("Error validating equipment loading:", error);
res.status(500).json({ error: error.message });
}
}));
// Valider tous les équipements pour le chargement
exports.validateAllLoading = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
if (!canManage) {
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
return;
}
const { eventId } = req.body.data;
if (!eventId) {
res.status(400).json({ error: 'eventId is required' });
return;
}
const eventDoc = await db.collection('events').doc(eventId).get();
if (!eventDoc.exists) {
res.status(404).json({ error: 'Event not found' });
return;
}
const eventData = eventDoc.data();
const assignedEquipment = eventData.assignedEquipment || [];
const updatedEquipment = assignedEquipment.map(eq => ({
...eq,
isLoaded: true,
}));
await db.collection('events').doc(eventId).update({
assignedEquipment: updatedEquipment,
loadingStatus: 'completed',
});
res.status(200).json({ success: true });
} catch (error) {
logger.error("Error validating all loading:", error);
res.status(500).json({ error: error.message });
}
}));
// Valider un équipement individuel pour le déchargement
exports.validateEquipmentUnloading = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
if (!canManage) {
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
return;
}
const { eventId, equipmentId } = req.body.data;
if (!eventId || !equipmentId) {
res.status(400).json({ error: 'eventId and equipmentId are required' });
return;
}
const eventDoc = await db.collection('events').doc(eventId).get();
if (!eventDoc.exists) {
res.status(404).json({ error: 'Event not found' });
return;
}
const eventData = eventDoc.data();
const assignedEquipment = eventData.assignedEquipment || [];
const updatedEquipment = assignedEquipment.map(eq => {
if (eq.equipmentId === equipmentId) {
return { ...eq, isUnloaded: true };
}
return eq;
});
const allUnloaded = updatedEquipment.every(eq => eq.isUnloaded);
const updateData = {
assignedEquipment: updatedEquipment,
unloadingStatus: allUnloaded ? 'completed' : 'inProgress',
};
await db.collection('events').doc(eventId).update(updateData);
res.status(200).json({ success: true, allUnloaded });
} catch (error) {
logger.error("Error validating equipment unloading:", error);
res.status(500).json({ error: error.message });
}
}));
// Valider tous les équipements pour le déchargement
exports.validateAllUnloading = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
if (!canManage) {
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
return;
}
const { eventId } = req.body.data;
if (!eventId) {
res.status(400).json({ error: 'eventId is required' });
return;
}
const eventDoc = await db.collection('events').doc(eventId).get();
if (!eventDoc.exists) {
res.status(404).json({ error: 'Event not found' });
return;
}
const eventData = eventDoc.data();
const assignedEquipment = eventData.assignedEquipment || [];
const updatedEquipment = assignedEquipment.map(eq => ({
...eq,
isUnloaded: true,
}));
await db.collection('events').doc(eventId).update({
assignedEquipment: updatedEquipment,
unloadingStatus: 'completed',
});
res.status(200).json({ success: true });
} catch (error) {
logger.error("Error validating all unloading:", error);
res.status(500).json({ error: error.message });
}
}));
// Valider un équipement individuel pour le retour
exports.validateEquipmentReturn = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
if (!canManage) {
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
return;
}
const { eventId, equipmentId, returnedQuantity } = req.body.data;
if (!eventId || !equipmentId) {
res.status(400).json({ error: 'eventId and equipmentId are required' });
return;
}
const eventDoc = await db.collection('events').doc(eventId).get();
if (!eventDoc.exists) {
res.status(404).json({ error: 'Event not found' });
return;
}
const eventData = eventDoc.data();
const assignedEquipment = eventData.assignedEquipment || [];
const updatedEquipment = assignedEquipment.map(eq => {
if (eq.equipmentId === equipmentId) {
return {
...eq,
isReturned: true,
returnedQuantity: returnedQuantity !== undefined ? returnedQuantity : eq.returnedQuantity,
};
}
return eq;
});
const allReturned = updatedEquipment.every(eq => eq.isReturned);
const updateData = {
assignedEquipment: updatedEquipment,
returnStatus: allReturned ? 'completed' : 'inProgress',
};
await db.collection('events').doc(eventId).update(updateData);
// Mettre à jour le stock si c'est un consommable
if (returnedQuantity !== undefined) {
const equipmentDoc = await db.collection('equipments').doc(equipmentId).get();
if (equipmentDoc.exists) {
const equipmentData = equipmentDoc.data();
if (equipmentData.hasQuantity) {
const currentAvailable = equipmentData.availableQuantity || 0;
await db.collection('equipments').doc(equipmentId).update({
availableQuantity: currentAvailable + returnedQuantity,
});
}
}
}
res.status(200).json({ success: true, allReturned });
} catch (error) {
logger.error("Error validating equipment return:", error);
res.status(500).json({ error: error.message });
}
}));
// Valider tous les retours
exports.validateAllReturn = onRequest(httpOptions, withCors(async (req, res) => {
try {
const decodedToken = await auth.authenticateUser(req);
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_events');
if (!canManage) {
res.status(403).json({ error: 'Forbidden: Requires manage_events permission' });
return;
}
const { eventId, returnedQuantities } = req.body.data;
if (!eventId) {
res.status(400).json({ error: 'eventId is required' });
return;
}
const eventDoc = await db.collection('events').doc(eventId).get();
if (!eventDoc.exists) {
res.status(404).json({ error: 'Event not found' });
return;
}
const eventData = eventDoc.data();
const assignedEquipment = eventData.assignedEquipment || [];
const updatedEquipment = assignedEquipment.map(eq => {
const returnedQty = returnedQuantities?.[eq.equipmentId] || eq.returnedQuantity || eq.quantity;
return {
...eq,
isReturned: true,
returnedQuantity: returnedQty,
};
});
await db.collection('events').doc(eventId).update({
assignedEquipment: updatedEquipment,
returnStatus: 'completed',
});
// Mettre à jour le statut des équipements à "available" et gérer les stocks
for (const equipment of updatedEquipment) {
const equipmentDoc = await db.collection('equipments').doc(equipment.equipmentId).get();
if (equipmentDoc.exists) {
const equipmentData = equipmentDoc.data();
// Mettre à jour le statut uniquement pour les équipements non quantifiables
if (!equipmentData.hasQuantity) {
await updateEquipmentStatus(equipment.equipmentId, 'available');
}
// Restaurer le stock pour les consommables
if (equipmentData.hasQuantity && equipment.returnedQuantity) {
const currentAvailable = equipmentData.availableQuantity || 0;
await db.collection('equipments').doc(equipment.equipmentId).update({
availableQuantity: currentAvailable + equipment.returnedQuantity,
updatedAt: admin.firestore.Timestamp.now(),
});
}
}
}
res.status(200).json({ success: true });
} catch (error) {
logger.error("Error validating all return:", error);
res.status(500).json({ error: error.message });
}
}));