data sync page
This commit is contained in:
parent
5b980d237f
commit
72a464b4c0
@ -2,6 +2,8 @@ import 'dart:async';
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:enaklo_pos/data/datasources/product/product_local_datasource.dart';
|
import 'package:enaklo_pos/data/datasources/product/product_local_datasource.dart';
|
||||||
|
import 'package:enaklo_pos/data/datasources/category/category_local_datasource.dart';
|
||||||
|
import 'package:enaklo_pos/data/repositories/category/category_repository.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import '../../../data/datasources/product_remote_datasource.dart';
|
import '../../../data/datasources/product_remote_datasource.dart';
|
||||||
|
|
||||||
@ -9,7 +11,7 @@ part 'data_sync_event.dart';
|
|||||||
part 'data_sync_state.dart';
|
part 'data_sync_state.dart';
|
||||||
part 'data_sync_bloc.freezed.dart';
|
part 'data_sync_bloc.freezed.dart';
|
||||||
|
|
||||||
enum SyncStep { products, categories, variants, completed }
|
enum SyncStep { categories, products, variants, completed }
|
||||||
|
|
||||||
class SyncStats {
|
class SyncStats {
|
||||||
final int totalProducts;
|
final int totalProducts;
|
||||||
@ -26,9 +28,13 @@ class SyncStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DataSyncBloc extends Bloc<DataSyncEvent, DataSyncState> {
|
class DataSyncBloc extends Bloc<DataSyncEvent, DataSyncState> {
|
||||||
final ProductRemoteDatasource _remoteDatasource = ProductRemoteDatasource();
|
final ProductRemoteDatasource _productRemoteDatasource =
|
||||||
final ProductLocalDatasource _localDatasource =
|
ProductRemoteDatasource();
|
||||||
|
final ProductLocalDatasource _productLocalDatasource =
|
||||||
ProductLocalDatasource.instance;
|
ProductLocalDatasource.instance;
|
||||||
|
final CategoryLocalDatasource _categoryLocalDatasource =
|
||||||
|
CategoryLocalDatasource.instance;
|
||||||
|
final CategoryRepository _categoryRepository = CategoryRepository.instance;
|
||||||
|
|
||||||
Timer? _progressTimer;
|
Timer? _progressTimer;
|
||||||
bool _isCancelled = false;
|
bool _isCancelled = false;
|
||||||
@ -48,36 +54,75 @@ class DataSyncBloc extends Bloc<DataSyncEvent, DataSyncState> {
|
|||||||
_StartSync event,
|
_StartSync event,
|
||||||
Emitter<DataSyncState> emit,
|
Emitter<DataSyncState> emit,
|
||||||
) async {
|
) async {
|
||||||
log('🔄 Starting data sync...');
|
log('🔄 Starting full data sync (categories + products)...');
|
||||||
_isCancelled = false;
|
_isCancelled = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 1: Clear existing local data
|
// Step 1: Clear existing local data
|
||||||
emit(const DataSyncState.syncing(
|
emit(const DataSyncState.syncing(
|
||||||
SyncStep.products, 0.1, 'Membersihkan data lama...'));
|
SyncStep.categories, 0.05, 'Membersihkan data lama...'));
|
||||||
await _localDatasource.clearAllProducts();
|
|
||||||
|
await _productLocalDatasource.clearAllProducts();
|
||||||
|
await _categoryLocalDatasource.clearAllCategories();
|
||||||
|
|
||||||
if (_isCancelled) return;
|
if (_isCancelled) return;
|
||||||
|
|
||||||
// Step 2: Sync products
|
// Step 2: Sync categories first (products depend on categories)
|
||||||
|
await _syncCategories(emit);
|
||||||
|
|
||||||
|
if (_isCancelled) return;
|
||||||
|
|
||||||
|
// Step 3: Sync products
|
||||||
await _syncProducts(emit);
|
await _syncProducts(emit);
|
||||||
|
|
||||||
if (_isCancelled) return;
|
if (_isCancelled) return;
|
||||||
|
|
||||||
// Step 3: Generate final stats
|
// Step 4: Generate final stats
|
||||||
emit(const DataSyncState.syncing(
|
emit(const DataSyncState.syncing(
|
||||||
SyncStep.completed, 0.9, 'Menyelesaikan sinkronisasi...'));
|
SyncStep.completed, 0.95, 'Menyelesaikan sinkronisasi...'));
|
||||||
|
|
||||||
final stats = await _generateSyncStats();
|
final stats = await _generateSyncStats();
|
||||||
|
|
||||||
emit(DataSyncState.completed(stats));
|
emit(DataSyncState.completed(stats));
|
||||||
log('✅ Sync completed successfully');
|
log('✅ Full sync completed successfully');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('❌ Sync failed: $e');
|
log('❌ Sync failed: $e');
|
||||||
emit(DataSyncState.error('Gagal sinkronisasi: $e'));
|
emit(DataSyncState.error('Gagal sinkronisasi: $e'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _syncProducts(Emitter<DataSyncState> emit) async {
|
Future<void> _syncProducts(Emitter<DataSyncState> emit) async {
|
||||||
log('📦 Syncing products...');
|
log('📦 Syncing products...');
|
||||||
|
|
||||||
@ -88,10 +133,10 @@ class DataSyncBloc extends Bloc<DataSyncEvent, DataSyncState> {
|
|||||||
bool shouldContinue = true;
|
bool shouldContinue = true;
|
||||||
|
|
||||||
while (!_isCancelled && shouldContinue) {
|
while (!_isCancelled && shouldContinue) {
|
||||||
// Calculate accurate progress based on total count
|
// Calculate accurate progress (categories = 0.2, products = 0.2-0.9)
|
||||||
double progress = 0.2;
|
double progress = 0.2;
|
||||||
if (totalCount != null && (totalCount ?? 0) > 0) {
|
if (totalCount != null && (totalCount ?? 0) > 0) {
|
||||||
progress = 0.2 + (totalSynced / (totalCount ?? 0)) * 0.6;
|
progress = 0.2 + (totalSynced / (totalCount ?? 0)) * 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(DataSyncState.syncing(
|
emit(DataSyncState.syncing(
|
||||||
@ -102,7 +147,7 @@ class DataSyncBloc extends Bloc<DataSyncEvent, DataSyncState> {
|
|||||||
: 'Mengunduh produk... ($totalSynced produk)',
|
: 'Mengunduh produk... ($totalSynced produk)',
|
||||||
));
|
));
|
||||||
|
|
||||||
final result = await _remoteDatasource.getProducts(
|
final result = await _productRemoteDatasource.getProducts(
|
||||||
page: page,
|
page: page,
|
||||||
limit: 50, // Bigger batch for sync
|
limit: 50, // Bigger batch for sync
|
||||||
);
|
);
|
||||||
@ -128,7 +173,7 @@ class DataSyncBloc extends Bloc<DataSyncEvent, DataSyncState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save to local database in batches
|
// Save to local database in batches
|
||||||
await _localDatasource.saveProductsBatch(products);
|
await _productLocalDatasource.saveProductsBatch(products);
|
||||||
|
|
||||||
totalSynced += products.length;
|
totalSynced += products.length;
|
||||||
page++;
|
page++;
|
||||||
@ -154,8 +199,8 @@ class DataSyncBloc extends Bloc<DataSyncEvent, DataSyncState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit(DataSyncState.syncing(
|
emit(DataSyncState.syncing(
|
||||||
SyncStep.completed,
|
SyncStep.products,
|
||||||
0.8,
|
0.9,
|
||||||
'Produk berhasil diunduh ($totalSynced dari ${totalCount ?? totalSynced})',
|
'Produk berhasil diunduh ($totalSynced dari ${totalCount ?? totalSynced})',
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -163,13 +208,15 @@ class DataSyncBloc extends Bloc<DataSyncEvent, DataSyncState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<SyncStats> _generateSyncStats() async {
|
Future<SyncStats> _generateSyncStats() async {
|
||||||
final dbStats = await _localDatasource.getDatabaseStats();
|
final productStats = await _productLocalDatasource.getDatabaseStats();
|
||||||
|
final categoryStats = await _categoryLocalDatasource.getDatabaseStats();
|
||||||
|
|
||||||
return SyncStats(
|
return SyncStats(
|
||||||
totalProducts: dbStats['total_products'] ?? 0,
|
totalProducts: productStats['total_products'] ?? 0,
|
||||||
totalCategories: dbStats['total_categories'] ?? 0,
|
totalCategories: categoryStats['total_categories'] ?? 0,
|
||||||
totalVariants: dbStats['total_variants'] ?? 0,
|
totalVariants: productStats['total_variants'] ?? 0,
|
||||||
databaseSizeMB: dbStats['database_size_mb'] ?? 0.0,
|
databaseSizeMB: (productStats['database_size_mb'] ?? 0.0) +
|
||||||
|
(categoryStats['database_size_mb'] ?? 0.0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -87,20 +87,15 @@ class _DataSyncPageState extends State<DataSyncPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Portrait layout (original)
|
// Portrait layout
|
||||||
Widget _buildPortraitLayout(DataSyncState state, double screenHeight) {
|
Widget _buildPortraitLayout(DataSyncState state, double screenHeight) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.all(24),
|
padding: EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: screenHeight * 0.08), // Responsive spacing
|
|
||||||
|
|
||||||
// Header
|
|
||||||
_buildHeader(false),
|
|
||||||
|
|
||||||
SizedBox(height: screenHeight * 0.08),
|
SizedBox(height: screenHeight * 0.08),
|
||||||
|
_buildHeader(false),
|
||||||
// Sync progress
|
SizedBox(height: screenHeight * 0.08),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: state.when(
|
child: state.when(
|
||||||
initial: () => _buildInitialState(false),
|
initial: () => _buildInitialState(false),
|
||||||
@ -110,24 +105,20 @@ class _DataSyncPageState extends State<DataSyncPage>
|
|||||||
error: (message) => _buildErrorState(message, false),
|
error: (message) => _buildErrorState(message, false),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
|
|
||||||
// Actions
|
|
||||||
_buildActions(state),
|
_buildActions(state),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Landscape layout (side by side)
|
// Landscape layout
|
||||||
Widget _buildLandscapeLayout(
|
Widget _buildLandscapeLayout(
|
||||||
DataSyncState state, double screenWidth, double screenHeight) {
|
DataSyncState state, double screenWidth, double screenHeight) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Left side - Header and info
|
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -139,10 +130,7 @@ class _DataSyncPageState extends State<DataSyncPage>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(width: 40),
|
SizedBox(width: 40),
|
||||||
|
|
||||||
// Right side - Sync progress
|
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -188,7 +176,7 @@ class _DataSyncPageState extends State<DataSyncPage>
|
|||||||
),
|
),
|
||||||
SizedBox(height: isLandscape ? 4 : 8),
|
SizedBox(height: isLandscape ? 4 : 8),
|
||||||
Text(
|
Text(
|
||||||
'Mengunduh data terbaru ke perangkat',
|
'Mengunduh kategori dan produk terbaru',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: isLandscape ? 14 : 16,
|
fontSize: isLandscape ? 14 : 16,
|
||||||
color: Colors.grey.shade600,
|
color: Colors.grey.shade600,
|
||||||
@ -319,8 +307,8 @@ class _DataSyncPageState extends State<DataSyncPage>
|
|||||||
|
|
||||||
Widget _buildStepIndicator(SyncStep currentStep, bool isLandscape) {
|
Widget _buildStepIndicator(SyncStep currentStep, bool isLandscape) {
|
||||||
final steps = [
|
final steps = [
|
||||||
('Produk', SyncStep.products, Icons.inventory_2),
|
|
||||||
('Kategori', SyncStep.categories, Icons.category),
|
('Kategori', SyncStep.categories, Icons.category),
|
||||||
|
('Produk', SyncStep.products, Icons.inventory_2),
|
||||||
('Variant', SyncStep.variants, Icons.tune),
|
('Variant', SyncStep.variants, Icons.tune),
|
||||||
('Selesai', SyncStep.completed, Icons.check_circle),
|
('Selesai', SyncStep.completed, Icons.check_circle),
|
||||||
];
|
];
|
||||||
@ -551,11 +539,11 @@ class _DataSyncPageState extends State<DataSyncPage>
|
|||||||
// Vertical layout for landscape
|
// Vertical layout for landscape
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
_buildStatItem('Produk', '${stats.totalProducts}',
|
|
||||||
Icons.inventory_2, Colors.blue, isLandscape),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
_buildStatItem('Kategori', '${stats.totalCategories}',
|
_buildStatItem('Kategori', '${stats.totalCategories}',
|
||||||
Icons.category, Colors.green, isLandscape),
|
Icons.category, Colors.blue, isLandscape),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_buildStatItem('Produk', '${stats.totalProducts}',
|
||||||
|
Icons.inventory_2, Colors.green, isLandscape),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
_buildStatItem('Variant', '${stats.totalVariants}',
|
_buildStatItem('Variant', '${stats.totalVariants}',
|
||||||
Icons.tune, Colors.orange, isLandscape),
|
Icons.tune, Colors.orange, isLandscape),
|
||||||
@ -566,10 +554,10 @@ class _DataSyncPageState extends State<DataSyncPage>
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
_buildStatItem('Produk', '${stats.totalProducts}',
|
|
||||||
Icons.inventory_2, Colors.blue, isLandscape),
|
|
||||||
_buildStatItem('Kategori', '${stats.totalCategories}',
|
_buildStatItem('Kategori', '${stats.totalCategories}',
|
||||||
Icons.category, Colors.green, isLandscape),
|
Icons.category, Colors.blue, isLandscape),
|
||||||
|
_buildStatItem('Produk', '${stats.totalProducts}',
|
||||||
|
Icons.inventory_2, Colors.green, isLandscape),
|
||||||
_buildStatItem('Variant', '${stats.totalVariants}',
|
_buildStatItem('Variant', '${stats.totalVariants}',
|
||||||
Icons.tune, Colors.orange, isLandscape),
|
Icons.tune, Colors.orange, isLandscape),
|
||||||
],
|
],
|
||||||
@ -765,10 +753,10 @@ class _DataSyncPageState extends State<DataSyncPage>
|
|||||||
|
|
||||||
IconData _getSyncIcon(SyncStep step) {
|
IconData _getSyncIcon(SyncStep step) {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case SyncStep.products:
|
|
||||||
return Icons.inventory_2;
|
|
||||||
case SyncStep.categories:
|
case SyncStep.categories:
|
||||||
return Icons.category;
|
return Icons.category;
|
||||||
|
case SyncStep.products:
|
||||||
|
return Icons.inventory_2;
|
||||||
case SyncStep.variants:
|
case SyncStep.variants:
|
||||||
return Icons.tune;
|
return Icons.tune;
|
||||||
case SyncStep.completed:
|
case SyncStep.completed:
|
||||||
@ -778,10 +766,10 @@ class _DataSyncPageState extends State<DataSyncPage>
|
|||||||
|
|
||||||
String _getStepLabel(SyncStep step) {
|
String _getStepLabel(SyncStep step) {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case SyncStep.products:
|
|
||||||
return 'Mengunduh Produk';
|
|
||||||
case SyncStep.categories:
|
case SyncStep.categories:
|
||||||
return 'Mengunduh Kategori';
|
return 'Mengunduh Kategori';
|
||||||
|
case SyncStep.products:
|
||||||
|
return 'Mengunduh Produk';
|
||||||
case SyncStep.variants:
|
case SyncStep.variants:
|
||||||
return 'Mengunduh Variant';
|
return 'Mengunduh Variant';
|
||||||
case SyncStep.completed:
|
case SyncStep.completed:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user