198 lines
6.0 KiB
Dart
198 lines
6.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../app/providers.dart';
|
|
import '../../../core/theme/app_colors.dart';
|
|
import '../../../core/theme/app_dimens.dart';
|
|
import '../../../core/widgets/numeric_keypad.dart';
|
|
import '../../../domain/entities/store_account.dart';
|
|
|
|
/// Prompts for an admin PIN before a theft-sensitive cart action — taking a
|
|
/// scanned item back out of the bill, or clearing it — and resolves `true`
|
|
/// only once a PIN belonging to an [StaffRole.admin] account is verified.
|
|
///
|
|
/// A cashier can always start a brand new sale; what this exists to stop is
|
|
/// quietly taking something back out of a bill a customer has already been
|
|
/// shown, after it was rung up.
|
|
Future<bool> requireAdminPin(
|
|
BuildContext context,
|
|
WidgetRef ref, {
|
|
required String reason,
|
|
}) async {
|
|
final ok = await showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (_) => _AdminPinDialog(reason: reason),
|
|
);
|
|
return ok ?? false;
|
|
}
|
|
|
|
class _AdminPinDialog extends ConsumerStatefulWidget {
|
|
const _AdminPinDialog({required this.reason});
|
|
|
|
final String reason;
|
|
|
|
@override
|
|
ConsumerState<_AdminPinDialog> createState() => _AdminPinDialogState();
|
|
}
|
|
|
|
class _AdminPinDialogState extends ConsumerState<_AdminPinDialog> {
|
|
String _pin = '';
|
|
String? _error;
|
|
bool _checking = false;
|
|
|
|
void _key(String digit) {
|
|
if (_checking || _pin.length >= 8) return;
|
|
setState(() {
|
|
_pin += digit;
|
|
_error = null;
|
|
});
|
|
}
|
|
|
|
void _backspace() {
|
|
if (_checking || _pin.isEmpty) return;
|
|
setState(() => _pin = _pin.substring(0, _pin.length - 1));
|
|
}
|
|
|
|
void _clear() {
|
|
if (_checking) return;
|
|
setState(() => _pin = '');
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
if (_pin.isEmpty || _checking) return;
|
|
setState(() {
|
|
_checking = true;
|
|
_error = null;
|
|
});
|
|
|
|
final store = ref.read(localStoreProvider);
|
|
final user = await store.staff.authenticate(_pin);
|
|
|
|
if (!mounted) return;
|
|
|
|
if (user == null) {
|
|
setState(() {
|
|
_checking = false;
|
|
_error = 'Incorrect PIN.';
|
|
_pin = '';
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (user.role != StaffRole.admin) {
|
|
setState(() {
|
|
_checking = false;
|
|
_error = "${user.name}'s PIN is ${user.role.label.toLowerCase()} — "
|
|
'this needs an admin.';
|
|
_pin = '';
|
|
});
|
|
return;
|
|
}
|
|
|
|
Navigator.of(context).pop(true);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Dialog(
|
|
shape: const RoundedRectangleBorder(borderRadius: AppRadius.brLg),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(AppSpacing.xl),
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 320),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.lock_outline_rounded,
|
|
color: AppColors.danger, size: 20,),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
const Expanded(
|
|
child: Text(
|
|
'Admin PIN required',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
InkWell(
|
|
onTap: () => Navigator.of(context).pop(false),
|
|
borderRadius: AppRadius.brSm,
|
|
child: const Padding(
|
|
padding: EdgeInsets.all(4),
|
|
child: Icon(Icons.close_rounded,
|
|
size: 20, color: AppColors.textSecondary,),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: AppSpacing.xs),
|
|
Text(
|
|
widget.reason,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: AppColors.textSecondary,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
SizedBox(
|
|
height: 20,
|
|
child: _pin.isEmpty
|
|
? const Center(
|
|
child: Text(
|
|
'Enter PIN',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: AppColors.textTertiary,
|
|
),
|
|
),
|
|
)
|
|
: Wrap(
|
|
alignment: WrapAlignment.center,
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (_error != null) ...[
|
|
const SizedBox(height: AppSpacing.sm),
|
|
Text(
|
|
_error!,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 12.5,
|
|
color: AppColors.danger,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: AppSpacing.lg),
|
|
NumericKeypad(
|
|
onKey: _key,
|
|
onBackspace: _backspace,
|
|
onClear: _clear,
|
|
onSubmit: _checking ? null : _submit,
|
|
submitLabel: _checking ? 'Checking…' : 'Approve',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|