added product import and billing integration

This commit is contained in:
2026-08-05 18:15:21 +05:30
parent 33b4337933
commit 6c0266c9c7
32 changed files with 889 additions and 457 deletions

View File

@@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../app/providers.dart';
import '../../../core/utils/extensions.dart';
import '../../../data/sync/sync_engine.dart';
import '../../../domain/entities/transaction.dart';
import '../../../domain/usecases/checkout_sale.dart';
import '../../auth/providers/auth_controller.dart';
@@ -80,29 +79,27 @@ class PaymentController extends StateNotifier<PaymentState> {
/// Whether Complete Sale should be enabled.
///
/// Cash is the strict case: the drawer cannot be reconciled and no change can
/// be calculated unless the cashier states what was handed over. Card, UPI
/// and wallet settle on the external terminal, so they need no amount here.
/// Every method now requires the cashier to state what was actually
/// received before the sale can complete — a tap on Exact for the common
/// case, or a typed amount for a partial tender. `cashTendered` is the
/// amount entered for whichever method is currently active, not literally
/// cash; the name stayed to keep this change out of the rest of the app.
bool get canConfirm {
if (_billTotal <= 0) return false;
// Staged splits already cover the bill.
if (balanceDue <= 0.01) return true;
if (state.activeMethod.needsChange) {
return state.cashTendered >= balanceDue;
}
return true;
return state.cashTendered >= balanceDue;
}
/// Why the button is disabled, for display next to it.
String? get blockedReason {
if (_billTotal <= 0) return 'Add at least one item before charging.';
if (canConfirm) return null;
if (state.activeMethod.needsChange) {
return 'Enter the cash received, or tap Exact.';
}
return null;
return state.activeMethod.needsChange
? 'Enter the cash received, or tap Exact.'
: 'Enter the amount received, or tap Exact.';
}
void selectMethod(PaymentMethod method) {
@@ -212,10 +209,11 @@ class PaymentController extends StateNotifier<PaymentState> {
_ref.invalidate(visibleProductsProvider);
_ref.read(orderVersionProvider.notifier).state++;
// The bill is safely on disk; getting it to the back office is the
// engine's problem now. Deliberately not awaited — the cashier must
// reach the receipt screen at network speed of zero.
_ref.read(syncEngineProvider).nudge(SyncTrigger.saleCommitted);
// Deliberately NOT nudging the sync engine here. The bill sits on this
// terminal, unsynced, until the receipt screen's cancellation window
// closes — either it is pressed past early with New Sale, or the
// window runs out — or the sale is voided if the cashier cancels
// instead. See ReceiptScreen.
return result;
} on CheckoutFailure catch (e) {

View File

@@ -350,20 +350,41 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
return GlassCard(
padding: const EdgeInsets.all(AppSpacing.lg),
radius: AppRadius.xl,
child: state.activeMethod.needsChange
? _cashTender(controller)
: _referenceTender(controller, state),
child: _amountTender(controller, state),
);
}
Widget _cashTender(PaymentController controller) {
/// Amount entry for whichever method is active. Every method works the
/// same way now — a keypad, an Exact shortcut, and an explicit amount —
/// rather than cash alone asking what was received while card/UPI/wallet
/// silently assumed the full balance. Cash additionally gets denomination
/// chips and a change-due row, since only cash can be over-tendered; a
/// method that captures a reference (card, UPI, gift card) additionally
/// gets that field below the keypad.
Widget _amountTender(PaymentController controller, PaymentState state) {
final cash = state.activeMethod.needsChange;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Cash received',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
Row(
children: [
Text(state.activeMethod.emoji, style: const TextStyle(fontSize: 20)),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
cash
? 'Cash received'
: '${state.activeMethod.label} amount received',
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: AppSpacing.md),
Container(
@@ -405,16 +426,20 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
label: const Text('Exact'),
onPressed: () => _setCash(controller.balanceDue),
),
for (final note in const [50, 100, 200, 500, 2000])
ActionChip(
label: Text('$note'),
onPressed: () =>
_setCash((double.tryParse(_cashBuffer) ?? 0) + note),
),
// Denomination shortcuts only make sense for physical notes.
if (cash)
for (final note in const [50, 100, 200, 500, 2000])
ActionChip(
label: Text('$note'),
onPressed: () =>
_setCash((double.tryParse(_cashBuffer) ?? 0) + note),
),
],
),
const SizedBox(height: AppSpacing.md),
_changeRow(controller.changeDue),
if (cash) ...[
const SizedBox(height: AppSpacing.md),
_changeRow(controller.changeDue),
],
const SizedBox(height: AppSpacing.md),
Center(
child: NumericKeypad(
@@ -424,6 +449,21 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
onBackspace: _backspaceCash,
),
),
if (state.activeMethod.needsReference) ...[
const SizedBox(height: AppSpacing.md),
TextField(
onChanged: controller.setReference,
decoration: InputDecoration(
labelText: switch (state.activeMethod) {
PaymentMethod.card => 'Approval code',
PaymentMethod.upi => 'UPI transaction ID',
PaymentMethod.giftCard => 'Gift card number',
_ => 'Reference',
},
prefixIcon: const Icon(Icons.tag_rounded),
),
),
],
const SizedBox(height: AppSpacing.md),
_splitButton(controller, amount: double.tryParse(_cashBuffer) ?? 0),
],
@@ -473,71 +513,6 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
);
}
Widget _referenceTender(PaymentController controller, PaymentState state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(state.activeMethod.emoji,
style: const TextStyle(fontSize: 20),),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
'${state.activeMethod.label} payment',
overflow: TextOverflow.ellipsis,
style:
const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
),
],
),
const SizedBox(height: AppSpacing.xxl),
Center(
child: Container(
width: 104,
height: 104,
decoration: const BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppRadius.brXl,
),
alignment: Alignment.center,
child: Text(state.activeMethod.emoji,
style: const TextStyle(fontSize: 46),),
),
),
const SizedBox(height: AppSpacing.lg),
Text(
'Charge ${Formatters.money(controller.balanceDue)} on the '
'${state.activeMethod.label.toLowerCase()} terminal',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 13.5,
color: AppColors.textSecondary,
height: 1.5,
),
),
const SizedBox(height: AppSpacing.xxl),
if (state.activeMethod.needsReference)
TextField(
onChanged: controller.setReference,
decoration: InputDecoration(
labelText: switch (state.activeMethod) {
PaymentMethod.card => 'Approval code',
PaymentMethod.upi => 'UPI transaction ID',
PaymentMethod.giftCard => 'Gift card number',
_ => 'Reference',
},
prefixIcon: const Icon(Icons.tag_rounded),
),
),
const SizedBox(height: AppSpacing.lg),
_splitButton(controller),
],
);
}
Widget _splitButton(PaymentController controller, {double? amount}) {
return OutlinedButton.icon(
onPressed: controller.balanceDue > 0
@@ -622,18 +597,17 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
),
),
),
if (state.activeMethod.needsChange)
TextButton(
onPressed: () => _setCash(controller.balanceDue),
style: TextButton.styleFrom(
minimumSize: const Size(0, 30),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
),
TextButton(
onPressed: () => _setCash(controller.balanceDue),
style: TextButton.styleFrom(
minimumSize: const Size(0, 30),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
),
child: const Text('Exact',
style: TextStyle(fontSize: 12.5),),
),
child: const Text('Exact',
style: TextStyle(fontSize: 12.5),),
),
],
),
),