added product import and billing integration
This commit is contained in:
197
lib/presentation/pos/widgets/admin_pin_dialog.dart
Normal file
197
lib/presentation/pos/widgets/admin_pin_dialog.dart
Normal file
@@ -0,0 +1,197 @@
|
||||
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',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user