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) <noreply@anthropic.com>
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -34,6 +34,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"html2pdf.js": "^0.14.0",
|
||||||
"jsbarcode": "^3.12.3",
|
"jsbarcode": "^3.12.3",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
"lucide-react": "^1.7.0",
|
"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=="],
|
"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=="],
|
"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=="],
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"html2pdf.js": "^0.14.0",
|
||||||
"jsbarcode": "^3.12.3",
|
"jsbarcode": "^3.12.3",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
|
|||||||
@@ -113,14 +113,6 @@ body {
|
|||||||
background-color: var(--muted-foreground);
|
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 */
|
/* Prevent browser autofill from overriding dark theme input colors */
|
||||||
input:-webkit-autofill,
|
input:-webkit-autofill,
|
||||||
input:-webkit-autofill:hover,
|
input:-webkit-autofill:hover,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Label } from '@/components/ui/label'
|
|||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { CheckCircle, Printer, Mail } from 'lucide-react'
|
import { CheckCircle, Printer, Mail } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { POSReceipt, printReceipt } from './pos-receipt'
|
import { POSReceipt, downloadReceiptPDF, printReceiptPDF } from './pos-receipt'
|
||||||
|
|
||||||
interface POSPaymentDialogProps {
|
interface POSPaymentDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -98,13 +98,18 @@ export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transactio
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={() => { setShowReceipt(false); handleDone() }}>
|
<Dialog open={open} onOpenChange={() => { setShowReceipt(false); handleDone() }}>
|
||||||
<DialogContent className="max-w-sm max-h-[90vh] overflow-y-auto print:max-w-none print:max-h-none print:overflow-visible print:shadow-none print:border-none">
|
<DialogContent className="max-w-sm max-h-[90vh] overflow-y-auto print:max-w-none print:max-h-none print:overflow-visible print:shadow-none print:border-none">
|
||||||
<div className="print:hidden flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<Button variant="ghost" size="sm" onClick={() => setShowReceipt(false)}>Back</Button>
|
<Button variant="ghost" size="sm" onClick={() => setShowReceipt(false)}>Back</Button>
|
||||||
<Button size="sm" onClick={printReceipt} className="gap-2">
|
<div className="flex gap-2">
|
||||||
<Printer className="h-4 w-4" />Print
|
<Button variant="outline" size="sm" onClick={() => downloadReceiptPDF(result.transactionNumber)} className="gap-2">
|
||||||
</Button>
|
Save PDF
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={() => printReceiptPDF(result.transactionNumber)} className="gap-2">
|
||||||
|
<Printer className="h-4 w-4" />Print
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="print:block">
|
<div id="pos-receipt-print">
|
||||||
<POSReceipt data={receiptData} config={receiptConfig} />
|
<POSReceipt data={receiptData} config={receiptConfig} />
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -256,6 +256,48 @@ export function POSReceipt({ data, size = 'thermal', footerText, config }: POSRe
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printReceipt() {
|
export async function saveReceiptPDF(txnNumber?: string): Promise<Blob | null> {
|
||||||
window.print()
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user