From a58d1040af188552e0c4582a1c6a85e9c1be713d Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 20 Sep 2025 05:02:01 +0700 Subject: [PATCH] sync data --- .../category/category_repository.dart | 12 + .../product/product_repository.dart | 77 ++- .../setting/pages/setting_page.dart | 10 + lib/presentation/setting/pages/sync_page.dart | 580 ++++++++++++++++++ 4 files changed, 678 insertions(+), 1 deletion(-) create mode 100644 lib/presentation/setting/pages/sync_page.dart diff --git a/lib/data/repositories/category/category_repository.dart b/lib/data/repositories/category/category_repository.dart index 1083987..641814e 100644 --- a/lib/data/repositories/category/category_repository.dart +++ b/lib/data/repositories/category/category_repository.dart @@ -282,4 +282,16 @@ class CategoryRepository { return false; } } + + Future clearAllCategories() async { + try { + log('🗑️ Clearing all categories from repository...'); + await _localDatasource.clearAllCategories(); + clearCache(); + log('✅ All categories cleared successfully'); + } catch (e) { + log('❌ Error clearing all categories: $e'); + rethrow; + } + } } diff --git a/lib/data/repositories/product/product_repository.dart b/lib/data/repositories/product/product_repository.dart index c611212..9234168 100644 --- a/lib/data/repositories/product/product_repository.dart +++ b/lib/data/repositories/product/product_repository.dart @@ -1,15 +1,18 @@ import 'dart:developer'; import 'package:dartz/dartz.dart'; import 'package:enaklo_pos/data/datasources/product/product_local_datasource.dart'; +import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart'; class ProductRepository { static ProductRepository? _instance; final ProductLocalDatasource _localDatasource; + final ProductRemoteDatasource _remoteDatasource; ProductRepository._internal() - : _localDatasource = ProductLocalDatasource.instance; + : _localDatasource = ProductLocalDatasource.instance, + _remoteDatasource = ProductRemoteDatasource(); static ProductRepository get instance { _instance ??= ProductRepository._internal(); @@ -85,6 +88,66 @@ class ProductRepository { } } + // ======================================== + // PRODUCT SYNC OPERATIONS + // ======================================== + Future> syncAllProducts() async { + try { + log('🔄 Starting manual sync of all products...'); + + int page = 1; + const limit = 50; // Higher limit for bulk sync + bool hasMore = true; + int totalSynced = 0; + + // Clear local data first for fresh sync + await _localDatasource.clearAllProducts(); + + while (hasMore) { + log('📄 Syncing page $page...'); + + final result = await _remoteDatasource.getProducts( + page: page, + limit: limit, + ); + + await result.fold( + (failure) async { + log('❌ Sync failed at page $page: $failure'); + throw Exception(failure); + }, + (response) async { + final products = response.data?.products ?? []; + + if (products.isNotEmpty) { + await _localDatasource.saveProductsBatch( + products, + clearFirst: false, // Don't clear on subsequent pages + ); + totalSynced += products.length; + + // Check if we have more pages + hasMore = page < (response.data?.totalPages ?? 0); + page++; + + log('📦 Page $page synced: ${products.length} products'); + } else { + hasMore = false; + } + }, + ); + } + + final message = 'Berhasil sinkronisasi $totalSynced produk'; + log('✅ $message'); + return Right(message); + } catch (e) { + final error = 'Gagal sinkronisasi produk: $e'; + log('❌ $error'); + return Left(error); + } + } + // ======================================== // LOCAL DATABASE OPERATIONS // ======================================== @@ -142,4 +205,16 @@ class ProductRepository { return false; } } + + Future clearAllProducts() async { + try { + log('🗑️ Clearing all products from repository...'); + await _localDatasource.clearAllProducts(); + clearCache(); + log('✅ All products cleared successfully'); + } catch (e) { + log('❌ Error clearing all products: $e'); + rethrow; + } + } } diff --git a/lib/presentation/setting/pages/setting_page.dart b/lib/presentation/setting/pages/setting_page.dart index aae5a8c..fe24cd4 100644 --- a/lib/presentation/setting/pages/setting_page.dart +++ b/lib/presentation/setting/pages/setting_page.dart @@ -2,6 +2,7 @@ import 'package:enaklo_pos/core/constants/colors.dart'; import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:enaklo_pos/presentation/setting/pages/printer_page.dart'; import 'package:enaklo_pos/presentation/setting/pages/setting_tile.dart'; +import 'package:enaklo_pos/presentation/setting/pages/sync_page.dart'; import 'package:flutter/material.dart'; class SettingPage extends StatefulWidget { @@ -84,6 +85,14 @@ class _SettingPageState extends State { icon: Icons.print_outlined, onTap: () => indexValue(0), ), + SettingTile( + index: 1, + currentIndex: currentIndex, + title: 'Sinkronisasi', + subtitle: 'Sinkronisasi data', + icon: Icons.sync_outlined, + onTap: () => indexValue(1), + ), ], ), ), @@ -101,6 +110,7 @@ class _SettingPageState extends State { index: currentIndex, children: [ SettingPrinterPage(), + SettingSyncPage(), ], ), ), diff --git a/lib/presentation/setting/pages/sync_page.dart b/lib/presentation/setting/pages/sync_page.dart new file mode 100644 index 0000000..963fdaf --- /dev/null +++ b/lib/presentation/setting/pages/sync_page.dart @@ -0,0 +1,580 @@ +import 'dart:developer'; +import 'package:enaklo_pos/core/components/flushbar.dart'; +import 'package:enaklo_pos/core/components/buttons.dart'; +import 'package:enaklo_pos/data/repositories/product/product_repository.dart'; +import 'package:enaklo_pos/data/repositories/category/category_repository.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/presentation/setting/widgets/settings_title.dart'; +import 'package:flutter/material.dart'; + +class SettingSyncPage extends StatefulWidget { + const SettingSyncPage({super.key}); + + @override + State createState() => _SettingSyncPageState(); +} + +class _SettingSyncPageState extends State { + final ProductRepository _productRepository = ProductRepository.instance; + final CategoryRepository _categoryRepository = CategoryRepository.instance; + final ProductLocalDatasource _productLocalDatasource = + ProductLocalDatasource.instance; + final CategoryLocalDatasource _categoryLocalDatasource = + CategoryLocalDatasource.instance; + + bool _isLoading = false; + bool _isSyncing = false; + Map _productStats = {}; + Map _categoryStats = {}; + + @override + void initState() { + super.initState(); + _loadStats(); + } + + Future _loadStats() async { + setState(() => _isLoading = true); + try { + final productStats = await _productRepository.getDatabaseStats(); + final categoryStats = await _categoryRepository.getDatabaseStats(); + + setState(() { + _productStats = productStats; + _categoryStats = categoryStats; + _isLoading = false; + }); + } catch (e) { + log('Error loading stats: $e'); + setState(() => _isLoading = false); + } + } + + Future _syncAllData() async { + setState(() => _isSyncing = true); + + try { + // Show loading dialog + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Sinkronisasi semua data...'), + ], + ), + ), + ); + + // Sync categories first + final categoryResult = await _categoryRepository.syncAllCategories(); + + await categoryResult.fold( + (error) async { + Navigator.of(context).pop(); + AppFlushbar.showError(context, 'Gagal sync kategori: $error'); + return; + }, + (success) async { + log('Categories synced successfully'); + }, + ); + + // Sync products after categories + final productResult = await _productRepository.syncAllProducts(); + + await productResult.fold( + (error) async { + Navigator.of(context).pop(); + AppFlushbar.showError(context, 'Gagal sync produk: $error'); + return; + }, + (success) async { + log('Products synced successfully'); + }, + ); + + Navigator.of(context).pop(); + AppFlushbar.showSuccess(context, 'Sinkronisasi berhasil'); + _loadStats(); // Refresh stats + } catch (e) { + Navigator.of(context).pop(); + AppFlushbar.showError(context, 'Gagal sinkronisasi: $e'); + } finally { + setState(() => _isSyncing = false); + } + } + + Future _clearAllData() async { + // Show confirmation dialog + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Hapus Semua Data'), + content: Text( + 'Apakah Anda yakin ingin menghapus semua data lokal? Tindakan ini tidak dapat dibatalkan.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text('Batal'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text('Hapus', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + + if (confirmed != true) return; + + setState(() => _isLoading = true); + + try { + // Clear products and categories using datasource + await _productLocalDatasource.clearAllProducts(); + await _categoryLocalDatasource.clearAllCategories(); + + // Clear caches + _productRepository.clearCache(); + _categoryRepository.clearCache(); + + AppFlushbar.showSuccess(context, 'Semua data berhasil dihapus'); + _loadStats(); // Refresh stats + } catch (e) { + AppFlushbar.showError(context, 'Gagal menghapus data: $e'); + } finally { + setState(() => _isLoading = false); + } + } + + Future _syncProducts() async { + setState(() => _isLoading = true); + + try { + final result = await _productRepository.syncAllProducts(); + + await result.fold( + (error) async { + AppFlushbar.showError(context, 'Gagal sync produk: $error'); + }, + (success) async { + AppFlushbar.showSuccess(context, success); + _loadStats(); // Refresh stats + }, + ); + } catch (e) { + AppFlushbar.showError(context, 'Gagal sync produk: $e'); + } finally { + setState(() => _isLoading = false); + } + } + + Future _syncCategories() async { + setState(() => _isLoading = true); + + try { + final result = await _categoryRepository.syncAllCategories(); + + await result.fold( + (error) async { + AppFlushbar.showError(context, 'Gagal sync kategori: $error'); + }, + (success) async { + AppFlushbar.showSuccess(context, success); + _loadStats(); // Refresh stats + }, + ); + } catch (e) { + AppFlushbar.showError(context, 'Gagal sync kategori: $e'); + } finally { + setState(() => _isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + SettingsTitle( + 'Sinkronisasi', + subtitle: 'Sinkronisasi data dengan server', + ), + + SizedBox(height: 24), + + // Quick Actions + _buildQuickActions(), + + SizedBox(height: 24), + + // Sync Tables + _buildSyncTables(), + + SizedBox(height: 24), + + // Database Stats + _buildDatabaseStats(), + ], + ), + ); + } + + Widget _buildQuickActions() { + return Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Aksi Cepat', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Button.filled( + onPressed: _isSyncing || _isLoading ? null : _syncAllData, + label: _isSyncing ? 'Menyinkronkan...' : 'Sync Semua Data', + ), + ), + SizedBox(width: 12), + Expanded( + child: Button.outlined( + onPressed: _isLoading || _isSyncing ? null : _clearAllData, + label: 'Hapus Semua Data', + textColor: Colors.red, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildSyncTables() { + return Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Sinkronisasi per Tabel', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 16), + + // Categories Sync + _buildSyncTableItem( + title: 'Kategori', + subtitle: 'Sinkronkan data kategori produk', + icon: Icons.category, + color: Colors.blue, + count: _categoryStats['total_categories'] ?? 0, + onSync: _syncCategories, + onClear: () async { + final confirmed = await _showClearConfirmation('kategori'); + if (confirmed) { + await _categoryLocalDatasource.clearAllCategories(); + _categoryRepository.clearCache(); + AppFlushbar.showSuccess( + context, 'Data kategori berhasil dihapus'); + _loadStats(); + } + }, + ), + + SizedBox(height: 12), + Divider(), + SizedBox(height: 12), + + // Products Sync + _buildSyncTableItem( + title: 'Produk', + subtitle: 'Sinkronkan data produk dan variant', + icon: Icons.inventory_2, + color: Colors.green, + count: _productStats['total_products'] ?? 0, + onSync: _syncProducts, + onClear: () async { + final confirmed = await _showClearConfirmation('produk'); + if (confirmed) { + await _productLocalDatasource.clearAllProducts(); + _productRepository.clearCache(); + AppFlushbar.showSuccess( + context, 'Data produk berhasil dihapus'); + _loadStats(); + } + }, + ), + ], + ), + ); + } + + Widget _buildSyncTableItem({ + required String title, + required String subtitle, + required IconData icon, + required Color color, + required int count, + required VoidCallback onSync, + required VoidCallback onClear, + }) { + return Row( + children: [ + // Icon and info + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: color, size: 24), + ), + + SizedBox(width: 12), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(width: 8), + Container( + padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '$count', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ), + ], + ), + Text( + subtitle, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + + // Actions + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: _isLoading || _isSyncing ? null : onSync, + icon: Icon(Icons.sync, size: 20), + tooltip: 'Sync $title', + style: IconButton.styleFrom( + backgroundColor: color.withOpacity(0.1), + foregroundColor: color, + ), + ), + SizedBox(width: 4), + IconButton( + onPressed: _isLoading || _isSyncing ? null : onClear, + icon: Icon(Icons.delete_outline, size: 20), + tooltip: 'Hapus $title', + style: IconButton.styleFrom( + backgroundColor: Colors.red.withOpacity(0.1), + foregroundColor: Colors.red, + ), + ), + ], + ), + ], + ); + } + + Widget _buildDatabaseStats() { + return Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Statistik Database', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + Spacer(), + if (_isLoading) + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + IconButton( + onPressed: _loadStats, + icon: Icon(Icons.refresh, size: 20), + tooltip: 'Refresh Stats', + ), + ], + ), + SizedBox(height: 16), + if (_isLoading) + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: CircularProgressIndicator(), + ), + ) + else + Column( + children: [ + // Category stats + _buildStatRow( + 'Kategori', + _categoryStats['total_categories']?.toString() ?? '0', + Icons.category, + Colors.blue, + ), + SizedBox(height: 8), + + // Product stats + _buildStatRow( + 'Produk', + _productStats['total_products']?.toString() ?? '0', + Icons.inventory_2, + Colors.green, + ), + SizedBox(height: 8), + + // Variant stats + _buildStatRow( + 'Variant', + _productStats['total_variants']?.toString() ?? '0', + Icons.tune, + Colors.orange, + ), + SizedBox(height: 8), + + // Cache stats + _buildStatRow( + 'Cache Entries', + '${(_productStats['cache_entries'] ?? 0) + (_categoryStats['cache_entries'] ?? 0)}', + Icons.memory, + Colors.purple, + ), + SizedBox(height: 8), + + // Database size + _buildStatRow( + 'Ukuran Database', + '${((_productStats['database_size_mb'] ?? 0.0) + (_categoryStats['database_size_mb'] ?? 0.0)).toStringAsFixed(2)} MB', + Icons.storage, + Colors.grey.shade600, + ), + ], + ), + ], + ), + ); + } + + Widget _buildStatRow(String label, String value, IconData icon, Color color) { + return Row( + children: [ + Icon(icon, size: 16, color: color), + SizedBox(width: 8), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + ), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ], + ); + } + + Future _showClearConfirmation(String dataType) async { + return await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Hapus Data $dataType'), + content: + Text('Apakah Anda yakin ingin menghapus semua data $dataType?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text('Batal'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text('Hapus', style: TextStyle(color: Colors.red)), + ), + ], + ), + ) ?? + false; + } +}