Fix billing data integrity, sale atomicity and stock safety

Bills were persisted correctly but read back wrong. The read path rebuilt a
cart from its lines alone, dropping bill-level discounts and loyalty, so every
figure derived from a stored bill was overstated: the upload payload, the day
archive and the shift report. A discounted 529 bill read back as 620.

Money and data integrity
- order_dao: restore bill_discount and points_redeemed when rebuilding a cart;
  keep the reconstruction tier-less so the membership discount is not applied
  twice. Trust the recorded total and points via SaleTransaction.storedTotal.
- checkout_sale + order_dao.commitSale: write the bill, its stock movement and
  the loyalty update in one transaction. Previously a failure part-way through
  left a persisted bill the cashier believed had failed, inviting a duplicate.
- checkout_sale: re-check every line against live stock. A parked bill resumed
  after its stock was sold passed validation and oversold.
- catalogue_dao: allocate the invoice sequence in one transaction; the previous
  read-modify-write could hand two sales the same number and fail UNIQUE.
- local_store: replay unsynced sales after a catalogue import, so a mid-shift
  re-import cannot restore stock that has already been sold.
- payment_controller: stamp the signed-in operator on the bill instead of the
  hardcoded seed session, and pass the terminal id through.
- cart: reconcile per-slab GST against the bill total so the parts sum to the
  whole on a tax invoice.

Sync and reporting
- sync_repository: drain unsynced bills in a loop rather than silently capping
  at one page; stop on rejection so rejected rows cannot loop forever.
- sync_log_dao (new): persist the sync history to the sync_log table, which the
  schema already defined but nothing used. It was in memory, so the only record
  that bills had been uploaded died at restart.
- Scope shift reports by cashier. day_archive is re-keyed to
  (business_date, cashier_name) so a till stays settleable after its bills are
  uploaded and deleted. Schema v4 with a migration that carries v3 rows across.

Input and UI
- barcode_service: consume machine-paced keystrokes so a scan cannot also land
  in the focused field, and raise the bar to 60ms/char while a text field has
  focus so typing a mobile number is not read as a scan. Clock and focus check
  injected so the behaviour is testable.
- primary_button: make the label flexible; label plus trailing total overflowed
  the Charge button by up to 131px.
- app_router: redirect instead of null-casting when the receipt route is
  entered without its transaction.
- customer_repository: reduce the search query to digits so a punctuated mobile
  number matches.

Cleanup
- Remove TransactionRepository.save, CustomerRepository.recordSale and
  OrderDao.insertOrder, all superseded by commitSale.
- dart fix across the tree; 251 analyzer issues down to 3 info-level.

Tests: 23 passing / 15 failing -> 90 passing. Fixed the two defects that broke
the existing suite (containsAll type argument, reset() needing a catalogue) and
deleted the leftover template test. Added coverage for the order round trip,
the day archive after a real sync, stock safety, checkout atomicity, the v3->v4
migration, scanner-versus-human input, and an app-level smoke test that renders
every module.

Note: bills already uploaded with a discount went up overstated. This stops it
happening again but does not correct historical server data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-07-31 18:34:10 +05:30
parent 9891a69a5f
commit af3933092f
62 changed files with 2035 additions and 561 deletions

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -408,14 +407,14 @@ class _FormPanel extends ConsumerWidget {
const SizedBox(height: AppSpacing.sm),
Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.dangerSurface,
borderRadius: AppRadius.brSm,
),
child: Row(
children: [
const Icon(Icons.error_outline_rounded,
color: AppColors.danger, size: 18),
color: AppColors.danger, size: 18,),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
@@ -509,13 +508,13 @@ class _DemoHint extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.info_outline_rounded,
size: 17, color: AppColors.primary),
size: 17, color: AppColors.primary,),
const SizedBox(width: AppSpacing.sm),
Expanded(
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'Demo account',
style: TextStyle(
fontSize: 12.5,
@@ -523,10 +522,10 @@ class _DemoHint extends StatelessWidget {
color: AppColors.primary,
),
),
const SizedBox(height: 2),
SizedBox(height: 2),
SelectableText(
'${DemoCredentials.email} · ${DemoCredentials.password}',
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),

View File

@@ -174,7 +174,7 @@ class _CustomerCaptureSheetState
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: AppSpacing.md),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.border,
borderRadius: AppRadius.brPill,
),
@@ -378,7 +378,7 @@ class _CustomerCaptureSheetState
children: [
Container(
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.successSurface,
borderRadius: AppRadius.brLg,
),
@@ -433,7 +433,7 @@ class _CustomerCaptureSheetState
children: [
Expanded(
child: _miniStat(
'${c.loyaltyPoints}', 'points held'),
'${c.loyaltyPoints}', 'points held',),
),
Expanded(
child: _miniStat(

View File

@@ -195,9 +195,9 @@ class _CustomersViewState extends ConsumerState<CustomersView> {
StatusPill.tier(c.tier, dense: true),
Cell('${c.loyaltyPoints}', mono: true),
Cell(Formatters.moneyCompact(c.lifetimeSpend),
mono: true, bold: true),
mono: true, bold: true,),
Cell('${c.visitCount}', mono: true),
])
],)
.toList(),
),
],

View File

@@ -146,7 +146,7 @@ class EventsView extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.shield_outlined,
size: 15, color: AppColors.textTertiary),
size: 15, color: AppColors.textTertiary,),
SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
@@ -181,14 +181,14 @@ class EventsView extends ConsumerWidget {
.map((o) => [
Cell(o.invoiceNumber, bold: true, mono: true),
Cell(Formatters.time(o.createdAt),
color: AppColors.textTertiary),
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(),
),
),
@@ -227,8 +227,8 @@ class EventsView extends ConsumerWidget {
: AppColors.textSecondary,
),
Cell(Formatters.time(e.createdAt),
color: AppColors.textTertiary),
])
color: AppColors.textTertiary,),
],)
.toList(),
),
),

View File

