Sync Page
This commit is contained in:
@@ -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(),
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user