POS Grosir Dutra Store: update kasir/produk/riwayat/pengaturan + offline queue

This commit is contained in:
onie
2026-10-17 06:38:50 +07:00
parent bec02d196c
commit 0121eca8bb
15 changed files with 1202 additions and 524 deletions

View File

@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'dart:async';
import '../state/app_state.dart';
import '../utils/colors.dart';
@@ -21,23 +24,61 @@ class _PengaturanScreenState extends State<PengaturanScreen> {
final TextEditingController _notesCtrl = TextEditingController();
bool _resellerEnabled = true;
bool _loading = true;
Timer? _settingsSyncTimer;
bool _loadingSettings = false;
@override
void initState() {
super.initState();
_loadSettings();
_settingsSyncTimer = Timer.periodic(const Duration(seconds: 30), (_) {
if (!_loadingSettings && mounted) {
_loadSettings();
}
});
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_storeNameCtrl.text = prefs.getString('store_name') ?? 'Toko Grosir';
_currencyCtrl.text = prefs.getString('currency') ?? 'IDR';
_whatsappCtrl.text = prefs.getString('store_whatsapp') ?? '';
_notesCtrl.text = prefs.getString('store_notes') ?? '';
_resellerEnabled = prefs.getBool('reseller_enabled') ?? true;
_loading = false;
});
if (_loadingSettings) return;
_loadingSettings = true;
try {
final prefs = await SharedPreferences.getInstance();
String storeName = prefs.getString('store_name') ?? 'Toko Grosir';
String currency = prefs.getString('currency') ?? 'IDR';
String whatsapp = prefs.getString('store_whatsapp') ?? '';
String notes = prefs.getString('store_notes') ?? '';
bool resellerEnabled = prefs.getBool('reseller_enabled') ?? true;
try {
final data = await Supabase.instance.client
.from('store_settings')
.select()
.eq('store_id', '00000000-0000-0000-0000-000000000000')
.maybeSingle();
if (data != null) {
storeName = (data['store_name'] as String?) ?? storeName;
currency = (data['currency'] as String?) ?? currency;
whatsapp = data['whatsapp'] as String? ?? whatsapp;
notes = data['notes'] as String? ?? notes;
resellerEnabled = data['reseller_enabled'] as bool? ?? resellerEnabled;
}
} catch (e) {
debugPrint('Gagal load store settings dari Supabase: $e');
}
if (!mounted) return;
setState(() {
_storeNameCtrl.text = storeName;
_currencyCtrl.text = currency;
_whatsappCtrl.text = whatsapp;
_notesCtrl.text = notes;
_resellerEnabled = resellerEnabled;
_loading = false;
});
} finally {
_loadingSettings = false;
}
}
Future<void> _saveSettings() async {
@@ -50,6 +91,21 @@ class _PengaturanScreenState extends State<PengaturanScreen> {
if (!mounted) return;
final state = context.read<AppState>();
state.setResellerEnabled(_resellerEnabled);
try {
await Supabase.instance.client.from('store_settings').upsert({
'id': 'store',
'store_id': '00000000-0000-0000-0000-000000000000',
'store_name': _storeNameCtrl.text.trim(),
'currency': _currencyCtrl.text.trim(),
'whatsapp': _whatsappCtrl.text.trim(),
'notes': _notesCtrl.text.trim(),
'reseller_enabled': _resellerEnabled,
'updated_at': DateTime.now().toUtc().toIso8601String(),
});
} catch (e) {
debugPrint('Gagal simpan store settings ke Supabase: $e');
}
}
@override
@@ -58,38 +114,49 @@ class _PengaturanScreenState extends State<PengaturanScreen> {
return const Center(child: CircularProgressIndicator(color: kAccent));
}
final state = context.watch<AppState>();
final isAdmin = state.isAdmin;
return RefreshIndicator(
onRefresh: () async {
context.read<AppState>().recordManualRefresh();
await context.read<AppState>().refresh();
_settingsSyncTimer?.cancel();
_settingsSyncTimer = Timer.periodic(const Duration(seconds: 30), (_) {
if (!_loadingSettings && mounted) {
_loadSettings();
}
});
},
color: kAccent,
child: ListView(
padding: const EdgeInsets.all(12),
children: [
_SettingsListTile(
icon: Icons.store_outlined,
title: 'Pengaturan Toko',
subtitle: _whatsappCtrl.text.isEmpty
? '${_storeNameCtrl.text} • ${_currencyCtrl.text}'
: '${_storeNameCtrl.text} • ${_currencyCtrl.text} • ${_whatsappCtrl.text}',
onTap: () => _showStoreSettingsDialog(context),
),
const SizedBox(height: 8),
_SettingsListTile(
icon: Icons.people_outline,
title: 'Akun Kasir',
subtitle: 'Tambah, edit, atau hapus akun kasir',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const KelolaKasirScreen(),
),
);
},
),
const SizedBox(height: 8),
if (isAdmin) ...[
_SettingsListTile(
icon: Icons.receipt_long_outlined,
title: 'Pengaturan Struk',
subtitle: _whatsappCtrl.text.isEmpty
? '${_storeNameCtrl.text} • ${_currencyCtrl.text}'
: '${_storeNameCtrl.text} • ${_currencyCtrl.text} • ${_whatsappCtrl.text}',
onTap: () => _showStoreSettingsDialog(context),
),
const SizedBox(height: 8),
_SettingsListTile(
icon: Icons.people_outline,
title: 'Akun Kasir',
subtitle: 'Tambah, edit, atau hapus akun kasir',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const KelolaKasirScreen(),
),
);
},
),
const SizedBox(height: 8),
],
_SettingsListTile(
icon: Icons.print_outlined,
title: 'Printer Bluetooth',
@@ -103,6 +170,35 @@ class _PengaturanScreenState extends State<PengaturanScreen> {
);
},
),
const SizedBox(height: 8),
if (!isAdmin)
_SettingsListTile(
icon: Icons.receipt_long_outlined,
title: 'Info Struk',
subtitle: _whatsappCtrl.text.isEmpty
? '${_storeNameCtrl.text} • ${_currencyCtrl.text}'
: '${_storeNameCtrl.text} • ${_currencyCtrl.text} • ${_whatsappCtrl.text}',
onTap: null,
),
const SizedBox(height: 24),
Center(
child: FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final info = snapshot.data!;
return Text(
'Versi ${info.version} (${info.buildNumber})',
style: const TextStyle(color: kMuted, fontSize: 12),
);
}
return const Text(
'Versi ...',
style: TextStyle(color: kMuted, fontSize: 12),
);
},
),
),
],
),
);
@@ -180,20 +276,27 @@ class _PengaturanScreenState extends State<PengaturanScreen> {
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Batal'),
),
FilledButton.icon(
onPressed: () async {
_storeNameCtrl.text = storeNameCtrl.text.trim();
_currencyCtrl.text = currencyCtrl.text.trim();
_whatsappCtrl.text = whatsappCtrl.text.trim();
_notesCtrl.text = notesCtrl.text.trim();
_resellerEnabled = resellerEnabled;
await _saveSettings();
if (ctx.mounted) {
Navigator.pop(ctx, true);
}
},
icon: const Icon(Icons.save_outlined, size: 18),
label: const Text('Simpan'),
const SizedBox(width: 8),
SizedBox(
width: 120,
child: FilledButton.icon(
onPressed: () async {
_storeNameCtrl.text = storeNameCtrl.text.trim();
_currencyCtrl.text = currencyCtrl.text.trim();
_whatsappCtrl.text = whatsappCtrl.text.trim();
_notesCtrl.text = notesCtrl.text.trim();
_resellerEnabled = resellerEnabled;
await _saveSettings();
if (ctx.mounted) {
Navigator.pop(ctx, true);
}
},
icon: const Icon(Icons.save_outlined, size: 18),
label: const Text('Simpan'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
),
],
),
@@ -212,6 +315,7 @@ class _PengaturanScreenState extends State<PengaturanScreen> {
@override
void dispose() {
_settingsSyncTimer?.cancel();
_storeNameCtrl.dispose();
_currencyCtrl.dispose();
_whatsappCtrl.dispose();
@@ -224,12 +328,12 @@ class _SettingsListTile extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final VoidCallback onTap;
final VoidCallback? onTap;
const _SettingsListTile({
required this.icon,
required this.title,
required this.subtitle,
required this.onTap,
this.onTap,
});
@override