@@ -102,14 +102,14 @@ class ProductImportView extends ConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(p.emoji,
style: const TextStyle(fontSize: 17)),
style: const TextStyle(fontSize: 17),),
const SizedBox(width: AppSpacing.sm),
Flexible(child: Cell(p.name, bold: true)),
],
),
Cell(p.sku, color: AppColors.textTertiary),
TagChip(p.category.label,
color: AppColors.textSecondary),
color: AppColors.textSecondary,),
Cell(Formatters.money(p.price), mono: true, bold: true),
TagChip(
p.isOutOfStock
@@ -121,7 +121,7 @@ class ProductImportView extends ConsumerWidget {
? AppColors.warning
: AppColors.success),
),
])
],)
.toList(),
),
),
@@ -146,13 +146,13 @@ class _NotImportedBanner extends StatelessWidget {
borderRadius: AppRadius.brLg,
border: Border.all(color: AppColors.warning.withValues(alpha: 0.35)),
),
child: Row(
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.cloud_download_outlined,
color: AppColors.warning, size: 22),
const SizedBox(width: AppSpacing.md),
const Expanded(
Icon(Icons.cloud_download_outlined,
color: AppColors.warning, size: 22,),
SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -225,7 +225,7 @@ class _ImportPanel extends ConsumerWidget {
Container(
padding: const EdgeInsets.all(AppSpacing.md),
margin: const EdgeInsets.only(bottom: AppSpacing.lg),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.dangerSurface,
borderRadius: AppRadius.brSm,
),
@@ -233,7 +233,7 @@ class _ImportPanel extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.wifi_off_rounded,
color: AppColors.danger, size: 18),
color: AppColors.danger, size: 18,),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
@@ -254,14 +254,14 @@ class _ImportPanel extends ConsumerWidget {
Container(
padding: const EdgeInsets.all(AppSpacing.md),
margin: const EdgeInsets.only(bottom: AppSpacing.lg),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.successSurface,
borderRadius: AppRadius.brSm,
),
child: Row(
children: [
const Icon(Icons.check_circle_outline_rounded,
color: AppColors.success, size: 18),
color: AppColors.success, size: 18,),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
@@ -312,13 +312,13 @@ class _ImportPanel extends ConsumerWidget {
),
const SizedBox(height: AppSpacing.lg),
Row(
const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.info_outline_rounded,
size: 15, color: AppColors.textTertiary),
const SizedBox(width: AppSpacing.sm),
const Expanded(
Icon(Icons.info_outline_rounded,
size: 15, color: AppColors.textTertiary,),
SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
'After this import the terminal works entirely offline. '
'Sales, customers and parked bills are held locally and are '

View File

@@ -64,21 +64,21 @@ class _PromosViewState extends State<PromosView> {
icon: Icons.campaign_rounded,
caption: 'of ${_campaigns.length} configured',
),
StatTile(
const StatTile(
label: 'Redemptions',
value: '970',
icon: Icons.confirmation_number_rounded,
color: AppColors.info,
caption: 'this month',
),
StatTile(
const StatTile(
label: 'Discount Given',
value: '₹48,240',
icon: Icons.local_offer_rounded,
color: AppColors.warning,
caption: '2.6% of sales',
),
StatTile(
const StatTile(
label: 'Incremental Sales',
value: '₹2.14L',
icon: Icons.trending_up_rounded,
@@ -133,7 +133,7 @@ class _PromosViewState extends State<PromosView> {
borderRadius: AppRadius.brSm,
),
child: Icon(Icons.sell_rounded,
size: 18, color: c.$6),
size: 18, color: c.$6,),
),
const SizedBox(width: AppSpacing.md),
Expanded(

View File

@@ -167,7 +167,7 @@ class _SettingsViewState extends ConsumerState<SettingsView> {
if (list.isEmpty) {
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.warningSurface,
borderRadius: AppRadius.brSm,
),
@@ -175,7 +175,7 @@ class _SettingsViewState extends ConsumerState<SettingsView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.print_disabled_rounded,
size: 18, color: AppColors.warning),
size: 18, color: AppColors.warning,),
SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
@@ -235,7 +235,7 @@ class _SettingsViewState extends ConsumerState<SettingsView> {
final ok = await ref
.read(receiptServiceProvider)
.printTestPage(
printerUrl: settings.printerUrl);
printerUrl: settings.printerUrl,);
if (!context.mounted) return;
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
@@ -245,8 +245,8 @@ class _SettingsViewState extends ConsumerState<SettingsView> {
: AppColors.danger,
content: Text(ok
? 'Test slip sent to the printer.'
: 'Could not reach the printer.'),
));
: 'Could not reach the printer.',),
),);
},
icon: const Icon(Icons.receipt_long_rounded, size: 17),
label: const Text('Print test slip'),
@@ -307,9 +307,9 @@ class _SettingsViewState extends ConsumerState<SettingsView> {
Cell(s.role.label, color: AppColors.textSecondary),
s.id == current?.id
? const TagChip('Signed in',
color: AppColors.success)
color: AppColors.success,)
: const SizedBox.shrink(),
])
],)
.toList(),
),
);

View File

