product repo

This commit is contained in:
efrilm
2025-10-24 22:03:35 +07:00
parent dea5de8828
commit 4fdd1e44f8
18 changed files with 6256 additions and 0 deletions
@@ -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());
}