category local
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user