@@ -407,7 +407,7 @@ class Cell extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: mono
? AppTypography.money(13.5,
weight: bold ? FontWeight.w700 : FontWeight.w500, color: color)
weight: bold ? FontWeight.w700 : FontWeight.w500, color: color,)
: TextStyle(
fontSize: 13.5,
fontWeight: bold ? FontWeight.w600 : FontWeight.w400,

View File

@@ -6,6 +6,7 @@ import '../../../app/providers.dart';
import '../../../core/utils/extensions.dart';
import '../../../domain/entities/transaction.dart';
import '../../../domain/usecases/checkout_sale.dart';
import '../../auth/providers/auth_controller.dart';
import '../../pos/providers/cart_controller.dart';
import '../../pos/providers/catalog_providers.dart';
import '../../sync/providers/sync_controller.dart';
@@ -44,6 +45,7 @@ class PaymentState {
String? error,
bool clearError = false,
CheckoutResult? result,
bool clearResult = false,
}) {
return PaymentState(
splits: splits ?? this.splits,
@@ -52,7 +54,7 @@ class PaymentState {
reference: reference ?? this.reference,
isProcessing: isProcessing ?? this.isProcessing,
error: clearError ? null : (error ?? this.error),
result: result ?? this.result,
result: clearResult ? null : (result ?? this.result),
);
}
}
@@ -156,7 +158,10 @@ class PaymentController extends StateNotifier<PaymentState> {
if (state.isProcessing) return null;
// A single-tender sale needn't be staged first — fold it in automatically.
var splits = state.splits;
// Remembered so it can be rolled back if the sale is rejected, otherwise a
// retry starts with a phantom tender already staged.
final stagedSplits = state.splits;
var splits = stagedSplits;
if (splits.isEmpty || balanceDue > 0.01) {
addSplit();
splits = state.splits;
@@ -168,10 +173,16 @@ class PaymentController extends StateNotifier<PaymentState> {
final cart = _ref.read(cartControllerProvider);
final session = _ref.read(cashierSessionProvider);
// The signed-in operator owns the bill. Falling back to the seed session
// stamped every sale with the same name, so a bill could not be traced to
// whoever actually rang it.
final user = _ref.read(currentUserProvider);
final result = await _ref.read(checkoutSaleProvider)(
cart: cart,
payments: splits,
cashierName: session.name,
cashierName: user?.name ?? session.name,
terminalId: session.terminalId,
);
state = state.copyWith(isProcessing: false, result: result);
@@ -190,12 +201,17 @@ class PaymentController extends StateNotifier<PaymentState> {
return result;
} on CheckoutFailure catch (e) {
state = state.copyWith(isProcessing: false, error: e.message);
state = state.copyWith(
isProcessing: false,
error: e.message,
splits: stagedSplits,
);
return null;
} catch (e) {
state = state.copyWith(
isProcessing: false,
error: 'Could not complete the sale. $e',
splits: stagedSplits,
);
return null;
}

View File

@@ -50,7 +50,7 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
void _backspaceCash() {
if (_cashBuffer.isEmpty) return;
setState(
() => _cashBuffer = _cashBuffer.substring(0, _cashBuffer.length - 1));
() => _cashBuffer = _cashBuffer.substring(0, _cashBuffer.length - 1),);
_syncCash();
}
@@ -196,7 +196,7 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
: AppColors.primarySurface,
child: customer == null
? const Icon(Icons.person_add_alt_1_outlined,
size: 19, color: AppColors.textSecondary)
size: 19, color: AppColors.textSecondary,)
: Text(
Formatters.initials(customer.name),
style: const TextStyle(
@@ -316,7 +316,7 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
child: Row(
children: [
Text(e.value.method.emoji,
style: const TextStyle(fontSize: 15)),
style: const TextStyle(fontSize: 15),),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
@@ -326,7 +326,7 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
),
),
Text(Formatters.money(e.value.amount),
style: AppTypography.money(13.5)),
style: AppTypography.money(13.5),),
IconButton(
onPressed: () => controller.removeSplit(e.key),
icon: const Icon(Icons.close_rounded, size: 16),
@@ -380,7 +380,7 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
children: [
const Text('',
style:
TextStyle(fontSize: 22, color: AppColors.textTertiary)),
TextStyle(fontSize: 22, color: AppColors.textTertiary),),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: FittedBox(
@@ -481,7 +481,7 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
Row(
children: [
Text(state.activeMethod.emoji,
style: const TextStyle(fontSize: 20)),
style: const TextStyle(fontSize: 20),),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
@@ -498,13 +498,13 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
child: Container(
width: 104,
height: 104,
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppRadius.brXl,
),
alignment: Alignment.center,
child: Text(state.activeMethod.emoji,
style: const TextStyle(fontSize: 46)),
style: const TextStyle(fontSize: 46),),
),
),
const SizedBox(height: AppSpacing.lg),
@@ -543,9 +543,7 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
onPressed: controller.balanceDue > 0
? () {
controller.addSplit(
amount: amount == null
? null
: amount.clamp(0, controller.balanceDue).toDouble(),
amount: amount?.clamp(0, controller.balanceDue).toDouble(),
);
setState(() => _cashBuffer = '');
}
@@ -586,14 +584,14 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.md),
margin: const EdgeInsets.only(bottom: AppSpacing.md),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.dangerSurface,
borderRadius: AppRadius.brSm,
),
child: Row(
children: [
const Icon(Icons.error_outline_rounded,
color: AppColors.danger, size: 18),
color: AppColors.danger, size: 18,),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
@@ -613,7 +611,7 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
child: Row(
children: [
const Icon(Icons.info_outline_rounded,
size: 15, color: AppColors.textTertiary),
size: 15, color: AppColors.textTertiary,),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
@@ -634,7 +632,7 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
),
),
child: const Text('Exact',
style: TextStyle(fontSize: 12.5)),
style: TextStyle(fontSize: 12.5),),
),
],
),

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uuid/uuid.dart';
@@ -80,7 +82,7 @@ class CartController extends StateNotifier<Cart> {
stamp: DateTime.now(),
product: product,
message: '${product.name} is out of stock',
));
),);
return;
}
@@ -98,7 +100,7 @@ class CartController extends StateNotifier<Cart> {
product: product,
message: 'Only ${product.stock.toStringAsFixed(0)} '
'${product.unit.symbol} left',
));
),);
return;
}
@@ -110,7 +112,7 @@ class CartController extends StateNotifier<Cart> {
quantity: quantity,
addedAt: DateTime.now(),
),
]);
],);
} else {
state = state.copyWith(
lines: _replace(existing.copyWith(quantity: requested)),
@@ -123,7 +125,7 @@ class CartController extends StateNotifier<Cart> {
outcome: existing == null ? ScanOutcome.added : ScanOutcome.incremented,
stamp: DateTime.now(),
product: product,
));
),);
}
/// Scanner entry point. Resolves the barcode and adds it with no dialogs.
@@ -131,12 +133,13 @@ class CartController extends StateNotifier<Cart> {
final product = await _products.findByBarcode(code);
if (product == null) {
_sound.scanError();
// Deliberately not awaited — the beep must never delay the next scan.
unawaited(_sound.scanError());
onFeedback(ScanFeedback(
outcome: ScanOutcome.notFound,
stamp: DateTime.now(),
message: 'No product for barcode $code',
));
),);
return;
}
@@ -163,7 +166,7 @@ class CartController extends StateNotifier<Cart> {
stamp: DateTime.now(),
product: line.product,
message: 'Only ${line.product.stock.toStringAsFixed(0)} in stock',
));
),);
return;
}
@@ -265,7 +268,7 @@ class CartController extends StateNotifier<Cart> {
cart: state,
parkedAt: DateTime.now(),
label: label,
));
),);
reset();
}

