Migration complète du backend pour utiliser des Cloud Functions comme couche API sécurisée, en remplacement des appels directs à Firestore depuis le client.
**Backend (Cloud Functions):**
- **Centralisation CORS :** Ajout d'un middleware `withCors` et d'une configuration `httpOptions` pour gérer uniformément les en-têtes CORS et les requêtes `OPTIONS` sur toutes les fonctions.
- **Nouvelles Fonctions de Lecture (GET) :**
- `getEquipments`, `getContainers`, `getEvents`, `getUsers`, `getOptions`, `getEventTypes`, `getRoles`, `getMaintenances`, `getAlerts`.
- Ces fonctions gèrent les permissions côté serveur, masquant les données sensibles (ex: prix des équipements) pour les utilisateurs non-autorisés.
- `getEvents` retourne également une map des utilisateurs (`usersMap`) pour optimiser le chargement des données de la main d'œuvre.
- **Nouvelle Fonction de Recherche :**
- `getContainersByEquipment` : Endpoint dédié pour trouver efficacement tous les containers qui contiennent un équipement spécifique.
- **Nouvelles Fonctions d'Écriture (CRUD) :**
- Fonctions CRUD complètes pour `eventTypes` (`create`, `update`, `delete`), incluant la validation (unicité du nom, vérification des événements futurs avant suppression).
- **Mise à jour de Fonctions Existantes :**
- Toutes les fonctions CRUD existantes (`create/update/deleteEquipment`, `create/update/deleteContainer`, etc.) sont wrappées avec le nouveau gestionnaire CORS.
**Frontend (Flutter):**
- **Introduction du `DataService` :** Nouveau service centralisant tous les appels aux Cloud Functions, servant d'intermédiaire entre l'UI/Providers et l'API.
- **Refactorisation des Providers :**
- `EquipmentProvider`, `ContainerProvider`, `EventProvider`, `UsersProvider`, `MaintenanceProvider` et `AlertProvider` ont été refactorisés pour utiliser le `DataService` au lieu d'accéder directement à Firestore.
- Les `Stream` Firestore sont remplacés par des chargements de données via des méthodes `Future` (`loadEquipments`, `loadEvents`, etc.).
- **Gestion des Relations Équipement-Container :**
- Le modèle `EquipmentModel` ne stocke plus `parentBoxIds`.
- La relation est maintenant gérée par le `ContainerModel` qui contient `equipmentIds`.
- Le `ContainerEquipmentService` est introduit pour utiliser la nouvelle fonction `getContainersByEquipment`.
- L'affichage des boîtes parentes (`EquipmentParentContainers`) et le formulaire d'équipement (`EquipmentFormPage`) ont été mis à jour pour refléter ce nouveau modèle de données, synchronisant les ajouts/suppressions d'équipements dans les containers.
- **Amélioration de l'UI :**
- Nouveau widget `ParentBoxesSelector` pour une sélection améliorée et visuelle des boîtes parentes dans le formulaire d'équipement.
- Refonte visuelle de `EquipmentParentContainers` pour une meilleure présentation.
1416 lines
46 KiB
JavaScript
1416 lines
46 KiB
JavaScript
/**
|
|
* EM2RP Cloud Functions
|
|
* Architecture backend sécurisée avec authentification et permissions
|
|
*/
|
|
|
|
const { onRequest } = require("firebase-functions/v2/https");
|
|
const logger = require("firebase-functions/logger");
|
|
const admin = require('firebase-admin');
|
|
const { Storage } = require('@google-cloud/storage');
|
|
|
|
// Utilitaires
|
|
const auth = require('./utils/auth');
|
|
const helpers = require('./utils/helpers');
|
|
|
|
// Initialisation
|
|
admin.initializeApp();
|
|
const storage = new Storage();
|
|
const db = admin.firestore();
|
|
|
|
// Configuration commune pour toutes les fonctions HTTP
|
|
const httpOptions = {
|
|
cors: true,
|
|
invoker: 'public', // Permet les invocations non authentifiées (l'auth est gérée par notre token Firebase)
|
|
// Version: 2.0 - Ajout de l'invoker public pour résoudre les problèmes CORS
|
|
};
|
|
|
|
// ============================================================================
|
|
// CORS Middleware
|
|
// ============================================================================
|
|
const setCorsHeaders = (res, req) => {
|
|
// Permettre toutes les origines en développement/production
|
|
const origin = req.headers.origin || req.headers.referer || '*';
|
|
res.set('Access-Control-Allow-Origin', origin);
|
|
res.set('Access-Control-Allow-Credentials', 'true');
|
|
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
res.set('Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Origin, X-Requested-With');
|
|
res.set('Access-Control-Max-Age', '3600');
|
|
};
|
|
|
|
// Wrapper pour les fonctions avec CORS
|
|
const withCors = (handler) => {
|
|
return async (req, res) => {
|
|
// Définir les headers CORS pour toutes les requêtes
|
|
setCorsHeaders(res, req);
|
|
|
|
// Gérer les requêtes preflight OPTIONS
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
// Exécuter le handler
|
|
try {
|
|
await handler(req, res);
|
|
} catch (error) {
|
|
logger.error("Unhandled error:", error);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
// ============================================================================
|
|
// STORAGE - Move Event File
|
|
// ============================================================================
|
|
exports.moveEventFileV2 = onRequest(httpOptions, withCors(async (req, res) => {
|
|
try {
|
|
const decodedToken = await auth.authenticateUser(req);
|
|
const { sourcePath, destinationPath } = req.body.data || {};
|
|
|
|
if (!sourcePath || !destinationPath) {
|
|
res.status(400).json({ error: 'Source and destination paths are required.' });
|
|
return;
|
|
}
|
|
|
|
const bucketName = admin.storage().bucket().name;
|
|
const bucket = storage.bucket(bucketName);
|
|
|
|
await bucket.file(sourcePath).copy(bucket.file(destinationPath));
|
|
await bucket.file(sourcePath).delete();
|
|
const [url] = await bucket.file(destinationPath).getSignedUrl({
|
|
action: 'read',
|
|
expires: '03-01-2500',
|
|
});
|
|
|
|
res.status(200).json({ url });
|
|
} catch (error) {
|
|
logger.error("Error moving file:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
// ============================================================================
|
|
// EQUIPMENT - CRUD
|
|
// ============================================================================
|
|
|
|
// Créer un équipement (admin ou manage_equipment)
|
|
exports.createEquipment = onRequest(httpOptions, withCors(async (req, res) => {
|
|
try {
|
|
const decodedToken = await auth.authenticateUser(req);
|
|
const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment');
|
|
|
|
if (!hasAccess) {
|
|
res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' });
|
|
return;
|
|
}
|
|
|
|
const equipmentData = req.body.data;
|
|
const equipmentId = equipmentData.id;
|
|
|
|
if (!equipmentId) {
|
|
res.status(400).json({ error: 'Equipment ID is required' });
|
|
return;
|
|
}
|
|
|
|
// Vérifier unicité de l'ID
|
|
const existingDoc = await db.collection('equipments').doc(equipmentId).get();
|
|
if (existingDoc.exists) {
|
|
res.status(409).json({ error: 'Equipment ID already exists' });
|
|
return;
|
|
}
|
|
|
|
// Convertir les timestamps
|
|
const dataToSave = helpers.deserializeTimestamps(equipmentData, [
|
|
'createdAt', 'updatedAt', 'purchaseDate', 'lastMaintenanceDate', 'nextMaintenanceDate'
|
|
]);
|
|
|
|
await db.collection('equipments').doc(equipmentId).set(dataToSave);
|
|
|
|
res.status(201).json({ id: equipmentId, message: 'Equipment created successfully' });
|
|
} catch (error) {
|
|
logger.error("Error creating equipment:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
// Mettre à jour un équipement
|
|
exports.updateEquipment = onRequest(httpOptions, withCors(async (req, res) => {
|
|
try {
|
|
const decodedToken = await auth.authenticateUser(req);
|
|
const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment');
|
|
|
|
if (!hasAccess) {
|
|
res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' });
|
|
return;
|
|
}
|
|
|
|
const { equipmentId, data } = req.body.data;
|
|
|
|
if (!equipmentId) {
|
|
res.status(400).json({ error: 'Equipment ID is required' });
|
|
return;
|
|
}
|
|
|
|
// Empêcher la modification de l'ID
|
|
delete data.id;
|
|
|
|
// Ajouter updatedAt
|
|
data.updatedAt = admin.firestore.Timestamp.now();
|
|
|
|
const dataToSave = helpers.deserializeTimestamps(data, [
|
|
'purchaseDate', 'lastMaintenanceDate', 'nextMaintenanceDate'
|
|
]);
|
|
|
|
await db.collection('equipments').doc(equipmentId).update(dataToSave);
|
|
|
|
res.status(200).json({ message: 'Equipment updated successfully' });
|
|
} catch (error) {
|
|
logger.error("Error updating equipment:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
// Supprimer un équipement
|
|
exports.deleteEquipment = onRequest(httpOptions, withCors(async (req, res) => {
|
|
try {
|
|
const decodedToken = await auth.authenticateUser(req);
|
|
const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment');
|
|
|
|
if (!hasAccess) {
|
|
res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' });
|
|
return;
|
|
}
|
|
|
|
const { equipmentId } = req.body.data;
|
|
|
|
if (!equipmentId) {
|
|
res.status(400).json({ error: 'Equipment ID is required' });
|
|
return;
|
|
}
|
|
|
|
// Vérifier si l'équipement est utilisé dans des événements actifs
|
|
const eventsSnapshot = await db.collection('events')
|
|
.where('status', '!=', 'CANCELLED')
|
|
.get();
|
|
|
|
for (const eventDoc of eventsSnapshot.docs) {
|
|
const eventData = eventDoc.data();
|
|
const assignedEquipment = eventData.assignedEquipment || [];
|
|
|
|
if (assignedEquipment.some(eq => eq.equipmentId === equipmentId)) {
|
|
res.status(409).json({
|
|
error: 'Cannot delete equipment: it is assigned to active events',
|
|
eventId: eventDoc.id
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
await db.collection('equipments').doc(equipmentId).delete();
|
|
|
|
res.status(200).json({ message: 'Equipment deleted successfully' });
|
|
} catch (error) {
|
|
logger.error("Error deleting equipment:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
// Récupérer un équipement par ID
|
|
exports.getEquipment = onRequest(httpOptions, withCors(async (req, res) => {
|
|
try {
|
|
const decodedToken = await auth.authenticateUser(req);
|
|
const hasViewAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment');
|
|
const hasManageAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment');
|
|
|
|
if (!hasViewAccess && !hasManageAccess) {
|
|
res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' });
|
|
return;
|
|
}
|
|
|
|
const { equipmentId } = req.body.data || req.query;
|
|
|
|
if (!equipmentId) {
|
|
res.status(400).json({ error: 'Equipment ID is required' });
|
|
return;
|
|
}
|
|
|
|
const doc = await db.collection('equipments').doc(equipmentId).get();
|
|
|
|
if (!doc.exists) {
|
|
res.status(404).json({ error: 'Equipment not found' });
|
|
return;
|
|
}
|
|
|
|
let data = { id: doc.id, ...doc.data() };
|
|
data = helpers.serializeTimestamps(data);
|
|
data = helpers.serializeReferences(data);
|
|
|
|
// Masquer les prix si pas de permission manage_equipment
|
|
data = helpers.maskSensitiveFields(data, hasManageAccess);
|
|
|
|
res.status(200).json({ equipment: data });
|
|
} catch (error) {
|
|
logger.error("Error getting equipment:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
// ============================================================================
|
|
// 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;
|
|
}
|
|
|
|
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 || req.query;
|
|
|
|
if (!equipmentId) {
|
|
res.status(400).json({ error: 'equipmentId is required' });
|
|
return;
|
|
}
|
|
|
|
// Récupérer tous les containers qui contiennent cet équipement
|
|
const containersSnapshot = await db.collection('containers')
|
|
.where('equipmentIds', 'array-contains', equipmentId)
|
|
.get();
|
|
|
|
const containers = [];
|
|
containersSnapshot.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 });
|
|
}
|
|
}));
|
|
|
|
// ============================================================================
|
|
// EVENTS - CRUD
|
|
// ============================================================================
|
|
|
|
// Créer un événement
|
|
exports.createEvent = onRequest(httpOptions, withCors(async (req, res) => {
|
|
try {
|
|
const decodedToken = await auth.authenticateUser(req);
|
|
const hasAccess = await auth.hasPermission(decodedToken.uid, 'edit_event');
|
|
|
|
if (!hasAccess) {
|
|
res.status(403).json({ error: 'Forbidden: Requires edit_event permission' });
|
|
return;
|
|
}
|
|
|
|
const eventData = req.body.data;
|
|
|
|
const dataToSave = helpers.deserializeTimestamps(eventData, [
|
|
'startDateTime', 'endDateTime', 'createdAt', 'updatedAt'
|
|
]);
|
|
|
|
const docRef = await db.collection('events').add(dataToSave);
|
|
|
|
res.status(201).json({ id: docRef.id, message: 'Event created successfully' });
|
|
} catch (error) {
|
|
logger.error("Error creating event:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
// Mettre à jour un événement
|
|
exports.updateEvent = onRequest(httpOptions, withCors(async (req, res) => {
|
|
try {
|
|
const decodedToken = await auth.authenticateUser(req);
|
|
const hasAccess = await auth.hasPermission(decodedToken.uid, 'edit_event');
|
|
|
|
if (!hasAccess) {
|
|
res.status(403).json({ error: 'Forbidden: Requires edit_event permission' });
|
|
return;
|
|
}
|
|
|
|
const requestData = req.body.data;
|
|
const eventId = requestData.eventId;
|
|
|
|
if (!eventId) {
|
|
res.status(400).json({ error: 'Event ID is required' });
|
|
return;
|
|
}
|
|
|
|
// Extraire eventId et préparer les données à sauvegarder
|
|
const { eventId: _, ...data } = requestData;
|
|
|
|
if (!data || Object.keys(data).length === 0) {
|
|
res.status(400).json({ error: 'No data to update' });
|
|
return;
|
|
}
|
|
|
|
delete data.id;
|
|
data.updatedAt = admin.firestore.Timestamp.now();
|
|
|
|
const dataToSave = helpers.deserializeTimestamps(data, [
|
|
'startDateTime', 'endDateTime'
|
|
]);
|
|
|
|
await db.collection('events').doc(eventId).update(dataToSave);
|
|
|
|
res.status(200).json({ message: 'Event updated successfully' });
|
|
} catch (error) {
|
|
logger.error("Error updating event:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
// Supprimer un événement
|
|
exports.deleteEvent = onRequest(httpOptions, withCors(async (req, res) => {
|
|
try {
|
|
const decodedToken = await auth.authenticateUser(req);
|
|
const hasAccess = await auth.hasPermission(decodedToken.uid, 'delete_event');
|
|
|
|
if (!hasAccess) {
|
|
res.status(403).json({ error: 'Forbidden: Requires delete_event permission' });
|
|
return;
|
|
}
|
|
|
|
const { eventId } = req.body.data;
|
|
|
|
if (!eventId) {
|
|
res.status(400).json({ error: 'Event ID is required' });
|
|
return;
|
|
}
|
|
|
|
await db.collection('events').doc(eventId).delete();
|
|
|
|
res.status(200).json({ message: 'Event deleted successfully' });
|
|
} catch (error) {
|
|
logger.error("Error deleting event:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
// ============================================================================
|
|
// MAINTENANCES - CRUD
|
|
// ============================================================================
|
|
|
|
// Créer une maintenance
|
|
exports.createMaintenance = onRequest(httpOptions, withCors(async (req, res) => {
|
|
try {
|
|
const decodedToken = await auth.authenticateUser(req);
|
|
const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_maintenances');
|
|
|
|
if (!hasAccess) {
|
|
res.status(403).json({ error: 'Forbidden: Requires manage_maintenances permission' });
|
|
return;
|
|
}
|
|
|
|
const maintenanceData = req.body.data;
|
|
|
|
const dataToSave = helpers.deserializeTimestamps(maintenanceData, [
|
|
'scheduledDate', 'completedDate', 'createdAt', 'updatedAt'
|
|
]);
|
|
|
|
const docRef = await db.collection('maintenances').add(dataToSave);
|
|
|
|
res.status(201).json({ id: docRef.id, 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 });
|
|
}
|
|
}));
|
|
|
|
// 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;
|
|
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 });
|
|
}
|
|
}));
|
|
|
|
// ============================================================================
|
|
// 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');
|
|
const isAssigned = eventData.workforce?.some(ref => ref.path.endsWith(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 });
|
|
}
|
|
}));
|
|
|
|
// ============================================================================
|
|
// 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;
|
|
}
|
|
|
|
const userData = userDoc.data();
|
|
res.status(200).json({
|
|
users: [{
|
|
id: userDoc.id,
|
|
...helpers.serializeTimestamps(userData)
|
|
}]
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Admin : tous les utilisateurs
|
|
const snapshot = await db.collection('users').get();
|
|
const users = snapshot.docs.map(doc => ({
|
|
id: doc.id,
|
|
...helpers.serializeTimestamps(doc.data())
|
|
}));
|
|
|
|
res.status(200).json({ users });
|
|
} catch (error) {
|
|
logger.error("Error fetching users:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|
|
// ============================================================================
|
|
// CONTAINERS - Récupération par équipement
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Récupère tous les containers contenant un équipement spécifique
|
|
* Accessible à tous les utilisateurs authentifiés
|
|
*/
|
|
exports.getContainersByEquipment = onRequest(httpOptions, withCors(async (req, res) => {
|
|
try {
|
|
// Vérifier l'authentification
|
|
const user = await auth.authenticateUser(req);
|
|
|
|
const equipmentId = req.body.data?.equipmentId;
|
|
|
|
if (!equipmentId) {
|
|
res.status(400).json({ error: 'equipmentId is required' });
|
|
return;
|
|
}
|
|
|
|
logger.info(`Fetching containers for equipment: ${equipmentId}`);
|
|
|
|
// Requête pour trouver tous les containers contenant cet équipement
|
|
const containersSnapshot = await db.collection('containers')
|
|
.where('equipmentIds', 'array-contains', equipmentId)
|
|
.get();
|
|
|
|
const containers = containersSnapshot.docs.map(doc => ({
|
|
id: doc.id,
|
|
...helpers.serializeTimestamps(doc.data())
|
|
}));
|
|
|
|
logger.info(`Found ${containers.length} container(s) for equipment ${equipmentId}`);
|
|
|
|
res.status(200).json({
|
|
containers,
|
|
count: containers.length
|
|
});
|
|
} catch (error) {
|
|
logger.error("Error fetching containers by equipment:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
}));
|
|
|