POS Grosir Dutra Store: update kasir/produk/riwayat/pengaturan + offline queue
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user