View File

@@ -6,10 +6,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
enum PosModule {
pos('Point of Sale', 'POS', Icons.point_of_sale_rounded, NavSection.billing),
customers('Customers', 'Customers', Icons.people_alt_rounded,
NavSection.billing),
NavSection.billing,),
productImport('Product Import', 'Product Import',
Icons.cloud_download_rounded, NavSection.catalogue),
Icons.cloud_download_rounded, NavSection.catalogue,),
promos('Promotions', 'Promo', Icons.sell_rounded, NavSection.catalogue),
events('Events', 'Events', Icons.sync_rounded, NavSection.session),

View File

@@ -13,6 +13,7 @@ import '../../modules/screens/promos_view.dart';
import '../../modules/screens/settings_view.dart';
import '../../sync/providers/sync_controller.dart';
import '../providers/cart_controller.dart';
import '../providers/catalog_providers.dart';
import '../providers/navigation_provider.dart';
import '../widgets/app_sidebar.dart';
import '../widgets/billing_panel.dart';
@@ -56,6 +57,9 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
// Without an imported catalogue there is nothing to resolve against.
if (!ref.read(catalogueReadyProvider)) return;
ref.read(activeModuleProvider.notifier).state = PosModule.pos;
// Drop any character the burst leaked into the search box before the
// scan was recognised, so the grid is not left filtered to nothing.
ref.read(searchQueryProvider.notifier).state = '';
ref.read(cartControllerProvider.notifier).scanBarcode(code);
},
)..attach();
@@ -73,13 +77,13 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => FractionallySizedBox(
builder: (_) => const FractionallySizedBox(
heightFactor: 0.92,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
borderRadius: BorderRadius.vertical(
top: Radius.circular(AppRadius.xxl),
),
child: const BillingPanel(inSheet: true),
child: BillingPanel(inSheet: true),
),
),
);

View File

@@ -98,7 +98,7 @@ class _CatalogueRequired extends ConsumerWidget {
shape: BoxShape.circle,
),
child: const Icon(Icons.cloud_download_outlined,
size: 42, color: AppColors.primary),
size: 42, color: AppColors.primary,),
),
const SizedBox(height: AppSpacing.xxl),
Text(
@@ -136,7 +136,7 @@ class _CatalogueRequired extends ConsumerWidget {
minHeight: 8,
backgroundColor: AppColors.divider,
valueColor: const AlwaysStoppedAnimation<Color>(
AppColors.primary),
AppColors.primary,),
),
),
const SizedBox(height: AppSpacing.lg),
@@ -146,7 +146,7 @@ class _CatalogueRequired extends ConsumerWidget {
Container(
padding: const EdgeInsets.all(AppSpacing.md),
margin: const EdgeInsets.only(bottom: AppSpacing.lg),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.dangerSurface,
borderRadius: AppRadius.brSm,
),
@@ -154,7 +154,7 @@ class _CatalogueRequired extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.wifi_off_rounded,
color: AppColors.danger, size: 18),
color: AppColors.danger, size: 18,),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(

View File

@@ -245,7 +245,7 @@ class _Section extends ConsumerWidget {
AppSpacing.sm,
),
child: Text(section.label.toUpperCase(),
style: AppTypography.sectionLabel()),
style: AppTypography.sectionLabel(),),
)
else
const Padding(
@@ -459,7 +459,7 @@ class _LogoutTile extends ConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.logout_rounded,
size: 19, color: AppColors.danger),
size: 19, color: AppColors.danger,),
if (expanded) ...[
const SizedBox(width: AppSpacing.md),
const Text(

View File

@@ -1,8 +1,9 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/constants/app_constants.dart';
import '../../../core/router/app_router.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
@@ -66,7 +67,7 @@ class BillingPanel extends ConsumerWidget {
),
if (cart.isNotEmpty) _Summary(cart: cart),
_Actions(cart: cart),
]),
],),
);
}
}
@@ -101,7 +102,7 @@ class _Header extends ConsumerWidget {
const SizedBox(width: AppSpacing.sm),
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppRadius.brPill,
),
@@ -213,13 +214,13 @@ class _Summary extends ConsumerWidget {
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + 2,
),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.successSurface,
borderRadius: AppRadius.brSm,
),
child: Row(children: [
const Icon(Icons.stars_rounded,
size: 16, color: AppColors.success),
size: 16, color: AppColors.success,),
const SizedBox(width: AppSpacing.sm),
Text(
'This sale earns +${cart.pointsEarned} pts',
@@ -229,7 +230,7 @@ class _Summary extends ConsumerWidget {
fontSize: 13,
),
),
]),
],),
),
_Row(label: 'Subtotal', value: Formatters.money(cart.subtotal)),
@@ -324,7 +325,7 @@ class _Summary extends ConsumerWidget {
),
),
),
]),
],),
);
}
}
@@ -365,7 +366,7 @@ class _Row extends StatelessWidget {
style: const TextStyle(
fontSize: 11.5,
color: AppColors.textTertiary,
)),
),),
],
const SizedBox(width: AppSpacing.sm),
const Spacer(),
@@ -381,7 +382,7 @@ class _Row extends StatelessWidget {
const SizedBox(width: AppSpacing.xs),
Icon(trailingIcon, size: 14, color: AppColors.textTertiary),
],
]),
],),
);
}
}
@@ -412,7 +413,8 @@ class _Actions extends ConsumerWidget {
if (ref.read(cartControllerProvider).customer == null) {
await showCustomerCaptureSheet(context);
}
if (context.mounted) context.push(AppRoutes.payment);
// Navigation result is not needed here.
if (context.mounted) unawaited(context.push(AppRoutes.payment));
}
: null,
trailing: enabled

