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/sync_event.dart'; import '../../../domain/entities/transaction.dart'; import '../../sync/providers/sync_controller.dart'; import '../widgets/module_widgets.dart'; /// The terminal's outbound half: what today produced, and what has been sent. class EventsView extends ConsumerWidget { const EventsView({super.key}); static Color statusColor(SyncStatus s) => switch (s) { SyncStatus.synced => AppColors.success, SyncStatus.failed => AppColors.danger, SyncStatus.syncing => AppColors.info, SyncStatus.pending => AppColors.warning, }; @override Widget build(BuildContext context, WidgetRef ref) { final report = ref.watch(shiftReportProvider); final events = ref.watch(syncEventsProvider); final pushing = ref.watch(reportPushProvider); return ModulePage( children: [ Wrap( spacing: AppSpacing.lg, runSpacing: AppSpacing.lg, children: [ StatTile( label: 'Bills Today', value: '${report.billCount}', icon: Icons.receipt_long_rounded, caption: report.firstBillAt == null ? 'no sales yet' : '${Formatters.time(report.firstBillAt!)} – ' '${Formatters.time(report.lastBillAt!)}', ), StatTile( label: 'Items Sold', value: report.itemCount.toStringAsFixed(0), icon: Icons.shopping_basket_rounded, color: AppColors.info, caption: 'units across all bills', ), StatTile( label: "Today's Sales", value: Formatters.money(report.grossSales), icon: Icons.payments_rounded, color: AppColors.success, caption: 'gross takings', ), StatTile( label: 'Average Basket', value: Formatters.money(report.averageBasket), icon: Icons.trending_up_rounded, color: AppColors.tierGold, caption: 'per bill', ), ], ), const SizedBox(height: AppSpacing.lg), PanelCard( title: 'Shift report', subtitle: '${Formatters.date(report.businessDate)} · ' '${report.cashierName} · ${report.terminalId}', action: TagChip( report.isEmpty ? 'Nothing to send' : 'Ready to push', color: report.isEmpty ? AppColors.textSecondary : AppColors.warning, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ _row('Bills', '${report.billCount}'), _row('Items sold', report.itemCount.toStringAsFixed(0)), _row('Gross sales', Formatters.money(report.grossSales)), _row('Net of tax', Formatters.money(report.netOfTax)), _row('GST collected', Formatters.money(report.taxCollected)), _row('Discount given', Formatters.money(report.discountGiven)), _row('Round off', Formatters.money(report.roundOff)), _row('Points issued', '${report.loyaltyPointsIssued}'), _row('Points redeemed', '${report.loyaltyPointsRedeemed}'), if (report.paymentBreakdown.isNotEmpty) ...[ const Divider(height: AppSpacing.xxl), const Align( alignment: Alignment.centerLeft, child: Text( 'By payment method', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), ), const SizedBox(height: AppSpacing.sm), for (final e in report.paymentBreakdown.entries) ProgressRow( label: '${e.key.emoji} ${e.key.label}', value: Formatters.money(e.value), fraction: report.grossSales <= 0 ? 0 : e.value / report.grossSales, color: _methodColor(e.key), ), ], const SizedBox(height: AppSpacing.xl), PrimaryButton( label: 'Push report to server', icon: Icons.cloud_upload_rounded, large: true, busy: pushing, onPressed: report.isEmpty ? null : () async { final event = await ref .read(reportPushProvider.notifier) .pushToday(); if (!context.mounted) return; final ok = event.status == SyncStatus.synced; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar( backgroundColor: ok ? AppColors.success : AppColors.danger, content: Text( ok ? 'Shift report sent.' : 'Push failed — the report is still saved ' 'on this terminal.', ), )); }, ), 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( 'A failed push never discards data. The report stays ' 'queued below and can be retried at any time.', style: TextStyle( fontSize: 12, color: AppColors.textTertiary, height: 1.5, ), ), ), ], ), ], ), ), const SizedBox(height: AppSpacing.lg), PanelCard( title: 'Event log', subtitle: '${events.length} recorded · ' '${events.where((e) => e.status != SyncStatus.synced).length} ' 'outstanding', child: events.isEmpty ? const Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.lg), child: Text( 'No sync activity yet. Importing the catalogue or pushing ' 'a report will appear here.', style: TextStyle(color: AppColors.textTertiary), ), ) : ResponsiveTable( columns: const [ TableCol('Event', flex: 3), TableCol('Detail', flex: 5, priority: 1), TableCol('Time', flex: 2, numeric: true, priority: 1), TableCol('Status', 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), e.status == SyncStatus.failed ? _RetryButton(eventId: e.id) : TagChip( e.status.label, color: statusColor(e.status), ), ]) .toList(), ), ), ], ); } 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, 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, color: AppColors.textPrimary, ), ), ], ), ); } class _RetryButton extends ConsumerWidget { const _RetryButton({required this.eventId}); final String eventId; @override Widget build(BuildContext context, WidgetRef ref) { final busy = ref.watch(reportPushProvider); return TextButton.icon( onPressed: busy ? null : () => ref.read(reportPushProvider.notifier).retry(eventId), icon: const Icon(Icons.refresh_rounded, size: 15), label: const Text('Retry', style: TextStyle(fontSize: 12.5)), style: TextButton.styleFrom( foregroundColor: AppColors.danger, minimumSize: const Size(0, 30), padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm), ), ); } }