324 lines
8.6 KiB
Dart
324 lines
8.6 KiB
Dart
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.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<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();
|
||
}
|
||
}
|