category repo

This commit is contained in:
efrilm
2025-10-24 20:06:42 +07:00
parent 683fff6eeb
commit 71fa4823fc
29 changed files with 5434 additions and 30 deletions
@@ -0,0 +1,312 @@
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../domain/category/category.dart';
part 'category_loader_event.dart';
part 'category_loader_state.dart';
part 'category_loader_bloc.freezed.dart';
class CategoryLoaderBloc
extends Bloc<CategoryLoaderEvent, CategoryLoaderState> {
final ICategoryRepository _categoryRepository;
Timer? _searchDebounce;
bool _isLoadingMore = false;
CategoryLoaderBloc(this._categoryRepository)
: super(CategoryLoaderState.initial()) {
on<CategoryLoaderEvent>(_onCategoryLoaderEvent);
}
Future<void> _onCategoryLoaderEvent(
CategoryLoaderEvent event,
Emitter<CategoryLoaderState> emit,
) {
return event.map(
getCategories: (e) async {
emit(state.copyWith(isLoadingMore: true));
log(
'📱 Loading categories - isActive: ${e.isActive}, forceRemote: ${e.forceRemote}',
);
final result = await _categoryRepository.getCategories(
page: 1,
limit: 50,
isActive: e.isActive,
search: e.search,
forceRemote: e.forceRemote,
);
await result.fold(
(failure) async {
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(failure),
),
);
},
(response) async {
final categories = [Category.all(), ...response.categories];
final totalPages = response.totalPages;
final hasReachedMax = categories.length < 50 || 1 >= totalPages;
log(
'✅ Categories loaded: ${categories.length}, hasReachedMax: $hasReachedMax',
);
emit(
state.copyWith(
categories: categories,
page: 1,
hasReachedMax: hasReachedMax,
isLoadingMore: false,
failureOptionCategory: none(),
),
);
},
);
},
loadMore: (e) async {
final currentState = state;
// ❌ HAPUS pengecekan is! _Loaded karena state cuma 1 class doang
if (currentState.hasReachedMax ||
_isLoadingMore ||
currentState.isLoadingMore) {
log(
'⏹️ Load more blocked - hasReachedMax: ${currentState.hasReachedMax}, isLoadingMore: $_isLoadingMore',
);
return;
}
_isLoadingMore = true;
emit(currentState.copyWith(isLoadingMore: true));
final nextPage = currentState.page + 1; // ✅ Ganti currentPage jadi page
log('📄 Loading more categories - page: $nextPage');
try {
final result = await _categoryRepository.getCategories(
page: nextPage,
limit: 10,
isActive: true,
search: currentState.searchQuery,
);
await result.fold(
(failure) async {
log('❌ Error loading more categories: $failure');
emit(currentState.copyWith(isLoadingMore: false));
},
(response) async {
final newCategories = response.categories;
final totalPages = response.totalPages;
// Prevent duplicate categories
final currentCategoryIds = currentState.categories
.map((c) => c.id)
.toSet();
final filteredNewCategories = newCategories
.where(
(category) => !currentCategoryIds.contains(category.id),
)
.toList();
final allCategories = List<Category>.from(currentState.categories)
..addAll(filteredNewCategories);
final hasReachedMax =
newCategories.length < 10 || nextPage >= totalPages;
log(
'✅ More categories loaded: ${filteredNewCategories.length} new, total: ${allCategories.length}',
);
emit(
currentState.copyWith(
categories: allCategories,
hasReachedMax: hasReachedMax,
page: nextPage, // ✅ Update page
isLoadingMore: false,
),
);
},
);
} catch (e) {
log('❌ Exception loading more categories: $e');
emit(currentState.copyWith(isLoadingMore: false));
} finally {
_isLoadingMore = false;
}
},
refresh: (e) async {
final currentState = state;
bool isActive = true;
String? searchQuery = currentState.searchQuery;
_isLoadingMore = false;
_searchDebounce?.cancel();
log('🔄 Refreshing categories');
// Clear local cache
_categoryRepository.clearCache();
add(
CategoryLoaderEvent.getCategories(
isActive: isActive,
search: searchQuery,
forceRemote: true, // Force remote refresh
),
);
},
search: (e) async {
// Cancel previous search
_searchDebounce?.cancel();
// Debounce search for better UX
_searchDebounce = Timer(Duration(milliseconds: 300), () async {
emit(state.copyWith(isLoadingMore: true));
_isLoadingMore = false;
log('🔍 Searching categories: "${e.query}"');
final result = await _categoryRepository.getCategories(
page: 1,
limit: 20, // More results for search
isActive: e.isActive,
search: e.query,
);
await result.fold(
(failure) async {
log('❌ Search error: $failure');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(failure),
),
);
},
(response) async {
final categories = [Category.all(), ...response.categories];
final totalPages = response.totalPages;
final hasReachedMax = categories.length < 20 || 1 >= totalPages;
log('✅ Search results: ${categories.length} categories found');
emit(
state.copyWith(
categories: categories,
hasReachedMax: hasReachedMax,
page: 1,
isLoadingMore: false,
failureOptionCategory: none(),
searchQuery: e.query,
),
);
},
);
});
},
syncAll: (e) async {
emit(state.copyWith(isLoadingMore: true));
log('🔄 Starting full category sync...');
final result = await _categoryRepository.syncAllCategories();
await result.fold(
(failure) async {
log('❌ Sync failed: $failure');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(failure),
),
);
// After sync error, try to load local data
Timer(Duration(seconds: 2), () {
add(const CategoryLoaderEvent.getCategories());
});
},
(successMessage) async {
log('✅ Sync completed: $successMessage');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: none(),
),
);
// After successful sync, load the updated data
Timer(Duration(seconds: 1), () {
add(const CategoryLoaderEvent.getCategories());
});
},
);
},
getAllCategories: (e) async {
try {
log('📋 Loading all categories for dropdown...');
// final categories = await _categoryRepository.getAllCategories();
// emit(
// state.copyWith(
// categories: categories,
// isLoadingMore: false,
// failureOptionCategory: none(),
// ),
// );
// log('✅ All categories loaded: ${categories.length}');
} catch (e) {
log('❌ Error loading all categories: $e');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(
CategoryFailure.dynamicErrorMessage(
'Gagal memuat semua kategori: $e',
),
),
),
);
}
},
getDatabaseStats: (e) async {
try {
final stats = await _categoryRepository.getDatabaseStats();
log('📊 Category 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 category database stats: $e');
}
},
clearCache: (e) async {
log('🧹 Manually clearing category cache');
_categoryRepository.clearCache();
// Refresh current data after cache clear
add(const CategoryLoaderEvent.refresh());
},
);
}
@override
Future<void> close() {
_searchDebounce?.cancel();
return super.close();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
part of 'category_loader_bloc.dart';
@freezed
class CategoryLoaderEvent with _$CategoryLoaderEvent {
const factory CategoryLoaderEvent.getCategories({
@Default(true) bool isActive,
String? search,
@Default(false) bool forceRemote,
}) = _GetCategories;
const factory CategoryLoaderEvent.loadMore() = _LoadMore;
const factory CategoryLoaderEvent.refresh() = _Refresh;
const factory CategoryLoaderEvent.search({
required String query,
@Default(true) bool isActive,
}) = _Search;
const factory CategoryLoaderEvent.syncAll() = _SyncAll;
const factory CategoryLoaderEvent.getAllCategories() = _GetAllCategories;
const factory CategoryLoaderEvent.getDatabaseStats() = _GetDatabaseStats;
const factory CategoryLoaderEvent.clearCache() = _ClearCache;
}
@@ -0,0 +1,16 @@
part of 'category_loader_bloc.dart';
@freezed
class CategoryLoaderState with _$CategoryLoaderState {
factory CategoryLoaderState({
required List<Category> categories,
required Option<CategoryFailure> failureOptionCategory,
@Default(false) bool hasReachedMax,
@Default(1) int page,
@Default(false) bool isLoadingMore,
String? searchQuery,
}) = _CategoryLoaderState;
factory CategoryLoaderState.initial() =>
CategoryLoaderState(categories: [], failureOptionCategory: none());
}