This commit is contained in:
2026-08-07 15:31:20 +05:30
parent 09e5e29df2
commit ad44402232
17 changed files with 598 additions and 1187 deletions

View File

@@ -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,