View File

@@ -36,7 +36,7 @@ class CartFab extends ConsumerWidget {
child: Container(
height: AppSizes.buttonHeightLarge,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xl),
decoration: BoxDecoration(
decoration: const BoxDecoration(
borderRadius: AppRadius.brLg,
boxShadow: AppColors.shadowLg,
),
@@ -79,7 +79,7 @@ class CartFab extends ConsumerWidget {
),
const SizedBox(width: AppSpacing.sm),
const Icon(Icons.keyboard_arrow_up_rounded,
color: Colors.white, size: 20),
color: Colors.white, size: 20,),
],
),
),

View File

@@ -34,12 +34,12 @@ class CartLineTile extends StatelessWidget {
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: AppSpacing.xl),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.dangerSurface,
borderRadius: AppRadius.brMd,
),
child: const Icon(Icons.delete_outline_rounded,
color: AppColors.danger),
color: AppColors.danger,),
),
child: Container(
padding: const EdgeInsets.all(AppSpacing.md),
@@ -83,18 +83,18 @@ class CartLineTile extends StatelessWidget {
Formatters.money(p.price),
style: AppTypography.money(13.5,
weight: FontWeight.w600,
color: AppColors.textSecondary),
color: AppColors.textSecondary,),
),
Text(' / ${p.unit.symbol}',
style: const TextStyle(
fontSize: 11.5,
color: AppColors.textTertiary,
)),
),),
if (line.discount.isActive) ...[
const SizedBox(width: AppSpacing.sm),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 5, vertical: 1),
horizontal: 5, vertical: 1,),
decoration: BoxDecoration(
color: AppColors.successSurface,
borderRadius: BorderRadius.circular(4),
@@ -109,7 +109,7 @@ class CartLineTile extends StatelessWidget {
),
),
],
]),
],),
],
),
),
@@ -121,7 +121,7 @@ class CartLineTile extends StatelessWidget {
padding: EdgeInsets.zero,
tooltip: 'Remove',
),
]),
],),
const SizedBox(height: AppSpacing.sm),
@@ -162,14 +162,14 @@ class CartLineTile extends StatelessWidget {
),
],
),
]),
],),
if (line.exceedsStock)
Padding(
padding: const EdgeInsets.only(top: AppSpacing.sm),
child: Row(children: [
const Icon(Icons.error_outline_rounded,
size: 14, color: AppColors.danger),
size: 14, color: AppColors.danger,),
const SizedBox(width: AppSpacing.xs),
Text(
'Only ${p.stock.toStringAsFixed(0)} ${p.unit.symbol} '
@@ -180,9 +180,9 @@ class CartLineTile extends StatelessWidget {
fontWeight: FontWeight.w600,
),
),
]),
],),
),
]),
],),
),
);
}
@@ -222,7 +222,7 @@ class _Stepper extends StatelessWidget {
),
),
_btn(Icons.add_rounded, onIncrement),
]),
],),
);
}

View File

@@ -108,7 +108,7 @@ class _Chip extends StatelessWidget {
),
),
],
]),
],),
),
),
),

View File

@@ -37,7 +37,7 @@ class CustomerBar extends ConsumerWidget {
: AppColors.primarySurface,
child: customer == null
? const Icon(Icons.directions_walk_rounded,
size: 20, color: AppColors.textSecondary)
size: 20, color: AppColors.textSecondary,)
: Text(
Formatters.initials(customer.name),
style: const TextStyle(
@@ -65,7 +65,7 @@ class CustomerBar extends ConsumerWidget {
const SizedBox(width: AppSpacing.sm),
StatusPill.tier(customer.tier, dense: true),
],
]),
],),
if (customer != null)
Text(
'${Formatters.mobile(customer.mobile)} · '
@@ -92,7 +92,7 @@ class CustomerBar extends ConsumerWidget {
icon: const Icon(Icons.person_off_outlined, size: 17),
label: const Text('Detach'),
style: TextButton.styleFrom(
foregroundColor: AppColors.textSecondary),
foregroundColor: AppColors.textSecondary,),
),
const SizedBox(width: AppSpacing.sm),
OutlinedButton.icon(
@@ -104,7 +104,7 @@ class CustomerBar extends ConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
),
),
]),
],),
);
}
}

View File

@@ -116,7 +116,7 @@ class _DiscountSheetState extends State<_DiscountSheet> {
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.border,
borderRadius: AppRadius.brPill,
),
@@ -124,13 +124,13 @@ class _DiscountSheetState extends State<_DiscountSheet> {
const SizedBox(height: AppSpacing.xl),
Text(widget.title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700),),
const SizedBox(height: 2),
Text(widget.subtitle,
style: const TextStyle(
fontSize: 13,
color: AppColors.textSecondary,
)),
),),
const SizedBox(height: AppSpacing.xxl),
SegmentedButton<DiscountType>(
@@ -177,10 +177,10 @@ class _DiscountSheetState extends State<_DiscountSheet> {
.map((v) => ActionChip(
label: Text(_type == DiscountType.percentage
? '$v%'
: '$v'),
: '$v',),
onPressed: () =>
setState(() => _value.text = v.toString()),
))
),)
.toList(),
),
const SizedBox(height: AppSpacing.xxl),
@@ -205,8 +205,8 @@ class _DiscountSheetState extends State<_DiscountSheet> {
onPressed: _apply,
),
),
]),
]),
],),
],),
),
);
}

