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 showSessionEndSheet(BuildContext context, WidgetRef ref) async { if (!ref.read(isCashierModeProvider)) { return showSignOutDialog(context, ref); } return showDialog( 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 _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,), ], ), ), ), ); } }