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:
ryan
2026-04-04 23:23:52 +00:00
parent e5ae7e6cae
commit f8cbce821d
5 changed files with 59 additions and 16 deletions

View File

@@ -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,

View File

@@ -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 (
<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">
<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 size="sm" onClick={printReceipt} className="gap-2">
<Printer className="h-4 w-4" />Print
</Button>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => downloadReceiptPDF(result.transactionNumber)} className="gap-2">
Save PDF
</Button>
<Button size="sm" onClick={() => printReceiptPDF(result.transactionNumber)} className="gap-2">
<Printer className="h-4 w-4" />Print
</Button>
</div>
</div>
<div className="print:block">
<div id="pos-receipt-print">
<POSReceipt data={receiptData} config={receiptConfig} />
</div>
</DialogContent>

View File

@@ -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<Blob | null> {
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)
}