sync product to local
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart';
|
||||
import 'dart:developer';
|
||||
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
|
||||
import 'package:enaklo_pos/data/repositories/product/product_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'product_loader_event.dart';
|
||||
@@ -10,102 +10,114 @@ part 'product_loader_state.dart';
|
||||
part 'product_loader_bloc.freezed.dart';
|
||||
|
||||
class ProductLoaderBloc extends Bloc<ProductLoaderEvent, ProductLoaderState> {
|
||||
final ProductRemoteDatasource _productRemoteDatasource;
|
||||
final ProductRepository _productRepository = ProductRepository.instance;
|
||||
|
||||
// Debouncing untuk mencegah multiple load more calls
|
||||
Timer? _loadMoreDebounce;
|
||||
Timer? _searchDebounce;
|
||||
bool _isLoadingMore = false;
|
||||
|
||||
ProductLoaderBloc(this._productRemoteDatasource)
|
||||
: super(ProductLoaderState.initial()) {
|
||||
ProductLoaderBloc() : super(const ProductLoaderState.initial()) {
|
||||
on<_GetProduct>(_onGetProduct);
|
||||
on<_LoadMore>(_onLoadMore);
|
||||
on<_Refresh>(_onRefresh);
|
||||
on<_SearchProduct>(_onSearchProduct);
|
||||
on<_GetDatabaseStats>(_onGetDatabaseStats);
|
||||
on<_ClearCache>(_onClearCache);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_loadMoreDebounce?.cancel();
|
||||
_searchDebounce?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
// Debounce transformer untuk load more
|
||||
// EventTransformer<T> _debounceTransformer<T>() {
|
||||
// return (events, mapper) {
|
||||
// return events
|
||||
// .debounceTime(const Duration(milliseconds: 300))
|
||||
// .asyncExpand(mapper);
|
||||
// };
|
||||
// }
|
||||
|
||||
// Initial load
|
||||
// Pure local product loading
|
||||
Future<void> _onGetProduct(
|
||||
_GetProduct event,
|
||||
Emitter<ProductLoaderState> emit,
|
||||
) async {
|
||||
emit(const _Loading());
|
||||
_isLoadingMore = false; // Reset loading state
|
||||
emit(const ProductLoaderState.loading());
|
||||
_isLoadingMore = false;
|
||||
|
||||
final result = await _productRemoteDatasource.getProducts(
|
||||
log('📱 Loading local products - categoryId: ${event.categoryId}');
|
||||
|
||||
// Check if local database is ready
|
||||
final isReady = await _productRepository.isLocalDatabaseReady();
|
||||
if (!isReady) {
|
||||
emit(const ProductLoaderState.error(
|
||||
'Database lokal belum siap. Silakan lakukan sinkronisasi data terlebih dahulu.'));
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await _productRepository.getProducts(
|
||||
page: 1,
|
||||
limit: 10,
|
||||
categoryId: event.categoryId,
|
||||
search: event.search,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => emit(_Error(failure)),
|
||||
(failure) async {
|
||||
log('❌ Error loading local products: $failure');
|
||||
emit(ProductLoaderState.error(failure));
|
||||
},
|
||||
(response) async {
|
||||
final products = response.data?.products ?? [];
|
||||
final hasReachedMax = products.length < 10;
|
||||
final totalPages = response.data?.totalPages ?? 1;
|
||||
final hasReachedMax = products.length < 10 || 1 >= totalPages;
|
||||
|
||||
emit(_Loaded(
|
||||
log('✅ Local products loaded: ${products.length}, hasReachedMax: $hasReachedMax, totalPages: $totalPages');
|
||||
|
||||
emit(ProductLoaderState.loaded(
|
||||
products: products,
|
||||
hasReachedMax: hasReachedMax,
|
||||
currentPage: 1,
|
||||
isLoadingMore: false,
|
||||
categoryId: event.categoryId,
|
||||
searchQuery: event.search,
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Load more with enhanced debouncing
|
||||
// Pure local load more
|
||||
Future<void> _onLoadMore(
|
||||
_LoadMore event,
|
||||
Emitter<ProductLoaderState> emit,
|
||||
) async {
|
||||
final currentState = state;
|
||||
|
||||
// Enhanced validation
|
||||
if (currentState is! _Loaded ||
|
||||
currentState.hasReachedMax ||
|
||||
_isLoadingMore ||
|
||||
currentState.isLoadingMore) {
|
||||
log('⏹️ Load more blocked - state: ${currentState.runtimeType}, isLoadingMore: $_isLoadingMore');
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoadingMore = true;
|
||||
|
||||
// Emit loading more state
|
||||
emit(currentState.copyWith(isLoadingMore: true));
|
||||
|
||||
final nextPage = currentState.currentPage + 1;
|
||||
log('📄 Loading more local products - page: $nextPage');
|
||||
|
||||
try {
|
||||
final result = await _productRemoteDatasource.getProducts(
|
||||
final result = await _productRepository.getProducts(
|
||||
page: nextPage,
|
||||
limit: 10,
|
||||
categoryId: event.categoryId,
|
||||
categoryId: currentState.categoryId,
|
||||
search: currentState.searchQuery,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
// On error, revert loading state but don't show error
|
||||
// Just silently fail and allow retry
|
||||
log('❌ Error loading more local products: $failure');
|
||||
emit(currentState.copyWith(isLoadingMore: false));
|
||||
_isLoadingMore = false;
|
||||
},
|
||||
(response) async {
|
||||
final newProducts = response.data?.products ?? [];
|
||||
final totalPages = response.data?.totalPages ?? 1;
|
||||
|
||||
// Prevent duplicate products
|
||||
final currentProductIds =
|
||||
@@ -117,32 +129,130 @@ class ProductLoaderBloc extends Bloc<ProductLoaderEvent, ProductLoaderState> {
|
||||
final allProducts = List<Product>.from(currentState.products)
|
||||
..addAll(filteredNewProducts);
|
||||
|
||||
final hasReachedMax = newProducts.length < 10;
|
||||
final hasReachedMax =
|
||||
newProducts.length < 10 || nextPage >= totalPages;
|
||||
|
||||
emit(_Loaded(
|
||||
log('✅ More local products loaded: ${filteredNewProducts.length} new, total: ${allProducts.length}');
|
||||
|
||||
emit(ProductLoaderState.loaded(
|
||||
products: allProducts,
|
||||
hasReachedMax: hasReachedMax,
|
||||
currentPage: nextPage,
|
||||
isLoadingMore: false,
|
||||
categoryId: currentState.categoryId,
|
||||
searchQuery: currentState.searchQuery,
|
||||
));
|
||||
|
||||
_isLoadingMore = false;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// Handle unexpected errors
|
||||
log('❌ Exception loading more local products: $e');
|
||||
emit(currentState.copyWith(isLoadingMore: false));
|
||||
} finally {
|
||||
_isLoadingMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
// Pure local refresh
|
||||
Future<void> _onRefresh(
|
||||
_Refresh event,
|
||||
Emitter<ProductLoaderState> emit,
|
||||
) async {
|
||||
final currentState = state;
|
||||
String? categoryId;
|
||||
String? searchQuery;
|
||||
|
||||
if (currentState is _Loaded) {
|
||||
categoryId = currentState.categoryId;
|
||||
searchQuery = currentState.searchQuery;
|
||||
}
|
||||
|
||||
_isLoadingMore = false;
|
||||
_loadMoreDebounce?.cancel();
|
||||
add(const _GetProduct());
|
||||
_searchDebounce?.cancel();
|
||||
|
||||
log('🔄 Refreshing local products');
|
||||
|
||||
// Clear local cache
|
||||
_productRepository.clearCache();
|
||||
|
||||
add(ProductLoaderEvent.getProduct(
|
||||
categoryId: categoryId,
|
||||
search: searchQuery,
|
||||
));
|
||||
}
|
||||
|
||||
// Fast local search (no debouncing needed for local data)
|
||||
Future<void> _onSearchProduct(
|
||||
_SearchProduct event,
|
||||
Emitter<ProductLoaderState> emit,
|
||||
) async {
|
||||
// Cancel previous search
|
||||
_searchDebounce?.cancel();
|
||||
|
||||
// Minimal debounce for local search (much faster)
|
||||
_searchDebounce = Timer(Duration(milliseconds: 150), () async {
|
||||
emit(const ProductLoaderState.loading());
|
||||
_isLoadingMore = false;
|
||||
|
||||
log('🔍 Local search: "${event.query}"');
|
||||
|
||||
final result = await _productRepository.getProducts(
|
||||
page: 1,
|
||||
limit: 20, // More results for search
|
||||
categoryId: event.categoryId,
|
||||
search: event.query,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
log('❌ Local search error: $failure');
|
||||
emit(ProductLoaderState.error(failure));
|
||||
},
|
||||
(response) async {
|
||||
final products = response.data?.products ?? [];
|
||||
final totalPages = response.data?.totalPages ?? 1;
|
||||
final hasReachedMax = products.length < 20 || 1 >= totalPages;
|
||||
|
||||
log('✅ Local search results: ${products.length} products found');
|
||||
|
||||
emit(ProductLoaderState.loaded(
|
||||
products: products,
|
||||
hasReachedMax: hasReachedMax,
|
||||
currentPage: 1,
|
||||
isLoadingMore: false,
|
||||
categoryId: event.categoryId,
|
||||
searchQuery: event.query,
|
||||
));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Get local database statistics
|
||||
Future<void> _onGetDatabaseStats(
|
||||
_GetDatabaseStats event,
|
||||
Emitter<ProductLoaderState> emit,
|
||||
) async {
|
||||
try {
|
||||
final stats = await _productRepository.getDatabaseStats();
|
||||
log('📊 Local database stats retrieved: $stats');
|
||||
|
||||
// You can emit a special state here if needed for UI updates
|
||||
// For now, just log the stats
|
||||
} catch (e) {
|
||||
log('❌ Error getting local database stats: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Clear local cache
|
||||
Future<void> _onClearCache(
|
||||
_ClearCache event,
|
||||
Emitter<ProductLoaderState> emit,
|
||||
) async {
|
||||
log('🧹 Manually clearing local cache');
|
||||
_productRepository.clearCache();
|
||||
|
||||
// Refresh current data after cache clear
|
||||
add(const ProductLoaderEvent.refresh());
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,25 @@ part of 'product_loader_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class ProductLoaderEvent with _$ProductLoaderEvent {
|
||||
const factory ProductLoaderEvent.getProduct(
|
||||
{String? categoryId, String? search}) = _GetProduct;
|
||||
const factory ProductLoaderEvent.loadMore(
|
||||
{String? categoryId, String? search}) = _LoadMore;
|
||||
const factory ProductLoaderEvent.getProduct({
|
||||
String? categoryId,
|
||||
String? search, // Added search parameter
|
||||
bool? forceRefresh, // Kept for compatibility but ignored
|
||||
}) = _GetProduct;
|
||||
|
||||
const factory ProductLoaderEvent.loadMore({
|
||||
String? categoryId,
|
||||
String? search,
|
||||
}) = _LoadMore;
|
||||
|
||||
const factory ProductLoaderEvent.refresh() = _Refresh;
|
||||
|
||||
const factory ProductLoaderEvent.searchProduct({
|
||||
String? query,
|
||||
String? categoryId,
|
||||
}) = _SearchProduct;
|
||||
|
||||
const factory ProductLoaderEvent.getDatabaseStats() = _GetDatabaseStats;
|
||||
|
||||
const factory ProductLoaderEvent.clearCache() = _ClearCache;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ class ProductLoaderState with _$ProductLoaderState {
|
||||
required bool hasReachedMax,
|
||||
required int currentPage,
|
||||
required bool isLoadingMore,
|
||||
String? categoryId,
|
||||
String? searchQuery,
|
||||
}) = _Loaded;
|
||||
const factory ProductLoaderState.error(String message) = _Error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user