category local

This commit is contained in:
efrilm
2025-09-20 04:36:22 +07:00
parent c12d6525fa
commit 5b980d237f
12 changed files with 3443 additions and 515 deletions
@@ -1,6 +1,8 @@
import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/datasources/category_remote_datasource.dart';
import 'dart:async';
import 'dart:developer';
import 'package:enaklo_pos/data/models/response/category_response_model.dart';
import 'package:enaklo_pos/data/repositories/category/category_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'category_loader_event.dart';
@@ -9,35 +11,310 @@ part 'category_loader_bloc.freezed.dart';
class CategoryLoaderBloc
extends Bloc<CategoryLoaderEvent, CategoryLoaderState> {
final CategoryRemoteDatasource _datasource;
CategoryLoaderBloc(this._datasource) : super(CategoryLoaderState.initial()) {
on<_Get>((event, emit) async {
emit(const _Loading());
final result = await _datasource.getCategories(limit: 50);
result.fold(
(l) => emit(_Error(l)),
(r) async {
List<CategoryModel> categories = r.data.categories;
categories.insert(
0,
CategoryModel(
id: "",
name: 'Semua',
organizationId: '',
businessType: '',
metadata: {},
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
);
emit(_Loaded(categories, null));
final CategoryRepository _categoryRepository = CategoryRepository.instance;
Timer? _searchDebounce;
bool _isLoadingMore = false;
CategoryLoaderBloc() : super(const CategoryLoaderState.initial()) {
on<_GetCategories>(_onGetCategories);
on<_LoadMore>(_onLoadMore);
on<_Refresh>(_onRefresh);
on<_Search>(_onSearch);
on<_SyncAll>(_onSyncAll);
on<_GetAllCategories>(_onGetAllCategories);
on<_ClearCache>(_onClearCache);
on<_GetDatabaseStats>(_onGetDatabaseStats);
}
@override
Future<void> close() {
_searchDebounce?.cancel();
return super.close();
}
// ========================================
// GET CATEGORIES (Remote-first with local fallback)
// ========================================
Future<void> _onGetCategories(
_GetCategories event,
Emitter<CategoryLoaderState> emit,
) async {
emit(const CategoryLoaderState.loading());
_isLoadingMore = false;
log('📱 Loading categories - isActive: ${event.isActive}, forceRemote: ${event.forceRemote}');
final result = await _categoryRepository.getCategories(
page: 1,
limit: 10,
isActive: event.isActive,
search: event.search,
forceRemote: event.forceRemote,
);
await result.fold(
(failure) async {
log('❌ Error loading categories: $failure');
emit(CategoryLoaderState.error(failure));
},
(response) async {
final categories = response.data.categories;
final totalPages = response.data.totalPages;
final hasReachedMax = categories.length < 10 || 1 >= totalPages;
log('✅ Categories loaded: ${categories.length}, hasReachedMax: $hasReachedMax');
emit(CategoryLoaderState.loaded(
categories: categories,
hasReachedMax: hasReachedMax,
currentPage: 1,
isLoadingMore: false,
isActive: event.isActive,
searchQuery: event.search,
));
},
);
}
// ========================================
// LOAD MORE CATEGORIES
// ========================================
Future<void> _onLoadMore(
_LoadMore event,
Emitter<CategoryLoaderState> emit,
) async {
final currentState = state;
if (currentState is! _Loaded ||
currentState.hasReachedMax ||
_isLoadingMore ||
currentState.isLoadingMore) {
log('⏹️ Load more blocked - state: ${currentState.runtimeType}, isLoadingMore: $_isLoadingMore');
return;
}
_isLoadingMore = true;
emit(currentState.copyWith(isLoadingMore: true));
final nextPage = currentState.currentPage + 1;
log('📄 Loading more categories - page: $nextPage');
try {
final result = await _categoryRepository.getCategories(
page: nextPage,
limit: 10,
isActive: currentState.isActive,
search: currentState.searchQuery,
);
await result.fold(
(failure) async {
log('❌ Error loading more categories: $failure');
emit(currentState.copyWith(isLoadingMore: false));
},
(response) async {
final newCategories = response.data.categories;
final totalPages = response.data.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<CategoryModel>.from(currentState.categories)
..addAll(filteredNewCategories);
final hasReachedMax =
newCategories.length < 10 || nextPage >= totalPages;
log('✅ More categories loaded: ${filteredNewCategories.length} new, total: ${allCategories.length}');
emit(CategoryLoaderState.loaded(
categories: allCategories,
hasReachedMax: hasReachedMax,
currentPage: nextPage,
isLoadingMore: false,
isActive: currentState.isActive,
searchQuery: currentState.searchQuery,
));
},
);
} catch (e) {
log('❌ Exception loading more categories: $e');
emit(currentState.copyWith(isLoadingMore: false));
} finally {
_isLoadingMore = false;
}
}
// ========================================
// REFRESH CATEGORIES
// ========================================
Future<void> _onRefresh(
_Refresh event,
Emitter<CategoryLoaderState> emit,
) async {
final currentState = state;
bool isActive = true;
String? searchQuery;
if (currentState is _Loaded) {
isActive = currentState.isActive;
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 CATEGORIES
// ========================================
Future<void> _onSearch(
_Search event,
Emitter<CategoryLoaderState> emit,
) async {
// Cancel previous search
_searchDebounce?.cancel();
// Debounce search for better UX
_searchDebounce = Timer(Duration(milliseconds: 300), () async {
emit(const CategoryLoaderState.loading());
_isLoadingMore = false;
log('🔍 Searching categories: "${event.query}"');
final result = await _categoryRepository.getCategories(
page: 1,
limit: 20, // More results for search
isActive: event.isActive,
search: event.query,
);
await result.fold(
(failure) async {
log('❌ Search error: $failure');
emit(CategoryLoaderState.error(failure));
},
(response) async {
final categories = response.data.categories;
final totalPages = response.data.totalPages;
final hasReachedMax = categories.length < 20 || 1 >= totalPages;
log('✅ Search results: ${categories.length} categories found');
emit(CategoryLoaderState.loaded(
categories: categories,
hasReachedMax: hasReachedMax,
currentPage: 1,
isLoadingMore: false,
isActive: event.isActive,
searchQuery: event.query,
));
},
);
});
on<_SetCategoryId>((event, emit) async {
var currentState = state as _Loaded;
}
emit(_Loaded(currentState.categories, event.categoryId));
});
// ========================================
// SYNC ALL CATEGORIES
// ========================================
Future<void> _onSyncAll(
_SyncAll event,
Emitter<CategoryLoaderState> emit,
) async {
emit(const CategoryLoaderState.syncing());
log('🔄 Starting full category sync...');
final result = await _categoryRepository.syncAllCategories();
await result.fold(
(failure) async {
log('❌ Sync failed: $failure');
emit(CategoryLoaderState.syncError(failure));
// After sync error, try to load local data
Timer(Duration(seconds: 2), () {
add(const CategoryLoaderEvent.getCategories());
});
},
(successMessage) async {
log('✅ Sync completed: $successMessage');
emit(CategoryLoaderState.syncSuccess(successMessage));
// After successful sync, load the updated data
Timer(Duration(seconds: 1), () {
add(const CategoryLoaderEvent.getCategories());
});
},
);
}
// ========================================
// GET ALL CATEGORIES (For Dropdown)
// ========================================
Future<void> _onGetAllCategories(
_GetAllCategories event,
Emitter<CategoryLoaderState> emit,
) async {
try {
log('📋 Loading all categories for dropdown...');
final categories = await _categoryRepository.getAllCategories();
emit(CategoryLoaderState.allCategoriesLoaded(categories));
log('✅ All categories loaded: ${categories.length}');
} catch (e) {
log('❌ Error loading all categories: $e');
emit(CategoryLoaderState.error('Gagal memuat semua kategori: $e'));
}
}
// ========================================
// GET DATABASE STATS
// ========================================
Future<void> _onGetDatabaseStats(
_GetDatabaseStats event,
Emitter<CategoryLoaderState> emit,
) 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');
}
}
// ========================================
// CLEAR CACHE
// ========================================
Future<void> _onClearCache(
_ClearCache event,
Emitter<CategoryLoaderState> emit,
) async {
log('🧹 Manually clearing category cache');
_categoryRepository.clearCache();
// Refresh current data after cache clear
add(const CategoryLoaderEvent.refresh());
}
}
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,26 @@ part of 'category_loader_bloc.dart';
@freezed
class CategoryLoaderEvent with _$CategoryLoaderEvent {
const factory CategoryLoaderEvent.get() = _Get;
const factory CategoryLoaderEvent.setCategoryId(String categoryId) =
_SetCategoryId;
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;
}
@@ -3,8 +3,29 @@ part of 'category_loader_bloc.dart';
@freezed
class CategoryLoaderState with _$CategoryLoaderState {
const factory CategoryLoaderState.initial() = _Initial;
const factory CategoryLoaderState.loading() = _Loading;
const factory CategoryLoaderState.loaded(
List<CategoryModel> categories, String? categoryId) = _Loaded;
const factory CategoryLoaderState.loaded({
required List<CategoryModel> categories,
required bool hasReachedMax,
required int currentPage,
required bool isLoadingMore,
required bool isActive,
String? searchQuery,
}) = _Loaded;
const factory CategoryLoaderState.error(String message) = _Error;
// Sync-specific states
const factory CategoryLoaderState.syncing() = _Syncing;
const factory CategoryLoaderState.syncSuccess(String message) = _SyncSuccess;
const factory CategoryLoaderState.syncError(String message) = _SyncError;
// For dropdown/all categories
const factory CategoryLoaderState.allCategoriesLoaded(
List<CategoryModel> categories,
) = _AllCategoriesLoaded;
}