Fix : probleme de la détection d'utilisation par un autre événement
This commit is contained in:
@@ -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<String, dynamic> | 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 !**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -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<Map<String, dynamic>> 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
|
||||
// ============================================================================
|
||||
|
||||
@@ -84,6 +84,8 @@ class EventAvailabilityService {
|
||||
final conflicts = <AvailabilityConflict>[];
|
||||
|
||||
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<dynamic>? ?? [];
|
||||
|
||||
// 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<String, dynamic>;
|
||||
final eventId = conflict['eventId'] as String;
|
||||
|
||||
// Trouver l'événement correspondant
|
||||
final eventData = eventsData.firstWhere(
|
||||
(e) => e['id'] == eventId,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
// Le backend retourne déjà eventData
|
||||
final eventData = conflict['eventData'] as Map<String, dynamic>?;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,8 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
}
|
||||
|
||||
Future<void> _processSelection(Map<String, SelectedItem> selection) async {
|
||||
print('[EventAssignedEquipmentSection] Processing selection of ${selection.length} items');
|
||||
|
||||
// Séparer équipements et conteneurs
|
||||
final newEquipment = <EventEquipment>[];
|
||||
final newContainers = <String>[];
|
||||
@@ -152,6 +154,8 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
}
|
||||
}
|
||||
|
||||
print('[EventAssignedEquipmentSection] Found ${newEquipment.length} equipment(s) and ${newContainers.length} container(s)');
|
||||
|
||||
// Charger les équipements et conteneurs
|
||||
final containerProvider = context.read<ContainerProvider>();
|
||||
final equipmentProvider = context.read<EquipmentProvider>();
|
||||
@@ -159,10 +163,14 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
final allContainers = await containerProvider.containersStream.first;
|
||||
final allEquipment = await equipmentProvider.equipmentStream.first;
|
||||
|
||||
print('[EventAssignedEquipmentSection] Starting conflict checks...');
|
||||
final allConflicts = <String, List<AvailabilityConflict>>{};
|
||||
|
||||
// 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<EventAssignedEquipmentSe
|
||||
),
|
||||
);
|
||||
|
||||
print('[EventAssignedEquipmentSection] Equipment ${eq.equipmentId}: hasQuantity=${equipment.hasQuantity}');
|
||||
|
||||
// Pour les équipements quantifiables (consommables/câbles)
|
||||
if (equipment.hasQuantity) {
|
||||
// Vérifier la quantité disponible
|
||||
@@ -215,12 +225,16 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
);
|
||||
|
||||
if (conflicts.isNotEmpty) {
|
||||
print('[EventAssignedEquipmentSection] Equipment ${eq.equipmentId}: ${conflicts.length} conflict(s) found');
|
||||
allConflicts[eq.equipmentId] = conflicts;
|
||||
} else {
|
||||
print('[EventAssignedEquipmentSection] Equipment ${eq.equipmentId}: no conflicts');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Vérifier les conflits pour les boîtes et leur contenu
|
||||
print('[EventAssignedEquipmentSection] Checking conflicts for ${newContainers.length} container(s)');
|
||||
for (var containerId in newContainers) {
|
||||
final container = allContainers.firstWhere(
|
||||
(c) => c.id == containerId,
|
||||
@@ -291,17 +305,25 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
}
|
||||
|
||||
if (containerConflicts.isNotEmpty) {
|
||||
print('[EventAssignedEquipmentSection] Container $containerId: ${containerConflicts.length} conflict(s) found');
|
||||
allConflicts[containerId] = containerConflicts;
|
||||
} else {
|
||||
print('[EventAssignedEquipmentSection] Container $containerId: no conflicts');
|
||||
}
|
||||
}
|
||||
|
||||
print('[EventAssignedEquipmentSection] Total conflicts found: ${allConflicts.length}');
|
||||
|
||||
if (allConflicts.isNotEmpty) {
|
||||
print('[EventAssignedEquipmentSection] Showing conflict dialog with ${allConflicts.length} items in conflict');
|
||||
// Afficher le dialog de conflits
|
||||
final action = await showDialog<String>(
|
||||
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') {
|
||||
|
||||
Reference in New Issue
Block a user