feat: Ajout de la gestion des utilisateurs et optimisation du chargement des données

Cette mise à jour introduit la gestion complète des utilisateurs (création, mise à jour, suppression) via des Cloud Functions et optimise de manière significative le chargement des données dans toute l'application.

**Features :**
- **Gestion des utilisateurs (Backend & Frontend) :**
    - Ajout des Cloud Functions `getUser`, `updateUser` et `deleteUser` pour gérer les utilisateurs de manière sécurisée, en respectant les permissions des rôles.
    - L'authentification passe désormais par `onCall` pour plus de sécurité.
- **Optimisation du chargement des données :**
    - Introduction de nouvelles Cloud Functions `getEquipmentsByIds` et `getContainersByIds` pour récupérer uniquement les documents nécessaires, réduisant ainsi la charge sur le client et Firestore.
    - Les fournisseurs (`EquipmentProvider`, `ContainerProvider`) ont été refactorisés pour utiliser un chargement à la demande (`ensureLoaded`) et mettre en cache les données récupérées.
    - Les écrans de détails et de préparation d'événements n'utilisent plus de `Stream` globaux, mais chargent les équipements et boites spécifiques via ces nouvelles fonctions, améliorant considérablement les performances.

**Refactorisation et Améliorations :**
- **Backend (Cloud Functions) :**
    - Le service de vérification de disponibilité (`checkEquipmentAvailability`) est désormais une Cloud Function, déplaçant la logique métier côté serveur.
    - La récupération des données (utilisateurs, événements, alertes) a été centralisée derrière des Cloud Functions, remplaçant les appels directs à Firestore depuis le client.
    - Amélioration de la sérialisation des données (timestamps, références) dans les réponses des fonctions.
- **Frontend (Flutter) :**
    - `LocalUserProvider` charge désormais les informations de l'utilisateur connecté via la fonction `getCurrentUser`, incluant son rôle et ses permissions en un seul appel.
    - `AlertProvider` utilise des fonctions pour charger et manipuler les alertes, abandonnant le `Stream` Firestore.
    - `EventAvailabilityService` utilise maintenant la Cloud Function `checkEquipmentAvailability` au lieu d'une logique client complexe.
    - Correction de la gestion des références de rôles (`roles/ADMIN`) et des `DocumentReference` pour les utilisateurs dans l'ensemble de l'application.
    - Le service d'export ICS (`IcsExportService`) a été simplifié, partant du principe que les données nécessaires (utilisateurs, options) sont déjà chargées dans l'application.
This commit is contained in:
ElPoyo
2026-01-13 01:40:28 +01:00
parent f38d75362c
commit 2bcd1ca4c3
16 changed files with 916 additions and 374 deletions

View File

