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
|
// 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
|
// 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 });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ class EventFormController extends ChangeNotifier {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final eventTypeRef = _selectedEventTypeId != null
|
final eventTypeRef = _selectedEventTypeId != null
|
||||||
? FirebaseFirestore.instance.collection('eventTypes').doc(_selectedEventTypeId)
|
? null // Les références Firestore ne sont plus nécessaires, l'ID suffit
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (existingEvent != null) {
|
if (existingEvent != null) {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import 'views/my_account_page.dart';
|
|||||||
import 'views/user_management_page.dart';
|
import 'views/user_management_page.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'providers/local_user_provider.dart';
|
import 'providers/local_user_provider.dart';
|
||||||
import 'services/user_service.dart';
|
|
||||||
import 'views/reset_password_page.dart';
|
import 'views/reset_password_page.dart';
|
||||||
import 'config/env.dart';
|
import 'config/env.dart';
|
||||||
import 'config/api_config.dart';
|
import 'config/api_config.dart';
|
||||||
|
|||||||
@@ -39,17 +39,16 @@ class ContainerProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Pour l'instant, on écoute le stream et on garde la première valeur
|
final containers = await _containerService.getContainers(
|
||||||
_containerService.getContainers(
|
|
||||||
type: _selectedType,
|
type: _selectedType,
|
||||||
status: _selectedStatus,
|
status: _selectedStatus,
|
||||||
searchQuery: _searchQuery,
|
searchQuery: _searchQuery,
|
||||||
).listen((containers) {
|
);
|
||||||
|
|
||||||
_containers = containers;
|
_containers = containers;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading containers: $e');
|
print('Error loading containers: $e');
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
@@ -57,15 +56,32 @@ class ContainerProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stream des containers avec filtres appliqués
|
/// Récupérer les containers avec filtres appliqués
|
||||||
Stream<List<ContainerModel>> get containersStream {
|
Future<List<ContainerModel>> getContainers() async {
|
||||||
return _containerService.getContainers(
|
return await _containerService.getContainers(
|
||||||
type: _selectedType,
|
type: _selectedType,
|
||||||
status: _selectedStatus,
|
status: _selectedStatus,
|
||||||
searchQuery: _searchQuery,
|
searchQuery: _searchQuery,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stream des containers - retourne un stream depuis les données en cache
|
||||||
|
/// Pour compatibilité avec les widgets existants qui utilisent StreamBuilder
|
||||||
|
Stream<List<ContainerModel>> get containersStream async* {
|
||||||
|
// Si les données ne sont pas chargées, charger d'abord
|
||||||
|
if (!_isInitialized) {
|
||||||
|
await loadContainers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Émettre les données actuelles
|
||||||
|
yield _containers;
|
||||||
|
|
||||||
|
// Continuer à émettre les mises à jour du cache
|
||||||
|
// Note: Pour un vrai temps réel, il faudrait implémenter un StreamController
|
||||||
|
// et notifier quand les données changent
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Définir le type sélectionné
|
||||||
/// Définir le type sélectionné
|
/// Définir le type sélectionné
|
||||||
void setSelectedType(ContainerType? type) {
|
void setSelectedType(ContainerType? type) {
|
||||||
_selectedType = type;
|
_selectedType = type;
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ class MaintenanceProvider extends ChangeNotifier {
|
|||||||
// Getters
|
// Getters
|
||||||
List<MaintenanceModel> get maintenances => _maintenances;
|
List<MaintenanceModel> get maintenances => _maintenances;
|
||||||
|
|
||||||
/// Stream des maintenances pour un équipement spécifique
|
/// Récupérer les maintenances pour un équipement spécifique
|
||||||
Stream<List<MaintenanceModel>> getMaintenancesStream(String equipmentId) {
|
Future<List<MaintenanceModel>> getMaintenances(String equipmentId) async {
|
||||||
return _service.getMaintenances(equipmentId);
|
return await _service.getMaintenancesByEquipment(equipmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stream de toutes les maintenances
|
/// Récupérer toutes les maintenances
|
||||||
Stream<List<MaintenanceModel>> get allMaintenancesStream {
|
Future<List<MaintenanceModel>> getAllMaintenances() async {
|
||||||
return _service.getAllMaintenances();
|
return await _service.getAllMaintenances();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Créer une nouvelle maintenance
|
/// Créer une nouvelle maintenance
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'package:em2rp/models/container_model.dart';
|
import 'package:em2rp/models/container_model.dart';
|
||||||
import 'package:em2rp/models/equipment_model.dart';
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
import 'package:em2rp/services/api_service.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
|
import 'package:em2rp/services/data_service.dart';
|
||||||
|
|
||||||
class ContainerService {
|
class ContainerService {
|
||||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
|
||||||
final ApiService _apiService = apiService;
|
final ApiService _apiService = apiService;
|
||||||
|
final DataService _dataService = DataService(apiService);
|
||||||
// Collection references
|
|
||||||
CollectionReference get _containersCollection => _firestore.collection('containers');
|
|
||||||
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CRUD Operations - Utilise le backend sécurisé
|
// CRUD Operations - Utilise le backend sécurisé
|
||||||
@@ -52,11 +48,10 @@ class ContainerService {
|
|||||||
/// Récupérer un container par ID
|
/// Récupérer un container par ID
|
||||||
Future<ContainerModel?> getContainerById(String id) async {
|
Future<ContainerModel?> getContainerById(String id) async {
|
||||||
try {
|
try {
|
||||||
final doc = await _containersCollection.doc(id).get();
|
final containersData = await _dataService.getContainersByIds([id]);
|
||||||
if (doc.exists) {
|
if (containersData.isEmpty) return null;
|
||||||
return ContainerModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
|
||||||
}
|
return ContainerModel.fromMap(containersData.first, id);
|
||||||
return null;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error getting container: $e');
|
print('Error getting container: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -64,30 +59,31 @@ class ContainerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Récupérer tous les containers
|
/// Récupérer tous les containers
|
||||||
Stream<List<ContainerModel>> getContainers({
|
Future<List<ContainerModel>> getContainers({
|
||||||
ContainerType? type,
|
ContainerType? type,
|
||||||
EquipmentStatus? status,
|
EquipmentStatus? status,
|
||||||
String? searchQuery,
|
String? searchQuery,
|
||||||
}) {
|
}) async {
|
||||||
try {
|
try {
|
||||||
Query query = _containersCollection;
|
final containersData = await _dataService.getContainers();
|
||||||
|
|
||||||
// Filtre par type
|
var containerList = containersData
|
||||||
if (type != null) {
|
.map((data) => ContainerModel.fromMap(data, data['id'] as String))
|
||||||
query = query.where('type', isEqualTo: containerTypeToString(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtre par statut
|
|
||||||
if (status != null) {
|
|
||||||
query = query.where('status', isEqualTo: equipmentStatusToString(status));
|
|
||||||
}
|
|
||||||
|
|
||||||
return query.snapshots().map((snapshot) {
|
|
||||||
List<ContainerModel> containerList = snapshot.docs
|
|
||||||
.map((doc) => ContainerModel.fromMap(doc.data() as Map<String, dynamic>, doc.id))
|
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Filtre par recherche texte (côté client)
|
// Filtres côté client
|
||||||
|
if (type != null) {
|
||||||
|
containerList = containerList
|
||||||
|
.where((c) => c.type == type)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status != null) {
|
||||||
|
containerList = containerList
|
||||||
|
.where((c) => c.status == status)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
if (searchQuery != null && searchQuery.isNotEmpty) {
|
if (searchQuery != null && searchQuery.isNotEmpty) {
|
||||||
final lowerSearch = searchQuery.toLowerCase();
|
final lowerSearch = searchQuery.toLowerCase();
|
||||||
containerList = containerList.where((container) {
|
containerList = containerList.where((container) {
|
||||||
@@ -97,7 +93,6 @@ class ContainerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return containerList;
|
return containerList;
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error getting containers: $e');
|
print('Error getting containers: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -111,67 +106,16 @@ class ContainerService {
|
|||||||
String? userId,
|
String? userId,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
// Récupérer le container
|
final response = await _apiService.call('addEquipmentToContainer', {
|
||||||
final container = await getContainerById(containerId);
|
'containerId': containerId,
|
||||||
if (container == null) {
|
'equipmentId': equipmentId,
|
||||||
return {'success': false, 'message': 'Container non trouvé'};
|
if (userId != null) 'userId': userId,
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si l'équipement n'est pas déjà dans ce container
|
|
||||||
if (container.equipmentIds.contains(equipmentId)) {
|
|
||||||
return {'success': false, 'message': 'Cet équipement est déjà dans ce container'};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer l'équipement pour vérifier s'il est déjà dans d'autres containers
|
|
||||||
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
|
||||||
if (!equipmentDoc.exists) {
|
|
||||||
return {'success': false, 'message': 'Équipement non trouvé'};
|
|
||||||
}
|
|
||||||
|
|
||||||
final equipment = EquipmentModel.fromMap(
|
|
||||||
equipmentDoc.data() as Map<String, dynamic>,
|
|
||||||
equipmentDoc.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Avertir si l'équipement est déjà dans d'autres containers
|
|
||||||
List<String> otherContainers = [];
|
|
||||||
if (equipment.parentBoxIds.isNotEmpty) {
|
|
||||||
for (final boxId in equipment.parentBoxIds) {
|
|
||||||
final box = await getContainerById(boxId);
|
|
||||||
if (box != null) {
|
|
||||||
otherContainers.add(box.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour le container
|
|
||||||
final updatedEquipmentIds = [...container.equipmentIds, equipmentId];
|
|
||||||
await updateContainer(containerId, {
|
|
||||||
'equipmentIds': updatedEquipmentIds,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mettre à jour l'équipement
|
|
||||||
final updatedParentBoxIds = [...equipment.parentBoxIds, containerId];
|
|
||||||
await _equipmentCollection.doc(equipmentId).update({
|
|
||||||
'parentBoxIds': updatedParentBoxIds,
|
|
||||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ajouter une entrée dans l'historique
|
|
||||||
await _addHistoryEntry(
|
|
||||||
containerId: containerId,
|
|
||||||
action: 'equipment_added',
|
|
||||||
equipmentId: equipmentId,
|
|
||||||
newValue: equipmentId,
|
|
||||||
userId: userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': true,
|
'success': response['success'] ?? false,
|
||||||
'message': 'Équipement ajouté avec succès',
|
'message': response['message'] ?? '',
|
||||||
'warnings': otherContainers.isNotEmpty
|
'warnings': response['warnings'],
|
||||||
? 'Attention : cet équipement est également dans les boites suivants : ${otherContainers.join(", ")}'
|
|
||||||
: null,
|
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error adding equipment to container: $e');
|
print('Error adding equipment to container: $e');
|
||||||
@@ -186,38 +130,11 @@ class ContainerService {
|
|||||||
String? userId,
|
String? userId,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
// Récupérer le container
|
await _apiService.call('removeEquipmentFromContainer', {
|
||||||
final container = await getContainerById(containerId);
|
'containerId': containerId,
|
||||||
if (container == null) throw Exception('Container non trouvé');
|
'equipmentId': equipmentId,
|
||||||
|
if (userId != null) 'userId': userId,
|
||||||
// Mettre à jour le container
|
|
||||||
final updatedEquipmentIds = container.equipmentIds.where((id) => id != equipmentId).toList();
|
|
||||||
await updateContainer(containerId, {
|
|
||||||
'equipmentIds': updatedEquipmentIds,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mettre à jour l'équipement
|
|
||||||
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
|
||||||
if (equipmentDoc.exists) {
|
|
||||||
final equipment = EquipmentModel.fromMap(
|
|
||||||
equipmentDoc.data() as Map<String, dynamic>,
|
|
||||||
equipmentDoc.id,
|
|
||||||
);
|
|
||||||
final updatedParentBoxIds = equipment.parentBoxIds.where((id) => id != containerId).toList();
|
|
||||||
await _equipmentCollection.doc(equipmentId).update({
|
|
||||||
'parentBoxIds': updatedParentBoxIds,
|
|
||||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ajouter une entrée dans l'historique
|
|
||||||
await _addHistoryEntry(
|
|
||||||
containerId: containerId,
|
|
||||||
action: 'equipment_removed',
|
|
||||||
equipmentId: equipmentId,
|
|
||||||
previousValue: equipmentId,
|
|
||||||
userId: userId,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error removing equipment from container: $e');
|
print('Error removing equipment from container: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -247,14 +164,12 @@ class ContainerService {
|
|||||||
|
|
||||||
// Vérifier la disponibilité de chaque équipement dans le container
|
// Vérifier la disponibilité de chaque équipement dans le container
|
||||||
List<String> unavailableEquipment = [];
|
List<String> unavailableEquipment = [];
|
||||||
for (final equipmentId in container.equipmentIds) {
|
|
||||||
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
|
||||||
if (equipmentDoc.exists) {
|
|
||||||
final equipment = EquipmentModel.fromMap(
|
|
||||||
equipmentDoc.data() as Map<String, dynamic>,
|
|
||||||
equipmentDoc.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if (container.equipmentIds.isNotEmpty) {
|
||||||
|
final equipmentsData = await _dataService.getEquipmentsByIds(container.equipmentIds);
|
||||||
|
|
||||||
|
for (var data in equipmentsData) {
|
||||||
|
final equipment = EquipmentModel.fromMap(data, data['id'] as String);
|
||||||
if (equipment.status != EquipmentStatus.available) {
|
if (equipment.status != EquipmentStatus.available) {
|
||||||
unavailableEquipment.add('${equipment.name} (${equipment.status})');
|
unavailableEquipment.add('${equipment.name} (${equipment.status})');
|
||||||
}
|
}
|
||||||
@@ -282,15 +197,13 @@ class ContainerService {
|
|||||||
final container = await getContainerById(containerId);
|
final container = await getContainerById(containerId);
|
||||||
if (container == null) return [];
|
if (container == null) return [];
|
||||||
|
|
||||||
List<EquipmentModel> equipment = [];
|
if (container.equipmentIds.isEmpty) return [];
|
||||||
for (final equipmentId in container.equipmentIds) {
|
|
||||||
final doc = await _equipmentCollection.doc(equipmentId).get();
|
|
||||||
if (doc.exists) {
|
|
||||||
equipment.add(EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return equipment;
|
final equipmentsData = await _dataService.getEquipmentsByIds(container.equipmentIds);
|
||||||
|
|
||||||
|
return equipmentsData
|
||||||
|
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
|
||||||
|
.toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error getting container equipment: $e');
|
print('Error getting container equipment: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -300,12 +213,10 @@ class ContainerService {
|
|||||||
/// Trouver tous les containers contenant un équipement spécifique
|
/// Trouver tous les containers contenant un équipement spécifique
|
||||||
Future<List<ContainerModel>> findContainersWithEquipment(String equipmentId) async {
|
Future<List<ContainerModel>> findContainersWithEquipment(String equipmentId) async {
|
||||||
try {
|
try {
|
||||||
final snapshot = await _containersCollection
|
final containersData = await _dataService.getContainersByEquipment(equipmentId);
|
||||||
.where('equipmentIds', arrayContains: equipmentId)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
return snapshot.docs
|
return containersData
|
||||||
.map((doc) => ContainerModel.fromMap(doc.data() as Map<String, dynamic>, doc.id))
|
.map((data) => ContainerModel.fromMap(data, data['id'] as String))
|
||||||
.toList();
|
.toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error finding containers with equipment: $e');
|
print('Error finding containers with equipment: $e');
|
||||||
@@ -354,8 +265,8 @@ class ContainerService {
|
|||||||
/// Vérifier si un ID de container existe déjà
|
/// Vérifier si un ID de container existe déjà
|
||||||
Future<bool> checkContainerIdExists(String id) async {
|
Future<bool> checkContainerIdExists(String id) async {
|
||||||
try {
|
try {
|
||||||
final doc = await _containersCollection.doc(id).get();
|
final container = await getContainerById(id);
|
||||||
return doc.exists;
|
return container != null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error checking container ID: $e');
|
print('Error checking container ID: $e');
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'package:em2rp/models/equipment_model.dart';
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
import 'package:em2rp/models/container_model.dart';
|
|
||||||
import 'package:em2rp/models/alert_model.dart';
|
|
||||||
import 'package:em2rp/models/maintenance_model.dart';
|
import 'package:em2rp/models/maintenance_model.dart';
|
||||||
|
import 'package:em2rp/models/container_model.dart';
|
||||||
import 'package:em2rp/services/api_service.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
import 'package:em2rp/services/data_service.dart';
|
import 'package:em2rp/services/data_service.dart';
|
||||||
|
import 'package:em2rp/services/maintenance_service.dart';
|
||||||
|
|
||||||
class EquipmentService {
|
class EquipmentService {
|
||||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
|
||||||
final ApiService _apiService = apiService;
|
final ApiService _apiService = apiService;
|
||||||
final DataService _dataService = DataService(apiService);
|
final DataService _dataService = DataService(apiService);
|
||||||
|
|
||||||
// Collection references (utilisées seulement pour les lectures)
|
|
||||||
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CRUD Operations - Utilise le backend sécurisé
|
// CRUD Operations - Utilise le backend sécurisé
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -58,48 +53,49 @@ class EquipmentService {
|
|||||||
/// Récupérer un équipement par ID
|
/// Récupérer un équipement par ID
|
||||||
Future<EquipmentModel?> getEquipmentById(String id) async {
|
Future<EquipmentModel?> getEquipmentById(String id) async {
|
||||||
try {
|
try {
|
||||||
final doc = await _equipmentCollection.doc(id).get();
|
final equipmentsData = await _dataService.getEquipmentsByIds([id]);
|
||||||
if (doc.exists) {
|
if (equipmentsData.isEmpty) return null;
|
||||||
return EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
|
||||||
}
|
return EquipmentModel.fromMap(equipmentsData.first, id);
|
||||||
return null;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error getting equipment: $e');
|
print('Error getting equipment: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Récupérer les équipements avec filtres (stream temps réel)
|
/// Récupérer les équipements avec filtres
|
||||||
Stream<List<EquipmentModel>> getEquipment({
|
Future<List<EquipmentModel>> getEquipment({
|
||||||
EquipmentCategory? category,
|
EquipmentCategory? category,
|
||||||
EquipmentStatus? status,
|
EquipmentStatus? status,
|
||||||
String? model,
|
String? model,
|
||||||
String? searchQuery,
|
String? searchQuery,
|
||||||
}) {
|
}) async {
|
||||||
try {
|
try {
|
||||||
Query query = _equipmentCollection;
|
final equipmentsData = await _dataService.getEquipments();
|
||||||
|
|
||||||
// Filtre par catégorie
|
var equipmentList = equipmentsData
|
||||||
if (category != null) {
|
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
|
||||||
query = query.where('category', isEqualTo: equipmentCategoryToString(category));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtre par statut
|
|
||||||
if (status != null) {
|
|
||||||
query = query.where('status', isEqualTo: equipmentStatusToString(status));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtre par modèle
|
|
||||||
if (model != null && model.isNotEmpty) {
|
|
||||||
query = query.where('model', isEqualTo: model);
|
|
||||||
}
|
|
||||||
|
|
||||||
return query.snapshots().map((snapshot) {
|
|
||||||
List<EquipmentModel> equipmentList = snapshot.docs
|
|
||||||
.map((doc) => EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id))
|
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Filtre par recherche texte (côté client car Firestore ne supporte pas les recherches texte complexes)
|
// Filtres côté client
|
||||||
|
if (category != null) {
|
||||||
|
equipmentList = equipmentList
|
||||||
|
.where((e) => e.category == category)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status != null) {
|
||||||
|
equipmentList = equipmentList
|
||||||
|
.where((e) => e.status == status)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model != null && model.isNotEmpty) {
|
||||||
|
equipmentList = equipmentList
|
||||||
|
.where((e) => e.model == model)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
if (searchQuery != null && searchQuery.isNotEmpty) {
|
if (searchQuery != null && searchQuery.isNotEmpty) {
|
||||||
final lowerSearch = searchQuery.toLowerCase();
|
final lowerSearch = searchQuery.toLowerCase();
|
||||||
equipmentList = equipmentList.where((equipment) {
|
equipmentList = equipmentList.where((equipment) {
|
||||||
@@ -110,9 +106,8 @@ class EquipmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return equipmentList;
|
return equipmentList;
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error streaming equipment: $e');
|
print('Error getting equipment: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,33 +117,21 @@ class EquipmentService {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Vérifier la disponibilité d'un équipement pour une période donnée
|
/// Vérifier la disponibilité d'un équipement pour une période donnée
|
||||||
Future<List<String>> checkAvailability(
|
Future<List<Map<String, dynamic>>> checkAvailability(
|
||||||
String equipmentId,
|
String equipmentId,
|
||||||
DateTime startDate,
|
DateTime startDate,
|
||||||
DateTime endDate,
|
DateTime endDate,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final conflicts = <String>[];
|
final response = await _apiService.call('checkEquipmentAvailability', {
|
||||||
|
'equipmentId': equipmentId,
|
||||||
|
'startDate': startDate.toIso8601String(),
|
||||||
|
'endDate': endDate.toIso8601String(),
|
||||||
|
});
|
||||||
|
|
||||||
// Récupérer tous les événements qui chevauchent la période
|
final conflicts = (response['conflicts'] as List?)
|
||||||
final eventsQuery = await _firestore.collection('events')
|
?.map((c) => c as Map<String, dynamic>)
|
||||||
.where('StartDateTime', isLessThanOrEqualTo: Timestamp.fromDate(endDate))
|
.toList() ?? [];
|
||||||
.where('EndDateTime', isGreaterThanOrEqualTo: Timestamp.fromDate(startDate))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
for (var eventDoc in eventsQuery.docs) {
|
|
||||||
final eventData = eventDoc.data();
|
|
||||||
final assignedEquipmentRaw = eventData['assignedEquipment'] ?? [];
|
|
||||||
|
|
||||||
if (assignedEquipmentRaw is List) {
|
|
||||||
for (var eq in assignedEquipmentRaw) {
|
|
||||||
if (eq is Map && eq['equipmentId'] == equipmentId) {
|
|
||||||
conflicts.add(eventDoc.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return conflicts;
|
return conflicts;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -164,26 +147,15 @@ class EquipmentService {
|
|||||||
DateTime endDate,
|
DateTime endDate,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
// Récupérer tous les équipements du même modèle
|
final response = await _apiService.call('findAlternativeEquipment', {
|
||||||
final equipmentQuery = await _firestore.collection('equipments')
|
'model': model,
|
||||||
.where('model', isEqualTo: model)
|
'startDate': startDate.toIso8601String(),
|
||||||
.get();
|
'endDate': endDate.toIso8601String(),
|
||||||
|
});
|
||||||
|
|
||||||
final alternatives = <EquipmentModel>[];
|
final alternatives = (response['alternatives'] as List?)
|
||||||
|
?.map((a) => EquipmentModel.fromMap(a as Map<String, dynamic>, a['id'] as String))
|
||||||
for (var doc in equipmentQuery.docs) {
|
.toList() ?? [];
|
||||||
final equipment = EquipmentModel.fromMap(
|
|
||||||
doc.data(),
|
|
||||||
doc.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Vérifier la disponibilité
|
|
||||||
final conflicts = await checkAvailability(equipment.id, startDate, endDate);
|
|
||||||
|
|
||||||
if (conflicts.isEmpty && equipment.status == EquipmentStatus.available) {
|
|
||||||
alternatives.add(equipment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return alternatives;
|
return alternatives;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -224,20 +196,15 @@ class EquipmentService {
|
|||||||
/// Vérifier les stocks critiques et créer des alertes
|
/// Vérifier les stocks critiques et créer des alertes
|
||||||
Future<void> checkCriticalStock() async {
|
Future<void> checkCriticalStock() async {
|
||||||
try {
|
try {
|
||||||
final equipmentQuery = await _firestore.collection('equipments')
|
final equipmentsData = await _dataService.getEquipments();
|
||||||
.where('category', whereIn: [
|
|
||||||
equipmentCategoryToString(EquipmentCategory.consumable),
|
|
||||||
equipmentCategoryToString(EquipmentCategory.cable),
|
|
||||||
])
|
|
||||||
.get();
|
|
||||||
|
|
||||||
for (var doc in equipmentQuery.docs) {
|
for (var data in equipmentsData) {
|
||||||
final equipment = EquipmentModel.fromMap(
|
final equipment = EquipmentModel.fromMap(data, data['id'] as String);
|
||||||
doc.data(),
|
|
||||||
doc.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (equipment.isCriticalStock) {
|
// Filtrer uniquement les consommables et câbles
|
||||||
|
if ((equipment.category == EquipmentCategory.consumable ||
|
||||||
|
equipment.category == EquipmentCategory.cable) &&
|
||||||
|
equipment.isCriticalStock) {
|
||||||
await _createLowStockAlert(equipment);
|
await _createLowStockAlert(equipment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,27 +217,19 @@ class EquipmentService {
|
|||||||
/// Créer une alerte de stock faible
|
/// Créer une alerte de stock faible
|
||||||
Future<void> _createLowStockAlert(EquipmentModel equipment) async {
|
Future<void> _createLowStockAlert(EquipmentModel equipment) async {
|
||||||
try {
|
try {
|
||||||
// Vérifier si une alerte existe déjà pour cet équipement
|
// Note: Cette fonction pourrait utiliser une Cloud Function dédiée dans le futur
|
||||||
final existingAlerts = await _firestore.collection('alerts')
|
// Pour l'instant, on utilise l'API directement pour éviter de créer trop de fonctions
|
||||||
.where('equipmentId', isEqualTo: equipment.id)
|
// Cette méthode est appelée rarement et en arrière-plan
|
||||||
.where('type', isEqualTo: alertTypeToString(AlertType.lowStock))
|
await _apiService.call('createAlert', {
|
||||||
.where('isRead', isEqualTo: false)
|
'type': 'LOW_STOCK',
|
||||||
.get();
|
'title': 'Stock critique',
|
||||||
|
'message': 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}',
|
||||||
if (existingAlerts.docs.isEmpty) {
|
'severity': 'HIGH',
|
||||||
final alert = AlertModel(
|
'equipmentId': equipment.id,
|
||||||
id: _firestore.collection('alerts').doc().id,
|
});
|
||||||
type: AlertType.lowStock,
|
|
||||||
message: 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}',
|
|
||||||
equipmentId: equipment.id,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _firestore.collection('alerts').doc(alert.id).set(alert.toMap());
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error creating low stock alert: $e');
|
print('Error creating low stock alert: $e');
|
||||||
rethrow;
|
// Ne pas rethrow pour ne pas bloquer le processus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,11 +243,10 @@ class EquipmentService {
|
|||||||
/// Récupérer tous les modèles uniques (pour l'indexation/autocomplete)
|
/// Récupérer tous les modèles uniques (pour l'indexation/autocomplete)
|
||||||
Future<List<String>> getAllModels() async {
|
Future<List<String>> getAllModels() async {
|
||||||
try {
|
try {
|
||||||
final equipmentQuery = await _firestore.collection('equipments').get();
|
final equipmentsData = await _dataService.getEquipments();
|
||||||
final models = <String>{};
|
final models = <String>{};
|
||||||
|
|
||||||
for (var doc in equipmentQuery.docs) {
|
for (var data in equipmentsData) {
|
||||||
final data = doc.data();
|
|
||||||
final model = data['model'] as String?;
|
final model = data['model'] as String?;
|
||||||
if (model != null && model.isNotEmpty) {
|
if (model != null && model.isNotEmpty) {
|
||||||
models.add(model);
|
models.add(model);
|
||||||
@@ -305,11 +263,10 @@ class EquipmentService {
|
|||||||
/// Récupérer toutes les marques uniques (pour l'indexation/autocomplete)
|
/// Récupérer toutes les marques uniques (pour l'indexation/autocomplete)
|
||||||
Future<List<String>> getAllBrands() async {
|
Future<List<String>> getAllBrands() async {
|
||||||
try {
|
try {
|
||||||
final equipmentQuery = await _firestore.collection('equipments').get();
|
final equipmentsData = await _dataService.getEquipments();
|
||||||
final brands = <String>{};
|
final brands = <String>{};
|
||||||
|
|
||||||
for (var doc in equipmentQuery.docs) {
|
for (var data in equipmentsData) {
|
||||||
final data = doc.data();
|
|
||||||
final brand = data['brand'] as String?;
|
final brand = data['brand'] as String?;
|
||||||
if (brand != null && brand.isNotEmpty) {
|
if (brand != null && brand.isNotEmpty) {
|
||||||
brands.add(brand);
|
brands.add(brand);
|
||||||
@@ -326,18 +283,17 @@ class EquipmentService {
|
|||||||
/// Récupérer les modèles filtrés par marque
|
/// Récupérer les modèles filtrés par marque
|
||||||
Future<List<String>> getModelsByBrand(String brand) async {
|
Future<List<String>> getModelsByBrand(String brand) async {
|
||||||
try {
|
try {
|
||||||
final equipmentQuery = await _firestore.collection('equipments')
|
final equipmentsData = await _dataService.getEquipments();
|
||||||
.where('brand', isEqualTo: brand)
|
|
||||||
.get();
|
|
||||||
final models = <String>{};
|
final models = <String>{};
|
||||||
|
|
||||||
for (var doc in equipmentQuery.docs) {
|
for (var data in equipmentsData) {
|
||||||
final data = doc.data();
|
if (data['brand'] == brand) {
|
||||||
final model = data['model'] as String?;
|
final model = data['model'] as String?;
|
||||||
if (model != null && model.isNotEmpty) {
|
if (model != null && model.isNotEmpty) {
|
||||||
models.add(model);
|
models.add(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return models.toList()..sort();
|
return models.toList()..sort();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -349,8 +305,8 @@ class EquipmentService {
|
|||||||
/// Vérifier si un ID existe déjà
|
/// Vérifier si un ID existe déjà
|
||||||
Future<bool> isIdUnique(String id) async {
|
Future<bool> isIdUnique(String id) async {
|
||||||
try {
|
try {
|
||||||
final doc = await _firestore.collection('equipments').doc(id).get();
|
final equipment = await getEquipmentById(id);
|
||||||
return !doc.exists;
|
return equipment == null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error checking ID uniqueness: $e');
|
print('Error checking ID uniqueness: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -381,27 +337,11 @@ class EquipmentService {
|
|||||||
try {
|
try {
|
||||||
if (ids.isEmpty) return [];
|
if (ids.isEmpty) return [];
|
||||||
|
|
||||||
final equipments = <EquipmentModel>[];
|
final equipmentsData = await _dataService.getEquipmentsByIds(ids);
|
||||||
|
|
||||||
// Firestore limite les requêtes whereIn à 10 éléments
|
return equipmentsData
|
||||||
// On doit donc diviser en plusieurs requêtes si nécessaire
|
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
|
||||||
for (int i = 0; i < ids.length; i += 10) {
|
.toList();
|
||||||
final batch = ids.skip(i).take(10).toList();
|
|
||||||
final query = await _firestore.collection('equipments')
|
|
||||||
.where(FieldPath.documentId, whereIn: batch)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
for (var doc in query.docs) {
|
|
||||||
equipments.add(
|
|
||||||
EquipmentModel.fromMap(
|
|
||||||
doc.data(),
|
|
||||||
doc.id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return equipments;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error getting equipments by IDs: $e');
|
print('Error getting equipments by IDs: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -409,25 +349,13 @@ class EquipmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Récupérer les maintenances pour un équipement
|
/// Récupérer les maintenances pour un équipement
|
||||||
|
/// Note: Cette méthode est maintenant déléguée au MaintenanceService
|
||||||
|
/// pour éviter la duplication de code
|
||||||
Future<List<MaintenanceModel>> getMaintenancesForEquipment(String equipmentId) async {
|
Future<List<MaintenanceModel>> getMaintenancesForEquipment(String equipmentId) async {
|
||||||
try {
|
try {
|
||||||
final maintenanceQuery = await _firestore
|
// Déléguer au MaintenanceService qui utilise déjà les Cloud Functions
|
||||||
.collection('maintenances')
|
final maintenanceService = MaintenanceService();
|
||||||
.where('equipmentIds', arrayContains: equipmentId)
|
return await maintenanceService.getMaintenancesByEquipment(equipmentId);
|
||||||
.orderBy('scheduledDate', descending: true)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
final maintenances = <MaintenanceModel>[];
|
|
||||||
for (var doc in maintenanceQuery.docs) {
|
|
||||||
maintenances.add(
|
|
||||||
MaintenanceModel.fromMap(
|
|
||||||
doc.data(),
|
|
||||||
doc.id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return maintenances;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error getting maintenances for equipment: $e');
|
print('Error getting maintenances for equipment: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'package:em2rp/models/equipment_model.dart';
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
|
|
||||||
/// Service pour calculer dynamiquement le statut réel d'un équipement
|
/// Service pour calculer dynamiquement le statut réel d'un équipement
|
||||||
/// basé sur les événements en cours
|
/// basé sur les événements en cours
|
||||||
class EquipmentStatusCalculator {
|
class EquipmentStatusCalculator {
|
||||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
final ApiService _apiService = apiService;
|
||||||
|
|
||||||
/// Cache des événements pour éviter de multiples requêtes
|
/// Cache des statuts pour éviter de multiples requêtes
|
||||||
List<EventModel>? _cachedEvents;
|
Map<String, EquipmentStatus>? _cachedStatuses;
|
||||||
DateTime? _cacheTime;
|
DateTime? _cacheTime;
|
||||||
static const _cacheDuration = Duration(minutes: 1);
|
static const _cacheDuration = Duration(minutes: 1);
|
||||||
|
|
||||||
@@ -25,205 +24,57 @@ class EquipmentStatusCalculator {
|
|||||||
Future<EquipmentStatus> calculateRealStatus(EquipmentModel equipment) async {
|
Future<EquipmentStatus> calculateRealStatus(EquipmentModel equipment) async {
|
||||||
print('[StatusCalculator] Calculating status for: ${equipment.id}');
|
print('[StatusCalculator] Calculating status for: ${equipment.id}');
|
||||||
|
|
||||||
// Si l'équipement est marqué comme perdu ou HS, on garde ce statut
|
try {
|
||||||
// car c'est une information métier importante
|
final statuses = await calculateMultipleStatuses([equipment]);
|
||||||
if (equipment.status == EquipmentStatus.lost ||
|
return statuses[equipment.id] ?? equipment.status;
|
||||||
equipment.status == EquipmentStatus.outOfService) {
|
} catch (e) {
|
||||||
print('[StatusCalculator] ${equipment.id} is lost/outOfService -> keeping status');
|
print('[StatusCalculator] Error calculating status: $e');
|
||||||
return equipment.status;
|
return equipment.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charger les événements (avec cache)
|
|
||||||
await _loadEventsIfNeeded();
|
|
||||||
print('[StatusCalculator] Events loaded: ${_cachedEvents?.length ?? 0}');
|
|
||||||
|
|
||||||
// Vérifier si l'équipement est utilisé dans un événement en cours
|
|
||||||
final isInUse = await _isEquipmentInUse(equipment.id);
|
|
||||||
print('[StatusCalculator] ${equipment.id} isInUse: $isInUse');
|
|
||||||
|
|
||||||
if (isInUse) {
|
|
||||||
return EquipmentStatus.inUse;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si l'équipement est en maintenance
|
|
||||||
if (equipment.status == EquipmentStatus.maintenance) {
|
|
||||||
// On pourrait vérifier si la maintenance est toujours valide
|
|
||||||
// Pour l'instant on garde le statut
|
|
||||||
return EquipmentStatus.maintenance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si l'équipement est loué
|
|
||||||
if (equipment.status == EquipmentStatus.rented) {
|
|
||||||
// On pourrait vérifier une date de retour prévue
|
|
||||||
// Pour l'instant on garde le statut
|
|
||||||
return EquipmentStatus.rented;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Par défaut, l'équipement est disponible
|
|
||||||
print('[StatusCalculator] ${equipment.id} -> AVAILABLE');
|
|
||||||
return EquipmentStatus.available;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calcule les statuts pour une liste d'équipements (optimisé)
|
/// Calcule les statuts pour une liste d'équipements (optimisé)
|
||||||
Future<Map<String, EquipmentStatus>> calculateMultipleStatuses(
|
Future<Map<String, EquipmentStatus>> calculateMultipleStatuses(
|
||||||
List<EquipmentModel> equipments,
|
List<EquipmentModel> equipments,
|
||||||
) async {
|
) async {
|
||||||
await _loadEventsIfNeeded();
|
try {
|
||||||
|
final equipmentIds = equipments.map((e) => e.id).toList();
|
||||||
|
|
||||||
|
final response = await _apiService.call('calculateEquipmentStatuses', {
|
||||||
|
'equipmentIds': equipmentIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
final statusesMap = response['statuses'] as Map<String, dynamic>?;
|
||||||
|
if (statusesMap == null) {
|
||||||
|
throw Exception('Invalid response from calculateEquipmentStatuses');
|
||||||
|
}
|
||||||
|
|
||||||
final statuses = <String, EquipmentStatus>{};
|
final statuses = <String, EquipmentStatus>{};
|
||||||
|
statusesMap.forEach((equipmentId, statusString) {
|
||||||
// Trouver tous les équipements en cours d'utilisation
|
if (statusString != null) {
|
||||||
final equipmentIdsInUse = <String>{};
|
statuses[equipmentId] = equipmentStatusFromString(statusString as String);
|
||||||
final containerIdsInUse = <String>{};
|
|
||||||
|
|
||||||
for (var event in _cachedEvents ?? []) {
|
|
||||||
// Un équipement est "en prestation" dès que la préparation est complétée
|
|
||||||
// et jusqu'à ce que le retour soit complété
|
|
||||||
final isPrepared = event.preparationStatus == PreparationStatus.completed ||
|
|
||||||
event.preparationStatus == PreparationStatus.completedWithMissing;
|
|
||||||
|
|
||||||
final isReturned = event.returnStatus == ReturnStatus.completed ||
|
|
||||||
event.returnStatus == ReturnStatus.completedWithMissing;
|
|
||||||
|
|
||||||
final isInProgress = isPrepared && !isReturned;
|
|
||||||
|
|
||||||
if (isInProgress) {
|
|
||||||
// Ajouter les équipements directs
|
|
||||||
for (var eq in event.assignedEquipment) {
|
|
||||||
equipmentIdsInUse.add(eq.equipmentId);
|
|
||||||
}
|
|
||||||
// Ajouter les conteneurs
|
|
||||||
containerIdsInUse.addAll(event.assignedContainers);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Récupérer les équipements dans les conteneurs en cours d'utilisation
|
// Mise en cache
|
||||||
if (containerIdsInUse.isNotEmpty) {
|
_cachedStatuses = statuses;
|
||||||
final containersSnapshot = await _firestore
|
_cacheTime = DateTime.now();
|
||||||
.collection('containers')
|
|
||||||
.where(FieldPath.documentId, whereIn: containerIdsInUse.toList())
|
|
||||||
.get();
|
|
||||||
|
|
||||||
for (var doc in containersSnapshot.docs) {
|
|
||||||
final data = doc.data();
|
|
||||||
final equipmentIds = List<String>.from(data['equipmentIds'] ?? []);
|
|
||||||
equipmentIdsInUse.addAll(equipmentIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculer le statut pour chaque équipement
|
|
||||||
for (var equipment in equipments) {
|
|
||||||
// Si perdu ou HS, on garde le statut
|
|
||||||
if (equipment.status == EquipmentStatus.lost ||
|
|
||||||
equipment.status == EquipmentStatus.outOfService) {
|
|
||||||
statuses[equipment.id] = equipment.status;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si en cours d'utilisation
|
|
||||||
if (equipmentIdsInUse.contains(equipment.id)) {
|
|
||||||
statuses[equipment.id] = EquipmentStatus.inUse;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si en maintenance ou loué, on garde le statut
|
|
||||||
if (equipment.status == EquipmentStatus.maintenance ||
|
|
||||||
equipment.status == EquipmentStatus.rented) {
|
|
||||||
statuses[equipment.id] = equipment.status;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Par défaut, disponible
|
|
||||||
statuses[equipment.id] = EquipmentStatus.available;
|
|
||||||
}
|
|
||||||
|
|
||||||
return statuses;
|
return statuses;
|
||||||
}
|
|
||||||
|
|
||||||
/// Vérifie si un équipement est actuellement en cours d'utilisation
|
|
||||||
Future<bool> _isEquipmentInUse(String equipmentId) async {
|
|
||||||
print('[StatusCalculator] Checking if $equipmentId is in use...');
|
|
||||||
|
|
||||||
// Vérifier dans les événements directs
|
|
||||||
for (var event in _cachedEvents ?? []) {
|
|
||||||
// Un équipement est "en prestation" dès que la préparation est complétée
|
|
||||||
// et jusqu'à ce que le retour soit complété
|
|
||||||
final isPrepared = event.preparationStatus == PreparationStatus.completed ||
|
|
||||||
event.preparationStatus == PreparationStatus.completedWithMissing;
|
|
||||||
|
|
||||||
final isReturned = event.returnStatus == ReturnStatus.completed ||
|
|
||||||
event.returnStatus == ReturnStatus.completedWithMissing;
|
|
||||||
|
|
||||||
final isInProgress = isPrepared && !isReturned;
|
|
||||||
|
|
||||||
if (!isInProgress) continue;
|
|
||||||
|
|
||||||
print('[StatusCalculator] Event ${event.name} is IN PROGRESS (prepared and not returned)');
|
|
||||||
|
|
||||||
// Vérifier si l'équipement est directement assigné
|
|
||||||
if (event.assignedEquipment.any((eq) => eq.equipmentId == equipmentId)) {
|
|
||||||
print('[StatusCalculator] $equipmentId found DIRECTLY in event ${event.name}');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si l'équipement est dans un conteneur assigné
|
|
||||||
if (event.assignedContainers.isNotEmpty) {
|
|
||||||
print('[StatusCalculator] Checking containers for event ${event.name}: ${event.assignedContainers}');
|
|
||||||
final containersSnapshot = await _firestore
|
|
||||||
.collection('containers')
|
|
||||||
.where(FieldPath.documentId, whereIn: event.assignedContainers)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
for (var doc in containersSnapshot.docs) {
|
|
||||||
final data = doc.data();
|
|
||||||
final equipmentIds = List<String>.from(data['equipmentIds'] ?? []);
|
|
||||||
print('[StatusCalculator] Container ${doc.id} contains: $equipmentIds');
|
|
||||||
if (equipmentIds.contains(equipmentId)) {
|
|
||||||
print('[StatusCalculator] $equipmentId found in CONTAINER ${doc.id}');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print('[StatusCalculator] $equipmentId is NOT in use');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Charge les événements si le cache est expiré
|
|
||||||
Future<void> _loadEventsIfNeeded() async {
|
|
||||||
if (_cachedEvents != null &&
|
|
||||||
_cacheTime != null &&
|
|
||||||
DateTime.now().difference(_cacheTime!) < _cacheDuration) {
|
|
||||||
return; // Cache encore valide
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final eventsSnapshot = await _firestore.collection('events').get();
|
|
||||||
|
|
||||||
_cachedEvents = eventsSnapshot.docs
|
|
||||||
.map((doc) {
|
|
||||||
try {
|
|
||||||
return EventModel.fromMap(doc.data(), doc.id);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[EquipmentStatusCalculator] Error parsing event ${doc.id}: $e');
|
print('[StatusCalculator] Error calculating multiple statuses: $e');
|
||||||
return null;
|
// En cas d'erreur, retourner les statuts actuels
|
||||||
|
final fallbackStatuses = <String, EquipmentStatus>{};
|
||||||
|
for (var equipment in equipments) {
|
||||||
|
fallbackStatuses[equipment.id] = equipment.status;
|
||||||
}
|
}
|
||||||
})
|
return fallbackStatuses;
|
||||||
.whereType<EventModel>()
|
|
||||||
.where((event) => event.status != EventStatus.canceled) // Ignorer les événements annulés
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
_cacheTime = DateTime.now();
|
|
||||||
} catch (e) {
|
|
||||||
print('[EquipmentStatusCalculator] Error loading events: $e');
|
|
||||||
_cachedEvents = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invalide le cache (à appeler après une modification d'événement)
|
/// Invalide le cache (à appeler après une modification d'événement)
|
||||||
void invalidateCache() {
|
void invalidateCache() {
|
||||||
_cachedEvents = null;
|
_cachedStatuses = null;
|
||||||
_cacheTime = null;
|
_cacheTime = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'package:em2rp/models/event_model.dart';
|
|
||||||
import 'package:em2rp/models/equipment_model.dart';
|
|
||||||
import 'package:em2rp/services/equipment_status_calculator.dart';
|
import 'package:em2rp/services/equipment_status_calculator.dart';
|
||||||
import 'package:em2rp/services/api_service.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
|
|
||||||
class EventPreparationService {
|
class EventPreparationService {
|
||||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
|
||||||
final ApiService _apiService = apiService;
|
final ApiService _apiService = apiService;
|
||||||
|
|
||||||
// Collection references (utilisées uniquement pour les lectures)
|
|
||||||
CollectionReference get _eventsCollection => _firestore.collection('events');
|
|
||||||
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
|
|
||||||
|
|
||||||
// === PRÉPARATION ===
|
// === PRÉPARATION ===
|
||||||
|
|
||||||
/// Valider un équipement individuel en préparation
|
/// Valider un équipement individuel en préparation
|
||||||
@@ -42,37 +34,18 @@ class EventPreparationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finaliser la préparation avec des équipements manquants
|
// Ces méthodes ne sont plus utilisées et devraient être remplacées par des Cloud Functions
|
||||||
|
// si nécessaire dans le futur
|
||||||
|
|
||||||
|
/*
|
||||||
|
@Deprecated('Use Cloud Functions instead')
|
||||||
Future<void> completePreparationWithMissing(
|
Future<void> completePreparationWithMissing(
|
||||||
String eventId,
|
String eventId,
|
||||||
List<String> missingEquipmentIds,
|
List<String> missingEquipmentIds,
|
||||||
) async {
|
) async {
|
||||||
try {
|
throw UnimplementedError('This method is deprecated. Use Cloud Functions instead.');
|
||||||
final event = await _getEvent(eventId);
|
|
||||||
if (event == null) {
|
|
||||||
throw Exception('Event not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marquer comme complété avec manquants
|
|
||||||
await _eventsCollection.doc(eventId).update({
|
|
||||||
'preparationStatus': preparationStatusToString(PreparationStatus.completedWithMissing),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mettre à jour le statut des équipements préparés à "inUse"
|
|
||||||
for (var equipment in event.assignedEquipment) {
|
|
||||||
if (equipment.isPrepared && !missingEquipmentIds.contains(equipment.equipmentId)) {
|
|
||||||
// Vérifier si l'équipement existe avant de mettre à jour son statut
|
|
||||||
final doc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
|
||||||
if (doc.exists) {
|
|
||||||
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error completing preparation with missing: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// === RETOUR ===
|
// === RETOUR ===
|
||||||
|
|
||||||
@@ -114,186 +87,18 @@ class EventPreparationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finaliser le retour avec des équipements manquants
|
/*
|
||||||
|
@Deprecated('Use Cloud Functions instead')
|
||||||
Future<void> completeReturnWithMissing(
|
Future<void> completeReturnWithMissing(
|
||||||
String eventId,
|
String eventId,
|
||||||
List<String> missingEquipmentIds,
|
List<String> missingEquipmentIds,
|
||||||
) async {
|
) async {
|
||||||
try {
|
throw UnimplementedError('This method is deprecated. Use Cloud Functions instead.');
|
||||||
final event = await _getEvent(eventId);
|
|
||||||
if (event == null) {
|
|
||||||
throw Exception('Event not found');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marquer comme complété avec manquants
|
// Les méthodes helper suivantes étaient uniquement utilisées par les méthodes deprecated ci-dessus.
|
||||||
await _eventsCollection.doc(eventId).update({
|
// Elles ont été supprimées car elles accédaient directement à Firestore.
|
||||||
'returnStatus': returnStatusToString(ReturnStatus.completedWithMissing),
|
// Si ces fonctionnalités sont nécessaires à l'avenir, elles doivent être implémentées
|
||||||
});
|
// via des Cloud Functions pour respecter l'architecture.
|
||||||
|
*/
|
||||||
// Mettre à jour le statut des équipements retournés à "available"
|
|
||||||
for (var equipment in event.assignedEquipment) {
|
|
||||||
// Vérifier si le document existe
|
|
||||||
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
|
||||||
if (!equipmentDoc.exists) {
|
|
||||||
continue; // Passer cet équipement s'il n'existe pas
|
|
||||||
}
|
|
||||||
|
|
||||||
final equipmentData = EquipmentModel.fromMap(
|
|
||||||
equipmentDoc.data() as Map<String, dynamic>,
|
|
||||||
equipmentDoc.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (equipment.isReturned && !missingEquipmentIds.contains(equipment.equipmentId)) {
|
|
||||||
// Mettre à jour le statut uniquement pour les équipements non quantifiables
|
|
||||||
if (!equipmentData.hasQuantity) {
|
|
||||||
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restaurer le stock pour les consommables
|
|
||||||
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
|
|
||||||
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
|
||||||
await _equipmentCollection.doc(equipment.equipmentId).update({
|
|
||||||
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
|
|
||||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (missingEquipmentIds.contains(equipment.equipmentId)) {
|
|
||||||
// Marquer comme perdu uniquement pour les équipements non quantifiables
|
|
||||||
if (!equipmentData.hasQuantity) {
|
|
||||||
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.lost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error completing return with missing: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HELPERS ===
|
|
||||||
|
|
||||||
/// Mettre à jour le statut d'un équipement
|
|
||||||
Future<void> updateEquipmentStatus(String equipmentId, EquipmentStatus status) async {
|
|
||||||
try {
|
|
||||||
// Vérifier que le document existe avant de le mettre à jour
|
|
||||||
final doc = await _equipmentCollection.doc(equipmentId).get();
|
|
||||||
if (!doc.exists) {
|
|
||||||
print('Warning: Equipment document $equipmentId does not exist, skipping status update');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _equipmentCollection.doc(equipmentId).update({
|
|
||||||
'status': equipmentStatusToString(status),
|
|
||||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
print('Error updating equipment status for $equipmentId: $e');
|
|
||||||
// Ne pas rethrow pour ne pas bloquer le processus si un équipement n'existe pas
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Récupérer un événement
|
|
||||||
Future<EventModel?> _getEvent(String eventId) async {
|
|
||||||
try {
|
|
||||||
final doc = await _eventsCollection.doc(eventId).get();
|
|
||||||
if (doc.exists) {
|
|
||||||
return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
|
||||||
print('Error getting event: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ajouter un équipement à un événement
|
|
||||||
Future<void> addEquipmentToEvent(
|
|
||||||
String eventId,
|
|
||||||
String equipmentId, {
|
|
||||||
int quantity = 1,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
final event = await _getEvent(eventId);
|
|
||||||
if (event == null) {
|
|
||||||
throw Exception('Event not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier que l'équipement n'est pas déjà ajouté
|
|
||||||
final alreadyAdded = event.assignedEquipment.any((eq) => eq.equipmentId == equipmentId);
|
|
||||||
if (alreadyAdded) {
|
|
||||||
throw Exception('Equipment already added to event');
|
|
||||||
}
|
|
||||||
|
|
||||||
final newEquipment = EventEquipment(
|
|
||||||
equipmentId: equipmentId,
|
|
||||||
quantity: quantity,
|
|
||||||
);
|
|
||||||
|
|
||||||
final updatedEquipment = [...event.assignedEquipment, newEquipment];
|
|
||||||
|
|
||||||
await _eventsCollection.doc(eventId).update({
|
|
||||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Décrémenter le stock pour les consommables
|
|
||||||
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
|
||||||
if (equipmentDoc.exists) {
|
|
||||||
final equipmentData = EquipmentModel.fromMap(
|
|
||||||
equipmentDoc.data() as Map<String, dynamic>,
|
|
||||||
equipmentDoc.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (equipmentData.hasQuantity) {
|
|
||||||
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
|
||||||
await _equipmentCollection.doc(equipmentId).update({
|
|
||||||
'availableQuantity': currentAvailable - quantity,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error adding equipment to event: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retirer un équipement d'un événement
|
|
||||||
Future<void> removeEquipmentFromEvent(String eventId, String equipmentId) async {
|
|
||||||
try {
|
|
||||||
final event = await _getEvent(eventId);
|
|
||||||
if (event == null) {
|
|
||||||
throw Exception('Event not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
final equipmentToRemove = event.assignedEquipment.firstWhere(
|
|
||||||
(eq) => eq.equipmentId == equipmentId,
|
|
||||||
);
|
|
||||||
|
|
||||||
final updatedEquipment = event.assignedEquipment
|
|
||||||
.where((eq) => eq.equipmentId != equipmentId)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
await _eventsCollection.doc(eventId).update({
|
|
||||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restaurer le stock pour les consommables
|
|
||||||
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
|
||||||
if (equipmentDoc.exists) {
|
|
||||||
final equipmentData = EquipmentModel.fromMap(
|
|
||||||
equipmentDoc.data() as Map<String, dynamic>,
|
|
||||||
equipmentDoc.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (equipmentData.hasQuantity) {
|
|
||||||
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
|
||||||
await _equipmentCollection.doc(equipmentId).update({
|
|
||||||
'availableQuantity': currentAvailable + equipmentToRemove.quantity,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error removing equipment from event: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'package:em2rp/models/maintenance_model.dart';
|
import 'package:em2rp/models/maintenance_model.dart';
|
||||||
import 'package:em2rp/models/alert_model.dart';
|
|
||||||
import 'package:em2rp/services/api_service.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
|
|
||||||
class MaintenanceService {
|
class MaintenanceService {
|
||||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
|
||||||
final ApiService _apiService = apiService;
|
final ApiService _apiService = apiService;
|
||||||
|
|
||||||
// Collection references
|
|
||||||
CollectionReference get _maintenancesCollection => _firestore.collection('maintenances');
|
|
||||||
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
|
|
||||||
CollectionReference get _alertsCollection => _firestore.collection('alerts');
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CRUD Operations - Utilise le backend sécurisé
|
// CRUD Operations - Utilise le backend sécurisé
|
||||||
@@ -56,54 +49,54 @@ class MaintenanceService {
|
|||||||
/// Récupérer une maintenance par ID
|
/// Récupérer une maintenance par ID
|
||||||
Future<MaintenanceModel?> getMaintenanceById(String id) async {
|
Future<MaintenanceModel?> getMaintenanceById(String id) async {
|
||||||
try {
|
try {
|
||||||
final doc = await _maintenancesCollection.doc(id).get();
|
final response = await _apiService.call('getMaintenances', {
|
||||||
if (doc.exists) {
|
'maintenanceId': id,
|
||||||
return MaintenanceModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
});
|
||||||
}
|
|
||||||
return null;
|
final maintenances = (response['maintenances'] as List?)
|
||||||
|
?.map((m) => MaintenanceModel.fromMap(m as Map<String, dynamic>, m['id'] as String))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return maintenances?.firstWhere(
|
||||||
|
(m) => m.id == id,
|
||||||
|
orElse: () => throw Exception('Maintenance not found'),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error getting maintenance: $e');
|
print('Error getting maintenance: $e');
|
||||||
rethrow;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Récupérer l'historique des maintenances pour un équipement
|
/// Récupérer l'historique des maintenances pour un équipement
|
||||||
Stream<List<MaintenanceModel>> getMaintenances(String equipmentId) {
|
Future<List<MaintenanceModel>> getMaintenancesByEquipment(String equipmentId) async {
|
||||||
try {
|
try {
|
||||||
return _maintenancesCollection
|
final response = await _apiService.call('getMaintenances', {
|
||||||
.where('equipmentIds', arrayContains: equipmentId)
|
'equipmentId': equipmentId,
|
||||||
.orderBy('scheduledDate', descending: true)
|
|
||||||
.snapshots()
|
|
||||||
.map((snapshot) {
|
|
||||||
return snapshot.docs
|
|
||||||
.map((doc) => MaintenanceModel.fromMap(
|
|
||||||
doc.data() as Map<String, dynamic>,
|
|
||||||
doc.id,
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final maintenances = (response['maintenances'] as List?)
|
||||||
|
?.map((m) => MaintenanceModel.fromMap(m as Map<String, dynamic>, m['id'] as String))
|
||||||
|
.toList() ?? [];
|
||||||
|
|
||||||
|
return maintenances;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error streaming maintenances: $e');
|
print('Error getting maintenances: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Récupérer toutes les maintenances
|
/// Récupérer toutes les maintenances
|
||||||
Stream<List<MaintenanceModel>> getAllMaintenances() {
|
Future<List<MaintenanceModel>> getAllMaintenances() async {
|
||||||
try {
|
try {
|
||||||
return _maintenancesCollection
|
final response = await _apiService.call('getMaintenances', {});
|
||||||
.orderBy('scheduledDate', descending: true)
|
|
||||||
.snapshots()
|
final maintenances = (response['maintenances'] as List?)
|
||||||
.map((snapshot) {
|
?.map((m) => MaintenanceModel.fromMap(m as Map<String, dynamic>, m['id'] as String))
|
||||||
return snapshot.docs
|
.toList() ?? [];
|
||||||
.map((doc) => MaintenanceModel.fromMap(
|
|
||||||
doc.data() as Map<String, dynamic>,
|
return maintenances;
|
||||||
doc.id,
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error streaming all maintenances: $e');
|
print('Error getting all maintenances: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,30 +104,11 @@ class MaintenanceService {
|
|||||||
/// Marquer une maintenance comme complétée
|
/// Marquer une maintenance comme complétée
|
||||||
Future<void> completeMaintenance(String id, {String? performedBy, double? cost}) async {
|
Future<void> completeMaintenance(String id, {String? performedBy, double? cost}) async {
|
||||||
try {
|
try {
|
||||||
final updateData = <String, dynamic>{
|
await _apiService.call('completeMaintenance', {
|
||||||
'completedDate': Timestamp.fromDate(DateTime.now()),
|
'maintenanceId': id,
|
||||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
if (performedBy != null) 'performedBy': performedBy,
|
||||||
};
|
if (cost != null) 'cost': cost,
|
||||||
|
|
||||||
if (performedBy != null) {
|
|
||||||
updateData['performedBy'] = performedBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cost != null) {
|
|
||||||
updateData['cost'] = cost;
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateMaintenance(id, updateData);
|
|
||||||
|
|
||||||
// Mettre à jour la date de dernière maintenance des équipements
|
|
||||||
final maintenance = await getMaintenanceById(id);
|
|
||||||
if (maintenance != null) {
|
|
||||||
for (String equipmentId in maintenance.equipmentIds) {
|
|
||||||
await _equipmentCollection.doc(equipmentId).update({
|
|
||||||
'lastMaintenanceDate': Timestamp.fromDate(DateTime.now()),
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error completing maintenance: $e');
|
print('Error completing maintenance: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -144,73 +118,10 @@ class MaintenanceService {
|
|||||||
/// Vérifier les maintenances à venir et créer des alertes
|
/// Vérifier les maintenances à venir et créer des alertes
|
||||||
Future<void> checkUpcomingMaintenances() async {
|
Future<void> checkUpcomingMaintenances() async {
|
||||||
try {
|
try {
|
||||||
final sevenDaysFromNow = DateTime.now().add(const Duration(days: 7));
|
await _apiService.call('checkUpcomingMaintenances', {});
|
||||||
|
|
||||||
// Récupérer les maintenances planifiées dans les 7 prochains jours
|
|
||||||
final maintenancesQuery = await _maintenancesCollection
|
|
||||||
.where('scheduledDate', isLessThanOrEqualTo: Timestamp.fromDate(sevenDaysFromNow))
|
|
||||||
.where('completedDate', isNull: true)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
for (var doc in maintenancesQuery.docs) {
|
|
||||||
final maintenance = MaintenanceModel.fromMap(
|
|
||||||
doc.data() as Map<String, dynamic>,
|
|
||||||
doc.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (String equipmentId in maintenance.equipmentIds) {
|
|
||||||
await _createMaintenanceAlert(equipmentId, maintenance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error checking upcoming maintenances: $e');
|
print('Error checking upcoming maintenances: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Créer une alerte de maintenance à venir
|
|
||||||
Future<void> _createMaintenanceAlert(String equipmentId, MaintenanceModel maintenance) async {
|
|
||||||
try {
|
|
||||||
// Vérifier si une alerte existe déjà
|
|
||||||
final existingAlerts = await _alertsCollection
|
|
||||||
.where('equipmentId', isEqualTo: equipmentId)
|
|
||||||
.where('type', isEqualTo: alertTypeToString(AlertType.maintenanceDue))
|
|
||||||
.where('isRead', isEqualTo: false)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
// Vérifier si l'alerte concerne la même maintenance
|
|
||||||
bool alertExists = false;
|
|
||||||
for (var alertDoc in existingAlerts.docs) {
|
|
||||||
final alertData = alertDoc.data() as Map<String, dynamic>;
|
|
||||||
if (alertData['message']?.contains(maintenance.name) ?? false) {
|
|
||||||
alertExists = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!alertExists) {
|
|
||||||
// Récupérer l'équipement pour le nom
|
|
||||||
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
|
||||||
String equipmentName = equipmentId;
|
|
||||||
if (equipmentDoc.exists) {
|
|
||||||
final equipmentData = equipmentDoc.data() as Map<String, dynamic>;
|
|
||||||
equipmentName = equipmentData['name'] ?? equipmentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
final daysUntil = maintenance.scheduledDate.difference(DateTime.now()).inDays;
|
|
||||||
final alert = AlertModel(
|
|
||||||
id: _alertsCollection.doc().id,
|
|
||||||
type: AlertType.maintenanceDue,
|
|
||||||
message: 'Maintenance "${maintenance.name}" prévue dans $daysUntil jour(s) pour $equipmentName',
|
|
||||||
equipmentId: equipmentId,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _alertsCollection.doc(alert.id).set(alert.toMap());
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error creating maintenance alert: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user