Files
nearle_pos/lib/presentation/shift/widgets/session_end_sheet.dart
2026-08-07 15:31:20 +05:30

236 lines
8.3 KiB
Dart

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.
///
/// 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.
///
/// 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);
}
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (_) => const _SessionEndDialog(),
);
}
class _SessionEndDialog extends ConsumerWidget {
const _SessionEndDialog();
/// Locks the screen and returns to the login screen.
///
/// 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();
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;
Navigator.of(context).pop();
context.go(AppRoutes.login);
}
@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 '
'the imported products are cleared either way.',
style: const TextStyle(
fontSize: 12.5,
color: AppColors.warning,
height: 1.45,
),
),
),
],
),
),
_Choice(
icon: Icons.lock_outline_rounded,
tone: AppColors.info,
title: 'Temporary logout',
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),
_Choice(
icon: Icons.point_of_sale_rounded,
tone: AppColors.primary,
title: 'End shift',
body: pending == 0
? '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);
},
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,),
],
),
),
),
);
}
}