@@ -1,7 +1,8 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:em2rp/services/data_service.dart';
/// Type de conflit
enum ConflictType {
@@ -63,9 +64,16 @@ class AvailabilityConflict {
/// Service pour vérifier la disponibilité du matériel
class EventAvailabilityService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final DataService _dataService = DataService(apiService);
/// Vérifie si un équipement est disponible pour une plage de dates
/// Helper pour récupérer uniquement la liste d'événements
Future<List<Map<String, dynamic>>> _getEventsList() async {
final result = await _dataService.getEvents();
final events = result['events'] as List<dynamic>? ?? [];
return events.map((e) => e as Map<String, dynamic>).toList();
}
/// Vérifie si un équipement est disponible pour une plage de dates via Cloud Function
Future<List<AvailabilityConflict>> checkEquipmentAvailability({
required String equipmentId,
required String equipmentName,
@@ -76,59 +84,44 @@ class EventAvailabilityService {
final conflicts = <AvailabilityConflict>[];
try {
// Récupérer TOUS les événements (on filtre côté client car arrayContains avec objet ne marche pas)
final eventsSnapshot = await _firestore.collection('events').get();
// Utiliser la Cloud Function pour vérifier la disponibilité
final result = await _dataService.checkEquipmentAvailability(
equipmentId: equipmentId,
startDate: startDate,
endDate: endDate,
excludeEventId: excludeEventId,
);
for (var doc in eventsSnapshot.docs) {
if (excludeEventId != null && doc.id == excludeEventId) {
continue; // Ignorer l'événement en cours d'édition
}
final available = result['available'] as bool? ?? true;
if (!available) {
final conflictsData = result['conflicts'] as List<dynamic>? ?? [];
try {
final data = doc.data();
final event = EventModel.fromMap(data, doc.id);
// Récupérer les détails des événements en conflit
final eventsData = await _getEventsList();
// Ignorer les événements annulés
if (event.status == EventStatus.canceled) {
continue;
}
for (final conflictData in conflictsData) {
final conflict = conflictData as Map<String, dynamic>;
final eventId = conflict['eventId'] as String;
// Vérifier si cet événement contient l'équipement recherché
final assignedEquipment = event.assignedEquipment.firstWhere(
(eq) => eq.equipmentId == equipmentId,
orElse: () => EventEquipment(equipmentId: ''),
// Trouver l'événement correspondant
final eventData = eventsData.firstWhere(
(e) => e['id'] == eventId,
orElse: () => <String, dynamic>{},
);
// Si l'équipement est assigné à cet événement, il est indisponible
// (peu importe le statut de préparation/chargement/retour)
if (assignedEquipment.equipmentId.isNotEmpty) {
// Calculer les dates réelles avec temps d'installation et démontage
final eventRealStartDate = event.startDateTime.subtract(
Duration(hours: event.installationTime),
);
final eventRealEndDate = event.endDateTime.add(
Duration(hours: event.disassemblyTime),
);
// Vérifier le chevauchement des dates
if (_datesOverlap(startDate, endDate, eventRealStartDate, eventRealEndDate)) {
final overlapDays = _calculateOverlapDays(
startDate,
endDate,
eventRealStartDate,
eventRealEndDate,
);
if (eventData.isNotEmpty) {
try {
final event = EventModel.fromMap(eventData, eventId);
conflicts.add(AvailabilityConflict(
equipmentId: equipmentId,
equipmentName: equipmentName,
conflictingEvent: event,
overlapDays: overlapDays,
overlapDays: conflict['overlapDays'] as int? ?? 0,
));
} catch (e) {
print('[EventAvailabilityService] Error creating EventModel: $e');
}
}
} catch (e) {
print('[EventAvailabilityService] Error processing event ${doc.id}: $e');
}
}
} catch (e) {
@@ -138,11 +131,6 @@ class EventAvailabilityService {
return conflicts;
}
/// Helper pour formater les dates dans les logs
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
}
/// Vérifie la disponibilité pour une liste d'équipements
Future<Map<String, List<AvailabilityConflict>>> checkMultipleEquipmentAvailability({
required List<String> equipmentIds,
@@ -203,16 +191,17 @@ class EventAvailabilityService {
int reservedQuantity = 0;
try {
// Récupérer tous les événements (on filtre côté client)
final eventsSnapshot = await _firestore.collection('events').get();
// Récupérer tous les événements via Cloud Function
final eventsData = await _getEventsList();
for (var doc in eventsSnapshot.docs) {
if (excludeEventId != null && doc.id == excludeEventId) {
for (var eventData in eventsData) {
final eventId = eventData['id'] as String;
if (excludeEventId != null && eventId == excludeEventId) {
continue;
}
try {
final event = EventModel.fromMap(doc.data(), doc.id);
final event = EventModel.fromMap(eventData, eventId);
// Ignorer les événements annulés
if (event.status == EventStatus.canceled) {
@@ -241,7 +230,7 @@ class EventAvailabilityService {
}
}
} catch (e) {
print('[EventAvailabilityService] Error processing event ${doc.id} for quantity: $e');
print('[EventAvailabilityService] Error processing event $eventId for quantity: $e');
}
}
} catch (e) {
@@ -275,13 +264,14 @@ class EventAvailabilityService {
// ✅ Ne créer un conflit que si la quantité est VRAIMENT insuffisante
if (availableQty < requestedQuantity) {
// Trouver les événements qui réservent cette quantité
final eventsSnapshot = await _firestore.collection('events').get();
final eventsData = await _getEventsList();
for (var doc in eventsSnapshot.docs) {
if (excludeEventId != null && doc.id == excludeEventId) continue;
for (var eventData in eventsData) {
final eventId = eventData['id'] as String;
if (excludeEventId != null && eventId == excludeEventId) continue;
try {
final event = EventModel.fromMap(doc.data(), doc.id);
final event = EventModel.fromMap(eventData, eventId);
if (_datesOverlap(startDate, endDate, event.startDateTime, event.endDateTime)) {
final assignedEquipment = event.assignedEquipment.firstWhere(
@@ -304,7 +294,7 @@ class EventAvailabilityService {
}
}
} catch (e) {
print('[EventAvailabilityService] Error processing event ${doc.id}: $e');
print('[EventAvailabilityService] Error processing event $eventId: $e');
}
}
}
@@ -334,15 +324,16 @@ class EventAvailabilityService {
final conflictingChildrenIds = <String>[];
// Vérifier d'abord si la boîte complète est utilisée
final eventsSnapshot = await _firestore.collection('events').get();
final eventsData = await _getEventsList();
bool isContainerFullyUsed = false;
EventModel? containerConflictingEvent;
for (var doc in eventsSnapshot.docs) {
if (excludeEventId != null && doc.id == excludeEventId) continue;
for (var eventData in eventsData) {
final eventId = eventData['id'] as String;
if (excludeEventId != null && eventId == excludeEventId) continue;
try {
final event = EventModel.fromMap(doc.data(), doc.id);
final event = EventModel.fromMap(eventData, eventId);
// Ignorer les événements annulés
if (event.status == EventStatus.canceled) {
@@ -366,7 +357,7 @@ class EventAvailabilityService {
}
}
} catch (e) {
print('[EventAvailabilityService] Error processing event ${doc.id}: $e');
print('[EventAvailabilityService] Error processing event $eventId: $e');
}
}