third commit

This commit is contained in:
2026-07-31 17:06:52 +05:30
parent d9886880cc
commit 9891a69a5f
32 changed files with 2083 additions and 557 deletions

View File

@@ -74,11 +74,31 @@ class PaymentController extends StateNotifier<PaymentState> {
return diff > 0 ? diff.asMoney : 0;
}
/// 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.
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 && balanceDue > 0;
return state.cashTendered >= balanceDue;
}
return balanceDue > 0;
return true;
}
/// 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;
}
void selectMethod(PaymentMethod method) {
@@ -156,10 +176,10 @@ class PaymentController extends StateNotifier<PaymentState> {
state = state.copyWith(isProcessing: false, result: result);
// Fire and forget — printing must never block the next sale.
final receipts = _ref.read(receiptServiceProvider);
unawaited(receipts.printDirect(result.transaction));
unawaited(receipts.openCashDrawer());
// No auto-print: with no roll printer attached this silently failed and
// looked like a bug. The receipt screen shows the bill on the terminal
// and offers Print, WhatsApp and Share explicitly.
unawaited(_ref.read(receiptServiceProvider).openCashDrawer());
unawaited(_ref.read(soundServiceProvider).saleComplete());
// Stock changed, so the grid must refresh; the new order changes the

View File

@@ -125,7 +125,7 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
);
},
),
bottomNavigationBar: _bottomBar(cart.grandTotal, state, cart.isEmpty),
bottomNavigationBar: _bottomBar(controller, state, cart.grandTotal),
);
}
@@ -557,7 +557,15 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
}
// ------------------------------------------------------------ Bottom bar
Widget _bottomBar(double total, PaymentState state, bool cartEmpty) {
Widget _bottomBar(
PaymentController controller,
PaymentState state,
double total,
) {
// A cash sale cannot complete until the cashier says what was handed over.
final canComplete = controller.canConfirm;
final reason = controller.blockedReason;
return SafeArea(
child: Container(
padding: const EdgeInsets.fromLTRB(
@@ -599,13 +607,45 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
],
),
).animate().shake(duration: 300.ms, hz: 3),
if (reason != null && state.error == null)
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Row(
children: [
const Icon(Icons.info_outline_rounded,
size: 15, color: AppColors.textTertiary),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
reason,
style: const TextStyle(
fontSize: 12.5,
color: AppColors.textTertiary,
),
),
),
if (state.activeMethod.needsChange)
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)),
),
],
),
),
PrimaryButton(
label: 'Complete Sale',
icon: Icons.check_circle_outline_rounded,
large: true,
tone: ButtonTone.success,
busy: state.isProcessing,
onPressed: cartEmpty ? null : _confirm,
onPressed: canComplete ? _confirm : null,
trailing: Text(
Formatters.money(total),
style: AppTypography.money(19, color: Colors.white),