Cette mise à jour verrouille l'accès direct à Firestore depuis le client pour renforcer la sécurité et introduit une gestion complète des prix HT/TTC dans toute l'application. Elle apporte également des améliorations significatives des permissions, des optimisations de performance et de nouvelles fonctionnalités.
### Sécurité et Backend
- **Firestore Rules :** Ajout de `firestore.rules` qui bloque par défaut tous les accès en lecture/écriture depuis le client. Toutes les opérations de données doivent maintenant passer par les Cloud Functions, renforçant considérablement la sécurité.
- **Index Firestore :** Création d'un fichier `firestore.indexes.json` pour optimiser les requêtes sur la collection `events`.
- **Cloud Functions :** Les fonctions de création/mise à jour d'événements ont été adaptées pour accepter des ID de documents (utilisateurs, type d'événement) et les convertir en `DocumentReference` côté serveur, simplifiant les appels depuis le client.
### Gestion des Prix HT/TTC
- **Calcul Automatisé :** Introduction d'un helper `PriceHelpers` et d'un widget `PriceHtTtcFields` pour calculer et synchroniser automatiquement les prix HT et TTC dans le formulaire d'événement.
- **Affichage Détaillé :**
- Les détails des événements et des options affichent désormais les prix HT, la TVA et le TTC séparément pour plus de clarté.
- Le prix de base (`basePrice`) est maintenant traité comme un prix TTC dans toute l'application.
### Permissions et Rôles
- **Centralisation (`AppPermission`) :** Création d'une énumération `AppPermission` pour centraliser toutes les permissions de l'application, avec descriptions et catégories.
- **Rôles Prédéfinis :** Définition de rôles standards (Admin, Manager, Technicien, User) avec des jeux de permissions prédéfinis.
- **Filtre par Utilisateur :** Ajout d'un filtre par utilisateur sur la page Calendrier, visible uniquement pour les utilisateurs ayant la permission `view_all_user_events`.
### Améliorations et Optimisations (Frontend)
- **`DebugLog` :** Ajout d'un utilitaire `DebugLog` pour gérer les logs, qui sont automatiquement désactivés en mode production.
- **Optimisation du Sélecteur d'Équipement :**
- La boîte de dialogue de sélection d'équipement a été lourdement optimisée pour éviter les reconstructions complètes de la liste lors de la sélection/désélection d'items.
- Utilisation de `ValueNotifier` et de caches locaux (`_cachedContainers`, `_cachedEquipment`) pour des mises à jour d'UI plus ciblées et fluides.
- La position du scroll est désormais préservée.
- **Catégorie d'Équipement :** Ajout de la catégorie `Vehicle` (Véhicule) pour les équipements.
- **Formulaires :** Les formulaires de création/modification d'événements et d'équipements ont été nettoyés de leurs logs de débogage excessifs.
3445 lines
115 KiB
JavaScript
3445 lines
115 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;
|
|
}
|
|
|
|
if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
|
|
res.status(400).json({ error: 'Update data is required and must be a non-empty object' });
|
|
return;
|
|
}
|
|
|
|
// Empêcher la modification de l'ID
|
|
delete data.id;
|
|
|
|
// Ajouter updatedAt
|
|
data.updatedAt = admin.firestore.Timestamp.now();
|
|
|
|
const dataToSave = helpers.deserializeTimestamps(data, [
|
|
'purchaseDate', 'lastMaintenanceDate', 'nextMaintenanceDate'
|
|
]);
|
|
|
|
await db.collection('equipments').doc(equipmentId).update(dataToSave);
|
|
|
|
res.status(200).json({ message: 'Equipment updated successfully' });
|
|
} catch (error) {
|
|
logger.error("Error updating equipment:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
// Supprimer un équipement
|
|
exports.deleteEquipment = 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;
|
|
|
|
// Désérialiser les timestamps
|
|
let dataToSave = helpers.deserializeTimestamps(eventData, [
|
|
'StartDateTime', 'EndDateTime', 'createdAt', 'updatedAt'
|
|
]);
|
|
|
|
// Convertir les IDs en DocumentReference pour compatibilité avec l'ancien format
|
|
dataToSave = helpers.convertIdsToReferences(dataToSave);
|
|
|
|
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();
|
|
|
|
// Désérialiser les timestamps
|
|
let dataToSave = helpers.deserializeTimestamps(data, [
|
|
'StartDateTime', 'EndDateTime'
|
|
]);
|
|
|
|
// Convertir les IDs en DocumentReference pour compatibilité avec l'ancien format
|
|
dataToSave = helpers.convertIdsToReferences(dataToSave);
|
|
|
|
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 });
|
|
}
|
|
}));
|
|
|