feat: refactor de la gestion des utilisateurs et migration de la logique métier vers les Cloud Functions
Cette mise à jour majeure refactorise entièrement la gestion des utilisateurs pour la faire passer par des Cloud Functions sécurisées et migre une part importante de la logique métier (gestion des événements, maintenances, containers) du client vers le backend.
**Gestion des Utilisateurs (Backend & Frontend):**
- **Nouvelle fonction `createUserWithInvite` :**
- Crée l'utilisateur dans Firebase Auth avec un mot de passe temporaire.
- Crée le document utilisateur correspondant dans Firestore.
- Envoie automatiquement un e-mail de réinitialisation de mot de passe (via l'API REST de Firebase et `axios`) pour que l'utilisateur définisse son propre mot de passe, améliorant la sécurité et l'expérience d'intégration.
- **Refactorisation de `updateUser` et `deleteUser` :**
- Les anciennes fonctions `onCall` sont remplacées par des fonctions `onRequest` (HTTP) standards, alignées avec le reste de l'API.
- La logique de suppression gère désormais la suppression dans Auth et Firestore.
- **Réinitialisation de Mot de Passe (UI) :**
- Ajout d'un bouton "Réinitialiser le mot de passe" sur la carte utilisateur, permettant aux administrateurs d'envoyer un e-mail de réinitialisation à n'importe quel utilisateur.
- **Amélioration de l'UI :**
- Boîte de dialogue de confirmation améliorée pour la suppression d'un utilisateur.
- Notifications (Snackbars) pour les opérations de création, suppression et réinitialisation de mot de passe.
**Migration de la Logique Métier vers les Cloud Functions:**
- **Gestion de la Préparation d'Événements :**
- Migration complète de la logique de validation des étapes (préparation, chargement, déchargement, retour) du client vers de nouvelles Cloud Functions (`validateEquipmentPreparation`, `validateAllLoading`, etc.).
- Le backend gère désormais la mise à jour des statuts de l'événement (`inProgress`, `completed`) et des équipements (`inUse`, `available`).
- Le code frontend (`EventPreparationService`) a été simplifié pour appeler ces nouvelles fonctions au lieu d'effectuer des écritures directes sur Firestore.
- **Création de Maintenance :**
- La fonction `createMaintenance` gère maintenant la mise à jour des équipements associés (`maintenanceIds`) et la création d'alertes (`maintenanceDue`) si une maintenance est prévue prochainement. La logique client a été supprimée.
- **Suppression de Container :**
- La fonction `deleteContainer` a été améliorée pour nettoyer automatiquement les références (`parentBoxIds`) dans tous les équipements contenus avant de supprimer le container.
**Refactorisation et Corrections (Backend & Frontend) :**
- **Fiabilisation des Appels API (Frontend) :**
- Le `ApiService` a été renforcé pour convertir de manière plus robuste les données (notamment les `Map` de type `_JsonMap`) en JSON standard avant de les envoyer aux Cloud Functions, évitant ainsi des erreurs de sérialisation.
- **Correction des Références (Backend) :**
- La fonction `updateUser` convertit correctement les `roleId` (string) en `DocumentReference` Firestore.
- Sécurisation de la vérification de l'assignation d'un utilisateur à un événement (`workforce`) pour éviter les erreurs sur des références nulles.
- **Dépendance (Backend) :**
- Ajout de la librairie `axios` pour effectuer des appels à l'API REST de Firebase.
This commit is contained in:
@@ -510,6 +510,181 @@ exports.getContainersByIds = onRequest(httpOptions, withCors(async (req, res) =>
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1702,6 +1877,79 @@ exports.deleteAlert = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -2793,3 +3041,391 @@ exports.validateAllReturn = onRequest(httpOptions, withCors(async (req, res) =>
|
||||
}
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// 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 });
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user