pos changes
This commit is contained in:
@@ -5,18 +5,16 @@ import '../../../app/providers.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 '../../../data/sync/sync_engine.dart';
|
||||
import '../../../domain/entities/transaction.dart';
|
||||
import '../../../domain/repositories/sync_repository.dart';
|
||||
import '../../sync/providers/sync_controller.dart';
|
||||
import '../widgets/module_widgets.dart';
|
||||
|
||||
/// End-of-day sync.
|
||||
/// What this terminal has traded, and what has reached the server.
|
||||
///
|
||||
/// Shows what the terminal produced today and uploads every bill still at
|
||||
/// `sync_status = 0`. Accepted bills flip to 1; anything that fails stays at 0
|
||||
/// and is retried on the next tap.
|
||||
/// Read-only. Uploading is the sync engine's job — it pushes after every sale
|
||||
/// and retries on its own — so this page reports rather than drives. The
|
||||
/// warning banner at the top is the exception: a bill the engine has given up
|
||||
/// on is the one thing here that needs a person to notice it.
|
||||
class EventsView extends ConsumerWidget {
|
||||
const EventsView({super.key});
|
||||
|
||||
@@ -25,8 +23,6 @@ class EventsView extends ConsumerWidget {
|
||||
final report = ref.watch(todayReportProvider);
|
||||
final pending = ref.watch(unsyncedCountProvider).value ?? 0;
|
||||
final rows = ref.watch(orderSyncRowsProvider).value ?? const [];
|
||||
final syncState = ref.watch(orderSyncProvider);
|
||||
final events = ref.watch(syncEventsProvider);
|
||||
|
||||
// The engine may not have emitted yet on a cold start, so fall back to
|
||||
// its current value rather than showing nothing. This is the same state
|
||||
@@ -83,118 +79,30 @@ class EventsView extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
PanelCard(
|
||||
title: 'Upload bills to server',
|
||||
subtitle: r == null
|
||||
? 'Reading today\u2019s trading from SQLite\u2026'
|
||||
: '${Formatters.date(r.businessDate)} \u00b7 ${r.cashierName} '
|
||||
'\u00b7 ${r.terminalId}',
|
||||
action: TagChip(
|
||||
pending > 0 ? '$pending pending' : 'All synced',
|
||||
color: pending > 0 ? AppColors.warning : AppColors.success,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (r != null && !r.isEmpty) ...[
|
||||
_row('Bills', '${r.billCount}'),
|
||||
_row('Items sold', r.itemCount.toStringAsFixed(0)),
|
||||
_row('Gross sales', Formatters.money(r.grossSales)),
|
||||
_row('GST collected', Formatters.money(r.taxCollected)),
|
||||
_row('Discount given', Formatters.money(r.discountGiven)),
|
||||
_row('Average basket', Formatters.money(r.averageBasket)),
|
||||
if (r.paymentBreakdown.isNotEmpty) ...[
|
||||
const Divider(height: AppSpacing.xxl),
|
||||
for (final e in r.paymentBreakdown.entries)
|
||||
ProgressRow(
|
||||
label: '${e.key.emoji} ${e.key.label}',
|
||||
value: Formatters.money(e.value),
|
||||
fraction:
|
||||
r.grossSales <= 0 ? 0 : e.value / r.grossSales,
|
||||
color: _methodColor(e.key),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
],
|
||||
|
||||
if (syncState is SyncRunning) ...[
|
||||
Text(
|
||||
syncState.stage,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
ClipRRect(
|
||||
borderRadius: AppRadius.brPill,
|
||||
child: LinearProgressIndicator(
|
||||
value: syncState.progress,
|
||||
minHeight: 8,
|
||||
backgroundColor: AppColors.divider,
|
||||
valueColor:
|
||||
const AlwaysStoppedAnimation<Color>(AppColors.primary),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
],
|
||||
|
||||
if (syncState is SyncFinished)
|
||||
_outcomeBanner(syncState.outcome),
|
||||
|
||||
PrimaryButton(
|
||||
label: pending > 0
|
||||
? 'Sync $pending bill${pending == 1 ? '' : 's'}'
|
||||
: 'Nothing to sync',
|
||||
icon: Icons.cloud_upload_rounded,
|
||||
large: true,
|
||||
busy: syncState is SyncRunning,
|
||||
onPressed: pending == 0
|
||||
? null
|
||||
: () => ref.read(orderSyncProvider.notifier).run(),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.shield_outlined,
|
||||
size: 15, color: AppColors.textTertiary,),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Bills are written to SQLite the moment a sale '
|
||||
'completes. A failed upload changes nothing on disk — '
|
||||
'every bill stays until the server confirms it.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textTertiary,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
PanelCard(
|
||||
title: 'Orders',
|
||||
subtitle: '${rows.length} stored \u00b7 $pending awaiting upload',
|
||||
child: ResponsiveTable(
|
||||
columns: const [
|
||||
TableCol('Invoice', flex: 3),
|
||||
TableCol('Time', flex: 2, priority: 1),
|
||||
TableCol('Date & time', flex: 3, priority: 1),
|
||||
TableCol('Items', flex: 1, numeric: true, priority: 2),
|
||||
TableCol('Total', flex: 2, numeric: true),
|
||||
TableCol('Sync', flex: 2, numeric: true),
|
||||
],
|
||||
rows: rows
|
||||
.map((o) => [
|
||||
Cell(o.invoiceNumber, bold: true, mono: true),
|
||||
Cell(Formatters.time(o.createdAt),
|
||||
color: AppColors.textTertiary,),
|
||||
// Date sits with the time because this table outlives the
|
||||
// day it was rung on — bills stay on the terminal until
|
||||
// they are purged, so a bare clock time is ambiguous the
|
||||
// moment the shop opens again.
|
||||
Cell(
|
||||
'${Formatters.date(o.createdAt)} \u00b7 '
|
||||
'${Formatters.time(o.createdAt)}',
|
||||
color: AppColors.textTertiary,
|
||||
),
|
||||
Cell(_units(o.itemCount), mono: true),
|
||||
Cell(Formatters.money(o.total), mono: true, bold: true),
|
||||
TagChip(
|
||||
o.isSynced ? 'Synced' : 'Pending',
|
||||
@@ -205,127 +113,17 @@ class EventsView extends ConsumerWidget {
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
|
||||
if (events.isNotEmpty) ...[
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
PanelCard(
|
||||
title: 'Sync history',
|
||||
subtitle: 'This session',
|
||||
child: ResponsiveTable(
|
||||
columns: const [
|
||||
TableCol('Event', flex: 3),
|
||||
TableCol('Detail', flex: 5, priority: 1),
|
||||
TableCol('Time', flex: 2, numeric: true),
|
||||
],
|
||||
rows: events
|
||||
.map((e) => [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
e.type.isInbound
|
||||
? Icons.cloud_download_rounded
|
||||
: Icons.cloud_upload_rounded,
|
||||
size: 15,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Flexible(child: Cell(e.type.label, bold: true)),
|
||||
],
|
||||
),
|
||||
Cell(
|
||||
e.error ?? e.summary,
|
||||
color: e.error != null
|
||||
? AppColors.danger
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
Cell(Formatters.time(e.createdAt),
|
||||
color: AppColors.textTertiary,),
|
||||
],)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _outcomeBanner(SyncOutcome outcome) {
|
||||
final ok = outcome.isSuccess;
|
||||
final uploaded = outcome.uploaded;
|
||||
final attempted = outcome.attempted;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: AppSpacing.lg),
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: ok ? AppColors.successSurface : AppColors.dangerSurface,
|
||||
borderRadius: AppRadius.brSm,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
ok ? Icons.check_circle_outline_rounded : Icons.wifi_off_rounded,
|
||||
size: 18,
|
||||
color: ok ? AppColors.success : AppColors.danger,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
ok
|
||||
? '$uploaded of $attempted bills uploaded and marked synced.'
|
||||
: '${outcome.error}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
height: 1.45,
|
||||
color: ok ? AppColors.success : AppColors.danger,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Color _methodColor(PaymentMethod m) => switch (m) {
|
||||
PaymentMethod.cash => AppColors.success,
|
||||
PaymentMethod.card => AppColors.info,
|
||||
PaymentMethod.upi => AppColors.primary,
|
||||
PaymentMethod.wallet => AppColors.warning,
|
||||
PaymentMethod.giftCard => AppColors.tierGold,
|
||||
PaymentMethod.loyalty => AppColors.tierSilver,
|
||||
};
|
||||
|
||||
Widget _row(String label, String value) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs + 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 13.5,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
/// Units on a bill. Whole where they are whole — loose goods are sold by
|
||||
/// weight, so "2.5" is a real answer here and rounding it would be a lie.
|
||||
static String _units(double count) =>
|
||||
count % 1 == 0 ? count.toStringAsFixed(0) : count.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
/// Flags a bill that could not reach the server on its own — the automatic
|
||||
/// push right after checkout, not the manual "Sync" button below.
|
||||
/// Flags a bill that could not reach the server on its own.
|
||||
///
|
||||
/// Sits above everything else on the page because a bill stuck here is the
|
||||
/// one thing on this screen that needs a person to notice it, rather than
|
||||
@@ -343,12 +141,10 @@ class _EngineWarningBanner extends StatelessWidget {
|
||||
|
||||
final retry = engine.nextAttemptAt;
|
||||
final retryNote = halted
|
||||
? 'Retrying will not help until this is fixed — press Sync below '
|
||||
'once it is sorted.'
|
||||
? 'Retrying will not help until this is fixed.'
|
||||
: retry == null
|
||||
? 'It will retry automatically, or press Sync below to try now.'
|
||||
: 'It will retry automatically at ${Formatters.time(retry)}, or '
|
||||
'press Sync below to try now.';
|
||||
? 'It will retry automatically.'
|
||||
: 'It will retry automatically at ${Formatters.time(retry)}.';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: AppSpacing.lg),
|
||||
|
||||
Reference in New Issue
Block a user