324 lines
8.6 KiB
Dart
Raw Normal View History

2025-10-24 22:25:01 +07:00
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<SyncEvent, SyncState> {
final IProductRepository _productRepository;
final ICategoryRepository _categoryRepository;
Timer? _progressTimer;
bool _isCancelled = false;
SyncBloc(this._productRepository, this._categoryRepository)
: super(SyncState.initial()) {
on<SyncEvent>(_onSyncEvent);
}
Future<void> _onSyncEvent(SyncEvent event, Emitter<SyncState> 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<void> _syncCategories(Emitter<SyncState> 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<void> _syncProducts(Emitter<SyncState> 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.00.2, produk 0.20.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.getProducts(
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<SyncStats> _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<String, dynamic> productStats = {};
Map<String, dynamic> 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<void> close() {
_progressTimer?.cancel();
return super.close();
}
}