From 3519db9bd9d5330a16b9bcbf3a7a4952e14714a0 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 4 Apr 2026 21:18:38 +0000 Subject: [PATCH] feat: printable receipts with barcode on payment complete - Receipt component with thermal (80mm) and full-page layout support - Code 128 barcode from transaction number via JsBarcode - Store name, address, line items, totals, payment info, barcode - Print button on sale complete screen (browser print dialog) - Email button placeholder (disabled, ready for SMTP integration) - @media print CSS hides everything except receipt content - Receipt data fetched from GET /transactions/:id/receipt Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 5 +- packages/admin/package.json | 1 + packages/admin/src/app.css | 8 + .../src/components/pos/pos-payment-dialog.tsx | 70 +++++-- .../admin/src/components/pos/pos-receipt.tsx | 197 ++++++++++++++++++ 5 files changed, 259 insertions(+), 22 deletions(-) create mode 100644 packages/admin/src/components/pos/pos-receipt.tsx diff --git a/bun.lock b/bun.lock index 196fc64..04d376c 100644 --- a/bun.lock +++ b/bun.lock @@ -34,6 +34,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "jsbarcode": "^3.12.3", "jspdf": "^4.2.1", "lucide-react": "^1.7.0", "radix-ui": "^1.4.3", @@ -59,7 +60,7 @@ }, "packages/backend": { "name": "@lunarfront/backend", - "version": "0.0.1", + "version": "0.1.1", "dependencies": { "@fastify/cors": "^10", "@fastify/jwt": "^9", @@ -842,6 +843,8 @@ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsbarcode": ["jsbarcode@3.12.3", "", {}, "sha512-CuHU9hC6dPsHF5oVFMo8NW76uQVjH4L22CsP4hW+dNnGywJHC/B0ThA1CTDVLnxKLrrpYdicBLnd2xsgTfRnvg=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], diff --git a/packages/admin/package.json b/packages/admin/package.json index 7ec737f..85a88b6 100644 --- a/packages/admin/package.json +++ b/packages/admin/package.json @@ -25,6 +25,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "jsbarcode": "^3.12.3", "jspdf": "^4.2.1", "lucide-react": "^1.7.0", "radix-ui": "^1.4.3", diff --git a/packages/admin/src/app.css b/packages/admin/src/app.css index f211d88..f68dccb 100644 --- a/packages/admin/src/app.css +++ b/packages/admin/src/app.css @@ -113,6 +113,14 @@ body { background-color: var(--muted-foreground); } +/* Print styles — only show receipt content */ +@media print { + body * { visibility: hidden; } + .print\:block, .print\:block * { visibility: visible; } + .print\:block { position: absolute; left: 0; top: 0; } + .print\:hidden { display: none !important; } +} + /* Prevent browser autofill from overriding dark theme input colors */ input:-webkit-autofill, input:-webkit-autofill:hover, diff --git a/packages/admin/src/components/pos/pos-payment-dialog.tsx b/packages/admin/src/components/pos/pos-payment-dialog.tsx index 06aa9f3..f4c7cf1 100644 --- a/packages/admin/src/components/pos/pos-payment-dialog.tsx +++ b/packages/admin/src/components/pos/pos-payment-dialog.tsx @@ -1,14 +1,16 @@ import { useState } from 'react' -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { usePOSStore } from '@/stores/pos.store' +import { api } from '@/lib/api-client' import { posMutations, posKeys, type Transaction } from '@/api/pos' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Separator } from '@/components/ui/separator' -import { CheckCircle } from 'lucide-react' +import { CheckCircle, Printer, Mail } from 'lucide-react' import { toast } from 'sonner' +import { POSReceipt, printReceipt } from './pos-receipt' interface POSPaymentDialogProps { open: boolean @@ -61,9 +63,40 @@ export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transactio const QUICK_AMOUNTS = [1, 5, 10, 20, 50, 100] + // Fetch full receipt data after completion + const { data: receiptData } = useQuery({ + queryKey: ['pos', 'receipt', result?.id], + queryFn: () => api.get<{ + transaction: Transaction & { lineItems: { description: string; qty: number; unitPrice: string; taxAmount: string; lineTotal: string; discountAmount: string }[] } + company: { name: string; phone: string | null; email: string | null; address: { street?: string; city?: string; state?: string; zip?: string } | null } + location: { name: string; phone: string | null; email: string | null; address: { street?: string; city?: string; state?: string; zip?: string } | null } + }>(`/v1/transactions/${result!.id}/receipt`), + enabled: !!result?.id, + }) + + const [showReceipt, setShowReceipt] = useState(false) + if (completed && result) { const changeGiven = parseFloat(result.changeGiven ?? '0') - const roundingAdj = parseFloat(result.roundingAdjustment ?? '0') + + // Receipt print view + if (showReceipt && receiptData) { + return ( + { setShowReceipt(false); handleDone() }}> + +
+ + +
+
+ +
+
+
+ ) + } return ( handleDone()}> @@ -78,26 +111,21 @@ export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transactio Total ${parseFloat(result.total).toFixed(2)} - {roundingAdj !== 0 && ( -
- Rounding - {roundingAdj > 0 ? '+' : ''}{roundingAdj.toFixed(2)} + {paymentMethod === 'cash' && changeGiven > 0 && ( +
+ Change Due + ${changeGiven.toFixed(2)}
)} - {paymentMethod === 'cash' && ( - <> -
- Tendered - ${parseFloat(result.amountTendered ?? '0').toFixed(2)} -
- {changeGiven > 0 && ( -
- Change Due - ${changeGiven.toFixed(2)} -
- )} - - )} +
+ +
+ +