sync product to local

This commit is contained in:
efrilm
2025-09-20 03:10:05 +07:00
parent 3022d8de9f
commit f104390141
21 changed files with 4461 additions and 514 deletions
@@ -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;
}