233 lines
6.8 KiB
Dart
Raw Normal View History

2025-09-20 03:10:05 +07:00
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/datasources/product/product_local_datasource.dart';
2025-09-20 04:45:08 +07:00
import 'package:enaklo_pos/data/datasources/category/category_local_datasource.dart';
import 'package:enaklo_pos/data/repositories/category/category_repository.dart';
2025-09-20 03:10:05 +07:00
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';
2025-09-20 04:45:08 +07:00
enum SyncStep { categories, products, variants, completed }
2025-09-20 03:10:05 +07:00
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> {
2025-09-20 04:45:08 +07:00
final ProductRemoteDatasource _productRemoteDatasource =
ProductRemoteDatasource();
final ProductLocalDatasource _productLocalDatasource =
2025-09-20 03:10:05 +07:00
ProductLocalDatasource.instance;
2025-09-20 04:45:08 +07:00
final CategoryLocalDatasource _categoryLocalDatasource =
CategoryLocalDatasource.instance;
final CategoryRepository _categoryRepository = CategoryRepository.instance;
2025-09-20 03:10:05 +07:00
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 {
2025-09-20 04:45:08 +07:00
log('🔄 Starting full data sync (categories + products)...');
2025-09-20 03:10:05 +07:00
_isCancelled = false;
try {
// Step 1: Clear existing local data
emit(const DataSyncState.syncing(
2025-09-20 04:45:08 +07:00
SyncStep.categories, 0.05, 'Membersihkan data lama...'));
await _productLocalDatasource.clearAllProducts();
await _categoryLocalDatasource.clearAllCategories();
2025-09-20 03:10:05 +07:00
if (_isCancelled) return;
2025-09-20 04:45:08 +07:00
// Step 2: Sync categories first (products depend on categories)
await _syncCategories(emit);
if (_isCancelled) return;
// Step 3: Sync products
2025-09-20 03:10:05 +07:00
await _syncProducts(emit);
if (_isCancelled) return;
2025-09-20 04:45:08 +07:00
// Step 4: Generate final stats
2025-09-20 03:10:05 +07:00
emit(const DataSyncState.syncing(
2025-09-20 04:45:08 +07:00
SyncStep.completed, 0.95, 'Menyelesaikan sinkronisasi...'));
2025-09-20 03:10:05 +07:00
final stats = await _generateSyncStats();
emit(DataSyncState.completed(stats));
2025-09-20 04:45:08 +07:00
log('✅ Full sync completed successfully');
2025-09-20 03:10:05 +07:00
} catch (e) {
log('❌ Sync failed: $e');
emit(DataSyncState.error('Gagal sinkronisasi: $e'));
}
}
2025-09-20 04:45:08 +07:00
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');
}
}
2025-09-20 03:10:05 +07:00
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) {
2025-09-20 04:45:08 +07:00
// Calculate accurate progress (categories = 0.2, products = 0.2-0.9)
2025-09-20 03:10:05 +07:00
double progress = 0.2;
if (totalCount != null && (totalCount ?? 0) > 0) {
2025-09-20 04:45:08 +07:00
progress = 0.2 + (totalSynced / (totalCount ?? 0)) * 0.7;
2025-09-20 03:10:05 +07:00
}
emit(DataSyncState.syncing(
SyncStep.products,
progress,
totalCount != null
? 'Mengunduh produk... ($totalSynced dari $totalCount)'
: 'Mengunduh produk... ($totalSynced produk)',
));
2025-09-20 04:45:08 +07:00
final result = await _productRemoteDatasource.getProducts(
2025-09-20 03:10:05 +07:00
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
2025-09-20 04:45:08 +07:00
await _productLocalDatasource.saveProductsBatch(products);
2025-09-20 03:10:05 +07:00
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(
2025-09-20 04:45:08 +07:00
SyncStep.products,
0.9,
2025-09-20 03:10:05 +07:00
'Produk berhasil diunduh ($totalSynced dari ${totalCount ?? totalSynced})',
));
log('✅ Products sync completed: $totalSynced products synced');
}
Future<SyncStats> _generateSyncStats() async {
2025-09-20 04:45:08 +07:00
final productStats = await _productLocalDatasource.getDatabaseStats();
final categoryStats = await _categoryLocalDatasource.getDatabaseStats();
2025-09-20 03:10:05 +07:00
return SyncStats(
2025-09-20 04:45:08 +07:00
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),
2025-09-20 03:10:05 +07:00
);
}
Future<void> _onCancelSync(
_CancelSync event,
Emitter<DataSyncState> emit,
) async {
log('⏹️ Cancelling sync...');
_isCancelled = true;
_progressTimer?.cancel();
emit(const DataSyncState.initial());
}
}