check
This commit is contained in:
@@ -199,20 +199,49 @@ class _EndShiftScreenState extends ConsumerState<EndShiftScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: _bottomBar(pending, variance),
|
||||
bottomNavigationBar: _bottomBar(pending, expected, variance),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _bottomBar(int pending, double variance) {
|
||||
/// Whether the drawer has been counted and agrees with what was rung.
|
||||
///
|
||||
/// Signing out is gated on this. A shift that ends with the cash unaccounted
|
||||
/// for is a shift nobody can settle afterwards — the person who worked it has
|
||||
/// gone home, and the difference becomes an argument rather than a number. To
|
||||
/// the rupee, because that is the smallest note anyone hands over.
|
||||
bool _balances(double expected) => (_countedTotal - expected).abs() < 0.5;
|
||||
|
||||
Widget _bottomBar(int pending, double expected, double variance) {
|
||||
final counted = _noteCount > 0;
|
||||
final short = variance < -0.5;
|
||||
final over = variance > 0.5;
|
||||
final balanced = _balances(expected);
|
||||
|
||||
// The gate. A shift closes on a drawer that reconciles and on nothing
|
||||
// else — an uncounted or mismatched till is settled at the counter, with
|
||||
// the person who worked it still there, not discovered by the back office
|
||||
// the next morning.
|
||||
final balanced = counted && !short && !over;
|
||||
final (icon, tone, message) = switch (0) {
|
||||
_ when !counted => (
|
||||
Icons.info_outline_rounded,
|
||||
AppColors.textTertiary,
|
||||
'Count the drawer to finish. Signing out needs the count to match '
|
||||
'${Formatters.money(expected)}.',
|
||||
),
|
||||
_ when short => (
|
||||
Icons.error_outline_rounded,
|
||||
AppColors.danger,
|
||||
'The drawer is ${Formatters.money(variance.abs())} short. Recount, '
|
||||
'or find the difference before signing out.',
|
||||
),
|
||||
_ when over => (
|
||||
Icons.error_outline_rounded,
|
||||
AppColors.warning,
|
||||
'The drawer is ${Formatters.money(variance)} over. Recount, or find '
|
||||
'the difference before signing out.',
|
||||
),
|
||||
_ => (
|
||||
Icons.check_circle_outline_rounded,
|
||||
AppColors.success,
|
||||
'The drawer matches what was rung. You can sign out.',
|
||||
),
|
||||
};
|
||||
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
@@ -231,40 +260,15 @@ class _EndShiftScreenState extends ConsumerState<EndShiftScreen> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
counted
|
||||
? (short || over
|
||||
? Icons.error_outline_rounded
|
||||
: Icons.check_circle_outline_rounded)
|
||||
: Icons.info_outline_rounded,
|
||||
size: 16,
|
||||
color: !counted
|
||||
? AppColors.textTertiary
|
||||
: (short
|
||||
? AppColors.danger
|
||||
: (over ? AppColors.warning : AppColors.success)),
|
||||
),
|
||||
Icon(icon, size: 16, color: tone),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
!counted
|
||||
? 'Count the drawer. The shift cannot be ended until '
|
||||
'it matches the expected amount.'
|
||||
: short
|
||||
? 'The drawer is '
|
||||
'${Formatters.money(variance.abs())} short. '
|
||||
'Recount, or settle the difference — the '
|
||||
'shift cannot be ended while it is off.'
|
||||
: over
|
||||
? 'The drawer is '
|
||||
'${Formatters.money(variance)} over. '
|
||||
'Recount — the shift cannot be ended '
|
||||
'while it is off.'
|
||||
: 'The drawer matches what was rung. Ready to '
|
||||
'end the shift.',
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: AppColors.textSecondary,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -272,25 +276,23 @@ class _EndShiftScreenState extends ConsumerState<EndShiftScreen> {
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
PrimaryButton(
|
||||
label: !balanced
|
||||
? 'Drawer must match to end shift'
|
||||
: pending > 0
|
||||
? 'Upload $pending bill(s) & end shift'
|
||||
: 'End shift',
|
||||
icon: balanced ? Icons.logout_rounded : Icons.lock_outline_rounded,
|
||||
label: pending > 0
|
||||
? 'Upload $pending bill(s) & end shift'
|
||||
: 'End shift',
|
||||
icon: Icons.logout_rounded,
|
||||
large: true,
|
||||
busy: _pushing,
|
||||
// Disabled until the count agrees. There is deliberately no way
|
||||
// past this on the screen: an override that a tired cashier can
|
||||
// press at the end of a long day is not a control.
|
||||
onPressed: (_pushing || !balanced)
|
||||
? null
|
||||
: () => _finish(sync: pending > 0),
|
||||
),
|
||||
if (pending > 0) ...[
|
||||
if (pending > 0 && balanced) ...[
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
TextButton(
|
||||
// Skipping the upload is still allowed; skipping the count is
|
||||
// not, so this is gated on the same condition.
|
||||
onPressed:
|
||||
(_pushing || !balanced) ? null : () => _finish(sync: false),
|
||||
onPressed: _pushing ? null : () => _finish(sync: false),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.textSecondary,
|
||||
),
|
||||
@@ -659,14 +661,10 @@ class _ReviewPanel extends StatelessWidget {
|
||||
'when you end the shift.',
|
||||
),
|
||||
_checkRow(
|
||||
!short && !over && counted > 0,
|
||||
counted == 0
|
||||
? 'Drawer not counted yet — required before the shift can '
|
||||
'be ended.'
|
||||
: (short || over)
|
||||
? 'Drawer does not match the expected amount. The '
|
||||
'shift stays open until it does.'
|
||||
: 'Drawer matches the expected amount.',
|
||||
counted > 0,
|
||||
counted > 0
|
||||
? 'Drawer counted.'
|
||||
: 'Drawer not counted yet.',
|
||||
),
|
||||
_checkRow(
|
||||
false,
|
||||
|
||||
@@ -14,17 +14,16 @@ import '../../sync/widgets/sign_out_dialog.dart';
|
||||
|
||||
/// Asks what "signing out" means before doing it.
|
||||
///
|
||||
/// A cashier stepping off the counter for a few minutes and a cashier
|
||||
/// finishing for the day want different things from the *shift*, not from the
|
||||
/// terminal. A temporary logout leaves the shift open — bills stay queued and
|
||||
/// today's totals keep accumulating — and simply locks the screen. Ending the
|
||||
/// shift means counting the drawer, reconciling it, and handing the till back.
|
||||
/// Both cashier paths clear the products — the till never keeps a catalogue
|
||||
/// across a sign-out, whatever the reason. What they differ on is the drawer:
|
||||
/// a temporary logout locks the screen and leaves the money alone, while
|
||||
/// ending the shift counts it and reconciles it against what was rung.
|
||||
/// Treating both as one button meant either the drawer was never settled, or a
|
||||
/// cashier stepping away for ten minutes had to count it first.
|
||||
///
|
||||
/// What both do identically: the products come off this terminal. A cashier
|
||||
/// session never leaves a catalogue sitting on an unattended screen, so an
|
||||
/// admin re-imports it before the counter is worked again.
|
||||
///
|
||||
/// Admins see the plain sign-out dialog: they have no drawer to settle.
|
||||
/// Admins see the plain sign-out dialog: they have no drawer to settle, and
|
||||
/// their sign-out deliberately leaves the catalogue in place for whoever picks
|
||||
/// the terminal up.
|
||||
Future<void> showSessionEndSheet(BuildContext context, WidgetRef ref) async {
|
||||
if (!ref.read(isCashierModeProvider)) {
|
||||
return showSignOutDialog(context, ref);
|
||||
@@ -40,20 +39,17 @@ Future<void> showSessionEndSheet(BuildContext context, WidgetRef ref) async {
|
||||
class _SessionEndDialog extends ConsumerWidget {
|
||||
const _SessionEndDialog();
|
||||
|
||||
/// Closes the session without closing the shift.
|
||||
/// Locks the screen and returns to the login screen.
|
||||
///
|
||||
/// Explicitly *not* a shift end: unsynced bills stay queued and today's
|
||||
/// totals keep accumulating against the same day, so the drawer is still
|
||||
/// settled once, at the end. What it does not leave behind is the
|
||||
/// catalogue — every cashier sign-out takes the products with it, this one
|
||||
/// included, so an admin imports them again before billing resumes.
|
||||
/// Explicitly *not* a shift end — the drawer is left alone, unsynced bills
|
||||
/// stay queued, and today's totals keep accumulating against the same day.
|
||||
/// The catalogue still goes: a terminal sitting unattended at a login screen
|
||||
/// must not be holding a shop's prices and stock, and an admin re-imports in
|
||||
/// seconds.
|
||||
Future<void> _temporaryLogout(BuildContext context, WidgetRef ref) async {
|
||||
ref.read(cartControllerProvider.notifier).reset();
|
||||
await ref.read(authControllerProvider.notifier).signOut();
|
||||
|
||||
// Bump the version so catalogueReadyProvider re-reads hasCatalogue, and
|
||||
// drop the cached lists so the next session's grid does not flash this
|
||||
// session's products before it re-checks what is on disk.
|
||||
ref.read(catalogueVersionProvider.notifier).state++;
|
||||
ref.invalidate(allProductsProvider);
|
||||
ref.invalidate(visibleProductsProvider);
|
||||
@@ -65,22 +61,8 @@ class _SessionEndDialog extends ConsumerWidget {
|
||||
ref.read(selectedCategoryProvider.notifier).state = null;
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Resolved while this context is still mounted — the messenger lives above
|
||||
// the router, so the bar survives the route change below.
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
|
||||
Navigator.of(context).pop();
|
||||
context.go(AppRoutes.login);
|
||||
|
||||
messenger
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'Logged out. The shift is still open, and the product catalogue has '
|
||||
'been removed from this terminal.',
|
||||
),
|
||||
),);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -119,8 +101,8 @@ class _SessionEndDialog extends ConsumerWidget {
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'The current bill has ${cart.lineCount} item(s) and '
|
||||
'will be cleared either way.',
|
||||
'The current bill has ${cart.lineCount} item(s), and '
|
||||
'the imported products are cleared either way.',
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: AppColors.warning,
|
||||
@@ -132,44 +114,13 @@ class _SessionEndDialog extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// The one thing both choices do, said once here rather than
|
||||
// repeated in each card and discovered at the next login.
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: AppSpacing.md),
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.infoSurface,
|
||||
borderRadius: AppRadius.brSm,
|
||||
),
|
||||
child: const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.delete_sweep_outlined,
|
||||
size: 18, color: AppColors.info,),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Either way, the product catalogue is removed from '
|
||||
'this terminal. An admin imports it again before the '
|
||||
'counter is worked.',
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: AppColors.info,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
_Choice(
|
||||
icon: Icons.lock_clock_outlined,
|
||||
icon: Icons.lock_outline_rounded,
|
||||
tone: AppColors.info,
|
||||
title: 'Temporary logout',
|
||||
body: 'Locks the terminal without closing the shift. Bills '
|
||||
'stay queued and today’s totals keep running, so the '
|
||||
'drawer is still counted once at the end.',
|
||||
body: 'Locks the screen. The drawer is left as it is and the '
|
||||
'day keeps running — an admin re-imports the products when '
|
||||
'you come back.',
|
||||
onTap: () => _temporaryLogout(context, ref),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
@@ -178,10 +129,11 @@ class _SessionEndDialog extends ConsumerWidget {
|
||||
tone: AppColors.primary,
|
||||
title: 'End shift',
|
||||
body: pending == 0
|
||||
? 'Count the drawer, check it against what was rung, then '
|
||||
'hand the till over.'
|
||||
: 'Count the drawer, check it against what was rung, then '
|
||||
'upload the $pending bill(s) still held here.',
|
||||
? 'Count the drawer. It has to match what was rung before '
|
||||
'the till can be handed over.'
|
||||
: 'Count the drawer and upload the $pending bill(s) still '
|
||||
'held here. The count has to match before you can sign '
|
||||
'out.',
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
context.push(AppRoutes.endShift);
|
||||
|
||||
Reference in New Issue
Block a user