second commit
This commit is contained in:
289
lib/presentation/receipt/widgets/receipt_preview.dart
Normal file
289
lib/presentation/receipt/widgets/receipt_preview.dart
Normal file
@@ -0,0 +1,289 @@
|
||||
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)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user