Files
EM2_ERP/em2rp/lib/providers/container_provider.dart

453 lines
13 KiB
Dart

import 'package:flutter/foundation.dart';
import 'dart:async';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/services/container_service.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:em2rp/utils/debug_log.dart';
class ContainerProvider with ChangeNotifier {
final ContainerService _containerService = ContainerService();
final DataService _dataService = DataService(FirebaseFunctionsApiService());
// Timer pour le debouncing de la recherche
Timer? _searchDebounceTimer;
// Liste paginée pour la page de gestion
final List<ContainerModel> _paginatedContainers = [];
bool _hasMore = true;
bool _isLoadingMore = false;
String? _lastVisible;
// Cache complet pour compatibilité
final List<ContainerModel> _containers = [];
// Filtres et recherche
ContainerType? _selectedType;
EquipmentStatus? _selectedStatus;
String _searchQuery = '';
bool _isLoading = false;
bool _isInitialized = false;
// Mode de chargement (pagination vs full)
bool _usePagination = false;
// Getters
List<ContainerModel> get containers => _usePagination ? _paginatedContainers : _containers;
ContainerType? get selectedType => _selectedType;
EquipmentStatus? get selectedStatus => _selectedStatus;
String get searchQuery => _searchQuery;
bool get isLoading => _isLoading;
bool get isLoadingMore => _isLoadingMore;
bool get hasMore => _hasMore;
bool get isInitialized => _isInitialized;
bool get usePagination => _usePagination;
/// S'assure que les conteneurs sont chargés (charge si nécessaire)
Future<void> ensureLoaded() async {
if (_isInitialized || _isLoading) {
return;
}
await loadContainers();
}
/// Charger tous les containers via l'API (avec pagination automatique)
Future<void> loadContainers() async {
_isLoading = true;
notifyListeners();
try {
_containers.clear();
String? lastVisible;
bool hasMore = true;
int pageCount = 0;
// Charger toutes les pages en boucle
while (hasMore) {
pageCount++;
print('[ContainerProvider] Loading page $pageCount...');
final result = await _dataService.getContainersPaginated(
limit: 100, // Charger 100 par page pour aller plus vite
startAfter: lastVisible,
sortBy: 'id',
sortOrder: 'asc',
type: _selectedType?.name,
status: _selectedStatus?.name,
searchQuery: _searchQuery,
);
final containers = (result['containers'] as List<dynamic>)
.map((data) => ContainerModel.fromMap(data, data['id'] as String))
.toList();
_containers.addAll(containers);
hasMore = result['hasMore'] as bool? ?? false;
lastVisible = result['lastVisible'] as String?;
print('[ContainerProvider] Loaded ${containers.length} containers, total: ${_containers.length}, hasMore: $hasMore');
}
_isLoading = false;
_isInitialized = true;
notifyListeners();
} catch (e) {
print('Error loading containers: $e');
_isLoading = false;
notifyListeners();
}
}
/// Récupérer les containers avec filtres appliqués
Future<List<ContainerModel>> getContainers() async {
return await _containerService.getContainers(
type: _selectedType,
status: _selectedStatus,
searchQuery: _searchQuery,
);
}
/// Stream des containers - retourne un stream depuis les données en cache
/// Pour compatibilité avec les widgets existants qui utilisent StreamBuilder
Stream<List<ContainerModel>> get containersStream async* {
// Si les données ne sont pas chargées, charger d'abord
if (!_isInitialized) {
await loadContainers();
}
// Émettre les données actuelles
yield _containers;
// Continuer à émettre les mises à jour du cache
// Note: Pour un vrai temps réel, il faudrait implémenter un StreamController
// et notifier quand les données changent
}
/// Définir le type sélectionné
void setSelectedType(ContainerType? type) async {
if (_selectedType == type) return;
_selectedType = type;
if (_usePagination) {
await reload();
} else {
notifyListeners();
}
}
/// Définir le statut sélectionné
void setSelectedStatus(EquipmentStatus? status) async {
if (_selectedStatus == status) return;
_selectedStatus = status;
if (_usePagination) {
await reload();
} else {
notifyListeners();
}
}
/// Définir la requête de recherche (avec debouncing)
void setSearchQuery(String query) {
if (_searchQuery == query) return;
_searchQuery = query;
// Annuler le timer précédent
_searchDebounceTimer?.cancel();
if (_usePagination) {
// Attendre 500ms avant de recharger (debouncing)
_searchDebounceTimer = Timer(const Duration(milliseconds: 500), () {
reload();
});
} else {
notifyListeners();
}
}
@override
void dispose() {
_searchDebounceTimer?.cancel();
super.dispose();
}
// ============================================================================
// PAGINATION - Nouvelles méthodes
// ============================================================================
/// Active le mode pagination (pour la page de gestion)
void enablePagination() {
if (!_usePagination) {
_usePagination = true;
DebugLog.info('[ContainerProvider] Pagination mode enabled');
}
}
/// Désactive le mode pagination (pour les autres pages)
void disablePagination() {
if (_usePagination) {
_usePagination = false;
DebugLog.info('[ContainerProvider] Pagination mode disabled');
}
}
/// Charge la première page (réinitialise tout)
Future<void> loadFirstPage() async {
DebugLog.info('[ContainerProvider] Loading first page...');
_paginatedContainers.clear();
_lastVisible = null;
_hasMore = true;
_isLoading = true;
notifyListeners();
try {
await loadNextPage();
_isInitialized = true;
} catch (e) {
DebugLog.error('[ContainerProvider] Error loading first page', e);
_isLoading = false;
notifyListeners();
rethrow;
}
}
/// Charge la page suivante (scroll infini)
Future<void> loadNextPage() async {
if (_isLoadingMore || !_hasMore) {
DebugLog.info('[ContainerProvider] Skip loadNextPage: isLoadingMore=$_isLoadingMore, hasMore=$_hasMore');
return;
}
DebugLog.info('[ContainerProvider] Loading next page... (current: ${_paginatedContainers.length})');
_isLoadingMore = true;
_isLoading = true;
notifyListeners();
try {
final result = await _dataService.getContainersPaginated(
limit: 20,
startAfter: _lastVisible,
type: _selectedType != null ? containerTypeToString(_selectedType!) : null,
searchQuery: _searchQuery.isNotEmpty ? _searchQuery : null,
sortBy: 'id',
sortOrder: 'asc',
);
final newContainers = (result['containers'] as List<dynamic>)
.map((data) {
final map = data as Map<String, dynamic>;
return ContainerModel.fromMap(map, map['id'] as String);
})
.toList();
_paginatedContainers.addAll(newContainers);
_hasMore = result['hasMore'] as bool? ?? false;
_lastVisible = result['lastVisible'] as String?;
DebugLog.info('[ContainerProvider] Loaded ${newContainers.length} containers, total: ${_paginatedContainers.length}, hasMore: $_hasMore');
_isLoadingMore = false;
_isLoading = false;
notifyListeners();
} catch (e) {
DebugLog.error('[ContainerProvider] Error loading next page', e);
_isLoadingMore = false;
_isLoading = false;
notifyListeners();
rethrow;
}
}
/// Recharge en changeant de filtre ou recherche
Future<void> reload() async {
DebugLog.info('[ContainerProvider] Reloading with new filters...');
await loadFirstPage();
}
/// Créer un nouveau container
Future<void> createContainer(ContainerModel container) async {
await _containerService.createContainer(container);
notifyListeners();
}
/// Mettre à jour un container
Future<void> updateContainer(String id, Map<String, dynamic> data) async {
await _containerService.updateContainer(id, data);
notifyListeners();
}
/// Supprimer un container
Future<void> deleteContainer(String id) async {
await _containerService.deleteContainer(id);
notifyListeners();
}
/// Récupérer un container par ID
Future<ContainerModel?> getContainerById(String id) async {
return await _containerService.getContainerById(id);
}
/// Charge plusieurs conteneurs par leurs IDs (optimisé pour les détails d'événement)
Future<List<ContainerModel>> getContainersByIds(List<String> containerIds) async {
if (containerIds.isEmpty) return [];
print('[ContainerProvider] Loading ${containerIds.length} containers by IDs...');
try {
// Vérifier d'abord le cache local
final cachedContainers = <ContainerModel>[];
final missingIds = <String>[];
for (final id in containerIds) {
final cached = _containers.firstWhere(
(c) => c.id == id,
orElse: () => ContainerModel(
id: '',
name: '',
type: ContainerType.flightCase,
status: EquipmentStatus.available,
equipmentIds: [],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
);
if (cached.id.isNotEmpty) {
cachedContainers.add(cached);
} else {
missingIds.add(id);
}
}
print('[ContainerProvider] Found ${cachedContainers.length} in cache, ${missingIds.length} missing');
// Si tous sont en cache, retourner directement
if (missingIds.isEmpty) {
return cachedContainers;
}
// Charger les manquants depuis l'API
final containersData = await _dataService.getContainersByIds(missingIds);
final loadedContainers = containersData.map((data) {
return ContainerModel.fromMap(data, data['id'] as String);
}).toList();
// Ajouter au cache
for (final container in loadedContainers) {
if (!_containers.any((c) => c.id == container.id)) {
_containers.add(container);
}
}
print('[ContainerProvider] Loaded ${loadedContainers.length} containers from API');
// Retourner tous les conteneurs (cache + chargés)
return [...cachedContainers, ...loadedContainers];
} catch (e) {
print('[ContainerProvider] Error loading containers by IDs: $e');
rethrow;
}
}
/// Ajouter un équipement à un container
Future<Map<String, dynamic>> addEquipmentToContainer({
required String containerId,
required String equipmentId,
String? userId,
}) async {
final result = await _containerService.addEquipmentToContainer(
containerId: containerId,
equipmentId: equipmentId,
userId: userId,
);
notifyListeners();
return result;
}
/// Retirer un équipement d'un container
Future<void> removeEquipmentFromContainer({
required String containerId,
required String equipmentId,
String? userId,
}) async {
await _containerService.removeEquipmentFromContainer(
containerId: containerId,
equipmentId: equipmentId,
userId: userId,
);
notifyListeners();
}
/// Vérifier la disponibilité d'un container
Future<Map<String, dynamic>> checkContainerAvailability({
required String containerId,
required DateTime startDate,
required DateTime endDate,
String? excludeEventId,
}) async {
return await _containerService.checkContainerAvailability(
containerId: containerId,
startDate: startDate,
endDate: endDate,
excludeEventId: excludeEventId,
);
}
/// Récupérer les équipements d'un container
Future<List<EquipmentModel>> getContainerEquipment(String containerId) async {
return await _containerService.getContainerEquipment(containerId);
}
/// Trouver tous les containers contenant un équipement
Future<List<ContainerModel>> findContainersWithEquipment(String equipmentId) async {
return await _containerService.findContainersWithEquipment(equipmentId);
}
/// Vérifier si un ID existe
Future<bool> checkContainerIdExists(String id) async {
return await _containerService.checkContainerIdExists(id);
}
/// Générer un ID unique pour un container
/// Format: BOX_{TYPE}_{NAME}_{NUMBER}
static String generateContainerId({
required ContainerType type,
required String name,
int? number,
}) {
// Obtenir le type en majuscules
final typeStr = containerTypeToString(type);
// Nettoyer le nom (enlever espaces, caractères spéciaux)
final cleanName = name
.replaceAll(' ', '_')
.replaceAll(RegExp(r'[^a-zA-Z0-9_-]'), '')
.toUpperCase();
if (number != null) {
return 'BOX_${typeStr}_${cleanName}_#$number';
}
return 'BOX_${typeStr}_$cleanName';
}
/// Assurer l'unicité d'un ID de container
static Future<String> ensureUniqueContainerId(
String baseId,
ContainerService service,
) async {
String uniqueId = baseId;
int counter = 1;
while (await service.checkContainerIdExists(uniqueId)) {
uniqueId = '${baseId}_$counter';
counter++;
}
return uniqueId;
}
}