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.
3432 lines
114 KiB
JavaScript
3432 lines
114 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 });
|
|
}
|
|
}));
|
|
|
|
/**
|
|
* Ajouter un équipement à un container
|
|
*/
|
|
exports.addEquipmentToContainer = 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, equipmentId, userId } = req.body.data;
|
|
|
|
if (!containerId || !equipmentId) {
|
|
res.status(400).json({ error: 'containerId and equipmentId are required' });
|
|
return;
|
|
}
|
|
|
|
// Récupérer le container
|
|
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 || [];
|
|
|
|
// Vérifier si l'équipement n'est pas déjà dans ce container
|
|
if (equipmentIds.includes(equipmentId)) {
|
|
res.status(400).json({ success: false, message: 'Cet équipement est déjà dans ce container' });
|
|
return;
|
|
}
|
|
|
|
// Récupérer l'équipement
|
|
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 || [];
|
|
|
|
// Vérifier les autres containers
|
|
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(", ")}`);
|
|
}
|
|
}
|
|
|
|
// Mettre à jour le container
|
|
await db.collection('containers').doc(containerId).update({
|
|
equipmentIds: [...equipmentIds, equipmentId],
|
|
updatedAt: admin.firestore.Timestamp.now(),
|
|
});
|
|
|
|
// Mettre à jour l'équipement
|
|
await db.collection('equipments').doc(equipmentId).update({
|
|
parentBoxIds: [...parentBoxIds, containerId],
|
|
updatedAt: admin.firestore.Timestamp.now(),
|
|
});
|
|
|
|
// Ajouter une entrée dans l'historique
|
|
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); // Garder les 100 dernières entrées
|
|
|
|
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 = 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, equipmentId, userId } = req.body.data;
|
|
|
|
if (!containerId || !equipmentId) {
|
|
res.status(400).json({ error: 'containerId and equipmentId are required' });
|
|
return;
|
|
}
|
|
|
|
// Récupérer le container
|
|
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 || [];
|
|
|
|
// Retirer l'équipement du container
|
|
const updatedEquipmentIds = equipmentIds.filter(id => id !== equipmentId);
|
|
|
|
await db.collection('containers').doc(containerId).update({
|
|
equipmentIds: updatedEquipmentIds,
|
|
updatedAt: admin.firestore.Timestamp.now(),
|
|
});
|
|
|
|
// Mettre à jour l'équipement
|
|
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(),
|
|
});
|
|
}
|
|
|
|
// Ajouter une entrée dans l'historique
|
|
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 });
|
|
}
|
|
}));
|
|
|
|
|
|
// ============================================================================
|
|
// 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 });
|
|
}
|
|
}));
|
|
|
|
/**
|
|
* Créer une nouvelle alerte
|
|
*/
|
|
exports.createAlert = 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 { type, title, message, severity, equipmentId } = req.body.data;
|
|
|
|
if (!type || !message) {
|
|
res.status(400).json({ error: 'type and message are required' });
|
|
return;
|
|
}
|
|
|
|
// Vérifier si une alerte similaire existe déjà (éviter les doublons)
|
|
const existingAlertsQuery = await db.collection('alerts')
|
|
.where('type', '==', type)
|
|
.where('isRead', '==', false)
|
|
.get();
|
|
|
|
let alertExists = false;
|
|
if (equipmentId) {
|
|
// Pour les alertes liées à un équipement, vérifier aussi l'equipmentId
|
|
alertExists = existingAlertsQuery.docs.some(doc =>
|
|
doc.data().equipmentId === equipmentId
|
|
);
|
|
} else {
|
|
// Pour les autres alertes, vérifier le message
|
|
alertExists = existingAlertsQuery.docs.some(doc =>
|
|
doc.data().message === message
|
|
);
|
|
}
|
|
|
|
if (alertExists) {
|
|
res.status(200).json({
|
|
success: true,
|
|
message: 'Alert already exists',
|
|
skipped: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Créer la nouvelle alerte
|
|
const alertData = {
|
|
type: type,
|
|
title: title || 'Alerte',
|
|
message: message,
|
|
severity: severity || 'MEDIUM',
|
|
isRead: false,
|
|
createdAt: admin.firestore.Timestamp.now(),
|
|
};
|
|
|
|
if (equipmentId) {
|
|
alertData.equipmentId = equipmentId;
|
|
}
|
|
|
|
const alertRef = await db.collection('alerts').add(alertData);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
alertId: alertRef.id
|
|
});
|
|
} catch (error) {
|
|
logger.error("Error creating 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 });
|
|
}
|
|
}));
|
|
|
|
// ============================================================================
|
|
// AVAILABILITY & STOCK CHECK
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Vérifier la disponibilité d'un équipement pour une période donnée
|
|
*/
|
|
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 } = req.body.data;
|
|
|
|
if (!equipmentId || !startDate || !endDate) {
|
|
res.status(400).json({ error: 'equipmentId, 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 les événements qui chevauchent la période
|
|
const eventsSnapshot = await db.collection('events')
|
|
.where('StartDateTime', '<=', end)
|
|
.where('EndDateTime', '>=', start)
|
|
.where('status', '!=', 'CANCELLED')
|
|
.get();
|
|
|
|
const conflicts = [];
|
|
|
|
eventsSnapshot.docs.forEach(doc => {
|
|
const eventData = doc.data();
|
|
const assignedEquipment = eventData.assignedEquipment || [];
|
|
|
|
for (const eq of assignedEquipment) {
|
|
if (eq.equipmentId === equipmentId) {
|
|
conflicts.push({
|
|
eventId: doc.id,
|
|
eventName: eventData.Name || 'Sans nom',
|
|
startDate: eventData.StartDateTime.toDate().toISOString(),
|
|
endDate: eventData.EndDateTime.toDate().toISOString(),
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
res.status(200).json({ conflicts });
|
|
} catch (error) {
|
|
logger.error("Error checking equipment availability:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
/**
|
|
* Trouver des alternatives (même modèle) disponibles pour une période donnée
|
|
*/
|
|
exports.findAlternativeEquipment = 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 { 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 = 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 { 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 = onRequest(httpOptions, withCors(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 });
|
|
}
|
|
}));
|
|
|
|
/**
|
|
* Vérifier les maintenances à venir et créer des alertes
|
|
*/
|
|
exports.checkUpcomingMaintenances = 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 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 = 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 { 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 });
|
|
}
|
|
}));
|
|
|