second commit

This commit is contained in:
2026-07-29 11:41:53 +05:30
parent fcccf22bac
commit d72522e737
211 changed files with 19260 additions and 0 deletions

View File

@@ -0,0 +1,287 @@
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),
),
);
}
}