sync data
This commit is contained in:
parent
72a464b4c0
commit
a58d1040af
@ -282,4 +282,16 @@ class CategoryRepository {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.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/product_remote_datasource.dart';
|
||||||
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
|
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
|
||||||
|
|
||||||
class ProductRepository {
|
class ProductRepository {
|
||||||
static ProductRepository? _instance;
|
static ProductRepository? _instance;
|
||||||
|
|
||||||
final ProductLocalDatasource _localDatasource;
|
final ProductLocalDatasource _localDatasource;
|
||||||
|
final ProductRemoteDatasource _remoteDatasource;
|
||||||
|
|
||||||
ProductRepository._internal()
|
ProductRepository._internal()
|
||||||
: _localDatasource = ProductLocalDatasource.instance;
|
: _localDatasource = ProductLocalDatasource.instance,
|
||||||
|
_remoteDatasource = ProductRemoteDatasource();
|
||||||
|
|
||||||
static ProductRepository get instance {
|
static ProductRepository get instance {
|
||||||
_instance ??= ProductRepository._internal();
|
_instance ??= ProductRepository._internal();
|
||||||
@ -85,6 +88,66 @@ class ProductRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// PRODUCT SYNC OPERATIONS
|
||||||
|
// ========================================
|
||||||
|
Future<Either<String, String>> 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
|
// LOCAL DATABASE OPERATIONS
|
||||||
// ========================================
|
// ========================================
|
||||||
@ -142,4 +205,16 @@ class ProductRepository {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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/core/extensions/build_context_ext.dart';
|
||||||
import 'package:enaklo_pos/presentation/setting/pages/printer_page.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/setting_tile.dart';
|
||||||
|
import 'package:enaklo_pos/presentation/setting/pages/sync_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SettingPage extends StatefulWidget {
|
class SettingPage extends StatefulWidget {
|
||||||
@ -84,6 +85,14 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
icon: Icons.print_outlined,
|
icon: Icons.print_outlined,
|
||||||
onTap: () => indexValue(0),
|
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<SettingPage> {
|
|||||||
index: currentIndex,
|
index: currentIndex,
|
||||||
children: [
|
children: [
|
||||||
SettingPrinterPage(),
|
SettingPrinterPage(),
|
||||||
|
SettingSyncPage(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
580
lib/presentation/setting/pages/sync_page.dart
Normal file
580
lib/presentation/setting/pages/sync_page.dart
Normal file
@ -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<SettingSyncPage> createState() => _SettingSyncPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingSyncPageState extends State<SettingSyncPage> {
|
||||||
|
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<String, dynamic> _productStats = {};
|
||||||
|
Map<String, dynamic> _categoryStats = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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<void> _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<void> _clearAllData() async {
|
||||||
|
// Show confirmation dialog
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
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<void> _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<void> _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<bool> _showClearConfirmation(String dataType) async {
|
||||||
|
return await showDialog<bool>(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user