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
|
// 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
|
// MAINTENANCES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ class EventAvailabilityService {
|
|||||||
final conflicts = <AvailabilityConflict>[];
|
final conflicts = <AvailabilityConflict>[];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
print('[EventAvailabilityService] Checking availability for equipment $equipmentId ($equipmentName)');
|
||||||
|
|
||||||
// Utiliser la Cloud Function pour vérifier la disponibilité
|
// Utiliser la Cloud Function pour vérifier la disponibilité
|
||||||
final result = await _dataService.checkEquipmentAvailability(
|
final result = await _dataService.checkEquipmentAvailability(
|
||||||
equipmentId: equipmentId,
|
equipmentId: equipmentId,
|
||||||
@@ -92,24 +94,23 @@ class EventAvailabilityService {
|
|||||||
excludeEventId: excludeEventId,
|
excludeEventId: excludeEventId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
print('[EventAvailabilityService] Result for $equipmentId: $result');
|
||||||
|
|
||||||
final available = result['available'] as bool? ?? true;
|
final available = result['available'] as bool? ?? true;
|
||||||
|
print('[EventAvailabilityService] Equipment $equipmentId available: $available');
|
||||||
|
|
||||||
if (!available) {
|
if (!available) {
|
||||||
final conflictsData = result['conflicts'] as List<dynamic>? ?? [];
|
final conflictsData = result['conflicts'] as List<dynamic>? ?? [];
|
||||||
|
print('[EventAvailabilityService] Found ${conflictsData.length} conflicts for equipment $equipmentId');
|
||||||
// Récupérer les détails des événements en conflit
|
|
||||||
final eventsData = await _getEventsList();
|
|
||||||
|
|
||||||
for (final conflictData in conflictsData) {
|
for (final conflictData in conflictsData) {
|
||||||
final conflict = conflictData as Map<String, dynamic>;
|
final conflict = conflictData as Map<String, dynamic>;
|
||||||
final eventId = conflict['eventId'] as String;
|
final eventId = conflict['eventId'] as String;
|
||||||
|
|
||||||
// Trouver l'événement correspondant
|
// Le backend retourne déjà eventData
|
||||||
final eventData = eventsData.firstWhere(
|
final eventData = conflict['eventData'] as Map<String, dynamic>?;
|
||||||
(e) => e['id'] == eventId,
|
|
||||||
orElse: () => <String, dynamic>{},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (eventData.isNotEmpty) {
|
if (eventData != null && eventData.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
final event = EventModel.fromMap(eventData, eventId);
|
final event = EventModel.fromMap(eventData, eventId);
|
||||||
conflicts.add(AvailabilityConflict(
|
conflicts.add(AvailabilityConflict(
|
||||||
@@ -118,8 +119,10 @@ class EventAvailabilityService {
|
|||||||
conflictingEvent: event,
|
conflictingEvent: event,
|
||||||
overlapDays: conflict['overlapDays'] as int? ?? 0,
|
overlapDays: conflict['overlapDays'] as int? ?? 0,
|
||||||
));
|
));
|
||||||
|
print('[EventAvailabilityService] Added conflict with event ${event.name}');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[EventAvailabilityService] Error creating EventModel: $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] Error checking availability: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('[EventAvailabilityService] Returning ${conflicts.length} conflicts for equipment $equipmentId');
|
||||||
return conflicts;
|
return conflicts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _processSelection(Map<String, SelectedItem> selection) async {
|
Future<void> _processSelection(Map<String, SelectedItem> selection) async {
|
||||||
|
print('[EventAssignedEquipmentSection] Processing selection of ${selection.length} items');
|
||||||
|
|
||||||
// Séparer équipements et conteneurs
|
// Séparer équipements et conteneurs
|
||||||
final newEquipment = <EventEquipment>[];
|
final newEquipment = <EventEquipment>[];
|
||||||
final newContainers = <String>[];
|
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
|
// Charger les équipements et conteneurs
|
||||||
final containerProvider = context.read<ContainerProvider>();
|
final containerProvider = context.read<ContainerProvider>();
|
||||||
final equipmentProvider = context.read<EquipmentProvider>();
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
@@ -159,10 +163,14 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
final allContainers = await containerProvider.containersStream.first;
|
final allContainers = await containerProvider.containersStream.first;
|
||||||
final allEquipment = await equipmentProvider.equipmentStream.first;
|
final allEquipment = await equipmentProvider.equipmentStream.first;
|
||||||
|
|
||||||
|
print('[EventAssignedEquipmentSection] Starting conflict checks...');
|
||||||
final allConflicts = <String, List<AvailabilityConflict>>{};
|
final allConflicts = <String, List<AvailabilityConflict>>{};
|
||||||
|
|
||||||
// 1. Vérifier les conflits pour les équipements directs
|
// 1. Vérifier les conflits pour les équipements directs
|
||||||
|
print('[EventAssignedEquipmentSection] Checking conflicts for ${newEquipment.length} equipment(s)');
|
||||||
for (var eq in newEquipment) {
|
for (var eq in newEquipment) {
|
||||||
|
print('[EventAssignedEquipmentSection] Checking equipment: ${eq.equipmentId}');
|
||||||
|
|
||||||
final equipment = allEquipment.firstWhere(
|
final equipment = allEquipment.firstWhere(
|
||||||
(e) => e.id == eq.equipmentId,
|
(e) => e.id == eq.equipmentId,
|
||||||
orElse: () => EquipmentModel(
|
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)
|
// Pour les équipements quantifiables (consommables/câbles)
|
||||||
if (equipment.hasQuantity) {
|
if (equipment.hasQuantity) {
|
||||||
// Vérifier la quantité disponible
|
// Vérifier la quantité disponible
|
||||||
@@ -215,12 +225,16 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (conflicts.isNotEmpty) {
|
if (conflicts.isNotEmpty) {
|
||||||
|
print('[EventAssignedEquipmentSection] Equipment ${eq.equipmentId}: ${conflicts.length} conflict(s) found');
|
||||||
allConflicts[eq.equipmentId] = conflicts;
|
allConflicts[eq.equipmentId] = conflicts;
|
||||||
|
} else {
|
||||||
|
print('[EventAssignedEquipmentSection] Equipment ${eq.equipmentId}: no conflicts');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Vérifier les conflits pour les boîtes et leur contenu
|
// 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) {
|
for (var containerId in newContainers) {
|
||||||
final container = allContainers.firstWhere(
|
final container = allContainers.firstWhere(
|
||||||
(c) => c.id == containerId,
|
(c) => c.id == containerId,
|
||||||
@@ -291,17 +305,25 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (containerConflicts.isNotEmpty) {
|
if (containerConflicts.isNotEmpty) {
|
||||||
|
print('[EventAssignedEquipmentSection] Container $containerId: ${containerConflicts.length} conflict(s) found');
|
||||||
allConflicts[containerId] = containerConflicts;
|
allConflicts[containerId] = containerConflicts;
|
||||||
|
} else {
|
||||||
|
print('[EventAssignedEquipmentSection] Container $containerId: no conflicts');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('[EventAssignedEquipmentSection] Total conflicts found: ${allConflicts.length}');
|
||||||
|
|
||||||
if (allConflicts.isNotEmpty) {
|
if (allConflicts.isNotEmpty) {
|
||||||
|
print('[EventAssignedEquipmentSection] Showing conflict dialog with ${allConflicts.length} items in conflict');
|
||||||
// Afficher le dialog de conflits
|
// Afficher le dialog de conflits
|
||||||
final action = await showDialog<String>(
|
final action = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => EquipmentConflictDialog(conflicts: allConflicts),
|
builder: (context) => EquipmentConflictDialog(conflicts: allConflicts),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
print('[EventAssignedEquipmentSection] Conflict dialog result: $action');
|
||||||
|
|
||||||
if (action == 'cancel') {
|
if (action == 'cancel') {
|
||||||
return; // Annuler l'ajout
|
return; // Annuler l'ajout
|
||||||
} else if (action == 'force_removed') {
|
} else if (action == 'force_removed') {
|
||||||
|
|||||||
Reference in New Issue
Block a user