pos changes
This commit is contained in:
283
lib/presentation/shift/widgets/session_end_sheet.dart
Normal file
283
lib/presentation/shift/widgets/session_end_sheet.dart
Normal file
@@ -0,0 +1,283 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../core/router/app_router.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_dimens.dart';
|
||||
import '../../auth/providers/auth_controller.dart';
|
||||
import '../../pos/providers/cart_controller.dart';
|
||||
import '../../pos/providers/catalog_providers.dart';
|
||||
import '../../pos/providers/navigation_provider.dart';
|
||||
import '../../sync/providers/sync_controller.dart';
|
||||
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.
|
||||
///
|
||||
/// 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.
|
||||
Future<void> showSessionEndSheet(BuildContext context, WidgetRef ref) async {
|
||||
if (!ref.read(isCashierModeProvider)) {
|
||||
return showSignOutDialog(context, ref);
|
||||
}
|
||||
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (_) => const _SessionEndDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
class _SessionEndDialog extends ConsumerWidget {
|
||||
const _SessionEndDialog();
|
||||
|
||||
/// Closes the session without closing the shift.
|
||||
///
|
||||
/// 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.
|
||||
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);
|
||||
ref.invalidate(categoryCountsProvider);
|
||||
ref.invalidate(lowStockProductsProvider);
|
||||
|
||||
ref.read(activeModuleProvider.notifier).state = PosModule.pos;
|
||||
ref.read(searchQueryProvider.notifier).state = '';
|
||||
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
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pending = ref.watch(unsyncedCountProvider).value ?? 0;
|
||||
final cart = ref.watch(cartControllerProvider);
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('Leaving the till'),
|
||||
contentPadding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.xxl,
|
||||
AppSpacing.lg,
|
||||
AppSpacing.xxl,
|
||||
AppSpacing.sm,
|
||||
),
|
||||
content: SizedBox(
|
||||
width: (MediaQuery.sizeOf(context).width - 96).clamp(280.0, 460.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (cart.isNotEmpty)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: AppSpacing.md),
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.warningSurface,
|
||||
borderRadius: AppRadius.brSm,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded,
|
||||
size: 18, color: AppColors.warning,),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'The current bill has ${cart.lineCount} item(s) and '
|
||||
'will be cleared either way.',
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: AppColors.warning,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 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,
|
||||
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.',
|
||||
onTap: () => _temporaryLogout(context, ref),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_Choice(
|
||||
icon: Icons.point_of_sale_rounded,
|
||||
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.',
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
context.push(AppRoutes.endShift);
|
||||
},
|
||||
emphasised: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.xxl,
|
||||
0,
|
||||
AppSpacing.xxl,
|
||||
AppSpacing.lg,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Stay signed in'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Choice extends StatelessWidget {
|
||||
const _Choice({
|
||||
required this.icon,
|
||||
required this.tone,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.onTap,
|
||||
this.emphasised = false,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final Color tone;
|
||||
final String title;
|
||||
final String body;
|
||||
final VoidCallback onTap;
|
||||
final bool emphasised;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: emphasised ? AppColors.primarySurface : AppColors.surfaceAlt,
|
||||
borderRadius: AppRadius.brLg,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: AppRadius.brLg,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: AppRadius.brLg,
|
||||
border: Border.all(
|
||||
color: emphasised ? AppColors.primaryBorder : AppColors.border,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 22, color: tone),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
body,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: AppColors.textSecondary,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
const Icon(Icons.chevron_right_rounded,
|
||||
size: 20, color: AppColors.textTertiary,),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user