View File

@@ -133,13 +133,13 @@ class _Breadcrumb extends StatelessWidget {
const Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.xs + 2),
child: Icon(Icons.chevron_right_rounded,
size: 13, color: AppColors.textTertiary),
size: 13, color: AppColors.textTertiary,),
),
Text(module.section.label, style: style),
const Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.xs + 2),
child: Icon(Icons.chevron_right_rounded,
size: 13, color: AppColors.textTertiary),
size: 13, color: AppColors.textTertiary,),
),
Text(
module.label,
@@ -285,7 +285,7 @@ class _ParkedBillsButton extends ConsumerWidget {
final bill = parked[i];
return ListTile(
leading: const Icon(Icons.receipt_long_rounded,
color: AppColors.primary),
color: AppColors.primary,),
title: Text(bill.displayLabel),
subtitle: Text(
'${bill.cart.lineCount} items · '

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import '../../../core/constants/app_constants.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/theme/app_typography.dart';
@@ -178,7 +177,7 @@ class _ProductCardState extends State<ProductCard> {
left: AppSpacing.sm,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
horizontal: 6, vertical: 2,),
decoration: BoxDecoration(
color: AppColors.success,
borderRadius: BorderRadius.circular(5),
@@ -199,7 +198,7 @@ class _ProductCardState extends State<ProductCard> {
top: AppSpacing.sm,
right: AppSpacing.sm,
child: Icon(Icons.warning_amber_rounded,
size: 15, color: AppColors.warning),
size: 15, color: AppColors.warning,),
),
// Quantity badge once the item is on the bill.
@@ -243,10 +242,10 @@ class _ProductCardState extends State<ProductCard> {
shape: BoxShape.circle,
),
child: const Icon(Icons.add_rounded,
size: 18, color: Colors.white),
size: 18, color: Colors.white,),
),
),
]),
],),
),
),
),

View File

@@ -67,7 +67,7 @@ class _ScanToastState extends ConsumerState<ScanToast> {
horizontal: AppSpacing.xl,
vertical: AppSpacing.md,
),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.textPrimary,
borderRadius: AppRadius.brPill,
boxShadow: AppColors.shadowLg,
@@ -110,7 +110,7 @@ class _ScanToastState extends ConsumerState<ScanToast> {
fontSize: 13.5,
),
),
]),
],),
)
.animate(key: ValueKey(feedback.stamp))
.fadeIn(duration: 140.ms)

View File

@@ -44,6 +44,13 @@ class _PosSearchFieldState extends ConsumerState<PosSearchField> {
Widget build(BuildContext context) {
final query = ref.watch(searchQueryProvider);
// A scan clears the query from outside this widget. Mirror that into the
// controller so any stray character the scanner leaked into the field goes
// with it.
ref.listen<String>(searchQueryProvider, (_, next) {
if (next.isEmpty && _controller.text.isNotEmpty) _controller.clear();
});
return TextField(
controller: _controller,
focusNode: widget.focusNode,
@@ -80,23 +87,23 @@ class _PosSearchFieldState extends ConsumerState<PosSearchField> {
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppRadius.brSm,
),
child: const Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.qr_code_scanner_rounded,
size: 18, color: AppColors.primary),
size: 18, color: AppColors.primary,),
SizedBox(width: AppSpacing.xs + 2),
Text('Scanner ready',
style: TextStyle(
color: AppColors.primary,
fontSize: 12,
fontWeight: FontWeight.w600,
)),
]),
),),
],),
),
]),
],),
),
);
}

View File

