eac103491f
- **Recherche d'événements** : Ajout d'une fonctionnalité de recherche (titre, description, lieu) dans le calendrier et d'une nouvelle fonction Cloud `searchEvents` avec gestion des permissions.
- **Suppression d'équipement avec forçage** :
- Mise à jour de la fonction Cloud `deleteEquipment` pour détecter les assignations à des événements futurs.
- Ajout d'une option `forceDelete` pour passer outre les conflits d'assignation.
- Création de `EquipmentDeleteUtils` pour gérer uniformément les dialogues de confirmation et les erreurs de conflit (HTTP 409).
- Intégration de la logique de suppression sécurisée dans `EquipmentDetailPage` et `EquipmentManagementPage`.
- **Calendrier** :
- Refonte de l'interface mobile pour intégrer la barre de recherche.
- Optimisation du chargement des événements lors de la sélection d'un résultat de recherche (lazy loading du mois concerné).
- Amélioration de la stabilité de la sélection d'événements et du filtrage par utilisateur.
- **Services & Providers** :
- Amélioration de la gestion des erreurs dans `ApiService` pour faciliter le re-throw des exceptions personnalisées.
- Ajout du support de la suppression forcée dans `DataService` et `EquipmentProvider`.
- **Refactoring** : Nettoyage du code, amélioration du formatage et ajout de logs de debug dans les services de données et d'équipements.
420 lines
13 KiB
Dart
420 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:em2rp/models/event_model.dart';
|
|
import 'package:em2rp/services/data_service.dart';
|
|
import 'package:em2rp/services/api_service.dart';
|
|
import 'package:em2rp/utils/performance_monitor.dart';
|
|
|
|
class EventProvider with ChangeNotifier {
|
|
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
|
List<EventModel> _events = [];
|
|
bool _isLoading = false;
|
|
|
|
// Cache des utilisateurs chargés depuis getEvents
|
|
Map<String, Map<String, dynamic>> _usersCache = {};
|
|
|
|
// Cache pour éviter les rechargements inutiles (ancien système)
|
|
DateTime? _lastLoadTime;
|
|
String? _lastUserId;
|
|
bool _lastCanViewAll = false;
|
|
|
|
// Nouveau: Cache par mois pour le lazy loading
|
|
final Map<String, List<EventModel>> _eventsByMonth =
|
|
{}; // "2026-02" => [events]
|
|
String? _currentMonth; // Mois actuellement affiché
|
|
|
|
List<EventModel> get events => _events;
|
|
bool get isLoading => _isLoading;
|
|
|
|
/// Vérifie si les données doivent être rechargées (cache de 30 secondes)
|
|
bool _shouldReload(String userId, bool canViewAllEvents) {
|
|
if (_lastLoadTime == null) return true;
|
|
if (_lastUserId != userId || _lastCanViewAll != canViewAllEvents)
|
|
return true;
|
|
|
|
final now = DateTime.now();
|
|
final difference = now.difference(_lastLoadTime!);
|
|
return difference.inSeconds > 30;
|
|
}
|
|
|
|
/// Charger les événements d'un utilisateur via l'API
|
|
Future<void> loadUserEvents(String userId,
|
|
{bool canViewAllEvents = false, bool forceReload = false}) async {
|
|
PerformanceMonitor.start('EventProvider.loadUserEvents');
|
|
|
|
// Éviter les rechargements inutiles
|
|
if (!forceReload && !_shouldReload(userId, canViewAllEvents)) {
|
|
print(
|
|
'Using cached events (loaded ${DateTime.now().difference(_lastLoadTime!).inSeconds}s ago)');
|
|
PerformanceMonitor.end('EventProvider.loadUserEvents');
|
|
return;
|
|
}
|
|
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
print(
|
|
'Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
|
|
|
|
PerformanceMonitor.start('EventProvider.getEvents_API');
|
|
// Charger via l'API - les permissions sont vérifiées côté serveur
|
|
final result = await _dataService.getEvents(userId: userId);
|
|
PerformanceMonitor.end('EventProvider.getEvents_API');
|
|
|
|
final eventsData = result['events'] as List<Map<String, dynamic>>;
|
|
final usersData = result['users'] as Map<String, dynamic>;
|
|
|
|
// Stocker les utilisateurs dans le cache
|
|
_usersCache = usersData
|
|
.map((key, value) => MapEntry(key, value as Map<String, dynamic>));
|
|
|
|
print('Found ${eventsData.length} events from API');
|
|
|
|
PerformanceMonitor.start('EventProvider.parseEvents');
|
|
List<EventModel> allEvents = [];
|
|
int failedCount = 0;
|
|
|
|
// Parser chaque événement
|
|
for (var eventData in eventsData) {
|
|
try {
|
|
final event =
|
|
EventModel.fromMap(eventData, eventData['id'] as String);
|
|
allEvents.add(event);
|
|
} catch (e) {
|
|
print('Failed to parse event ${eventData['id']}: $e');
|
|
failedCount++;
|
|
}
|
|
}
|
|
PerformanceMonitor.end('EventProvider.parseEvents');
|
|
|
|
_events = allEvents;
|
|
_lastLoadTime = DateTime.now();
|
|
_lastUserId = userId;
|
|
_lastCanViewAll = canViewAllEvents;
|
|
|
|
print(
|
|
'Successfully loaded ${_events.length} events ($failedCount failed)');
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
PerformanceMonitor.end('EventProvider.loadUserEvents');
|
|
} catch (e) {
|
|
print('Error loading events: $e');
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
PerformanceMonitor.end('EventProvider.loadUserEvents');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Charger les événements d'un mois spécifique (lazy loading optimisé)
|
|
Future<void> loadMonthEvents(String userId, int year, int month,
|
|
{bool canViewAllEvents = false,
|
|
bool forceReload = false,
|
|
bool silent = false}) async {
|
|
final monthKey = '$year-${month.toString().padLeft(2, '0')}';
|
|
|
|
// Vérifier le cache
|
|
if (!forceReload && _eventsByMonth.containsKey(monthKey)) {
|
|
print('[EventProvider] Using cached events for $monthKey');
|
|
|
|
if (!silent) {
|
|
_currentMonth = monthKey;
|
|
_events = _eventsByMonth[monthKey]!;
|
|
notifyListeners();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!silent) {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
try {
|
|
print('[EventProvider] Loading events for month: $monthKey');
|
|
|
|
PerformanceMonitor.start('EventProvider.loadMonthEvents_API');
|
|
final result = await _dataService.getEventsByMonth(
|
|
userId: userId, year: year, month: month);
|
|
PerformanceMonitor.end('EventProvider.loadMonthEvents_API');
|
|
|
|
final eventsData = result['events'] as List<Map<String, dynamic>>;
|
|
final usersData = result['users'] as Map<String, dynamic>;
|
|
|
|
// Mettre à jour le cache utilisateurs (addAll pour cumuler)
|
|
_usersCache.addAll(usersData
|
|
.map((key, value) => MapEntry(key, value as Map<String, dynamic>)));
|
|
|
|
print('[EventProvider] Found ${eventsData.length} events for $monthKey');
|
|
|
|
PerformanceMonitor.start('EventProvider.parseMonthEvents');
|
|
List<EventModel> monthEvents = [];
|
|
int failedCount = 0;
|
|
|
|
// Parser les événements
|
|
for (var eventData in eventsData) {
|
|
try {
|
|
final event =
|
|
EventModel.fromMap(eventData, eventData['id'] as String);
|
|
monthEvents.add(event);
|
|
} catch (e) {
|
|
print('[EventProvider] Failed to parse event ${eventData['id']}: $e');
|
|
failedCount++;
|
|
}
|
|
}
|
|
PerformanceMonitor.end('EventProvider.parseMonthEvents');
|
|
|
|
// Stocker dans le cache par mois
|
|
_eventsByMonth[monthKey] = monthEvents;
|
|
|
|
// Mettre à jour _events et _currentMonth seulement si ce n'est pas un préchargement silencieux
|
|
if (!silent) {
|
|
_currentMonth = monthKey;
|
|
_events = monthEvents;
|
|
}
|
|
|
|
// Mettre à jour les infos de cache global
|
|
_lastLoadTime = DateTime.now();
|
|
_lastUserId = userId;
|
|
_lastCanViewAll = canViewAllEvents;
|
|
|
|
print(
|
|
'[EventProvider] Successfully loaded ${monthEvents.length} events for $monthKey ($failedCount failed)');
|
|
|
|
if (!silent) {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
} catch (e) {
|
|
print('[EventProvider] Error loading month events: $e');
|
|
if (!silent) {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Précharger les mois adjacents en arrière-plan
|
|
void preloadAdjacentMonths(String userId, int year, int month,
|
|
{bool canViewAllEvents = false}) {
|
|
// Mois précédent
|
|
final prevMonth = month == 1 ? 12 : month - 1;
|
|
final prevYear = month == 1 ? year - 1 : year;
|
|
|
|
// Mois suivant
|
|
final nextMonth = month == 12 ? 1 : month + 1;
|
|
final nextYear = month == 12 ? year + 1 : year;
|
|
|
|
print('[EventProvider] Preloading adjacent months...');
|
|
|
|
// Charger en arrière-plan (sans bloquer l'UI ni notifier)
|
|
Future.microtask(() async {
|
|
try {
|
|
await loadMonthEvents(userId, prevYear, prevMonth,
|
|
canViewAllEvents: canViewAllEvents, silent: true);
|
|
await loadMonthEvents(userId, nextYear, nextMonth,
|
|
canViewAllEvents: canViewAllEvents, silent: true);
|
|
print('[EventProvider] Adjacent months preloaded successfully');
|
|
} catch (e) {
|
|
print('[EventProvider] Error preloading adjacent months: $e');
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Vide entièrement le cache (mois + métadonnées) pour forcer un rechargement complet
|
|
void clearAllCache() {
|
|
_eventsByMonth.clear();
|
|
_lastLoadTime = null;
|
|
_lastUserId = null;
|
|
_currentMonth = null;
|
|
print('[EventProvider] Cache entièrement vidé');
|
|
}
|
|
|
|
/// Recharger les événements (utilise le dernier userId)
|
|
Future<void> refreshEvents(String userId,
|
|
{bool canViewAllEvents = false}) async {
|
|
await loadUserEvents(userId,
|
|
canViewAllEvents: canViewAllEvents, forceReload: true);
|
|
}
|
|
|
|
/// Récupérer un événement spécifique par ID
|
|
EventModel? getEventById(String eventId) {
|
|
try {
|
|
return _events.firstWhere((event) => event.id == eventId);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Recherche des événements accessibles à l'utilisateur.
|
|
Future<List<EventModel>> searchEvents({
|
|
required String userId,
|
|
required String query,
|
|
int limit = 20,
|
|
}) async {
|
|
final trimmedQuery = query.trim();
|
|
if (trimmedQuery.isEmpty) {
|
|
return [];
|
|
}
|
|
|
|
final result = await _dataService.searchEvents(
|
|
userId: userId,
|
|
query: trimmedQuery,
|
|
limit: limit,
|
|
);
|
|
|
|
final events = <EventModel>[];
|
|
for (final eventData in result) {
|
|
try {
|
|
final eventId = eventData['id'] as String?;
|
|
if (eventId == null || eventId.isEmpty) {
|
|
continue;
|
|
}
|
|
|
|
events.add(EventModel.fromMap(eventData, eventId));
|
|
} catch (e) {
|
|
print('Failed to parse searched event ${eventData['id']}: $e');
|
|
}
|
|
}
|
|
|
|
events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
|
return events;
|
|
}
|
|
|
|
/// Ajouter un nouvel événement
|
|
Future<void> addEvent(EventModel event) async {
|
|
try {
|
|
// Ajouter l'événement localement dans _events
|
|
_events.add(event);
|
|
|
|
// Ajouter dans le cache par mois
|
|
final monthKey =
|
|
'${event.startDateTime.year}-${event.startDateTime.month.toString().padLeft(2, '0')}';
|
|
if (_eventsByMonth.containsKey(monthKey)) {
|
|
_eventsByMonth[monthKey]!.add(event);
|
|
}
|
|
|
|
notifyListeners();
|
|
} catch (e) {
|
|
print('Error adding event: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Mettre à jour un événement
|
|
Future<void> updateEvent(EventModel event) async {
|
|
try {
|
|
// Mise à jour dans _events
|
|
final index = _events.indexWhere((e) => e.id == event.id);
|
|
if (index != -1) {
|
|
final oldEvent = _events[index];
|
|
_events[index] = event;
|
|
|
|
// Mettre à jour dans le cache par mois
|
|
final oldMonthKey =
|
|
'${oldEvent.startDateTime.year}-${oldEvent.startDateTime.month.toString().padLeft(2, '0')}';
|
|
final newMonthKey =
|
|
'${event.startDateTime.year}-${event.startDateTime.month.toString().padLeft(2, '0')}';
|
|
|
|
// Si le mois a changé, supprimer de l'ancien et ajouter au nouveau
|
|
if (oldMonthKey != newMonthKey) {
|
|
if (_eventsByMonth.containsKey(oldMonthKey)) {
|
|
_eventsByMonth[oldMonthKey]!.removeWhere((e) => e.id == event.id);
|
|
}
|
|
if (_eventsByMonth.containsKey(newMonthKey)) {
|
|
_eventsByMonth[newMonthKey]!.add(event);
|
|
}
|
|
} else {
|
|
// Même mois, juste mettre à jour
|
|
if (_eventsByMonth.containsKey(newMonthKey)) {
|
|
final monthIndex = _eventsByMonth[newMonthKey]!
|
|
.indexWhere((e) => e.id == event.id);
|
|
if (monthIndex != -1) {
|
|
_eventsByMonth[newMonthKey]![monthIndex] = event;
|
|
}
|
|
}
|
|
}
|
|
|
|
notifyListeners();
|
|
}
|
|
} catch (e) {
|
|
print('Error updating event: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Supprimer un événement
|
|
Future<void> deleteEvent(String eventId) async {
|
|
try {
|
|
await _dataService.deleteEvent(eventId);
|
|
|
|
// Trouver l'événement pour obtenir sa date avant de le supprimer
|
|
final eventToDelete = _events.firstWhere((e) => e.id == eventId);
|
|
final monthKey =
|
|
'${eventToDelete.startDateTime.year}-${eventToDelete.startDateTime.month.toString().padLeft(2, '0')}';
|
|
|
|
// Supprimer de _events
|
|
_events.removeWhere((event) => event.id == eventId);
|
|
|
|
// Supprimer du cache par mois
|
|
if (_eventsByMonth.containsKey(monthKey)) {
|
|
_eventsByMonth[monthKey]!.removeWhere((event) => event.id == eventId);
|
|
}
|
|
|
|
notifyListeners();
|
|
} catch (e) {
|
|
print('Error deleting event: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer les données d'un utilisateur depuis le cache
|
|
Map<String, dynamic>? getUserFromCache(String uid) {
|
|
return _usersCache[uid];
|
|
}
|
|
|
|
/// Récupérer les utilisateurs de la workforce d'un événement
|
|
List<Map<String, dynamic>> getWorkforceUsers(EventModel event) {
|
|
final users = <Map<String, dynamic>>[];
|
|
|
|
for (final dynamic userRef in event.workforce) {
|
|
try {
|
|
String? uid;
|
|
|
|
// Tenter d'extraire l'UID
|
|
if (userRef is String) {
|
|
uid = userRef;
|
|
} else {
|
|
// Essayer d'extraire l'ID si c'est une DocumentReference
|
|
final ref = userRef as DocumentReference?;
|
|
uid = ref?.id;
|
|
}
|
|
|
|
if (uid != null) {
|
|
final userData = getUserFromCache(uid);
|
|
if (userData != null) {
|
|
users.add(userData);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Ignorer les références invalides
|
|
print('Skipping invalid workforce reference: $userRef');
|
|
}
|
|
}
|
|
|
|
return users;
|
|
}
|
|
|
|
/// Vider la liste des événements
|
|
void clearEvents() {
|
|
_events = [];
|
|
_lastLoadTime = null;
|
|
_lastUserId = null;
|
|
_lastCanViewAll = false;
|
|
notifyListeners();
|
|
}
|
|
}
|