pos changes

This commit is contained in:
2026-08-06 19:26:53 +05:30
parent cb065a0f69
commit eebd10da6d
42 changed files with 3451 additions and 2531 deletions

View File

@@ -9,6 +9,7 @@ import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/utils/formatters.dart';
import '../../../data/local/order_dao.dart';
import '../../../data/local/void_pin_store.dart';
import '../../../domain/entities/store_account.dart';
import '../../auth/providers/auth_controller.dart';
import '../providers/printer_settings.dart';
@@ -17,6 +18,7 @@ import '../widgets/staff_dialogs.dart';
import '../widgets/store_details_dialog.dart';
import '../../sync/providers/sync_controller.dart';
import '../widgets/module_widgets.dart';
import '../../../core/widgets/numeric_keypad.dart';
/// Terminal and store configuration.
class SettingsView extends ConsumerStatefulWidget {
@@ -32,6 +34,10 @@ class _SettingsViewState extends ConsumerState<SettingsView> {
bool _testingDrawer = false;
bool _loadedDrawerFields = false;
/// Null until the first read comes back from the meta table.
bool? _hasRemovalPin;
bool _loadedRemovalPin = false;
bool _scannerSound = true;
bool _roundOff = true;
bool _autoLoyalty = true;
@@ -50,6 +56,16 @@ class _SettingsViewState extends ConsumerState<SettingsView> {
// Seeded once, from whatever was persisted. Assigning on every build would
// fight the cashier for the cursor while they type.
if (!_loadedRemovalPin) {
_loadedRemovalPin = true;
final store = ref.read(localStoreProvider);
if (store.isReady) {
store.voidPin.isConfigured.then((has) {
if (mounted) setState(() => _hasRemovalPin = has);
});
}
}
final printer = ref.watch(printerSettingsProvider);
if (!_loadedDrawerFields && printer.hasDrawer) {
_loadedDrawerFields = true;
@@ -80,6 +96,8 @@ class _SettingsViewState extends ConsumerState<SettingsView> {
const SizedBox(height: AppSpacing.lg),
_staffCard(store, user),
const SizedBox(height: AppSpacing.lg),
_removalPinCard(user),
const SizedBox(height: AppSpacing.lg),
_aboutCard(),
],
);
@@ -430,6 +448,94 @@ class _SettingsViewState extends ConsumerState<SettingsView> {
),
);
// ------------------------------------------------------- Removal PIN
/// The PIN a cashier types to take a rung item back off a bill.
///
/// Admin-only, and deliberately not a staff PIN. A staff PIN identifies the
/// person a bill is stamped with; handing one out so the counter can void a
/// line would put the whole shift under the wrong name. This is a shared
/// secret for one specific action, and the admin's own staff PIN keeps
/// working whether or not it is set.
Widget _removalPinCard(StaffUser? user) {
final isAdmin = user?.role == StaffRole.admin;
return PanelCard(
title: 'Item removal PIN',
subtitle: 'Asked for when an item is taken off a bill',
action: isAdmin
? TextButton(
onPressed: () => _setRemovalPin(),
child: Text(_hasRemovalPin == true ? 'Change' : 'Set PIN'),
)
: null,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_row(
'Status',
_hasRemovalPin == null
? 'Checking…'
: (_hasRemovalPin! ? 'Set' : 'Not set'),
),
const SizedBox(height: AppSpacing.sm),
Text(
_hasRemovalPin == true
? 'A cashier can remove an item using this PIN. An admin PIN '
'still works too.'
: 'No PIN is set, so a removal currently needs an admin PIN. '
'Set one and a cashier can void a line without an admin '
'walking over.',
style: const TextStyle(
fontSize: 12.5,
color: AppColors.textSecondary,
height: 1.5,
),
),
if (!isAdmin) ...[
const SizedBox(height: AppSpacing.sm),
const Text(
'Only an admin can change it.',
style: TextStyle(fontSize: 12, color: AppColors.textTertiary),
),
],
],
),
);
}
Future<void> _setRemovalPin() async {
final pin = await showDialog<String>(
context: context,
builder: (_) => const _RemovalPinDialog(),
);
if (pin == null) return;
final store = ref.read(localStoreProvider);
try {
if (pin.isEmpty) {
await store.voidPin.clearPin();
} else {
await store.voidPin.setPin(pin);
}
if (!mounted) return;
setState(() => _hasRemovalPin = pin.isNotEmpty);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(
content: Text(pin.isEmpty
? 'Removal PIN cleared. Removals now need an admin PIN.'
: 'Removal PIN saved.',),
),);
} on VoidPinException catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(e.message)));
}
}
Widget _connectivityCard() {
final ready = ref.watch(catalogueReadyProvider);
final lastImport = ref.watch(lastImportAtProvider);
@@ -609,3 +715,96 @@ class _SettingsViewState extends ConsumerState<SettingsView> {
),
);
}
/// Four-to-eight digits, keyed on the same pad the till uses everywhere else.
class _RemovalPinDialog extends StatefulWidget {
const _RemovalPinDialog();
@override
State<_RemovalPinDialog> createState() => _RemovalPinDialogState();
}
class _RemovalPinDialogState extends State<_RemovalPinDialog> {
String _pin = '';
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Set removal PIN'),
content: SizedBox(
width: (MediaQuery.sizeOf(context).width - 96).clamp(260.0, 340.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Four digits or more. Give it to whoever is on the counter — it '
'authorises removing an item from a bill and nothing else.',
style: TextStyle(
fontSize: 12.5,
color: AppColors.textSecondary,
height: 1.5,
),
),
const SizedBox(height: AppSpacing.lg),
SizedBox(
height: 22,
child: Center(
child: _pin.isEmpty
? const Text(
'Enter PIN',
style: TextStyle(
fontSize: 13,
color: AppColors.textTertiary,
),
)
: Wrap(
spacing: 10,
children: [
for (var i = 0; i < _pin.length; i++)
Container(
width: 12,
height: 12,
decoration: const BoxDecoration(
color: AppColors.primary,
shape: BoxShape.circle,
),
),
],
),
),
),
const SizedBox(height: AppSpacing.lg),
NumericKeypad(
onKey: (d) {
if (_pin.length >= 8) return;
setState(() => _pin += d);
},
onBackspace: () {
if (_pin.isEmpty) return;
setState(() => _pin = _pin.substring(0, _pin.length - 1));
},
onClear: () => setState(() => _pin = ''),
onSubmit: _pin.length >= 4
? () => Navigator.of(context).pop(_pin)
: null,
submitLabel: 'Save PIN',
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(''),
style: TextButton.styleFrom(foregroundColor: AppColors.danger),
child: const Text('Remove PIN'),
),
],
);
}
}