product repo
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../domain/product/product.dart';
|
||||
|
||||
part 'product_loader_event.dart';
|
||||
part 'product_loader_state.dart';
|
||||
part 'product_loader_bloc.freezed.dart';
|
||||
|
||||
@injectable
|
||||
class ProductLoaderBloc extends Bloc<ProductLoaderEvent, ProductLoaderState> {
|
||||
final IProductRepository _productRepository;
|
||||
|
||||
Timer? _loadMoreDebounce;
|
||||
Timer? _searchDebounce;
|
||||
|
||||
ProductLoaderBloc(this._productRepository)
|
||||
: super(ProductLoaderState.initial()) {
|
||||
on<ProductLoaderEvent>(_onProductLoaderEvent);
|
||||
}
|
||||
|
||||
Future<void> _onProductLoaderEvent(
|
||||
ProductLoaderEvent event,
|
||||
Emitter<ProductLoaderState> emit,
|
||||
) {
|
||||
return event.map(
|
||||
getProduct: (e) async {
|
||||
emit(state.copyWith(isLoadingMore: true));
|
||||
|
||||
log(
|
||||
'📱 Loading local products - categoryId: ${e.categoryId}, search: ${e.search}',
|
||||
);
|
||||
|
||||
// Pastikan database lokal sudah siap
|
||||
final isReady = await _productRepository.isLocalDatabaseReady();
|
||||
if (!isReady) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionProduct: optionOf(
|
||||
ProductFailure.dynamicErrorMessage(
|
||||
'Database lokal belum siap. Silakan lakukan sinkronisasi data terlebih dahulu.',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await _productRepository.getProducts(
|
||||
page: 1,
|
||||
limit: 10,
|
||||
categoryId: e.categoryId,
|
||||
search: e.search,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
log('❌ Error loading local products: $failure');
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionProduct: optionOf(failure),
|
||||
),
|
||||
);
|
||||
},
|
||||
(response) async {
|
||||
final products = response.products;
|
||||
final totalPages = response.totalPages;
|
||||
final hasReachedMax = products.length < 10 || 1 >= totalPages;
|
||||
|
||||
log(
|
||||
'✅ Local products loaded: ${products.length}, hasReachedMax: $hasReachedMax, totalPages: $totalPages',
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
products: products,
|
||||
page: 1,
|
||||
hasReachedMax: hasReachedMax,
|
||||
isLoadingMore: false,
|
||||
failureOptionProduct: none(),
|
||||
categoryId: e.categoryId,
|
||||
searchQuery: e.search,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loadMore: (e) async {
|
||||
final currentState = state;
|
||||
|
||||
// Cegah double load
|
||||
if (currentState.isLoadingMore || currentState.hasReachedMax) {
|
||||
log(
|
||||
'⏹️ Load more blocked - isLoadingMore: ${currentState.isLoadingMore}, hasReachedMax: ${currentState.hasReachedMax}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(currentState.copyWith(isLoadingMore: true));
|
||||
|
||||
final nextPage = currentState.page + 1;
|
||||
log('📄 Loading more local products - page: $nextPage');
|
||||
|
||||
try {
|
||||
final result = await _productRepository.getProducts(
|
||||
page: nextPage,
|
||||
limit: 10,
|
||||
categoryId: currentState.categoryId,
|
||||
search: currentState.searchQuery,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
log('❌ Error loading more local products: $failure');
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionProduct: optionOf(failure),
|
||||
),
|
||||
);
|
||||
},
|
||||
(response) async {
|
||||
final newProducts = response.products;
|
||||
final totalPages = response.totalPages;
|
||||
|
||||
// Hindari duplikat produk
|
||||
final currentProductIds = currentState.products
|
||||
.map((p) => p.id)
|
||||
.toSet();
|
||||
final filteredNewProducts = newProducts
|
||||
.where((product) => !currentProductIds.contains(product.id))
|
||||
.toList();
|
||||
|
||||
final allProducts = [
|
||||
...currentState.products,
|
||||
...filteredNewProducts,
|
||||
];
|
||||
|
||||
final hasReachedMax =
|
||||
filteredNewProducts.length < 10 || nextPage >= totalPages;
|
||||
|
||||
log(
|
||||
'✅ More local products loaded: ${filteredNewProducts.length} new, total: ${allProducts.length}, hasReachedMax: $hasReachedMax',
|
||||
);
|
||||
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
products: allProducts,
|
||||
page: nextPage,
|
||||
hasReachedMax: hasReachedMax,
|
||||
isLoadingMore: false,
|
||||
failureOptionProduct: none(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
log('❌ Exception loading more local products: $e');
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionProduct: optionOf(
|
||||
ProductFailure.dynamicErrorMessage(
|
||||
'Gagal memuat produk tambahan: $e',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
refresh: (e) async {
|
||||
final categoryId = state.categoryId;
|
||||
final searchQuery = state.searchQuery;
|
||||
|
||||
_loadMoreDebounce?.cancel();
|
||||
_searchDebounce?.cancel();
|
||||
|
||||
log(
|
||||
'🔄 Refreshing local products - categoryId: $categoryId, search: $searchQuery',
|
||||
);
|
||||
|
||||
emit(state.copyWith(isLoadingMore: true));
|
||||
|
||||
try {
|
||||
_productRepository.clearCache();
|
||||
|
||||
final result = await _productRepository.refreshProducts(
|
||||
categoryId: categoryId,
|
||||
search: searchQuery,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
log('❌ Failed to refresh local products: $failure');
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionProduct: optionOf(failure),
|
||||
),
|
||||
);
|
||||
},
|
||||
(response) async {
|
||||
final products = response.products;
|
||||
final totalPages = response.totalPages;
|
||||
final hasReachedMax = products.length < 10 || 1 >= totalPages;
|
||||
|
||||
log('✅ Refreshed local products: ${products.length}');
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
products: products,
|
||||
hasReachedMax: hasReachedMax,
|
||||
page: 1,
|
||||
isLoadingMore: false,
|
||||
failureOptionProduct: none(),
|
||||
categoryId: categoryId,
|
||||
searchQuery: searchQuery,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
log('❌ Exception refreshing local products: $e');
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionProduct: optionOf(
|
||||
ProductFailure.dynamicErrorMessage(e.toString()),
|
||||
),
|
||||
),
|
||||
);
|
||||
} finally {}
|
||||
},
|
||||
searchProduct: (e) async {
|
||||
_searchDebounce?.cancel();
|
||||
|
||||
// Debounce ringan agar UX lebih halus
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 150), () async {
|
||||
emit(state.copyWith(isLoadingMore: true));
|
||||
|
||||
log('🔍 Local search: "${e.query}"');
|
||||
|
||||
try {
|
||||
final result = await _productRepository.getProducts(
|
||||
page: 1,
|
||||
limit: 20, // lebih banyak hasil untuk pencarian
|
||||
categoryId: e.categoryId,
|
||||
search: e.query,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
log('❌ Local search error: $failure');
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionProduct: optionOf(failure),
|
||||
),
|
||||
);
|
||||
},
|
||||
(response) async {
|
||||
final products = response.products;
|
||||
final totalPages = response.totalPages;
|
||||
final hasReachedMax = products.length < 20 || 1 >= totalPages;
|
||||
|
||||
log(
|
||||
'✅ Local search results: ${products.length} products found',
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
products: products,
|
||||
hasReachedMax: hasReachedMax,
|
||||
page: 1,
|
||||
isLoadingMore: false,
|
||||
categoryId: e.categoryId,
|
||||
searchQuery: e.query,
|
||||
failureOptionProduct: none(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
log('❌ Exception during local search: $e');
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionProduct: optionOf(
|
||||
ProductFailure.dynamicErrorMessage(e.toString()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
getDatabaseStats: (e) async {
|
||||
log('📊 Getting local database stats...');
|
||||
|
||||
try {
|
||||
final result = await _productRepository.getDatabaseStats();
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
log('❌ Failed to get database stats: $failure');
|
||||
emit(state.copyWith(failureOptionProduct: optionOf(failure)));
|
||||
},
|
||||
(stats) async {
|
||||
log('✅ Local database stats retrieved: $stats');
|
||||
// Jika UI kamu perlu tampilkan, bisa simpan ke state, misalnya:
|
||||
// emit(state.copyWith(databaseStats: some(stats)));
|
||||
// Tapi kalau hanya untuk log/debug, tidak perlu ubah state
|
||||
},
|
||||
);
|
||||
} catch (e, s) {
|
||||
log(
|
||||
'❌ Exception while getting database stats: $e',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
failureOptionProduct: optionOf(
|
||||
ProductFailure.dynamicErrorMessage(e.toString()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
clearCache: (e) 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
@@ -0,0 +1,26 @@
|
||||
part of 'product_loader_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class ProductLoaderEvent with _$ProductLoaderEvent {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
part of 'product_loader_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class ProductLoaderState with _$ProductLoaderState {
|
||||
factory ProductLoaderState({
|
||||
required List<Product> products,
|
||||
required Option<ProductFailure> failureOptionProduct,
|
||||
@Default(false) bool hasReachedMax,
|
||||
@Default(1) int page,
|
||||
@Default(false) bool isLoadingMore,
|
||||
String? searchQuery,
|
||||
String? categoryId,
|
||||
}) = _ProductLoaderState;
|
||||
|
||||
factory ProductLoaderState.initial() =>
|
||||
ProductLoaderState(products: [], failureOptionProduct: none());
|
||||
}
|
||||
Reference in New Issue
Block a user