From fe40b563d5414497f69b35771c387f22d7458478 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 4 Apr 2026 23:33:06 +0000 Subject: [PATCH] fix: receipt uses inline styles for PDF/print compatibility, thermal width - Replace all Tailwind classes with inline styles (fixes oklch color error in html2pdf) - Narrow receipt to 260px / 10px font for 72mm thermal paper - Print uses hidden iframe instead of window.open (fixes Safari about:blank) - PDF canvas width matches thermal format Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/pos/pos-payment-dialog.tsx | 4 +- .../admin/src/components/pos/pos-receipt.tsx | 188 ++++++++---------- 2 files changed, 88 insertions(+), 104 deletions(-) diff --git a/packages/admin/src/components/pos/pos-payment-dialog.tsx b/packages/admin/src/components/pos/pos-payment-dialog.tsx index c05fd64..609e918 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, downloadReceiptPDF, printReceiptPDF } from './pos-receipt' +import { POSReceipt, downloadReceiptPDF, printReceipt } from './pos-receipt' interface POSPaymentDialogProps { open: boolean @@ -104,7 +104,7 @@ export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transactio - diff --git a/packages/admin/src/components/pos/pos-receipt.tsx b/packages/admin/src/components/pos/pos-receipt.tsx index 5001e69..2625eb3 100644 --- a/packages/admin/src/components/pos/pos-receipt.tsx +++ b/packages/admin/src/components/pos/pos-receipt.tsx @@ -130,58 +130,64 @@ export function POSReceipt({ data, size = 'thermal', footerText, config }: POSRe const addr = location.address ?? company.address const phone = location.phone ?? company.phone + const s = { + row: { display: 'flex', justifyContent: 'space-between' } as const, + section: { padding: '8px 0', borderBottom: '1px dashed #999' } as const, + gray: { color: '#666' } as const, + light: { color: '#999' } as const, + bold: { fontWeight: 'bold' } as const, + center: { textAlign: 'center' } as const, + nums: { fontVariantNumeric: 'tabular-nums' } as const, + } + return ( -
+
{/* Store header */} -
+
{logoSrc ? ( - {company.name} + {company.name} ) : ( -
{company.name}
+
{company.name}
)} - {location.name !== company.name && ( -
{location.name}
- )} - {addr && ( - <> - {addr.street &&
{addr.street}
} - {(addr.city || addr.state || addr.zip) && ( -
{[addr.city, addr.state].filter(Boolean).join(', ')} {addr.zip}
- )} - + {location.name !== company.name &&
{location.name}
} + {addr?.street &&
{addr.street}
} + {(addr?.city || addr?.state || addr?.zip) && ( +
{[addr?.city, addr?.state].filter(Boolean).join(', ')} {addr?.zip}
)} {phone &&
{phone}
} - {config?.header &&
{config.header}
} + {config?.header &&
{config.header}
}
{/* Transaction info */} -
-
+
+
{txn.transactionNumber} {txn.transactionType.replace('_', ' ')}
-
+
{date.toLocaleDateString()} {date.toLocaleTimeString()}
{/* Line items */} -
+
{txn.lineItems.map((item, i) => ( -
-
- {item.description} - ${parseFloat(item.lineTotal).toFixed(2)} +
+
+ {item.description} + ${parseFloat(item.lineTotal).toFixed(2)}
{(item.qty > 1 || parseFloat(item.discountAmount ?? '0') > 0) && ( -
+
{item.qty > 1 && {item.qty} x ${parseFloat(item.unitPrice).toFixed(2)}} {parseFloat(item.discountAmount ?? '0') > 0 && ( - disc -${parseFloat(item.discountAmount!).toFixed(2)} + disc -${parseFloat(item.discountAmount!).toFixed(2)} )}
)} @@ -190,114 +196,92 @@ export function POSReceipt({ data, size = 'thermal', footerText, config }: POSRe
{/* Totals */} -
-
- Subtotal - ${subtotal.toFixed(2)} -
+
+
Subtotal${subtotal.toFixed(2)}
{discountTotal > 0 && ( -
- Discount - -${discountTotal.toFixed(2)} -
+
Discount-${discountTotal.toFixed(2)}
)} -
- Tax - ${taxTotal.toFixed(2)} -
+
Tax${taxTotal.toFixed(2)}
{rounding !== 0 && ( -
- Rounding - {rounding > 0 ? '+' : ''}{rounding.toFixed(2)} -
+
Rounding{rounding > 0 ? '+' : ''}{rounding.toFixed(2)}
)} -
- TOTAL - ${total.toFixed(2)} +
+ TOTAL${total.toFixed(2)}
{/* Payment */} -
-
- Payment - {txn.paymentMethod?.replace('_', ' ') ?? 'N/A'} -
+
+
Payment{txn.paymentMethod?.replace('_', ' ') ?? 'N/A'}
{tendered !== null && ( -
- Tendered - ${tendered.toFixed(2)} -
+
Tendered${tendered.toFixed(2)}
)} {change !== null && change > 0 && ( -
- Change - ${change.toFixed(2)} -
+
Change${change.toFixed(2)}
)}
{/* Barcode */} -
+
{/* Footer */} {(config?.footer || footerText) && ( -
{config?.footer || footerText}
+
{config?.footer || footerText}
)} {config?.returnPolicy && ( -
{config.returnPolicy}
+
{config.returnPolicy}
)} {config?.social && ( -
{config.social}
+
{config.social}
)}
) } -export async function saveReceiptPDF(txnNumber?: string): Promise { +export function printReceipt() { const el = document.getElementById('pos-receipt-print') - if (!el) return null + if (!el) return - 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') + // Clone the receipt into an iframe for clean printing + const iframe = document.createElement('iframe') + iframe.style.cssText = 'position:fixed;left:-9999px;width:400px;height:800px;border:none;' + document.body.appendChild(iframe) - return blob + const doc = iframe.contentDocument + if (!doc) { document.body.removeChild(iframe); return } + + doc.open() + doc.write(`${el.innerHTML}`) + doc.close() + + // Wait for content to render then print + setTimeout(() => { + try { + iframe.contentWindow?.focus() + iframe.contentWindow?.print() + } catch { + // Fallback: just use window.print + window.print() + } + setTimeout(() => document.body.removeChild(iframe), 2000) + }, 300) } export async function downloadReceiptPDF(txnNumber?: string) { - const blob = await saveReceiptPDF(txnNumber) - if (!blob) return + const el = document.getElementById('pos-receipt-print') + if (!el) 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) + const html2pdf = (await import('html2pdf.js')).default + html2pdf() + .set({ + margin: [2, 2, 2, 2], + filename: `receipt-${txnNumber ?? 'unknown'}.pdf`, + image: { type: 'jpeg', quality: 0.95 }, + html2canvas: { scale: 2, useCORS: true, width: 280 }, + jsPDF: { unit: 'mm', format: [72, 250], orientation: 'portrait' }, + }) + .from(el) + .save() }