second commit
This commit is contained in:
233
lib/presentation/sync/widgets/sign_out_dialog.dart
Normal file
233
lib/presentation/sync/widgets/sign_out_dialog.dart
Normal file
@@ -0,0 +1,233 @@
|
||||
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 '../../../domain/entities/sync_event.dart';
|
||||
import '../../auth/providers/auth_controller.dart';
|
||||
import '../../pos/providers/cart_controller.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<void> showSignOutDialog(BuildContext context, WidgetRef ref) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const _SignOutDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
class _SignOutDialog extends ConsumerStatefulWidget {
|
||||
const _SignOutDialog();
|
||||
|
||||
@override
|
||||
ConsumerState<_SignOutDialog> createState() => _SignOutDialogState();
|
||||
}
|
||||
|
||||
class _SignOutDialogState extends ConsumerState<_SignOutDialog> {
|
||||
SyncEvent? _result;
|
||||
|
||||
void _finish() {
|
||||
ref.read(cartControllerProvider.notifier).reset();
|
||||
ref.read(authControllerProvider.notifier).signOut();
|
||||
Navigator.of(context).pop();
|
||||
context.go(AppRoutes.login);
|
||||
}
|
||||
|
||||
Future<void> _pushThenFinish() async {
|
||||
final event = await ref.read(reportPushProvider.notifier).pushToday();
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() => _result = event);
|
||||
|
||||
if (event.status == SyncStatus.synced) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 700));
|
||||
if (mounted) _finish();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final report = ref.watch(shiftReportProvider);
|
||||
final cart = ref.watch(cartControllerProvider);
|
||||
final pushing = ref.watch(reportPushProvider);
|
||||
final failed = _result?.status == SyncStatus.failed;
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('End shift'),
|
||||
contentPadding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.xxl,
|
||||
AppSpacing.lg,
|
||||
AppSpacing.xxl,
|
||||
AppSpacing.sm,
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 420,
|
||||
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 (report.isEmpty)
|
||||
const _Banner(
|
||||
icon: Icons.info_outline_rounded,
|
||||
color: AppColors.textSecondary,
|
||||
background: AppColors.surfaceAlt,
|
||||
message: 'No sales were recorded today, so there is nothing '
|
||||
'to push.',
|
||||
)
|
||||
else ...[
|
||||
const Text(
|
||||
"Today's takings",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_row('Bills', '${report.billCount}'),
|
||||
_row('Items sold', report.itemCount.toStringAsFixed(0)),
|
||||
_row('Gross sales', Formatters.money(report.grossSales)),
|
||||
_row('GST collected', Formatters.money(report.taxCollected)),
|
||||
_row('Average basket',
|
||||
Formatters.money(report.averageBasket)),
|
||||
],
|
||||
|
||||
if (failed) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_Banner(
|
||||
icon: Icons.wifi_off_rounded,
|
||||
color: AppColors.danger,
|
||||
background: AppColors.dangerSurface,
|
||||
message: _result?.error ??
|
||||
'The push failed. The report is still saved 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(
|
||||
report.isEmpty ? 'Sign out' : 'Sign out without pushing',
|
||||
),
|
||||
),
|
||||
if (!report.isEmpty)
|
||||
SizedBox(
|
||||
width: 190,
|
||||
child: PrimaryButton(
|
||||
label: failed ? 'Retry push' : 'Push & 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user