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:
@@ -393,6 +393,30 @@ exports.deleteContainer = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
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' });
|
||||
@@ -613,8 +637,73 @@ exports.createMaintenance = onRequest(httpOptions, withCors(async (req, res) =>
|
||||
]);
|
||||
|
||||
const docRef = await db.collection('maintenances').add(dataToSave);
|
||||
const maintenanceId = docRef.id;
|
||||
|
||||
res.status(201).json({ id: docRef.id, message: 'Maintenance created successfully' });
|
||||
// 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 });
|
||||
@@ -775,6 +864,96 @@ exports.createUser = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
}
|
||||
}));
|
||||
|
||||
// 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 {
|
||||
@@ -810,6 +989,12 @@ exports.updateUser = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -820,6 +1005,49 @@ exports.updateUser = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
}
|
||||
}));
|
||||
|
||||
// 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
|
||||
// ============================================================================
|
||||
@@ -946,7 +1174,15 @@ exports.updateEventEquipment = onRequest(httpOptions, withCors(async (req, res)
|
||||
|
||||
const eventData = eventDoc.data();
|
||||
const isAdminUser = await auth.hasPermission(decodedToken.uid, 'edit_event');
|
||||
const isAssigned = eventData.workforce?.some(ref => ref.path.endsWith(decodedToken.uid));
|
||||
|
||||
// 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' });
|
||||
@@ -1573,148 +1809,6 @@ exports.getUser = onCall(async (request) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// USER MANAGEMENT - Delete & Update
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Supprime un utilisateur (Auth + Firestore)
|
||||
* Permissions: 'delete_user' OU propriétaire
|
||||
*/
|
||||
exports.deleteUser = onCall(async (request) => {
|
||||
const { auth, data } = request;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error("Unauthorized: Authentication required");
|
||||
}
|
||||
|
||||
const { userId } = data;
|
||||
if (!userId) {
|
||||
throw new Error("userId is required");
|
||||
}
|
||||
|
||||
try {
|
||||
// Vérifier les permissions
|
||||
const callerDoc = await db.collection("users").doc(auth.uid).get();
|
||||
const callerData = callerDoc.data();
|
||||
|
||||
if (!callerData) {
|
||||
throw new Error("Caller user not found");
|
||||
}
|
||||
|
||||
// Vérifier si l'utilisateur a la permission delete_user
|
||||
let canDelete = false;
|
||||
if (callerData.role) {
|
||||
const roleDoc = await callerData.role.get();
|
||||
const roleData = roleDoc.data();
|
||||
canDelete = roleData?.permissions?.includes("delete_user") || false;
|
||||
}
|
||||
|
||||
// Ou si c'est le propriétaire (mais on ne peut pas se supprimer soi-même)
|
||||
if (userId === auth.uid) {
|
||||
throw new Error("Cannot delete your own account");
|
||||
}
|
||||
|
||||
if (!canDelete) {
|
||||
throw new Error("Unauthorized: Missing delete_user permission");
|
||||
}
|
||||
|
||||
// Supprimer de Firebase Auth
|
||||
try {
|
||||
await admin.auth().deleteUser(userId);
|
||||
} catch (authError) {
|
||||
logger.warn(`Could not delete user from Auth: ${authError.message}`);
|
||||
// Continuer même si Auth échoue (l'utilisateur peut ne plus exister dans Auth)
|
||||
}
|
||||
|
||||
// Supprimer de Firestore
|
||||
await db.collection("users").doc(userId).delete();
|
||||
|
||||
logger.info(`User ${userId} deleted by ${auth.uid}`);
|
||||
return { success: true, message: "User deleted successfully" };
|
||||
} catch (error) {
|
||||
logger.error("Error deleting user:", error);
|
||||
throw new Error(error.message || "Failed to delete user");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Met à jour un utilisateur
|
||||
* Permissions: 'edit_user' OU propriétaire (modifications limitées)
|
||||
*/
|
||||
exports.updateUser = onCall(async (request) => {
|
||||
const { auth, data } = request;
|
||||
|
||||
if (!auth) {
|
||||
throw new Error("Unauthorized: Authentication required");
|
||||
}
|
||||
|
||||
const { userId, userData } = data;
|
||||
if (!userId || !userData) {
|
||||
throw new Error("userId and userData are required");
|
||||
}
|
||||
|
||||
try {
|
||||
// Vérifier les permissions
|
||||
const callerDoc = await db.collection("users").doc(auth.uid).get();
|
||||
const callerData = callerDoc.data();
|
||||
|
||||
if (!callerData) {
|
||||
throw new Error("Caller user not found");
|
||||
}
|
||||
|
||||
let canEditAll = false;
|
||||
if (callerData.role) {
|
||||
const roleDoc = await callerData.role.get();
|
||||
const roleData = roleDoc.data();
|
||||
canEditAll = roleData?.permissions?.includes("edit_user") || false;
|
||||
}
|
||||
|
||||
const isOwner = userId === auth.uid;
|
||||
|
||||
// Si pas de permission edit_user et pas propriétaire, refuser
|
||||
if (!canEditAll && !isOwner) {
|
||||
throw new Error("Unauthorized: Missing edit_user permission");
|
||||
}
|
||||
|
||||
// Préparer les données à mettre à jour
|
||||
const updateData = {
|
||||
firstName: userData.firstName,
|
||||
lastName: userData.lastName,
|
||||
email: userData.email,
|
||||
phoneNumber: userData.phoneNumber || "",
|
||||
};
|
||||
|
||||
// Seuls ceux avec edit_user peuvent changer le rôle
|
||||
if (userData.role) {
|
||||
if (!canEditAll) {
|
||||
throw new Error("Unauthorized: Cannot change role without edit_user permission");
|
||||
}
|
||||
// Créer la référence au rôle
|
||||
updateData.role = db.collection("roles").doc(userData.role);
|
||||
}
|
||||
|
||||
// Mettre à jour Firestore
|
||||
await db.collection("users").doc(userId).update(updateData);
|
||||
|
||||
// Mettre à jour Firebase Auth si email a changé (seulement avec edit_user)
|
||||
if (userData.email && canEditAll) {
|
||||
try {
|
||||
await admin.auth().updateUser(userId, {
|
||||
email: userData.email,
|
||||
});
|
||||
} catch (authError) {
|
||||
logger.warn(`Could not update email in Auth: ${authError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`User ${userId} updated by ${auth.uid}`);
|
||||
return { success: true, message: "User updated successfully" };
|
||||
} catch (error) {
|
||||
logger.error("Error updating user:", error);
|
||||
throw new Error(error.message || "Failed to update user");
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// EQUIPMENT AVAILABILITY - Vérification de disponibilité
|
||||
@@ -2264,3 +2358,438 @@ exports.deleteMaintenance = onRequest(httpOptions, withCors(async (req, res) =>
|
||||
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 });
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user