import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/transaction.dart'; import '../../../domain/repositories/sync_repository.dart'; import '../../sync/providers/sync_controller.dart'; import '../widgets/module_widgets.dart'; /// End-of-day sync. /// /// 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. class EventsView extends ConsumerWidget { const EventsView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { 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); final r = report.value; return ModulePage( children: [ Wrap( spacing: AppSpacing.lg, runSpacing: AppSpacing.lg, children: [ StatTile( label: 'Bills Today', value: '${r?.billCount ?? 0}', icon: Icons.receipt_long_rounded, caption: r?.firstBillAt == null ? 'no sales yet' : '${Formatters.time(r!.firstBillAt!)} – ' '${Formatters.time(r.lastBillAt!)}', ), StatTile( label: 'Items Sold', value: (r?.itemCount ?? 0).toStringAsFixed(0), icon: Icons.shopping_basket_rounded, color: AppColors.info, caption: 'units across all bills', ), StatTile( label: "Today's Sales", value: Formatters.money(r?.grossSales ?? 0), icon: Icons.payments_rounded, color: AppColors.success, caption: 'gross takings', ), StatTile( label: 'Awaiting Sync', value: '$pending', icon: Icons.cloud_off_rounded, color: pending > 0 ? AppColors.warning : AppColors.success, caption: pending > 0 ? 'held on this terminal' : 'everything uploaded', ), ], ), 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(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('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,), Cell(Formatters.money(o.total), mono: true, bold: true), TagChip( o.isSynced ? 'Synced' : 'Pending', color: o.isSynced ? AppColors.success : AppColors.warning, ), ],) .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, ), ), ], ), ); }