2025-10-24 23:20:41 +07:00

324 lines
8.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.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<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();
}
}