import 'dart:async'; import 'dart:developer'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import '../../domain/category/category.dart'; import '../../domain/product/product.dart'; part 'sync_event.dart'; part 'sync_state.dart'; part '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, }); } @injectable class SyncBloc extends Bloc { final IProductRepository _productRepository; final ICategoryRepository _categoryRepository; Timer? _progressTimer; bool _isCancelled = false; SyncBloc(this._productRepository, this._categoryRepository) : super(SyncState.initial()) { on(_onSyncEvent); } Future _onSyncEvent(SyncEvent event, Emitter emit) { return event.map( startSync: (e) async { log('🔄 Starting full data sync (categories + products)...'); _isCancelled = false; emit( state.copyWith( isSyncing: true, currentStep: SyncStep.categories, progress: 0.05, message: 'Membersihkan data lama...', errorMessage: null, ), ); try { // Step 1: Clear existing local data await _productRepository.clearAllProducts(); await _categoryRepository.clearAllCategories(); if (_isCancelled) return; // Step 2: Sync categories first await _syncCategories(emit); if (_isCancelled) return; // Step 3: Sync products await _syncProducts(emit); if (_isCancelled) return; // Step 4: Final stats emit( state.copyWith( currentStep: SyncStep.completed, progress: 0.95, message: 'Menyelesaikan sinkronisasi...', ), ); final stats = await _generateSyncStats(); emit( state.copyWith( isSyncing: false, stats: stats, progress: 1.0, message: 'Sinkronisasi selesai ✅', ), ); log('✅ Full sync completed successfully'); } catch (e) { log('❌ Sync failed: $e'); emit( state.copyWith( isSyncing: false, errorMessage: 'Gagal sinkronisasi: $e', message: 'Sinkronisasi gagal ❌', ), ); } }, cancelSync: (e) async { log('âšī¸ Cancelling sync...'); _isCancelled = true; _progressTimer?.cancel(); emit(SyncState.initial()); }, ); } Future _syncCategories(Emitter emit) async { log('📁 Syncing categories...'); emit( state.copyWith( isSyncing: true, currentStep: SyncStep.categories, progress: 0.1, message: 'Mengunduh kategori...', errorMessage: null, ), ); try { // Gunakan 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( state.copyWith( currentStep: SyncStep.categories, progress: 0.2, message: 'Kategori berhasil diunduh ✅', ), ); }, ); } catch (e) { log('❌ Category sync failed: $e'); emit( state.copyWith( isSyncing: false, errorMessage: 'Gagal sync kategori: $e', message: 'Sinkronisasi kategori gagal ❌', ), ); rethrow; // penting agar _onStartSync tahu kalau gagal } } Future _syncProducts(Emitter emit) async { log('đŸ“Ļ Syncing products...'); int page = 1; int totalSynced = 0; int? totalCount; int? totalPages; bool shouldContinue = true; while (!_isCancelled && shouldContinue) { // Hitung progress dinamis (kategori 0.0–0.2, produk 0.2–0.9) double progress = 0.2; if (totalCount != null && totalCount! > 0) { progress = 0.2 + (totalSynced / totalCount!) * 0.7; } emit( state.copyWith( isSyncing: true, currentStep: SyncStep.products, progress: progress, message: totalCount != null ? 'Mengunduh produk... ($totalSynced dari $totalCount)' : 'Mengunduh produk... ($totalSynced produk)', errorMessage: null, ), ); final result = await _productRepository.getRemoteProducts( page: page, limit: 50, // ambil batch besar biar cepat ); await result.fold( (failure) async { emit( state.copyWith( isSyncing: false, errorMessage: 'Gagal sync produk: $failure', message: 'Sinkronisasi produk gagal ❌', ), ); throw Exception(failure); }, (response) async { final products = response.products; final responseData = response; // Ambil total count & total page dari respons pertama if (page == 1) { totalCount = responseData.totalCount; totalPages = responseData.totalPages; log('📊 Total products to sync: $totalCount ($totalPages pages)'); } if (products.isEmpty) { shouldContinue = false; return; } // Simpan batch produk ke local DB await _productRepository.saveProductsBatch(products); totalSynced += products.length; page++; log( 'đŸ“Ļ Synced page ${page - 1}: ${products.length} products (Total: $totalSynced)', ); // Cek apakah sudah selesai sync if (totalPages != null && page > totalPages!) { shouldContinue = false; return; } // Fallback jika pagination info tidak lengkap if (products.length < 50) { shouldContinue = false; return; } // Tambahkan delay kecil agar tidak overload server await Future.delayed(const Duration(milliseconds: 100)); }, ); } // Selesai sync produk emit( state.copyWith( progress: 0.9, message: 'Sinkronisasi produk selesai ✅ ($totalSynced total)', ), ); } Future _generateSyncStats() async { try { log('📊 Generating sync statistics via repository...'); // Jalankan kedua query secara paralel untuk efisiensi final results = await Future.wait([ _productRepository.getDatabaseStats(), _categoryRepository.getDatabaseStats(), ]); // Default kosong Map productStats = {}; Map categoryStats = {}; // Ambil hasil product stats await results[0].fold( (failure) async { log('âš ī¸ Failed to get product stats: $failure'); }, (data) async { productStats = data; }, ); // Ambil hasil category stats await results[1].fold( (failure) async { log('âš ī¸ Failed to get category stats: $failure'); }, (data) async { categoryStats = data; }, ); // Bangun objek SyncStats akhir final stats = SyncStats( totalProducts: productStats['total_products'] ?? 0, totalCategories: categoryStats['total_categories'] ?? 0, totalVariants: productStats['total_variants'] ?? 0, databaseSizeMB: ((productStats['database_size_mb'] ?? 0.0) as num).toDouble() + ((categoryStats['database_size_mb'] ?? 0.0) as num).toDouble(), ); log('✅ Sync stats generated: $stats'); return stats; } catch (e, stack) { log('❌ Error generating sync stats: $e\n$stack'); return SyncStats( totalProducts: 0, totalCategories: 0, totalVariants: 0, databaseSizeMB: 0.0, ); } } @override Future close() { _progressTimer?.cancel(); return super.close(); } }