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'; import '../../../core/utils/formatters.dart'; import '../../../domain/entities/transaction.dart'; /// Paper-like preview of what the thermal printer produced. class ReceiptPreview extends StatelessWidget { const ReceiptPreview({super.key, required this.transaction}); final SaleTransaction transaction; @override Widget build(BuildContext context) { final txn = transaction; final cart = txn.cart; return Container( decoration: BoxDecoration( color: AppColors.surface, borderRadius: AppRadius.brLg, boxShadow: AppColors.shadowMd, ), child: Column(children: [ const _Perforation(top: true), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.xxl, vertical: AppSpacing.xl, ), child: DefaultTextStyle( style: AppTypography.mono(11.5, color: AppColors.textPrimary), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Center( child: Column(children: [ Text( AppConstants.storeName.toUpperCase(), style: AppTypography.mono(15) .copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 3), Text(AppConstants.storeAddress, textAlign: TextAlign.center, style: AppTypography.mono(9.5)), Text('GSTIN: ${AppConstants.storeGstin}', style: AppTypography.mono(9.5)), const SizedBox(height: AppSpacing.sm), Text('TAX INVOICE', style: AppTypography.mono(11.5) .copyWith(fontWeight: FontWeight.w700)), ]), ), const _Dashes(), _row('Invoice', txn.invoiceNumber), _row('Date', Formatters.receiptStamp(txn.createdAt)), _row('Cashier', txn.cashierName), _row('Customer', txn.customer?.name ?? 'Walk-in'), const _Dashes(), Row(children: [ Expanded(flex: 5, child: _bold('Item')), Expanded( flex: 2, child: _bold('Qty', align: TextAlign.center), ), Expanded( flex: 3, child: _bold('Amount', align: TextAlign.right), ), ]), const SizedBox(height: AppSpacing.xs), ...cart.lines.map((line) => Padding( padding: const EdgeInsets.symmetric(vertical: 2.5), child: Row(children: [ Expanded(flex: 5, child: Text(line.product.name)), Expanded( flex: 2, child: Text( line.quantity % 1 == 0 ? line.quantity.toStringAsFixed(0) : line.quantity.toStringAsFixed(2), textAlign: TextAlign.center, ), ), Expanded( flex: 3, child: Text( line.payable.toStringAsFixed(2), textAlign: TextAlign.right, ), ), ]), )), const _Dashes(), _row('Subtotal', cart.subtotal.toStringAsFixed(2)), if (cart.billDiscountTotal > 0) _row('Discount', '-${cart.billDiscountTotal.toStringAsFixed(2)}'), if (cart.loyaltyRedemptionValue > 0) _row('Points redeemed', '-${cart.loyaltyRedemptionValue.toStringAsFixed(2)}'), _row('CGST', cart.cgst.toStringAsFixed(2)), _row('SGST', cart.sgst.toStringAsFixed(2)), if (cart.roundOff != 0) _row('Round off', cart.roundOff.toStringAsFixed(2)), const _Dashes(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('TOTAL', style: AppTypography.mono(15).copyWith( fontWeight: FontWeight.w800, color: AppColors.textPrimary, )), Text( Formatters.money(txn.total), style: AppTypography.mono(15).copyWith( fontWeight: FontWeight.w800, color: AppColors.textPrimary, ), ), ], ), const _Dashes(), ...txn.payments.map((p) => _row(p.method.label, p.amount.toStringAsFixed(2))), if (txn.changeDue > 0) _row('Change', txn.changeDue.toStringAsFixed(2)), if (txn.customer != null) ...[ const _Dashes(), _row('Points earned', '+${txn.pointsEarned}'), _row('Membership', txn.customer!.tier.label), ], const SizedBox(height: AppSpacing.lg), Center( child: Column(children: [ _FakeBarcode(value: txn.invoiceNumber), const SizedBox(height: AppSpacing.sm), Text('Thank you for shopping with us!', style: AppTypography.mono(11) .copyWith(fontWeight: FontWeight.w700)), const SizedBox(height: 2), Text('Powered by Nearle POS', style: AppTypography.mono(9)), ]), ), ], ), ), ), ), const _Perforation(top: false), ]), ); } Widget _row(String label, String value) => Padding( padding: const EdgeInsets.symmetric(vertical: 1.5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(label), Text(value)], ), ); Widget _bold(String text, {TextAlign align = TextAlign.left}) => Text( text, textAlign: align, style: AppTypography.mono(11.5).copyWith( fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ); } class _Dashes extends StatelessWidget { const _Dashes(); @override Widget build(BuildContext context) { return const Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.sm), child: Text( '- - - - - - - - - - - - - - - - - - - - - - - - - -', maxLines: 1, overflow: TextOverflow.clip, style: TextStyle(color: AppColors.textTertiary, fontSize: 10), ), ); } } /// Zig-zag torn-paper edge. class _Perforation extends StatelessWidget { const _Perforation({required this.top}); final bool top; @override Widget build(BuildContext context) { return SizedBox( height: 10, child: CustomPaint( size: const Size(double.infinity, 10), painter: _PerforationPainter(top: top), ), ); } } class _PerforationPainter extends CustomPainter { const _PerforationPainter({required this.top}); final bool top; @override void paint(Canvas canvas, Size size) { const tooth = 12.0; final path = Path(); if (top) { path.moveTo(0, size.height); for (var x = 0.0; x < size.width; x += tooth) { path.lineTo(x + tooth / 2, 0); path.lineTo(x + tooth, size.height); } path.lineTo(size.width, size.height); } else { path.moveTo(0, 0); for (var x = 0.0; x < size.width; x += tooth) { path.lineTo(x + tooth / 2, size.height); path.lineTo(x + tooth, 0); } path.lineTo(size.width, 0); } path.close(); canvas.drawPath(path, Paint()..color = AppColors.surface); } @override bool shouldRepaint(covariant _PerforationPainter oldDelegate) => oldDelegate.top != top; } /// Decorative Code-128-style bar rendering for the preview only. class _FakeBarcode extends StatelessWidget { const _FakeBarcode({required this.value}); final String value; @override Widget build(BuildContext context) { final bars = value.codeUnits; return Column(children: [ SizedBox( height: 38, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ for (var i = 0; i < bars.length * 2; i++) Container( width: (bars[i ~/ 2] % 3 == 0) ? 3 : 1.5, margin: const EdgeInsets.symmetric(horizontal: 0.7), color: i.isEven ? AppColors.textPrimary : Colors.transparent, ), ], ), ), const SizedBox(height: 3), Text(value, style: AppTypography.mono(9)), ]); } }