@@ -58,7 +58,7 @@ class _ReceiptScreenState extends ConsumerState<ReceiptScreen> {
content: Text(
'Printer did not respond — use Print to send it again.',
),
));
),);
}
});
@@ -112,7 +112,7 @@ class _ReceiptScreenState extends ConsumerState<ReceiptScreen> {
height: 480,
child: ReceiptPreview(transaction: txn),
),
]),
],),
)
: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -148,7 +148,7 @@ class _ReceiptScreenState extends ConsumerState<ReceiptScreen> {
shape: BoxShape.circle,
),
child: const Icon(Icons.check_rounded,
size: 44, color: AppColors.success),
size: 44, color: AppColors.success,),
)
.animate()
.scale(
@@ -161,7 +161,7 @@ class _ReceiptScreenState extends ConsumerState<ReceiptScreen> {
const SizedBox(height: AppSpacing.xl),
Text('Sale complete',
textAlign: TextAlign.center,
style: context.text.headlineMedium),
style: context.text.headlineMedium,),
const SizedBox(height: AppSpacing.xs),
Text(
'${txn.invoiceNumber} · ${Formatters.dateTime(txn.createdAt)}',
@@ -172,7 +172,7 @@ class _ReceiptScreenState extends ConsumerState<ReceiptScreen> {
const SizedBox(height: AppSpacing.xxl),
Container(
padding: const EdgeInsets.all(AppSpacing.xl),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppRadius.brLg,
),
@@ -188,12 +188,12 @@ class _ReceiptScreenState extends ConsumerState<ReceiptScreen> {
_row('Paid via', txn.paymentSummary),
if (txn.changeDue > 0)
_row('Change returned', Formatters.money(txn.changeDue),
highlight: AppColors.success),
highlight: AppColors.success,),
_row('Items', '${txn.cart.lineCount}'),
if (txn.customer != null) ...[
_row('Customer', txn.customer!.name),
_row('Points earned', '+${txn.pointsEarned}',
highlight: AppColors.success),
highlight: AppColors.success,),
if (txn.pointsRedeemed > 0)
_row('Points redeemed', '-${txn.pointsRedeemed}'),
],
@@ -203,7 +203,7 @@ class _ReceiptScreenState extends ConsumerState<ReceiptScreen> {
Formatters.money(txn.cart.totalSavings),
highlight: AppColors.success,
),
]),
],),
),
const SizedBox(height: AppSpacing.xxl),
@@ -256,7 +256,7 @@ class _ReceiptScreenState extends ConsumerState<ReceiptScreen> {
content: Text(
'Could not open WhatsApp on this device.',
),
));
),);
},
),
),
@@ -314,7 +314,7 @@ class _ReceiptScreenState extends ConsumerState<ReceiptScreen> {
style: const TextStyle(
fontSize: 13.5,
color: AppColors.textSecondary,
)),
),),
Text(
value,
style: TextStyle(

View File

@@ -43,7 +43,7 @@ class ReceiptPreview extends StatelessWidget {
final discount = gross - cart.netAmount;
return Container(
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.surface,
borderRadius: AppRadius.brLg,
boxShadow: AppColors.shadowMd,
@@ -70,20 +70,20 @@ class ReceiptPreview extends StatelessWidget {
.copyWith(fontWeight: FontWeight.w700),
),
Text(AppConstants.storeLegalName,
style: AppTypography.mono(8.5)),
style: AppTypography.mono(8.5),),
const SizedBox(height: 2),
Text(AppConstants.storeAddress,
textAlign: TextAlign.center,
style: AppTypography.mono(8.5)),
style: AppTypography.mono(8.5),),
Text('Customer care : ${AppConstants.storePhone}',
style: AppTypography.mono(8.5)),
style: AppTypography.mono(8.5),),
Text('CIN No : ${AppConstants.storeCin}',
style: AppTypography.mono(8.5)),
style: AppTypography.mono(8.5),),
Text('GSTIN : ${AppConstants.storeGstin}',
style: AppTypography.mono(8.5)),
style: AppTypography.mono(8.5),),
Text('FSSAI Lic No : ${AppConstants.storeFssai}',
style: AppTypography.mono(8.5)),
]),
style: AppTypography.mono(8.5),),
],),
),
if (discount > 0) ...[
@@ -104,10 +104,10 @@ class ReceiptPreview extends StatelessWidget {
child: Column(children: [
Text('TAX INVOICE',
style: AppTypography.mono(11)
.copyWith(fontWeight: FontWeight.w700)),
.copyWith(fontWeight: FontWeight.w700),),
Text('xxxxxx Original for Recipient xxxxxx',
style: AppTypography.mono(8)),
]),
style: AppTypography.mono(8),),
],),
),
const SizedBox(height: AppSpacing.sm),
@@ -117,7 +117,7 @@ class ReceiptPreview extends StatelessWidget {
_plain('Customer Type: '
'${txn.customer == null ? 'URD' : 'REG'}'),
_row('Date:${Formatters.receiptStamp(txn.createdAt)}',
'Bill No:${txn.invoiceNumber.split('-').last}'),
'Bill No:${txn.invoiceNumber.split('-').last}',),
_row(
'Store:${AppConstants.storeCode} '
'Cashier:${txn.cashierName}',
@@ -135,13 +135,13 @@ class ReceiptPreview extends StatelessWidget {
Expanded(flex: 34, child: _bold('Item Description')),
Expanded(
flex: 16,
child: _bold('Net Price', align: TextAlign.right)),
child: _bold('Net Price', align: TextAlign.right),),
Expanded(
flex: 8, child: _bold('Qty', align: TextAlign.right)),
flex: 8, child: _bold('Qty', align: TextAlign.right),),
Expanded(
flex: 18,
child: _bold('Value', align: TextAlign.right)),
]),
child: _bold('Value', align: TextAlign.right),),
],),
const SizedBox(height: 3),
for (final slab in slabs) ...[
@@ -162,28 +162,28 @@ class ReceiptPreview extends StatelessWidget {
child: Row(children: [
Expanded(
flex: 12,
child: _cell(line.product.hsnCode ?? '-')),
child: _cell(line.product.hsnCode ?? '-'),),
Expanded(
flex: 34,
child:
_cell(line.product.name.toUpperCase())),
_cell(line.product.name.toUpperCase()),),
Expanded(
flex: 16,
child: _cell(
line.product.price.toStringAsFixed(2),
align: TextAlign.right),
align: TextAlign.right,),
),
Expanded(
flex: 8,
child: _cell(_qty(line.quantity),
align: TextAlign.right),
align: TextAlign.right,),
),
Expanded(
flex: 18,
child: _cell(line.payable.toStringAsFixed(2),
align: TextAlign.right),
align: TextAlign.right,),
),
]),
],),
),
],
@@ -197,7 +197,7 @@ class ReceiptPreview extends StatelessWidget {
_amount('Gross Sales Value', gross),
if (discount > 0) _amount('Total Discount', discount),
_amount(
'Net Sales Value (Inclusive of GST)', cart.netAmount),
'Net Sales Value (Inclusive of GST)', cart.netAmount,),
if (cart.roundOff != 0)
_amount('Round Off', cart.roundOff),
_amount('Total Amount Paid', txn.total, bold: true),
@@ -206,34 +206,34 @@ class ReceiptPreview extends StatelessWidget {
if (txn.changeDue > 0)
_amount('Change Returned', txn.changeDue),
Text('(AMOUNT INCLUSIVE OF APPLICABLE TAXES)',
style: AppTypography.mono(8)),
style: AppTypography.mono(8),),
const SizedBox(height: AppSpacing.md),
// ------------------------------------------- gst breakup
Center(
child: Text('------GST Breakup Details------ Amount (INR)',
style: AppTypography.mono(8.5)),
style: AppTypography.mono(8.5),),
),
const SizedBox(height: 4),
Row(children: [
Expanded(flex: 10, child: _bold('GST')),
Expanded(
flex: 20,
child: _bold('Taxable', align: TextAlign.right)),
child: _bold('Taxable', align: TextAlign.right),),
Expanded(
flex: 16,
child: _bold('CGST', align: TextAlign.right)),
child: _bold('CGST', align: TextAlign.right),),
Expanded(
flex: 16,
child: _bold('SGST', align: TextAlign.right)),
child: _bold('SGST', align: TextAlign.right),),
Expanded(
flex: 14,
child: _bold('CESS', align: TextAlign.right)),
child: _bold('CESS', align: TextAlign.right),),
Expanded(
flex: 20,
child: _bold('Total', align: TextAlign.right)),
]),
child: _bold('Total', align: TextAlign.right),),
],),
const SizedBox(height: 2),
for (final s in slabs)
Padding(
@@ -243,23 +243,23 @@ class ReceiptPreview extends StatelessWidget {
Expanded(
flex: 20,
child: _cell(s.taxable.toStringAsFixed(2),
align: TextAlign.right)),
align: TextAlign.right,),),
Expanded(
flex: 16,
child: _cell(s.cgst.toStringAsFixed(2),
align: TextAlign.right)),
align: TextAlign.right,),),
Expanded(
flex: 16,
child: _cell(s.sgst.toStringAsFixed(2),
align: TextAlign.right)),
align: TextAlign.right,),),
Expanded(
flex: 14,
child: _cell('0.00', align: TextAlign.right)),
child: _cell('0.00', align: TextAlign.right),),
Expanded(
flex: 20,
child: _cell(s.total.toStringAsFixed(2),
align: TextAlign.right)),
]),
align: TextAlign.right,),),
],),
),
const _Dashes(),
Row(children: [
@@ -270,32 +270,32 @@ class ReceiptPreview extends StatelessWidget {
slabs
.fold(0.0, (a, s) => a + s.taxable)
.toStringAsFixed(2),
align: TextAlign.right)),
align: TextAlign.right,),),
Expanded(
flex: 16,
child: _bold(
slabs
.fold(0.0, (a, s) => a + s.cgst)
.toStringAsFixed(2),
align: TextAlign.right)),
align: TextAlign.right,),),
Expanded(
flex: 16,
child: _bold(
slabs
.fold(0.0, (a, s) => a + s.sgst)
.toStringAsFixed(2),
align: TextAlign.right)),
align: TextAlign.right,),),
Expanded(
flex: 14,
child: _bold('0.00', align: TextAlign.right)),
child: _bold('0.00', align: TextAlign.right),),
Expanded(
flex: 20,
child: _bold(
slabs
.fold(0.0, (a, s) => a + s.total)
.toStringAsFixed(2),
align: TextAlign.right)),
]),
align: TextAlign.right,),),
],),
const _Dashes(),
_plain('TaxInvoice# ${txn.invoiceNumber}'),
@@ -311,10 +311,10 @@ class ReceiptPreview extends StatelessWidget {
const SizedBox(height: AppSpacing.sm),
Text('* Thank You for Shopping with us *',
style: AppTypography.mono(10)
.copyWith(fontWeight: FontWeight.w700)),
.copyWith(fontWeight: FontWeight.w700),),
Text('Powered by Nearle POS',
style: AppTypography.mono(8)),
]),
style: AppTypography.mono(8),),
],),
),
],
),
@@ -322,7 +322,7 @@ class ReceiptPreview extends StatelessWidget {
),
),
const _Perforation(top: false),
]),
],),
);
}
@@ -342,7 +342,7 @@ class ReceiptPreview extends StatelessWidget {
Flexible(
child: Text(left,
overflow: TextOverflow.ellipsis,
style: AppTypography.mono(9)),
style: AppTypography.mono(9),),
),
const SizedBox(width: AppSpacing.sm),
Text(right, style: AppTypography.mono(9)),
@@ -518,6 +518,6 @@ class _FakeBarcode extends StatelessWidget {
),
const SizedBox(height: 3),
Text(value, style: AppTypography.mono(9)),
]);
],);
}
}

