233 lines
6.8 KiB
Dart
233 lines
6.8 KiB
Dart
import 'dart:async';
|
|
import 'dart:developer';
|
|
import 'package:bloc/bloc.dart';
|
|
import 'package:enaklo_pos/data/datasources/product/product_local_datasource.dart';
|
|
import 'package:enaklo_pos/data/datasources/category/category_local_datasource.dart';
|
|
import 'package:enaklo_pos/data/repositories/category/category_repository.dart';
|
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
import '../../../data/datasources/product_remote_datasource.dart';
|
|
|
|
part 'data_sync_event.dart';
|
|
part 'data_sync_state.dart';
|
|
part 'data_sync_bloc.freezed.dart';
|
|
|
|
enum SyncStep { categories, products, variants, completed }
|
|
|
|
class SyncStats {
|
|
final int totalProducts;
|
|
final int totalCategories;
|
|
final int totalVariants;
|
|
final double databaseSizeMB;
|
|
|
|
SyncStats({
|
|
required this.totalProducts,
|
|
required this.totalCategories,
|
|
required this.totalVariants,
|
|
required this.databaseSizeMB,
|
|
});
|
|
}
|
|
|
|
class DataSyncBloc extends Bloc<DataSyncEvent, DataSyncState> {
|
|
final ProductRemoteDatasource _productRemoteDatasource =
|
|
ProductRemoteDatasource();
|
|
final ProductLocalDatasource _productLocalDatasource =
|
|
ProductLocalDatasource.instance;
|
|
final CategoryLocalDatasource _categoryLocalDatasource =
|
|
CategoryLocalDatasource.instance;
|
|
final CategoryRepository _categoryRepository = CategoryRepository.instance;
|
|
|
|
Timer? _progressTimer;
|
|
bool _isCancelled = false;
|
|
|
|
DataSyncBloc() : super(const DataSyncState.initial()) {
|
|
on<_StartSync>(_onStartSync);
|
|
on<_CancelSync>(_onCancelSync);
|
|
}
|
|
|
|
@override
|
|
Future<void> close() {
|
|
_progressTimer?.cancel();
|
|
return super.close();
|
|
}
|
|
|
|
Future<void> _onStartSync(
|
|
_StartSync event,
|
|
Emitter<DataSyncState> emit,
|
|
) async {
|
|
log('đ Starting full data sync (categories + products)...');
|
|
_isCancelled = false;
|
|
|
|
try {
|
|
// Step 1: Clear existing local data
|
|
emit(const DataSyncState.syncing(
|
|
SyncStep.categories, 0.05, 'Membersihkan data lama...'));
|
|
|
|
await _productLocalDatasource.clearAllProducts();
|
|
await _categoryLocalDatasource.clearAllCategories();
|
|
|
|
if (_isCancelled) return;
|
|
|
|
// Step 2: Sync categories first (products depend on categories)
|
|
await _syncCategories(emit);
|
|
|
|
if (_isCancelled) return;
|
|
|
|
// Step 3: Sync products
|
|
await _syncProducts(emit);
|
|
|
|
if (_isCancelled) return;
|
|
|
|
// Step 4: Generate final stats
|
|
emit(const DataSyncState.syncing(
|
|
SyncStep.completed, 0.95, 'Menyelesaikan sinkronisasi...'));
|
|
|
|
final stats = await _generateSyncStats();
|
|
|
|
emit(DataSyncState.completed(stats));
|
|
log('â
Full sync completed successfully');
|
|
} catch (e) {
|
|
log('â Sync failed: $e');
|
|
emit(DataSyncState.error('Gagal sinkronisasi: $e'));
|
|
}
|
|
}
|
|
|
|
Future<void> _syncCategories(Emitter<DataSyncState> emit) async {
|
|
log('đ Syncing categories...');
|
|
|
|
emit(const DataSyncState.syncing(
|
|
SyncStep.categories,
|
|
0.1,
|
|
'Mengunduh kategori...',
|
|
));
|
|
|
|
try {
|
|
// Use CategoryRepository sync method
|
|
final result = await _categoryRepository.syncAllCategories();
|
|
|
|
await result.fold(
|
|
(failure) async {
|
|
throw Exception('Gagal sync kategori: $failure');
|
|
},
|
|
(successMessage) async {
|
|
log('â
Categories sync completed: $successMessage');
|
|
emit(const DataSyncState.syncing(
|
|
SyncStep.categories,
|
|
0.2,
|
|
'Kategori berhasil diunduh',
|
|
));
|
|
},
|
|
);
|
|
} catch (e) {
|
|
log('â Category sync failed: $e');
|
|
throw Exception('Gagal sync kategori: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _syncProducts(Emitter<DataSyncState> emit) async {
|
|
log('đĻ Syncing products...');
|
|
|
|
int page = 1;
|
|
int totalSynced = 0;
|
|
int? totalCount;
|
|
int? totalPages;
|
|
bool shouldContinue = true;
|
|
|
|
while (!_isCancelled && shouldContinue) {
|
|
// Calculate accurate progress (categories = 0.2, products = 0.2-0.9)
|
|
double progress = 0.2;
|
|
if (totalCount != null && (totalCount ?? 0) > 0) {
|
|
progress = 0.2 + (totalSynced / (totalCount ?? 0)) * 0.7;
|
|
}
|
|
|
|
emit(DataSyncState.syncing(
|
|
SyncStep.products,
|
|
progress,
|
|
totalCount != null
|
|
? 'Mengunduh produk... ($totalSynced dari $totalCount)'
|
|
: 'Mengunduh produk... ($totalSynced produk)',
|
|
));
|
|
|
|
final result = await _productRemoteDatasource.getProducts(
|
|
page: page,
|
|
limit: 50, // Bigger batch for sync
|
|
);
|
|
|
|
await result.fold(
|
|
(failure) async {
|
|
throw Exception(failure);
|
|
},
|
|
(response) async {
|
|
final products = response.data?.products ?? [];
|
|
final responseData = response.data;
|
|
|
|
// Get pagination info from first response
|
|
if (page == 1 && responseData != null) {
|
|
totalCount = responseData.totalCount;
|
|
totalPages = responseData.totalPages;
|
|
log('đ Total products to sync: $totalCount (${totalPages} pages)');
|
|
}
|
|
|
|
if (products.isEmpty) {
|
|
shouldContinue = false;
|
|
return;
|
|
}
|
|
|
|
// Save to local database in batches
|
|
await _productLocalDatasource.saveProductsBatch(products);
|
|
|
|
totalSynced += products.length;
|
|
page++;
|
|
|
|
log('đĻ Synced page ${page - 1}: ${products.length} products (Total: $totalSynced)');
|
|
|
|
// Check if we reached the end using pagination info
|
|
if (totalPages != null && page > (totalPages ?? 0)) {
|
|
shouldContinue = false;
|
|
return;
|
|
}
|
|
|
|
// Fallback check if pagination info not available
|
|
if (products.length < 50) {
|
|
shouldContinue = false;
|
|
return;
|
|
}
|
|
|
|
// Small delay to prevent overwhelming the server
|
|
await Future.delayed(Duration(milliseconds: 100));
|
|
},
|
|
);
|
|
}
|
|
|
|
emit(DataSyncState.syncing(
|
|
SyncStep.products,
|
|
0.9,
|
|
'Produk berhasil diunduh ($totalSynced dari ${totalCount ?? totalSynced})',
|
|
));
|
|
|
|
log('â
Products sync completed: $totalSynced products synced');
|
|
}
|
|
|
|
Future<SyncStats> _generateSyncStats() async {
|
|
final productStats = await _productLocalDatasource.getDatabaseStats();
|
|
final categoryStats = await _categoryLocalDatasource.getDatabaseStats();
|
|
|
|
return SyncStats(
|
|
totalProducts: productStats['total_products'] ?? 0,
|
|
totalCategories: categoryStats['total_categories'] ?? 0,
|
|
totalVariants: productStats['total_variants'] ?? 0,
|
|
databaseSizeMB: (productStats['database_size_mb'] ?? 0.0) +
|
|
(categoryStats['database_size_mb'] ?? 0.0),
|
|
);
|
|
}
|
|
|
|
Future<void> _onCancelSync(
|
|
_CancelSync event,
|
|
Emitter<DataSyncState> emit,
|
|
) async {
|
|
log('âšī¸ Cancelling sync...');
|
|
_isCancelled = true;
|
|
_progressTimer?.cancel();
|
|
emit(const DataSyncState.initial());
|
|
}
|
|
}
|