From 49db60e31f5423ea2e1094d01494cebf6cc47000 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 4 Apr 2026 23:23:52 +0000 Subject: [PATCH] feat: receipt PDF save and print via html2pdf.js - Save PDF button downloads receipt directly - Print button opens PDF in new window and triggers print dialog - Replaces previous window.print() approach that lost styles - Receipt generated on demand from transaction data, no file storage needed Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 3 ++ packages/admin/package.json | 1 + packages/admin/src/app.css | 8 ---- .../src/components/pos/pos-payment-dialog.tsx | 17 ++++--- .../admin/src/components/pos/pos-receipt.tsx | 46 ++++++++++++++++++- 5 files changed, 59 insertions(+), 16 deletions(-) diff --git a/bun.lock b/bun.lock index 04d376c..b945545 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", + "html2pdf.js": "^0.14.0", "jsbarcode": "^3.12.3", "jspdf": "^4.2.1", "lucide-react": "^1.7.0", @@ -807,6 +808,8 @@ "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], + "html2pdf.js": ["html2pdf.js@0.14.0", "", { "dependencies": { "dompurify": "^3.3.1", "html2canvas": "^1.0.0", "jspdf": "^4.0.0" } }, "sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], diff --git a/packages/admin/package.json b/packages/admin/package.json index 85a88b6..0034b31 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", + "html2pdf.js": "^0.14.0", "jsbarcode": "^3.12.3", "jspdf": "^4.2.1", "lucide-react": "^1.7.0", diff --git a/packages/admin/src/app.css b/packages/admin/src/app.css index f68dccb..f211d88 100644 --- a/packages/admin/src/app.css +++ b/packages/admin/src/app.css @@ -113,14 +113,6 @@ 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 63c1339..c05fd64 100644 --- a/packages/admin/src/components/pos/pos-payment-dialog.tsx +++ b/packages/admin/src/components/pos/pos-payment-dialog.tsx @@ -10,7 +10,7 @@ import { Label } from '@/components/ui/label' import { Separator } from '@/components/ui/separator' import { CheckCircle, Printer, Mail } from 'lucide-react' import { toast } from 'sonner' -import { POSReceipt, printReceipt } from './pos-receipt' +import { POSReceipt, downloadReceiptPDF, printReceiptPDF } from './pos-receipt' interface POSPaymentDialogProps { open: boolean @@ -98,13 +98,18 @@ export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transactio return ( { setShowReceipt(false); handleDone() }}> -
+
- +
+ + +
-
+
diff --git a/packages/admin/src/components/pos/pos-receipt.tsx b/packages/admin/src/components/pos/pos-receipt.tsx index 1d72266..5001e69 100644 --- a/packages/admin/src/components/pos/pos-receipt.tsx +++ b/packages/admin/src/components/pos/pos-receipt.tsx @@ -256,6 +256,48 @@ export function POSReceipt({ data, size = 'thermal', footerText, config }: POSRe ) } -export function printReceipt() { - window.print() +export async function saveReceiptPDF(txnNumber?: string): Promise { + const el = document.getElementById('pos-receipt-print') + if (!el) return null + + const html2pdf = (await import('html2pdf.js')).default + const blob: Blob = await html2pdf() + .set({ + margin: [4, 4, 4, 4], + filename: `receipt-${txnNumber ?? 'unknown'}.pdf`, + image: { type: 'jpeg', quality: 0.95 }, + html2canvas: { scale: 2, useCORS: true }, + jsPDF: { unit: 'mm', format: [80, 200], orientation: 'portrait' }, + }) + .from(el) + .outputPdf('blob') + + return blob +} + +export async function downloadReceiptPDF(txnNumber?: string) { + const blob = await saveReceiptPDF(txnNumber) + if (!blob) return + + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `receipt-${txnNumber ?? 'unknown'}.pdf` + a.click() + URL.revokeObjectURL(url) +} + +export async function printReceiptPDF(txnNumber?: string) { + const blob = await saveReceiptPDF(txnNumber) + if (!blob) return + + const url = URL.createObjectURL(blob) + const printWindow = window.open(url) + if (printWindow) { + printWindow.onload = () => { + printWindow.print() + } + } + // Clean up after a delay + setTimeout(() => URL.revokeObjectURL(url), 30000) }