Sync Page

This commit is contained in:
efrilm
2025-11-10 16:56:12 +07:00
parent f5256fb33d
commit 0a8f8d93bb
9 changed files with 2195 additions and 1 deletions
+2
View File
@@ -15,6 +15,7 @@ import '../application/printer/print_struck/print_struck_bloc.dart';
import '../application/printer/printer_form/printer_form_bloc.dart';
import '../application/printer/printer_loader/printer_loader_bloc.dart';
import '../application/product/product_loader/product_loader_bloc.dart';
import '../application/sync/sync_setting/sync_setting_bloc.dart';
import '../application/table/table_form/table_form_bloc.dart';
import '../application/table/table_loader/table_loader_bloc.dart';
import '../application/void/void_form/void_form_bloc.dart';
@@ -55,6 +56,7 @@ class _AppWidgetState extends State<AppWidget> {
BlocProvider(create: (context) => getIt<PrinterFormBloc>()),
BlocProvider(create: (context) => getIt<PrinterLoaderBloc>()),
BlocProvider(create: (context) => getIt<PrintStruckBloc>()),
BlocProvider(create: (context) => getIt<SyncSettingBloc>()),
],
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
@@ -0,0 +1,511 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../../application/sync/sync_setting/sync_setting_bloc.dart';
import '../../../../../../common/theme/theme.dart';
import '../../../../../../injection.dart';
import '../../../../../components/button/button.dart';
import '../../../../../components/loader/loader_with_text.dart';
import '../../../../../components/page/page_title.dart';
import '../../../../../components/spaces/space.dart';
import '../../../../../components/toast/flushbar.dart';
class SettingSyncSection extends StatelessWidget {
const SettingSyncSection({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
getIt<SyncSettingBloc>()..add(const SyncSettingEvent.loadStats()),
child: const _SettingSyncPageContent(),
);
}
}
class _SettingSyncPageContent extends StatelessWidget {
const _SettingSyncPageContent();
@override
Widget build(BuildContext context) {
return BlocConsumer<SyncSettingBloc, SyncSettingState>(
listener: (context, state) {
// Handle product stats failure
state.failureOptionProduct.fold(() {}, (failure) {
AppFlushbar.showError(
context,
'Gagal memuat stats produk: ${failure.toString()}',
);
});
// Handle category stats failure
state.failureOptionCategory.fold(() {}, (failure) {
AppFlushbar.showError(
context,
'Gagal memuat stats kategori: ${failure.toString()}',
);
});
// Handle sync product result
state.failureOptionSyncProduct.fold(() {}, (result) {
result.fold(
(failure) {
AppFlushbar.showError(
context,
'Gagal sync produk: ${failure.toString()}',
);
},
(success) {
AppFlushbar.showSuccess(context, success);
},
);
});
// Handle sync category result
state.failureOptionSyncCategory.fold(() {}, (result) {
result.fold(
(failure) {
AppFlushbar.showError(
context,
'Gagal sync kategori: ${failure.toString()}',
);
},
(success) {
AppFlushbar.showSuccess(context, success);
},
);
});
},
builder: (context, state) {
return Column(
children: [
PageTitle(
title: 'Sinkronisasi',
subtitle: 'Sinkronisasi data dengan server',
isBack: false,
),
Expanded(
child: Material(
color: AppColor.background,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
SpaceHeight(24),
_buildQuickActions(context, state),
SpaceHeight(24),
_buildSyncTables(context, state),
SpaceHeight(24),
_buildDatabaseStats(context, state),
],
),
),
),
),
],
);
},
);
}
Widget _buildQuickActions(BuildContext context, SyncSettingState state) {
return Container(
padding: const 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: AppStyle.lg.copyWith(fontWeight: FontWeight.w600),
),
SpaceHeight(16),
Row(
children: [
Expanded(
child: AppElevatedButton.filled(
onPressed: state.isSyncing || state.isLoading
? null
: () => _syncAllData(context),
label: state.isSyncing
? 'Menyinkronkan...'
: 'Sync Semua Data',
),
),
SpaceWidth(12),
Expanded(
child: AppElevatedButton.outlined(
onPressed: state.isLoading || state.isSyncing
? null
: () => _clearAllData(context),
label: 'Hapus Semua Data',
textColor: Colors.red,
),
),
],
),
],
),
);
}
Widget _buildSyncTables(BuildContext context, SyncSettingState state) {
return Container(
padding: const 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: AppStyle.lg.copyWith(fontWeight: FontWeight.w600),
),
SpaceHeight(16),
_buildSyncTableItem(
context: context,
state: state,
title: 'Kategori',
subtitle: 'Sinkronkan data kategori produk',
icon: Icons.category,
color: Colors.blue,
count: state.categoryStats['total_categories'] ?? 0,
onSync: () {
context.read<SyncSettingBloc>().add(
const SyncSettingEvent.syncCategories(),
);
},
onClear: () => _clearCategories(context),
),
SpaceHeight(12),
const Divider(),
SpaceHeight(12),
_buildSyncTableItem(
context: context,
state: state,
title: 'Produk',
subtitle: 'Sinkronkan data produk dan variant',
icon: Icons.inventory_2,
color: Colors.green,
count: state.productStats['total_products'] ?? 0,
onSync: () {
context.read<SyncSettingBloc>().add(
const SyncSettingEvent.syncProducts(),
);
},
onClear: () => _clearProducts(context),
),
],
),
);
}
Widget _buildSyncTableItem({
required BuildContext context,
required SyncSettingState state,
required String title,
required String subtitle,
required IconData icon,
required Color color,
required int count,
required VoidCallback onSync,
required VoidCallback onClear,
}) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 24),
),
SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
title,
style: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
),
SpaceWidth(8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'$count',
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: color,
),
),
),
],
),
Text(
subtitle,
style: AppStyle.sm.copyWith(color: Colors.grey.shade600),
),
],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: state.isLoading || state.isSyncing ? null : onSync,
icon: const Icon(Icons.sync, size: 20),
tooltip: 'Sync $title',
style: IconButton.styleFrom(
backgroundColor: color.withOpacity(0.1),
foregroundColor: color,
),
),
SpaceWidth(4),
IconButton(
onPressed: state.isLoading || state.isSyncing ? null : onClear,
icon: const Icon(Icons.delete_outline, size: 20),
tooltip: 'Hapus $title',
style: IconButton.styleFrom(
backgroundColor: Colors.red.withOpacity(0.1),
foregroundColor: Colors.red,
),
),
],
),
],
);
}
Widget _buildDatabaseStats(BuildContext context, SyncSettingState state) {
final totalCacheEntries =
(state.productStats['cache_entries'] ?? 0) +
(state.categoryStats['cache_entries'] ?? 0);
final totalDatabaseSize =
(state.productStats['database_size_mb'] ?? 0.0) +
(state.categoryStats['database_size_mb'] ?? 0.0);
return Container(
padding: const 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: AppStyle.lg.copyWith(fontWeight: FontWeight.w600),
),
const Spacer(),
if (state.isLoading)
Center(child: LoaderWithText())
else
IconButton(
onPressed: () {
context.read<SyncSettingBloc>().add(
const SyncSettingEvent.loadStats(),
);
},
icon: const Icon(Icons.refresh, size: 20),
tooltip: 'Refresh Stats',
),
],
),
SpaceHeight(16),
if (state.isLoading)
const Center(child: LoaderWithText())
else
Column(
children: [
_buildStatRow(
'Kategori',
state.categoryStats['total_categories']?.toString() ?? '0',
Icons.category,
Colors.blue,
),
SpaceHeight(8),
_buildStatRow(
'Produk',
state.productStats['total_products']?.toString() ?? '0',
Icons.inventory_2,
Colors.green,
),
SpaceHeight(8),
_buildStatRow(
'Variant',
state.productStats['total_variants']?.toString() ?? '0',
Icons.tune,
Colors.orange,
),
SpaceHeight(8),
_buildStatRow(
'Cache Entries',
'$totalCacheEntries',
Icons.memory,
Colors.purple,
),
SpaceHeight(8),
_buildStatRow(
'Ukuran Database',
'${totalDatabaseSize.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),
SpaceWidth(8),
Expanded(
child: Text(
label,
style: AppStyle.md.copyWith(color: Colors.grey.shade700),
),
),
Text(
value,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: color,
),
),
],
);
}
Future<void> _syncAllData(BuildContext context) async {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SpaceHeight(16),
Text('Sinkronisasi semua data...'),
],
),
),
);
context.read<SyncSettingBloc>().add(const SyncSettingEvent.syncAllData());
// Wait a bit for the sync to complete, then close dialog
await Future.delayed(const Duration(milliseconds: 500));
if (context.mounted) {
Navigator.of(context).pop();
}
}
Future<void> _clearAllData(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Hapus Semua Data'),
content: const Text(
'Apakah Anda yakin ingin menghapus semua data lokal? Tindakan ini tidak dapat dibatalkan.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Batal'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(
'Hapus',
style: AppStyle.md.copyWith(color: Colors.red),
),
),
],
),
);
if (confirmed == true && context.mounted) {
context.read<SyncSettingBloc>().add(
const SyncSettingEvent.clearAllData(),
);
}
}
Future<void> _clearCategories(BuildContext context) async {
final confirmed = await _showClearConfirmation(context, 'kategori');
if (confirmed && context.mounted) {
context.read<SyncSettingBloc>().add(
const SyncSettingEvent.clearCategories(),
);
AppFlushbar.showSuccess(context, 'Data kategori berhasil dihapus');
context.read<SyncSettingBloc>().add(const SyncSettingEvent.loadStats());
}
}
Future<void> _clearProducts(BuildContext context) async {
final confirmed = await _showClearConfirmation(context, 'produk');
if (confirmed && context.mounted) {
context.read<SyncSettingBloc>().add(
const SyncSettingEvent.clearProducts(),
);
AppFlushbar.showSuccess(context, 'Data produk berhasil dihapus');
context.read<SyncSettingBloc>().add(const SyncSettingEvent.loadStats());
}
}
Future<bool> _showClearConfirmation(
BuildContext context,
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: const Text('Batal'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(
'Hapus',
style: AppStyle.md.copyWith(color: Colors.red),
),
),
],
),
) ??
false;
}
}
@@ -6,6 +6,7 @@ import '../../../../../application/printer/printer_bloc.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../../injection.dart';
import 'sections/setting_printer_section.dart';
import 'sections/setting_sync_section.dart';
import 'widgets/setting_left_panel.dart';
@RoutePage()
@@ -25,7 +26,7 @@ class SettingPage extends StatelessWidget implements AutoRouteWrapper {
flex: 4,
child: switch (state.index) {
0 => SettingPrinterSection(),
1 => Container(),
1 => SettingSyncSection(),
_ => Container(),
},
),