453 lines
13 KiB
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;
|
|
}
|
|
}
|
|
|