added local db
This commit is contained in:
@@ -7,18 +7,26 @@ 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 '../../../core/widgets/status_pill.dart';
|
||||
import '../../../domain/entities/transaction.dart';
|
||||
import '../../customer/widgets/customer_capture_sheet.dart';
|
||||
import '../../pos/providers/cart_controller.dart';
|
||||
import '../providers/payment_controller.dart';
|
||||
|
||||
/// Tender capture and sale completion.
|
||||
///
|
||||
/// Customer identification happens here rather than at the start of the sale:
|
||||
/// a mobile number is all that is asked for, and the step can be skipped.
|
||||
class PaymentScreen extends ConsumerStatefulWidget {
|
||||
const PaymentScreen({super.key});
|
||||
|
||||
/// Below this the two columns stack into one scrolling page.
|
||||
static const double twoColumnAbove = 1080;
|
||||
|
||||
@override
|
||||
ConsumerState<PaymentScreen> createState() => _PaymentScreenState();
|
||||
}
|
||||
@@ -27,8 +35,9 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
|
||||
String _cashBuffer = '';
|
||||
|
||||
void _syncCash() {
|
||||
final value = double.tryParse(_cashBuffer) ?? 0;
|
||||
ref.read(paymentControllerProvider.notifier).setCashTendered(value);
|
||||
ref
|
||||
.read(paymentControllerProvider.notifier)
|
||||
.setCashTendered(double.tryParse(_cashBuffer) ?? 0);
|
||||
}
|
||||
|
||||
void _appendCash(String d) {
|
||||
@@ -40,7 +49,8 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
|
||||
|
||||
void _backspaceCash() {
|
||||
if (_cashBuffer.isEmpty) return;
|
||||
setState(() => _cashBuffer = _cashBuffer.substring(0, _cashBuffer.length - 1));
|
||||
setState(
|
||||
() => _cashBuffer = _cashBuffer.substring(0, _cashBuffer.length - 1));
|
||||
_syncCash();
|
||||
}
|
||||
|
||||
@@ -53,7 +63,6 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
|
||||
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);
|
||||
}
|
||||
@@ -71,221 +80,321 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
onPressed: () => context.pop(),
|
||||
tooltip: 'Back to bill',
|
||||
),
|
||||
),
|
||||
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),
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final twoColumn = constraints.maxWidth >= PaymentScreen.twoColumnAbove;
|
||||
final pad =
|
||||
constraints.maxWidth < 700 ? AppSpacing.lg : AppSpacing.xxl;
|
||||
|
||||
final left = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_amountCard(controller, state, cart.grandTotal, cart.lineCount),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_customerCard(),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_methodsCard(controller, state),
|
||||
],
|
||||
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),
|
||||
);
|
||||
|
||||
final right = _tenderCard(controller, state);
|
||||
|
||||
if (!twoColumn) {
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.all(pad),
|
||||
child: Column(
|
||||
children: [left, const SizedBox(height: AppSpacing.md), right],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(pad),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(flex: 4, child: SingleChildScrollView(child: left)),
|
||||
const SizedBox(width: AppSpacing.lg),
|
||||
Expanded(flex: 5, child: SingleChildScrollView(child: right)),
|
||||
],
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: _bottomBar(cart.grandTotal, state, cart.isEmpty),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- 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) {
|
||||
// ------------------------------------------------------------ Amount due
|
||||
Widget _amountCard(
|
||||
PaymentController controller,
|
||||
PaymentState state,
|
||||
double total,
|
||||
int lineCount,
|
||||
) {
|
||||
return GlassCard(
|
||||
padding: const EdgeInsets.all(AppSpacing.xl),
|
||||
radius: AppRadius.xl,
|
||||
tinted: true,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Amount due',
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textSecondary,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
// Scales instead of clipping when the amount runs long.
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
Formatters.money(controller.balanceDue),
|
||||
style: AppTypography.money(38, color: AppColors.primary),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: AppSpacing.lg,
|
||||
runSpacing: AppSpacing.xs,
|
||||
children: [
|
||||
_mini('Items', '$lineCount'),
|
||||
_mini('Bill total', Formatters.money(total)),
|
||||
if (state.settled > 0)
|
||||
_mini('Settled', Formatters.money(state.settled)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- Customer
|
||||
Widget _customerCard() {
|
||||
final cart = ref.watch(cartControllerProvider);
|
||||
final customer = cart.customer;
|
||||
|
||||
return GlassCard(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
radius: AppRadius.xl,
|
||||
onTap: () => showCustomerCaptureSheet(context),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: customer == null
|
||||
? AppColors.surfaceAlt
|
||||
: AppColors.primarySurface,
|
||||
child: customer == null
|
||||
? const Icon(Icons.person_add_alt_1_outlined,
|
||||
size: 19, color: AppColors.textSecondary)
|
||||
: Text(
|
||||
Formatters.initials(customer.name),
|
||||
style: const TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
customer?.name ?? 'Walk-in customer',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (customer != null) ...[
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
StatusPill.tier(customer.tier, dense: true),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
customer == null
|
||||
? 'Add a mobile number for loyalty — or skip'
|
||||
: '${Formatters.mobile(customer.mobile)} · earns '
|
||||
'+${cart.pointsEarned} pts',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
TextButton(
|
||||
onPressed: () => showCustomerCaptureSheet(context),
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: const Size(0, 38),
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
),
|
||||
child: Text(customer == null ? 'Add' : 'Change'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- Methods
|
||||
Widget _methodsCard(PaymentController controller, PaymentState state) {
|
||||
return GlassCard(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
radius: AppRadius.xl,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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(),
|
||||
const Text(
|
||||
'Payment method',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Wrap(
|
||||
spacing: AppSpacing.sm,
|
||||
runSpacing: AppSpacing.sm,
|
||||
children: [
|
||||
for (final m in PaymentMethod.values)
|
||||
if (m != PaymentMethod.loyalty)
|
||||
_MethodTile(
|
||||
method: m,
|
||||
selected: state.activeMethod == m,
|
||||
onTap: () {
|
||||
setState(() => _cashBuffer = '');
|
||||
controller.selectMethod(m);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
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: [
|
||||
const Divider(height: AppSpacing.xxl),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Split tenders',
|
||||
style:
|
||||
TextStyle(fontSize: 13.5, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: controller.clearSplits,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.danger,
|
||||
minimumSize: const Size(0, 32),
|
||||
),
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
),
|
||||
for (final e in state.splits.asMap().entries)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(e.value.method.emoji,
|
||||
style: const TextStyle(fontSize: 16)),
|
||||
style: const TextStyle(fontSize: 15)),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(child: Text(e.value.method.label)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
e.value.method.label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 13.5),
|
||||
),
|
||||
),
|
||||
Text(Formatters.money(e.value.amount),
|
||||
style: AppTypography.money(14.5)),
|
||||
style: AppTypography.money(13.5)),
|
||||
IconButton(
|
||||
onPressed: () => controller.removeSplit(e.key),
|
||||
icon: const Icon(Icons.close_rounded, size: 17),
|
||||
icon: const Icon(Icons.close_rounded, size: 16),
|
||||
color: AppColors.textTertiary,
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: 30, minHeight: 30),
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: 'Remove tender',
|
||||
),
|
||||
]),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Tender
|
||||
Widget _tenderCard(PaymentController controller, PaymentState state) {
|
||||
final isCash = state.activeMethod.needsChange;
|
||||
|
||||
return GlassCard(
|
||||
padding: const EdgeInsets.all(AppSpacing.xl),
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
radius: AppRadius.xl,
|
||||
child: isCash
|
||||
? _cashTender(controller, state)
|
||||
child: state.activeMethod.needsChange
|
||||
? _cashTender(controller)
|
||||
: _referenceTender(controller, state),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _cashTender(PaymentController controller, PaymentState state) {
|
||||
final change = controller.changeDue;
|
||||
|
||||
Widget _cashTender(PaymentController controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Cash received', style: context.text.titleMedium),
|
||||
const Text(
|
||||
'Cash received',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xl,
|
||||
vertical: AppSpacing.lg,
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
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),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('₹',
|
||||
style:
|
||||
TextStyle(fontSize: 22, color: AppColors.textTertiary)),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
_cashBuffer.isEmpty ? '0' : _cashBuffer,
|
||||
style: AppTypography.money(28),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Wrap(
|
||||
spacing: AppSpacing.sm,
|
||||
@@ -296,58 +405,17 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
|
||||
label: const Text('Exact'),
|
||||
onPressed: () => _setCash(controller.balanceDue),
|
||||
),
|
||||
...[50, 100, 200, 500, 2000].map(
|
||||
(note) => ActionChip(
|
||||
for (final note in const [50, 100, 200, 500, 2000])
|
||||
ActionChip(
|
||||
label: Text('₹$note'),
|
||||
onPressed: () => _setCash(
|
||||
(double.tryParse(_cashBuffer) ?? 0) + 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),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_changeRow(controller.changeDue),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Center(
|
||||
child: NumericKeypad(
|
||||
allowDecimal: true,
|
||||
@@ -356,61 +424,100 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
|
||||
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'),
|
||||
),
|
||||
_splitButton(controller, amount: double.tryParse(_cashBuffer) ?? 0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _changeRow(double change) {
|
||||
final active = change > 0;
|
||||
return AnimatedContainer(
|
||||
duration: AppMotion.normal,
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? AppColors.successSurface : AppColors.surfaceAlt,
|
||||
borderRadius: AppRadius.brMd,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
active
|
||||
? Icons.currency_exchange_rounded
|
||||
: Icons.info_outline_rounded,
|
||||
size: 18,
|
||||
color: active ? AppColors.success : AppColors.textTertiary,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Change to return',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: active ? AppColors.success : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
Formatters.money(change),
|
||||
style: AppTypography.money(
|
||||
20,
|
||||
color: active ? AppColors.success : AppColors.textTertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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: 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,
|
||||
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),
|
||||
),
|
||||
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),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 104,
|
||||
height: 104,
|
||||
decoration: 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(
|
||||
@@ -425,37 +532,103 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
|
||||
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'),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_splitButton(controller),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _mini(String label, String value) => Column(children: [
|
||||
Text(label,
|
||||
Widget _splitButton(PaymentController controller, {double? amount}) {
|
||||
return OutlinedButton.icon(
|
||||
onPressed: controller.balanceDue > 0
|
||||
? () {
|
||||
controller.addSplit(
|
||||
amount: amount == null
|
||||
? null
|
||||
: amount.clamp(0, controller.balanceDue).toDouble(),
|
||||
);
|
||||
setState(() => _cashBuffer = '');
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.call_split_rounded, size: 17),
|
||||
label: const Text('Add as split payment'),
|
||||
style: OutlinedButton.styleFrom(minimumSize: const Size(0, 44)),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Bottom bar
|
||||
Widget _bottomBar(double total, PaymentState state, bool cartEmpty) {
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.xxl,
|
||||
AppSpacing.md,
|
||||
AppSpacing.xxl,
|
||||
AppSpacing.lg,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
border: Border(top: BorderSide(color: AppColors.border)),
|
||||
),
|
||||
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,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).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: cartEmpty ? null : _confirm,
|
||||
trailing: Text(
|
||||
Formatters.money(total),
|
||||
style: AppTypography.money(19, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _mini(String label, String value) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(value, style: AppTypography.money(13.5)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -480,10 +653,10 @@ class _MethodTile extends StatelessWidget {
|
||||
borderRadius: AppRadius.brMd,
|
||||
child: AnimatedContainer(
|
||||
duration: AppMotion.fast,
|
||||
width: 118,
|
||||
width: 104,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.lg,
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: AppRadius.brMd,
|
||||
@@ -491,19 +664,24 @@ class _MethodTile extends StatelessWidget {
|
||||
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,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(method.emoji, style: const TextStyle(fontSize: 22)),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
method.label,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: selected ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user