View File

@@ -97,7 +97,7 @@ final unsyncedCountProvider = FutureProvider<int>((ref) {
return ref.watch(syncRepositoryProvider).unsyncedCount();
});
/// Today's totals, read back from SQLite.
/// Everything this terminal traded today, across every operator.
final todayReportProvider = FutureProvider<ShiftReport>((ref) {
ref.watch(orderVersionProvider);
final session = ref.watch(cashierSessionProvider);
@@ -109,6 +109,22 @@ final todayReportProvider = FutureProvider<ShiftReport>((ref) {
);
});
/// Only the bills the signed-in operator rang.
///
/// This is the figure a cashier counts their drawer against at the end of a
/// shift, so it must not include anyone else's sales.
final myShiftReportProvider = FutureProvider<ShiftReport>((ref) {
ref.watch(orderVersionProvider);
final session = ref.watch(cashierSessionProvider);
final user = ref.watch(currentUserProvider);
return ref.watch(syncRepositoryProvider).todayReport(
terminalId: session.terminalId,
cashierName: user?.name ?? session.name,
scopeToCashier: true,
);
});
/// Per-order sync state for the events log.
final orderSyncRowsProvider = FutureProvider<List<OrderSyncRow>>((ref) {
ref.watch(orderVersionProvider);

View File

@@ -56,7 +56,8 @@ class _SignOutDialogState extends ConsumerState<_SignOutDialog> {
@override
Widget build(BuildContext context) {
final report = ref.watch(todayReportProvider).value;
// The operator is settling their own till, so this covers only their bills.
final report = ref.watch(myShiftReportProvider).value;
final pending = ref.watch(unsyncedCountProvider).value ?? 0;
final cart = ref.watch(cartControllerProvider);
final pushing = ref.watch(orderSyncProvider) is SyncRunning;
@@ -111,7 +112,7 @@ class _SignOutDialogState extends ConsumerState<_SignOutDialog> {
_row('Items sold', report.itemCount.toStringAsFixed(0)),
_row('Gross sales', Formatters.money(report.grossSales)),
_row('GST collected',
Formatters.money(report.taxCollected)),
Formatters.money(report.taxCollected),),
],
],