Fix : probleme de la détection d'utilisation par un autre événement

This commit is contained in:
ElPoyo
2026-01-13 18:07:16 +01:00
parent 0f7a886cf7
commit 272b4bc9c9
7 changed files with 601 additions and 326 deletions

View File

@@ -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 !**

View File

@@ -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

View File

@@ -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

View File

@@ -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 });
}
}));

View File

@@ -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
// ============================================================================ // ============================================================================

View File

@@ -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;
} }

View File

@@ -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') {