import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/router/app_router.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_typography.dart'; import '../../../core/utils/extensions.dart'; import '../../../core/utils/formatters.dart'; import '../../../core/widgets/glass_card.dart'; import '../../../core/widgets/numeric_keypad.dart'; import '../../../core/widgets/primary_button.dart'; import '../../../domain/entities/transaction.dart'; import '../../pos/providers/cart_controller.dart'; import '../providers/payment_controller.dart'; class PaymentScreen extends ConsumerStatefulWidget { const PaymentScreen({super.key}); @override ConsumerState createState() => _PaymentScreenState(); } class _PaymentScreenState extends ConsumerState { String _cashBuffer = ''; void _syncCash() { final value = double.tryParse(_cashBuffer) ?? 0; ref.read(paymentControllerProvider.notifier).setCashTendered(value); } void _appendCash(String d) { if (d == '.' && _cashBuffer.contains('.')) return; if (_cashBuffer.length >= 8) return; setState(() => _cashBuffer += d); _syncCash(); } void _backspaceCash() { if (_cashBuffer.isEmpty) return; setState(() => _cashBuffer = _cashBuffer.substring(0, _cashBuffer.length - 1)); _syncCash(); } void _setCash(double value) { setState(() => _cashBuffer = value.toStringAsFixed(0)); _syncCash(); } Future _confirm() async { final result = await ref.read(paymentControllerProvider.notifier).confirm(); if (result == null || !mounted) return; // Sale is banked — clear the terminal and show the receipt. ref.read(cartControllerProvider.notifier).reset(); context.go(AppRoutes.receipt, extra: result.transaction); } @override Widget build(BuildContext context) { final cart = ref.watch(cartControllerProvider); final state = ref.watch(paymentControllerProvider); final controller = ref.read(paymentControllerProvider.notifier); return Scaffold( backgroundColor: AppColors.background, appBar: AppBar( title: const Text('Payment'), leading: IconButton( icon: const Icon(Icons.arrow_back_rounded), onPressed: () => context.pop(), ), ), body: Padding( padding: const EdgeInsets.all(AppSpacing.xxl), child: context.isCompact ? SingleChildScrollView( child: Column(children: [ _amountCard(controller, state), const SizedBox(height: AppSpacing.lg), _methodsCard(controller, state), const SizedBox(height: AppSpacing.lg), _tenderCard(controller, state), ]), ) : Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( flex: 4, child: Column(children: [ _amountCard(controller, state), const SizedBox(height: AppSpacing.lg), Expanded(child: _methodsCard(controller, state)), ]), ), const SizedBox(width: AppSpacing.lg), Expanded(flex: 5, child: _tenderCard(controller, state)), ], ), ), bottomNavigationBar: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB( AppSpacing.xxl, 0, AppSpacing.xxl, AppSpacing.xxl, ), child: Column(mainAxisSize: MainAxisSize.min, children: [ if (state.error != null) ...[ Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), margin: const EdgeInsets.only(bottom: AppSpacing.md), decoration: BoxDecoration( color: AppColors.dangerSurface, borderRadius: AppRadius.brSm, ), child: Row(children: [ const Icon(Icons.error_outline_rounded, color: AppColors.danger, size: 18), const SizedBox(width: AppSpacing.sm), Expanded( child: Text(state.error!, style: const TextStyle(color: AppColors.danger)), ), ]), ).animate().shake(duration: 300.ms, hz: 3), ], PrimaryButton( label: 'Complete Sale', icon: Icons.check_circle_outline_rounded, large: true, tone: ButtonTone.success, busy: state.isProcessing, onPressed: cart.isEmpty ? null : _confirm, trailing: Text( Formatters.money(cart.grandTotal), style: AppTypography.money(21, color: Colors.white), ), ), ]), ), ), ); } // ------------------------------------------------------------- Sections Widget _amountCard(PaymentController controller, PaymentState state) { final cart = ref.watch(cartControllerProvider); return GlassCard( padding: const EdgeInsets.all(AppSpacing.xxl), radius: AppRadius.xl, tinted: true, child: Column(children: [ Text('Amount due', style: context.text.labelMedium), const SizedBox(height: AppSpacing.xs), Text( Formatters.money(controller.balanceDue), style: AppTypography.money(40, color: AppColors.primary), ), const SizedBox(height: AppSpacing.md), Row(mainAxisAlignment: MainAxisAlignment.center, children: [ _mini('Items', '${cart.lineCount}'), _dot(), _mini('Bill total', Formatters.money(cart.grandTotal)), if (state.settled > 0) ...[ _dot(), _mini('Settled', Formatters.money(state.settled)), ], ]), ]), ); } Widget _methodsCard(PaymentController controller, PaymentState state) { return GlassCard( padding: const EdgeInsets.all(AppSpacing.xl), radius: AppRadius.xl, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('Payment method', style: context.text.titleMedium), const SizedBox(height: AppSpacing.lg), Wrap( spacing: AppSpacing.md, runSpacing: AppSpacing.md, children: PaymentMethod.values .where((m) => m != PaymentMethod.loyalty) .map((m) => _MethodTile( method: m, selected: state.activeMethod == m, onTap: () { setState(() => _cashBuffer = ''); controller.selectMethod(m); }, )) .toList(), ), if (state.splits.isNotEmpty) ...[ const SizedBox(height: AppSpacing.xl), const Divider(), const SizedBox(height: AppSpacing.md), Row(children: [ Text('Split tenders', style: context.text.titleSmall), const Spacer(), TextButton( onPressed: controller.clearSplits, style: TextButton.styleFrom( foregroundColor: AppColors.danger), child: const Text('Clear all'), ), ]), const SizedBox(height: AppSpacing.sm), ...state.splits.asMap().entries.map((e) => Padding( padding: const EdgeInsets.only(bottom: AppSpacing.sm), child: Row(children: [ Text(e.value.method.emoji, style: const TextStyle(fontSize: 16)), const SizedBox(width: AppSpacing.sm), Expanded(child: Text(e.value.method.label)), Text(Formatters.money(e.value.amount), style: AppTypography.money(14.5)), IconButton( onPressed: () => controller.removeSplit(e.key), icon: const Icon(Icons.close_rounded, size: 17), color: AppColors.textTertiary, constraints: const BoxConstraints(minWidth: 30, minHeight: 30), padding: EdgeInsets.zero, ), ]), )), ], ], ), ); } Widget _tenderCard(PaymentController controller, PaymentState state) { final isCash = state.activeMethod.needsChange; return GlassCard( padding: const EdgeInsets.all(AppSpacing.xl), radius: AppRadius.xl, child: isCash ? _cashTender(controller, state) : _referenceTender(controller, state), ); } Widget _cashTender(PaymentController controller, PaymentState state) { final change = controller.changeDue; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('Cash received', style: context.text.titleMedium), const SizedBox(height: AppSpacing.md), Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.xl, vertical: AppSpacing.lg, ), decoration: BoxDecoration( color: AppColors.surfaceAlt, borderRadius: AppRadius.brLg, border: Border.all(color: AppColors.border), ), child: Row(children: [ const Text('₹', style: TextStyle(fontSize: 24, color: AppColors.textTertiary)), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( _cashBuffer.isEmpty ? '0' : _cashBuffer, style: AppTypography.money(30), ), ), ]), ), const SizedBox(height: AppSpacing.md), Wrap( spacing: AppSpacing.sm, runSpacing: AppSpacing.sm, children: [ ActionChip( avatar: const Icon(Icons.done_all_rounded, size: 15), label: const Text('Exact'), onPressed: () => _setCash(controller.balanceDue), ), ...[50, 100, 200, 500, 2000].map( (note) => ActionChip( label: Text('₹$note'), onPressed: () => _setCash( (double.tryParse(_cashBuffer) ?? 0) + note, ), ), ), ], ), const SizedBox(height: AppSpacing.lg), AnimatedContainer( duration: AppMotion.normal, padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: change > 0 ? AppColors.successSurface : AppColors.surfaceAlt, borderRadius: AppRadius.brMd, ), child: Row(children: [ Icon( change > 0 ? Icons.currency_exchange_rounded : Icons.info_outline_rounded, size: 19, color: change > 0 ? AppColors.success : AppColors.textTertiary, ), const SizedBox(width: AppSpacing.md), Text( 'Change to return', style: TextStyle( fontWeight: FontWeight.w600, color: change > 0 ? AppColors.success : AppColors.textSecondary, ), ), const Spacer(), Text( Formatters.money(change), style: AppTypography.money( 22, color: change > 0 ? AppColors.success : AppColors.textTertiary, ), ), ]), ), const SizedBox(height: AppSpacing.lg), Center( child: NumericKeypad( allowDecimal: true, maxWidth: 330, onKey: _appendCash, onBackspace: _backspaceCash, ), ), const SizedBox(height: AppSpacing.md), OutlinedButton.icon( onPressed: controller.balanceDue > 0 ? () { controller.addSplit( amount: (double.tryParse(_cashBuffer) ?? 0) .clamp(0, controller.balanceDue) .toDouble(), ); setState(() => _cashBuffer = ''); } : null, icon: const Icon(Icons.call_split_rounded, size: 17), label: const Text('Add as split payment'), ), ], ); } Widget _referenceTender(PaymentController controller, PaymentState state) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row(children: [ Text(state.activeMethod.emoji, style: const TextStyle(fontSize: 22)), const SizedBox(width: AppSpacing.sm), Text('${state.activeMethod.label} payment', style: context.text.titleMedium), ]), const SizedBox(height: AppSpacing.xxl), Center( child: Column(children: [ Container( width: 120, height: 120, decoration: BoxDecoration( color: AppColors.primarySurface, borderRadius: AppRadius.brXl, ), alignment: Alignment.center, child: Text(state.activeMethod.emoji, style: const TextStyle(fontSize: 52)), ), const SizedBox(height: AppSpacing.lg), Text( 'Charge ${Formatters.money(controller.balanceDue)} ' 'on the ${state.activeMethod.label.toLowerCase()} terminal', textAlign: TextAlign.center, style: context.text.bodyMedium, ), ]), ), 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.xxxl), OutlinedButton.icon( onPressed: controller.balanceDue > 0 ? () => controller.addSplit() : null, icon: const Icon(Icons.call_split_rounded, size: 17), label: const Text('Add as split payment'), ), ], ); } Widget _mini(String label, String value) => Column(children: [ Text(label, style: const TextStyle( fontSize: 11, color: AppColors.textSecondary, )), Text(value, style: AppTypography.money(14, weight: FontWeight.w600)), ]); Widget _dot() => Container( width: 3, height: 3, margin: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), decoration: const BoxDecoration( color: AppColors.textTertiary, shape: BoxShape.circle, ), ); } class _MethodTile extends StatelessWidget { const _MethodTile({ required this.method, required this.selected, required this.onTap, }); final PaymentMethod method; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { return Material( color: selected ? AppColors.primary : AppColors.surfaceAlt, borderRadius: AppRadius.brMd, child: InkWell( onTap: onTap, borderRadius: AppRadius.brMd, child: AnimatedContainer( duration: AppMotion.fast, width: 118, padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.lg, ), decoration: BoxDecoration( borderRadius: AppRadius.brMd, border: Border.all( color: selected ? AppColors.primary : AppColors.border, ), ), child: Column(children: [ Text(method.emoji, style: const TextStyle(fontSize: 24)), const SizedBox(height: AppSpacing.sm), Text( method.label, textAlign: TextAlign.center, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: selected ? Colors.white : AppColors.textPrimary, ), ), ]), ), ), ); } }