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 '../../../core/utils/formatters.dart'; import '../../../core/widgets/primary_button.dart'; import '../../auth/providers/auth_controller.dart'; import '../../pos/providers/cart_controller.dart'; import '../../pos/providers/catalog_providers.dart'; import '../../../domain/repositories/sync_repository.dart'; import '../providers/sync_controller.dart'; /// End-of-shift flow. /// /// The day's takings are pushed here — the second and last moment this /// terminal needs a connection. Signing out without pushing is allowed, but /// the report stays queued locally rather than being discarded. Future showSignOutDialog(BuildContext context, WidgetRef ref) { return showDialog( context: context, barrierDismissible: false, builder: (_) => const _SignOutDialog(), ); } class _SignOutDialog extends ConsumerStatefulWidget { const _SignOutDialog(); @override ConsumerState<_SignOutDialog> createState() => _SignOutDialogState(); } class _SignOutDialogState extends ConsumerState<_SignOutDialog> { SyncOutcome? _result; Future _finish() async { ref.read(cartControllerProvider.notifier).reset(); await ref.read(authControllerProvider.notifier).signOut(); // Mirrors what a successful import does on the way in: bump the version // so catalogueReadyProvider re-reads hasCatalogue as false, and drop the // cached product lists so the next session's grid doesn't flash this // session's now-cleared data before it re-fetches. ref.read(catalogueVersionProvider.notifier).state++; ref.invalidate(allProductsProvider); ref.invalidate(visibleProductsProvider); ref.invalidate(categoryCountsProvider); ref.invalidate(lowStockProductsProvider); if (!mounted) return; Navigator.of(context).pop(); context.go(AppRoutes.login); } Future _pushThenFinish() async { final outcome = await ref.read(orderSyncProvider.notifier).run(); if (!mounted) return; setState(() => _result = outcome); if (outcome.isSuccess) { await Future.delayed(const Duration(milliseconds: 700)); if (mounted) _finish(); } } @override Widget build(BuildContext context) { // The operator is settling their own till, so this covers only their bills. final report = ref.watch(myShiftReportProvider).value; final pending = ref.watch(unsyncedCountProvider).value ?? 0; final cart = ref.watch(cartControllerProvider); final pushing = ref.watch(orderSyncProvider) is SyncRunning; final failed = _result != null && !_result!.isSuccess; return AlertDialog( title: const Text('End shift'), contentPadding: const EdgeInsets.fromLTRB( AppSpacing.xxl, AppSpacing.lg, AppSpacing.xxl, AppSpacing.sm, ), content: SizedBox( // Never wider than the viewport allows. width: (MediaQuery.sizeOf(context).width - 96).clamp(280.0, 420.0), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (cart.isNotEmpty) _Banner( icon: Icons.warning_amber_rounded, color: AppColors.warning, background: AppColors.warningSurface, message: 'The current bill has ${cart.lineCount} item(s) ' 'and will be cleared. Park it first if you need it.', ), if (pending == 0) const _Banner( icon: Icons.check_circle_outline_rounded, color: AppColors.success, background: AppColors.successSurface, message: 'Every bill has already been uploaded. Nothing is ' 'waiting on this terminal.', ) else ...[ const Text( 'Waiting to upload', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), const SizedBox(height: AppSpacing.sm), _row('Bills pending sync', '$pending'), if (report != null) ...[ _row('Bills today', '${report.billCount}'), _row('Items sold', report.itemCount.toStringAsFixed(0)), _row('Gross sales', Formatters.money(report.grossSales)), _row('GST collected', Formatters.money(report.taxCollected),), ], ], if (failed) ...[ const SizedBox(height: AppSpacing.md), _Banner( icon: Icons.wifi_off_rounded, color: AppColors.danger, background: AppColors.dangerSurface, message: _result?.error ?? 'Upload failed. Every bill is still stored on this ' 'terminal and can be retried from Events.', ), ], ], ), ), ), actionsPadding: const EdgeInsets.fromLTRB( AppSpacing.xxl, 0, AppSpacing.xxl, AppSpacing.lg, ), actions: [ // Wrap keeps three actions from overflowing a narrow dialog. Wrap( alignment: WrapAlignment.end, spacing: AppSpacing.sm, runSpacing: AppSpacing.sm, children: [ TextButton( onPressed: pushing ? null : () => Navigator.of(context).pop(), child: const Text('Cancel'), ), TextButton( onPressed: pushing ? null : _finish, style: TextButton.styleFrom( foregroundColor: AppColors.textSecondary, ), child: Text( pending == 0 ? 'Sign out' : 'Sign out without syncing', ), ), if (pending > 0) SizedBox( width: 190, child: PrimaryButton( label: failed ? 'Retry sync' : 'Sync & sign out', icon: Icons.cloud_upload_rounded, busy: pushing, onPressed: _pushThenFinish, ), ), ], ), ], ); } Widget _row(String label, String value) => Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Row( children: [ Expanded( child: Text( label, style: const TextStyle( fontSize: 13.5, color: AppColors.textSecondary, ), ), ), Text( value, style: const TextStyle( fontSize: 13.5, fontWeight: FontWeight.w700, ), ), ], ), ); } class _Banner extends StatelessWidget { const _Banner({ required this.icon, required this.color, required this.background, required this.message, }); final IconData icon; final Color color; final Color background; final String message; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: background, borderRadius: AppRadius.brSm, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 18, color: color), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( message, style: TextStyle(fontSize: 12.5, color: color, height: 1.45), ), ), ], ), ); } }