pos changes
This commit is contained in:
@@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user