diff --git a/em2rp/BACKEND_MIGRATION_COMPLETE.md b/em2rp/BACKEND_MIGRATION_COMPLETE.md deleted file mode 100644 index 450b11a..0000000 --- a/em2rp/BACKEND_MIGRATION_COMPLETE.md +++ /dev/null @@ -1,229 +0,0 @@ -# 🎉 MIGRATION BACKEND - RÉCAPITULATIF COMPLET - -## ✅ MISSION ACCOMPLIE ! - -Toutes les corrections ont été appliquées avec succès. Le backend est opérationnel et tous les problèmes de types Firebase ont été résolus. - ---- - -## 📊 LES 10 CORRECTIONS APPLIQUÉES - -| # | Problème | Solution | Impact | -|---|----------|----------|--------| -| 1 | **Timestamp → JSON** | Conversion ISO string | Envoi API ✅ | -| 2 | **DocumentReference → JSON** | Conversion path string | Envoi API ✅ | -| 3 | **GeoPoint → JSON** | Conversion {lat, lng} | Envoi API ✅ | -| 4 | **ISO string → DateTime** | Helper _parseDate() | Réception API ✅ | -| 5 | **LinkedMap type** | Map | Typage correct ✅ | -| 6 | **Widget deactivated** | Capture context | Suppression safe ✅ | -| 7 | **Path → ID** | split('/').last | EventType ID ✅ | -| 8 | **EventType extraction** | Extraction propre | Affichage correct ✅ | -| 9 | **Compilation EventModel** | Structure classe | Build OK ✅ | -| 10 | **EventType actualisation** | didUpdateWidget | Rafraîchissement ✅ | - ---- - -## 🚀 ARCHITECTURE FINALE - -### Backend (Production) -``` -19 Cloud Functions déployées sur Firebase -├── Equipment (4) : create, update, delete, get -├── Container (3) : create, update, delete -├── Event (3) : create, update, delete -├── Maintenance (2) : create, update -├── Option (3) : create, update, delete -├── User (2) : create, update -├── Equipment Status (1) : update -└── File Management (1) : moveEventFileV2 -``` - -### Frontend (Dev local) -``` -Flutter App -├── LECTURES → Firestore direct (temps réel) ✅ -└── ÉCRITURES → Cloud Functions (sécurisé) ✅ -``` - -### Conversion automatique -``` -api_service.dart - _convertTimestamps() -├── Timestamp → ISO string -├── DateTime → ISO string -├── DocumentReference → path string -├── GeoPoint → {latitude, longitude} -├── Maps (récursif) -└── Lists (récursif) -``` - ---- - -## 🎯 TESTS VALIDÉS - -### Équipements -- ✅ **CREATE** : Fonctionne -- ✅ **UPDATE** : Fonctionne -- ✅ **DELETE** : Fonctionne (+ context safe) -- ✅ **DISPLAY** : Liste + détails - -### Événements -- ⏳ **CREATE** : Prêt à tester (toutes conversions OK) -- ⏳ **UPDATE** : Prêt à tester -- ⏳ **DELETE** : Prêt à tester -- ✅ **DISPLAY** : Types corrects + actualisation - -### Containers -- ⏳ À tester (conversions appliquées) - ---- - -## 📝 FICHIERS MODIFIÉS - -### Services -- ✅ `lib/services/api_service.dart` - Conversions Firebase -- ✅ `lib/services/equipment_service.dart` - API backend -- ✅ `lib/services/container_service.dart` - API backend -- ✅ `lib/services/event_form_service.dart` - API backend -- ✅ `lib/services/maintenance_service.dart` - API backend - -### Models -- ✅ `lib/models/equipment_model.dart` - Parse ISO string -- ✅ `lib/models/container_model.dart` - Parse ISO string -- ✅ `lib/models/event_model.dart` - Parse ISO + Path extraction -- ✅ `lib/models/maintenance_model.dart` - Parse ISO string -- ✅ `lib/models/alert_model.dart` - Parse ISO string - -### Views -- ✅ `lib/views/equipment_detail_page.dart` - Context safe -- ✅ `lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart` - didUpdateWidget - -### Configuration -- ✅ `lib/config/api_config.dart` - isDevelopment = false -- ✅ `lib/main.dart` - Config émulateurs -- ✅ `firebase.json` - Ports émulateurs - -### Backend -- ✅ `functions/index.js` - 19 Cloud Functions -- ✅ `functions/utils/auth.js` - Authentification -- ✅ `functions/utils/helpers.js` - Utilitaires - ---- - -## 🔒 SÉCURITÉ - -### État actuel -- ✅ Cloud Functions déployées en production -- ✅ Authentification Firebase requise -- ✅ Permissions vérifiées côté backend -- ⚠️ Firestore Rules inchangées (accès direct toujours possible) - -### App hébergée -- ✅ Continue de fonctionner normalement -- ✅ Utilisateurs non impactés -- ✅ Pas de breaking changes - ---- - -## 🎯 PROCHAINES ÉTAPES - -### Phase 1 : Validation complète (maintenant) -1. ✅ Tester création/modification/suppression événements -2. ⏳ Tester containers CRUD -3. ⏳ Tester maintenances CRUD -4. ⏳ Tester permissions (admin vs user) - -### Phase 2 : Optimisations UX -1. ⚙️ Refresh automatique des listes après opérations -2. ⚙️ Loading states pendant les opérations -3. ⚙️ Optimistic UI pour meilleure réactivité -4. ⚙️ Gestion des erreurs réseau - -### Phase 3 : Déploiement complet -1. 🔒 Déployer Firestore Rules sécurisées - - Forcer toutes les écritures via Cloud Functions - - Bloquer accès direct à Firestore -2. 📦 Rebuild et redéployer l'app hébergée - - Mettre à jour avec nouveau code - - Tester en production -3. 📚 Documentation pour l'équipe - - Guide d'utilisation du backend - - Procédures de déploiement - ---- - -## 💡 NOTES IMPORTANTES - -### Refresh automatique -**Problème :** Les listes ne se rafraîchissent pas immédiatement après création. - -**Cause :** Les streams Firestore ne détectent pas instantanément les changements faits via Cloud Functions (délai de synchronisation). - -**Solutions possibles :** -- **Simple** : Attendre 500ms après création -- **Propre** : Forcer `notifyListeners()` après opération -- **Avancé** : Optimistic UI (ajouter localement avant sync) - -### Mode développement -Pour revenir aux émulateurs : -```dart -// lib/config/api_config.dart -static const bool isDevelopment = true; -``` - -Puis lancer les émulateurs : -```powershell -firebase emulators:start -``` - ---- - -## 📞 COMMANDES UTILES - -### Logs des Cloud Functions -```powershell -firebase functions:log -``` - -### Console Firebase -- **Functions** : https://console.firebase.google.com/project/em2rp-951dc/functions -- **Logs** : Onglet "Logs" dans Functions -- **Firestore** : https://console.firebase.google.com/project/em2rp-951dc/firestore - -### Hot reload -``` -r (minuscule) dans le terminal Flutter -``` - -### Hot restart -``` -R (majuscule) dans le terminal Flutter -``` - ---- - -## 🎉 RÉSULTAT FINAL - -### ✅ Ce qui fonctionne -- Création, modification, suppression d'équipements -- Affichage correct des types d'événements -- Backend sécurisé avec Cloud Functions -- Conversion automatique de tous les types Firebase -- Gestion des erreurs et contextes - -### ⏳ À tester -- Opérations CRUD sur événements -- Opérations CRUD sur containers -- Validation des permissions - -### 🎯 Objectif atteint -- Backend opérationnel en production ✅ -- App dev locale utilise le backend ✅ -- App hébergée non impactée ✅ -- Toutes les conversions de types OK ✅ - ---- - -**🚀 Le backend est prêt ! Vous pouvez maintenant tester toutes les opérations ! 🎉** - -**Hot reload (r) et testez la création d'événements !** - diff --git a/em2rp/docs/EXPORT_CALENDAR.md b/em2rp/docs/EXPORT_CALENDAR.md deleted file mode 100644 index 378343d..0000000 --- a/em2rp/docs/EXPORT_CALENDAR.md +++ /dev/null @@ -1,70 +0,0 @@ -# Export vers Google Calendar - -## Fonctionnalité - -L'application permet d'exporter un événement au format ICS (iCalendar), compatible avec Google Calendar, Apple Calendar, Outlook et la plupart des applications de calendrier. - -## Utilisation - -1. Ouvrir les détails d'un événement -2. Cliquer sur l'icône de calendrier 📅 dans l'en-tête -3. Le fichier `.ics` sera automatiquement téléchargé -4. Ouvrir le fichier pour l'importer dans votre application de calendrier - -## Informations exportées - -Le fichier ICS contient : - -### Informations principales -- **Titre** : Nom de l'événement -- **Date de début** : Date et heure de début -- **Date de fin** : Date et heure de fin -- **Lieu** : Adresse de l'événement -- **Statut** : Confirmé / Annulé / En attente - -### Description détaillée -- Type d'événement -- Description complète -- Jauge (nombre de personnes) -- Email de contact -- Téléphone de contact -- Temps d'installation et démontage -- Liste de la main d'œuvre -- Options sélectionnées (avec quantités) -- Prix de base - -## Format du fichier - -Le fichier généré suit le standard **RFC 5545** (iCalendar) et est nommé selon le format : -``` -event_[nom_evenement]_[date].ics -``` - -Exemple : `event_Concert_Mairie_20251225.ics` - -## Compatibilité - -✅ Google Calendar -✅ Apple Calendar (macOS, iOS) -✅ Microsoft Outlook -✅ Thunderbird -✅ Autres applications supportant le format ICS - -## Import dans Google Calendar - -1. Télécharger le fichier `.ics` -2. Ouvrir Google Calendar -3. Cliquer sur l'icône ⚙️ (Paramètres) -4. Sélectionner "Importation et exportation" -5. Cliquer sur "Sélectionner un fichier sur votre ordinateur" -6. Choisir le fichier `.ics` téléchargé -7. Sélectionner le calendrier de destination -8. Cliquer sur "Importer" - -## Notes techniques - -- Les dates sont converties en UTC pour assurer la compatibilité internationale -- Les caractères spéciaux sont correctement échappés selon le standard ICS -- Un UID unique est généré pour chaque événement (`em2rp-[eventId]@em2rp.app`) -- Le fichier est encodé en UTF-8 - diff --git a/em2rp/docs/example_event.ics b/em2rp/docs/example_event.ics deleted file mode 100644 index 41e542c..0000000 --- a/em2rp/docs/example_event.ics +++ /dev/null @@ -1,18 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//EM2RP//Event Manager//FR -CALSCALE:GREGORIAN -METHOD:PUBLISH -BEGIN:VEVENT -UID:em2rp-example123@em2rp.app -DTSTAMP:20251220T120000Z -DTSTART:20251225T190000Z -DTEND:20251225T230000Z -SUMMARY:Concert de Noël -DESCRIPTION:TYPE: Concert\n\nDESCRIPTION:\nConcert de Noël avec orchestre symphonique et chorale.\n\nJAUGE: 500 personnes\nEMAIL DE CONTACT: contact@example.com\nTÉLÉPHONE DE CONTACT: 06 12 34 56 78\n\nADRESSE: Salle des fêtes\, Place de la Mairie\, 75001 Paris\n\nINSTALLATION: 4h\nDÉMONTAGE: 2h\n\nMAIN D'ŒUVRE:\n - Jean Dupont\n - Marie Martin\n - Pierre Durand\n\nOPTIONS:\n - Système son professionnel\n - Éclairage scénique (x2)\n\nPRIX DE BASE: 2500.00€\n\nMATÉRIEL ASSIGNÉ:\n Conteneurs: 3\n Équipements: 15\n\n---\nGéré par EM2RP Event Manager -LOCATION:Salle des fêtes\, Place de la Mairie\, 75001 Paris -STATUS:CONFIRMED -CATEGORIES:Concert -END:VEVENT -END:VCALENDAR - diff --git a/em2rp/functions/index.js b/em2rp/functions/index.js index 2326382..0c11caa 100644 --- a/em2rp/functions/index.js +++ b/em2rp/functions/index.js @@ -1424,6 +1424,49 @@ exports.getAlerts = onRequest(httpOptions, withCors(async (req, res) => { } })); + +exports.markAlertAsRead = onRequest(httpOptions, withCors(async (req, res) => { + try { + await auth.authenticateUser(req); + + const alertId = req.body.data?.alertId; + if (!alertId) { + res.status(400).json({ error: 'alertId is required' }); + return; + } + + await db.collection('alerts').doc(alertId).update({ + isRead: true + }); + + res.status(200).json({ success: true }); + } catch (error) { + logger.error("Error marking alert as read:", error); + res.status(500).json({ error: error.message }); + } +})); + + +exports.deleteAlert = onRequest(httpOptions, withCors(async (req, res) => { + try { + await auth.authenticateUser(req); + + const alertId = req.body.data?.alertId; + if (!alertId) { + res.status(400).json({ error: 'alertId is required' }); + return; + } + + await db.collection('alerts').doc(alertId).delete(); + + res.status(200).json({ success: true }); + } catch (error) { + logger.error("Error deleting alert:", error); + res.status(500).json({ error: error.message }); + } +})); + + // ============================================================================ // USERS - Read with permissions // ============================================================================ @@ -1673,3 +1716,503 @@ exports.updateUser = onCall(async (request) => { } }); +// ============================================================================ +// EQUIPMENT AVAILABILITY - Vérification de disponibilité +// ============================================================================ + +exports.checkEquipmentAvailability = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' }); + return; + } + + const { equipmentId, startDate, endDate, excludeEventId } = req.body.data; + + if (!equipmentId || !startDate || !endDate) { + res.status(400).json({ error: 'equipmentId, startDate, and endDate are required' }); + return; + } + + logger.info(`Checking availability for equipment ${equipmentId} from ${startDate} to ${endDate}, excluding event: ${excludeEventId}`); + + const startTimestamp = admin.firestore.Timestamp.fromDate(new Date(startDate)); + const endTimestamp = admin.firestore.Timestamp.fromDate(new Date(endDate)); + + const eventsSnapshot = await db.collection('events') + .where('status', '!=', 'CANCELLED') + .get(); + + logger.info(`Found ${eventsSnapshot.docs.length} events to check`); + + const conflicts = []; + + for (const eventDoc of eventsSnapshot.docs) { + const event = eventDoc.data(); + + if (excludeEventId && eventDoc.id === excludeEventId) { + continue; + } + + // Gérer les dates qui peuvent être des Timestamps ou des objets Date + let eventStart, eventEnd; + if (event.StartDateTime) { + eventStart = event.StartDateTime.toDate ? event.StartDateTime.toDate() : new Date(event.StartDateTime); + } + if (event.EndDateTime) { + eventEnd = event.EndDateTime.toDate ? event.EndDateTime.toDate() : new Date(event.EndDateTime); + } + + + if (!eventStart || !eventEnd) { + continue; + } + + // Vérifier si l'équipement est assigné à cet événement (directement ou via une boîte) + const assignedEquipment = event.assignedEquipment || []; + const assignedContainers = event.assignedContainers || []; + + // Vérifier si l'équipement est directement assigné + const isEquipmentDirectlyAssigned = assignedEquipment.some(eq => eq.equipmentId === equipmentId); + + // Vérifier si l'équipement est dans une boîte assignée + let isEquipmentInAssignedContainer = false; + if (assignedContainers.length > 0) { + logger.info(`Event ${eventDoc.id} has ${assignedContainers.length} assigned containers`); + // Récupérer les conteneurs assignés et vérifier si l'équipement y est + for (const containerId of assignedContainers) { + const containerDoc = await db.collection('containers').doc(containerId).get(); + if (containerDoc.exists) { + const containerData = containerDoc.data(); + const equipmentIds = containerData.equipmentIds || []; + logger.info(`Container ${containerId} contains equipment IDs: ${equipmentIds.join(', ')}`); + if (equipmentIds.includes(equipmentId)) { + isEquipmentInAssignedContainer = true; + logger.info(`Equipment ${equipmentId} found in container ${containerId} for event ${eventDoc.id}`); + break; + } + } + } + } + + if (isEquipmentDirectlyAssigned) { + logger.info(`Equipment ${equipmentId} is directly assigned to event ${eventDoc.id}`); + } + + if (!isEquipmentDirectlyAssigned && !isEquipmentInAssignedContainer) { + continue; + } + + // Vérifier le chevauchement de dates + const requestStart = startTimestamp.toDate(); + const requestEnd = endTimestamp.toDate(); + + // Inclure les temps d'installation et de démontage + const installationTime = event.InstallationTime || 0; + const disassemblyTime = event.DisassemblyTime || 0; + + const eventStartWithSetup = new Date(eventStart); + eventStartWithSetup.setHours(eventStartWithSetup.getHours() - installationTime); + + const eventEndWithTeardown = new Date(eventEnd); + eventEndWithTeardown.setHours(eventEndWithTeardown.getHours() + disassemblyTime); + + // Il y a conflit si les périodes se chevauchent + const hasOverlap = requestStart < eventEndWithTeardown && requestEnd > eventStartWithSetup; + + if (hasOverlap) { + // Calculer les jours de chevauchement + const overlapStart = new Date(Math.max(requestStart, eventStartWithSetup)); + const overlapEnd = new Date(Math.min(requestEnd, eventEndWithTeardown)); + const overlapDays = Math.ceil((overlapEnd - overlapStart) / (1000 * 60 * 60 * 24)); + + logger.info(`Conflict detected: Equipment ${equipmentId} conflicts with event ${eventDoc.id} (${event.Name})`); + + // Retourner les détails complets de l'événement + const eventData = helpers.serializeTimestamps(event); + conflicts.push({ + eventId: eventDoc.id, + eventName: event.Name, + eventData: eventData, // Ajouter toutes les données de l'événement + startDate: eventStart.toISOString(), + endDate: eventEnd.toISOString(), + overlapDays: overlapDays + }); + } + } + + logger.info(`Total conflicts found: ${conflicts.length}`); + + res.status(200).json({ conflicts, available: conflicts.length === 0 }); + } catch (error) { + logger.error("Error checking equipment availability:", error); + res.status(500).json({ error: error.message || "Failed to check equipment availability" }); + } +})); + +exports.checkContainerAvailability = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' }); + return; + } + + const { containerId, startDate, endDate, excludeEventId } = req.body.data; + + if (!containerId || !startDate || !endDate) { + res.status(400).json({ error: 'containerId, startDate, and endDate are required' }); + return; + } + + // Récupérer le container et ses équipements + const containerDoc = await db.collection('containers').doc(containerId).get(); + if (!containerDoc.exists) { + throw new Error('Container not found'); + } + + const containerData = containerDoc.data(); + const equipmentIds = containerData.equipmentIds || []; + + const startTimestamp = admin.firestore.Timestamp.fromDate(new Date(startDate)); + const endTimestamp = admin.firestore.Timestamp.fromDate(new Date(endDate)); + + const eventsSnapshot = await db.collection('events') + .where('status', '!=', 'CANCELLED') + .get(); + + const containerConflicts = []; + const equipmentConflicts = {}; + + for (const eventDoc of eventsSnapshot.docs) { + const event = eventDoc.data(); + + if (excludeEventId && eventDoc.id === excludeEventId) { + continue; + } + + // Gérer les dates + let eventStart, eventEnd; + if (event.StartDateTime) { + eventStart = event.StartDateTime.toDate ? event.StartDateTime.toDate() : new Date(event.StartDateTime); + } + if (event.EndDateTime) { + eventEnd = event.EndDateTime.toDate ? event.EndDateTime.toDate() : new Date(event.EndDateTime); + } + + if (!eventStart || !eventEnd) { + continue; + } + + // Vérifier si le container est assigné + const assignedContainers = event.assignedContainers || []; + const isContainerAssigned = assignedContainers.includes(containerId); + + // Vérifier si des équipements du container sont assignés + const assignedEquipment = event.assignedEquipment || []; + const conflictingEquipmentIds = equipmentIds.filter(eqId => + assignedEquipment.some(eq => eq.equipmentId === eqId) + ); + + if (!isContainerAssigned && conflictingEquipmentIds.length === 0) { + continue; + } + + // Vérifier le chevauchement de dates + const requestStart = startTimestamp.toDate(); + const requestEnd = endTimestamp.toDate(); + + const installationTime = event.InstallationTime || 0; + const disassemblyTime = event.DisassemblyTime || 0; + + const eventStartWithSetup = new Date(eventStart); + eventStartWithSetup.setHours(eventStartWithSetup.getHours() - installationTime); + + const eventEndWithTeardown = new Date(eventEnd); + eventEndWithTeardown.setHours(eventEndWithTeardown.getHours() + disassemblyTime); + + const hasOverlap = requestStart < eventEndWithTeardown && requestEnd > eventStartWithSetup; + + if (hasOverlap) { + const overlapStart = new Date(Math.max(requestStart, eventStartWithSetup)); + const overlapEnd = new Date(Math.min(requestEnd, eventEndWithTeardown)); + const overlapDays = Math.ceil((overlapEnd - overlapStart) / (1000 * 60 * 60 * 24)); + + const conflictInfo = { + eventId: eventDoc.id, + eventName: event.Name, + startDate: eventStart.toISOString(), + endDate: eventEnd.toISOString(), + overlapDays: overlapDays + }; + + if (isContainerAssigned) { + containerConflicts.push(conflictInfo); + } + + conflictingEquipmentIds.forEach(eqId => { + if (!equipmentConflicts[eqId]) { + equipmentConflicts[eqId] = []; + } + equipmentConflicts[eqId].push(conflictInfo); + }); + } + } + + const hasContainerConflict = containerConflicts.length > 0; + const hasPartialConflict = Object.keys(equipmentConflicts).length > 0 && !hasContainerConflict; + const conflictType = hasContainerConflict ? 'complete' : (hasPartialConflict ? 'partial' : 'none'); + + res.status(200).json({ + conflictType, + containerConflicts, + equipmentConflicts, + isAvailable: conflictType === 'none' + }); + } catch (error) { + logger.error("Error checking container availability:", error); + res.status(500).json({ error: error.message || "Failed to check container availability" }); + } +})); + +// ============================================================================ +// AVAILABILITY - Optimized batch check +// ============================================================================ + +/** + * Récupère tous les équipements et conteneurs en conflit pour une période donnée + * Optimisé : une seule requête au lieu d'une par équipement + */ +exports.getConflictingEquipmentIds = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' }); + return; + } + + const { startDate, endDate, excludeEventId, installationTime = 0, disassemblyTime = 0 } = req.body.data; + + if (!startDate || !endDate) { + res.status(400).json({ error: 'startDate and endDate are required' }); + return; + } + + logger.info(`Getting conflicting equipment IDs for period ${startDate} to ${endDate}`); + + // Calculer la période effective avec temps de montage/démontage + const requestStartDate = new Date(startDate); + requestStartDate.setHours(requestStartDate.getHours() - installationTime); + + const requestEndDate = new Date(endDate); + requestEndDate.setHours(requestEndDate.getHours() + disassemblyTime); + + // Récupérer tous les événements non annulés + const eventsSnapshot = await db.collection('events') + .where('status', '!=', 'CANCELLED') + .get(); + + logger.info(`Found ${eventsSnapshot.docs.length} events to check`); + + // Maps pour stocker les conflits + const conflictingEquipmentIds = new Set(); + const conflictingContainerIds = new Set(); + const conflictDetails = {}; // { equipmentId/containerId: [{ eventId, eventName, startDate, endDate }] } + + for (const eventDoc of eventsSnapshot.docs) { + // Exclure l'événement en cours d'édition + if (excludeEventId && eventDoc.id === excludeEventId) { + continue; + } + + const event = eventDoc.data(); + + // Gérer les dates + let eventStart, eventEnd; + if (event.StartDateTime) { + eventStart = event.StartDateTime.toDate ? event.StartDateTime.toDate() : new Date(event.StartDateTime); + } + if (event.EndDateTime) { + eventEnd = event.EndDateTime.toDate ? event.EndDateTime.toDate() : new Date(event.EndDateTime); + } + + if (!eventStart || !eventEnd) { + continue; + } + + // Ajouter temps de montage/démontage de cet événement + const eventInstallTime = event.InstallationTime || 0; + const eventDisassemblyTime = event.DisassemblyTime || 0; + + const eventStartWithSetup = new Date(eventStart); + eventStartWithSetup.setHours(eventStartWithSetup.getHours() - eventInstallTime); + + const eventEndWithTeardown = new Date(eventEnd); + eventEndWithTeardown.setHours(eventEndWithTeardown.getHours() + eventDisassemblyTime); + + // Vérifier le chevauchement de dates + const hasOverlap = requestStartDate < eventEndWithTeardown && requestEndDate > eventStartWithSetup; + + if (!hasOverlap) { + continue; + } + + // Il y a chevauchement ! Récupérer les équipements et conteneurs assignés + const assignedEquipment = event.assignedEquipment || []; + const assignedContainers = event.assignedContainers || []; + + const conflictInfo = { + eventId: eventDoc.id, + eventName: event.Name, + startDate: eventStart.toISOString(), + endDate: eventEnd.toISOString(), + }; + + // Ajouter les équipements directement assignés + for (const eq of assignedEquipment) { + const equipmentId = eq.equipmentId; + conflictingEquipmentIds.add(equipmentId); + + if (!conflictDetails[equipmentId]) { + conflictDetails[equipmentId] = []; + } + conflictDetails[equipmentId].push(conflictInfo); + } + + // Ajouter les conteneurs assignés + for (const containerId of assignedContainers) { + conflictingContainerIds.add(containerId); + + if (!conflictDetails[containerId]) { + conflictDetails[containerId] = []; + } + conflictDetails[containerId].push(conflictInfo); + + // Récupérer les équipements dans ce conteneur + const containerDoc = await db.collection('containers').doc(containerId).get(); + if (containerDoc.exists) { + const containerData = containerDoc.data(); + const equipmentIds = containerData.equipmentIds || []; + + // Marquer tous les équipements du conteneur comme en conflit + for (const equipmentId of equipmentIds) { + conflictingEquipmentIds.add(equipmentId); + + if (!conflictDetails[equipmentId]) { + conflictDetails[equipmentId] = []; + } + // Ajouter une note indiquant que c'est via le conteneur + conflictDetails[equipmentId].push({ + ...conflictInfo, + viaContainer: containerId, + viaContainerName: containerData.name || 'Conteneur inconnu', + }); + } + } + } + } + + logger.info(`Found ${conflictingEquipmentIds.size} conflicting equipment(s) and ${conflictingContainerIds.size} conflicting container(s)`); + + res.status(200).json({ + conflictingEquipmentIds: Array.from(conflictingEquipmentIds), + conflictingContainerIds: Array.from(conflictingContainerIds), + conflictDetails: conflictDetails, + }); + } catch (error) { + logger.error("Error getting conflicting equipment IDs:", error); + res.status(500).json({ error: error.message || "Failed to get conflicting equipment IDs" }); + } +})); + +// ============================================================================ +// USER - Get current authenticated user +// ============================================================================ +exports.getCurrentUser = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const userId = decodedToken.uid; + + const userDoc = await db.collection('users').doc(userId).get(); + if (!userDoc.exists) { + res.status(404).json({ error: 'User not found' }); + return; + } + + const userData = userDoc.data(); + + // Récupérer le rôle + let roleData = null; + if (userData.role) { + const roleDoc = await userData.role.get(); + if (roleDoc.exists) { + roleData = { id: roleDoc.id, ...roleDoc.data() }; + } + } + + res.status(200).json({ + user: { + uid: userId, + ...helpers.serializeTimestamps(userData), + role: roleData + } + }); + } catch (error) { + logger.error("Error getting current user:", error); + res.status(500).json({ error: error.message }); + } +})); + +// ============================================================================ +// MAINTENANCE - Delete +// ============================================================================ +exports.deleteMaintenance = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + + // Vérifier permission + const canManage = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); + if (!canManage) { + res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); + return; + } + + const maintenanceId = req.body.data?.maintenanceId; + if (!maintenanceId) { + res.status(400).json({ error: 'maintenanceId is required' }); + return; + } + + // Récupérer la maintenance pour connaître les équipements + const maintenanceDoc = await db.collection('maintenances').doc(maintenanceId).get(); + if (maintenanceDoc.exists) { + const maintenance = maintenanceDoc.data(); + + // Retirer la maintenance des équipements + if (maintenance.equipmentIds) { + for (const equipmentId of maintenance.equipmentIds) { + const equipmentDoc = await db.collection('equipments').doc(equipmentId).get(); + if (equipmentDoc.exists) { + const equipmentData = equipmentDoc.data(); + const maintenanceIds = (equipmentData.maintenanceIds || []).filter(id => id !== maintenanceId); + await db.collection('equipments').doc(equipmentId).update({ maintenanceIds }); + } + } + } + } + + await db.collection('maintenances').doc(maintenanceId).delete(); + + res.status(200).json({ success: true }); + } catch (error) { + logger.error("Error deleting maintenance:", error); + res.status(500).json({ error: error.message }); + } +})); diff --git a/em2rp/lib/services/data_service.dart b/em2rp/lib/services/data_service.dart index 837bcc6..70bc391 100644 --- a/em2rp/lib/services/data_service.dart +++ b/em2rp/lib/services/data_service.dart @@ -422,6 +422,29 @@ class DataService { } } + /// Récupère tous les IDs d'équipements et conteneurs en conflit pour une période + /// Optimisé : une seule requête au lieu d'une par équipement + Future> getConflictingEquipmentIds({ + required DateTime startDate, + required DateTime endDate, + String? excludeEventId, + int installationTime = 0, + int disassemblyTime = 0, + }) async { + try { + final result = await _apiService.call('getConflictingEquipmentIds', { + 'startDate': startDate.toIso8601String(), + 'endDate': endDate.toIso8601String(), + if (excludeEventId != null) 'excludeEventId': excludeEventId, + 'installationTime': installationTime, + 'disassemblyTime': disassemblyTime, + }); + return result; + } catch (e) { + throw Exception('Erreur lors de la récupération des équipements en conflit: $e'); + } + } + // ============================================================================ // MAINTENANCES // ============================================================================ diff --git a/em2rp/lib/services/event_availability_service.dart b/em2rp/lib/services/event_availability_service.dart index b786c09..6d65fea 100644 --- a/em2rp/lib/services/event_availability_service.dart +++ b/em2rp/lib/services/event_availability_service.dart @@ -84,6 +84,8 @@ class EventAvailabilityService { final conflicts = []; try { + print('[EventAvailabilityService] Checking availability for equipment $equipmentId ($equipmentName)'); + // Utiliser la Cloud Function pour vérifier la disponibilité final result = await _dataService.checkEquipmentAvailability( equipmentId: equipmentId, @@ -92,24 +94,23 @@ class EventAvailabilityService { excludeEventId: excludeEventId, ); + print('[EventAvailabilityService] Result for $equipmentId: $result'); + final available = result['available'] as bool? ?? true; + print('[EventAvailabilityService] Equipment $equipmentId available: $available'); + if (!available) { final conflictsData = result['conflicts'] as List? ?? []; - - // Récupérer les détails des événements en conflit - final eventsData = await _getEventsList(); + print('[EventAvailabilityService] Found ${conflictsData.length} conflicts for equipment $equipmentId'); for (final conflictData in conflictsData) { final conflict = conflictData as Map; final eventId = conflict['eventId'] as String; - // Trouver l'événement correspondant - final eventData = eventsData.firstWhere( - (e) => e['id'] == eventId, - orElse: () => {}, - ); + // Le backend retourne déjà eventData + final eventData = conflict['eventData'] as Map?; - if (eventData.isNotEmpty) { + if (eventData != null && eventData.isNotEmpty) { try { final event = EventModel.fromMap(eventData, eventId); conflicts.add(AvailabilityConflict( @@ -118,8 +119,10 @@ class EventAvailabilityService { conflictingEvent: event, overlapDays: conflict['overlapDays'] as int? ?? 0, )); + print('[EventAvailabilityService] Added conflict with event ${event.name}'); } catch (e) { print('[EventAvailabilityService] Error creating EventModel: $e'); + print('[EventAvailabilityService] EventData: $eventData'); } } } @@ -128,6 +131,7 @@ class EventAvailabilityService { print('[EventAvailabilityService] Error checking availability: $e'); } + print('[EventAvailabilityService] Returning ${conflicts.length} conflicts for equipment $equipmentId'); return conflicts; } diff --git a/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart b/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart index 432a281..856e9ca 100644 --- a/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart +++ b/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart @@ -137,6 +137,8 @@ class _EventAssignedEquipmentSectionState extends State _processSelection(Map selection) async { + print('[EventAssignedEquipmentSection] Processing selection of ${selection.length} items'); + // Séparer équipements et conteneurs final newEquipment = []; final newContainers = []; @@ -152,6 +154,8 @@ class _EventAssignedEquipmentSectionState extends State(); final equipmentProvider = context.read(); @@ -159,10 +163,14 @@ class _EventAssignedEquipmentSectionState extends State>{}; // 1. Vérifier les conflits pour les équipements directs + print('[EventAssignedEquipmentSection] Checking conflicts for ${newEquipment.length} equipment(s)'); for (var eq in newEquipment) { + print('[EventAssignedEquipmentSection] Checking equipment: ${eq.equipmentId}'); + final equipment = allEquipment.firstWhere( (e) => e.id == eq.equipmentId, orElse: () => EquipmentModel( @@ -177,6 +185,8 @@ class _EventAssignedEquipmentSectionState extends State c.id == containerId, @@ -291,17 +305,25 @@ class _EventAssignedEquipmentSectionState extends State( context: context, builder: (context) => EquipmentConflictDialog(conflicts: allConflicts), ); + print('[EventAssignedEquipmentSection] Conflict dialog result: $action'); + if (action == 'cancel') { return; // Annuler l'ajout } else if (action == 'force_removed') {