import { Document, Page, Text, View, Image } from '@react-pdf/renderer' import { formatCurrency, formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format' import { createStyles, gray, getFontBold } from '@/features/vehicles/Components/invoice-pdf/styles' import { HtmlToPdf } from '@/features/vehicles/Components/invoice-pdf/Notes' import type { TemplateConfig } from '@/features/vehicles/Components/invoice-pdf/types' function fillTemplate(template: string, values: Record): string { return Object.entries(values).reduce( (str, [key, val]) => str.replace(`{${key}}`, val), template ) } interface QuoteData { quoteNumber: string | null title: string description: string | null validUntil: Date | null createdAt: Date subtotal: number taxRate: number taxAmount: number discountType: string | null discountValue: number discountAmount: number totalAmount: number notes: string | null partItems: { partNumber: string | null name: string quantity: number unitPrice: number total: number excluded?: boolean }[] laborItems: { description: string; hours: number; rate: number; total: number; excluded?: boolean }[] customer: { name: string email: string | null phone: string | null address: string | null company: string | null } | null vehicle: { make: string model: string year: number vin: string | null licensePlate: string | null } | null } interface WorkshopInfo { name: string address: string phone: string email: string } interface ImageAttachmentPDF { fileName: string dataUri: string description?: string } interface OtherAttachmentPDF { fileName: string fileType: string } export function QuotePDF({ data, workshop, currencyCode = 'USD', logoDataUri, torqvoiceLogoDataUri, dateFormat, timezone, template, portalUrl, imageAttachments = [], otherAttachments = [], pdfAttachmentNames = [], labels = {}, }: { data: QuoteData workshop?: WorkshopInfo currencyCode?: string logoDataUri?: string torqvoiceLogoDataUri?: string dateFormat?: string timezone?: string template?: TemplateConfig portalUrl?: string imageAttachments?: ImageAttachmentPDF[] otherAttachments?: OtherAttachmentPDF[] pdfAttachmentNames?: string[] labels?: Record }) { const primaryColor = template?.primaryColor || '#d97706' const fontFamily = template?.fontFamily || 'Helvetica' const headerStyle = template?.headerStyle || 'standard' const showLogo = template?.showLogo !== false const showCompanyName = template?.showCompanyName !== false const styles = createStyles(primaryColor, fontFamily) const fontBold = getFontBold(fontFamily) const quoteNum = data.quoteNumber || 'QUOTE' const df = dateFormat || DEFAULT_DATE_FORMAT const tz = timezone || undefined const createdDate = formatDateForPdf(data.createdAt, df, tz) const validDate = data.validUntil ? formatDateForPdf(data.validUntil, df, tz) : null const shopName = workshop?.name || 'Torqvoice' const renderCompactHeader = () => ( {showLogo && logoDataUri && ( )} {showCompanyName && ( {shopName} {workshop?.address && ( {workshop.address} )} )} {labels.title || 'QUOTE'} {quoteNum} {createdDate} {validDate && {labels.validUntil ? fillTemplate(labels.validUntil, { date: validDate }) : `Valid until: ${validDate}`}} {workshop?.phone && ( {labels.tel ? fillTemplate(labels.tel, { phone: workshop.phone }) : `Tel: ${workshop.phone}`} )} {workshop?.email && ( {workshop.email} )} {torqvoiceLogoDataUri && ( Torqvoice )} ) const renderModernHeader = () => ( {showLogo && logoDataUri && ( )} {showCompanyName && ( {shopName} )} {workshop?.address && ( {workshop.address} )} {workshop?.phone && ( {labels.tel ? fillTemplate(labels.tel, { phone: workshop.phone }) : `Tel: ${workshop.phone}`} )} {workshop?.email && ( {workshop.email} )} {torqvoiceLogoDataUri && ( Torqvoice )} {labels.title || 'QUOTE'} {quoteNum} {createdDate} {validDate && {labels.validUntil ? fillTemplate(labels.validUntil, { date: validDate }) : `Valid until: ${validDate}`}} ) const renderStandardHeader = () => ( {showLogo && logoDataUri && ( )} {showCompanyName && {shopName}} {workshop?.address && {workshop.address}} {workshop?.phone && {labels.tel ? fillTemplate(labels.tel, { phone: workshop.phone }) : `Tel: ${workshop.phone}`}} {workshop?.email && {workshop.email}} {torqvoiceLogoDataUri && ( Torqvoice )} {labels.title || 'QUOTE'} {quoteNum} {createdDate} {validDate && {labels.validUntil ? fillTemplate(labels.validUntil, { date: validDate }) : `Valid until: ${validDate}`}} ) return ( {headerStyle === 'compact' ? renderCompactHeader() : headerStyle === 'modern' ? renderModernHeader() : renderStandardHeader()} {data.customer && ( {labels.to || 'To'} {data.customer.name} {data.customer.company && ( {data.customer.company} )} {data.customer.address && ( {data.customer.address} )} {data.customer.email && ( {data.customer.email} )} {data.customer.phone && ( {data.customer.phone} )} )} {data.vehicle && ( {labels.vehicle || 'Vehicle'} {data.vehicle.year} {data.vehicle.make} {data.vehicle.model} {data.vehicle.vin && ( {labels.vin ? fillTemplate(labels.vin, { vin: data.vehicle.vin }) : `VIN: ${data.vehicle.vin}`} )} {data.vehicle.licensePlate && ( {labels.plate ? fillTemplate(labels.plate, { plate: data.vehicle.licensePlate }) : `Plate: ${data.vehicle.licensePlate}`} )} )} {labels.quoteDetails || 'Quote Details'} {data.title} {data.partItems.length > 0 && ( {labels.parts || 'Parts'} {labels.partNumber || 'Part #'} {labels.description || 'Description'} {labels.qty || 'Qty'} {labels.unitPrice || 'Unit Price'} {labels.total || 'Total'} {data.partItems.map((p, i) => ( {p.partNumber || '-'} {p.name} {p.quantity} {formatCurrency(p.unitPrice, currencyCode)} {formatCurrency(p.total, currencyCode)} ))} )} {data.laborItems.length > 0 && ( {labels.labor || 'Labor'} {labels.description || 'Description'} {labels.hours || 'Hours'} {labels.rate || 'Rate'} {labels.total || 'Total'} {data.laborItems.map((l, i) => ( {l.description} {l.hours} {labels.ratePerHour ? fillTemplate(labels.ratePerHour, { rate: formatCurrency(l.rate, currencyCode) }) : `${formatCurrency(l.rate, currencyCode)}/hr`} {formatCurrency(l.total, currencyCode)} ))} )} {(() => { const laborTotal = data.laborItems.reduce((sum, l) => l.excluded ? sum : sum + l.total, 0) const partsTotal = data.partItems.reduce((sum, p) => p.excluded ? sum : sum + p.total, 0) const sub = laborTotal + partsTotal const disc = data.discountType === 'percentage' ? sub * (data.discountValue / 100) : data.discountType === 'fixed' ? Math.min(data.discountValue, sub) : 0 const tax = (sub - disc) * (data.taxRate / 100) const total = sub - disc + tax return ( {data.laborItems.length > 0 && ( {labels.labor || 'Labor'} {formatCurrency(laborTotal, currencyCode)} )} {data.partItems.length > 0 && ( {labels.parts || 'Parts'} {formatCurrency(partsTotal, currencyCode)} )} {sub > 0 && ( {labels.subtotal || 'Subtotal'} {formatCurrency(sub, currencyCode)} )} {disc > 0 && ( {data.discountType === 'percentage' ? (labels.discountPercent ? fillTemplate(labels.discountPercent, { percent: String(data.discountValue) }) : `Discount (${data.discountValue}%)`) : (labels.discount || 'Discount')} {formatCurrency(-disc, currencyCode)} )} {data.taxRate > 0 && ( {labels.tax ? fillTemplate(labels.tax, { rate: String(data.taxRate) }) : `Tax (${data.taxRate}%)`} {formatCurrency(tax, currencyCode)} )} {labels.total || 'Total'} {formatCurrency(total, currencyCode)} ) })()} {torqvoiceLogoDataUri && ( {labels.poweredBy || 'Powered by'} Torqvoice )} {data.description && ( Description )} {portalUrl && ( {labels.viewPortal ? fillTemplate(labels.viewPortal, { url: portalUrl }) : `View your portal: ${portalUrl}`} )} {/* Document attachment names */} {(otherAttachments.length > 0 || pdfAttachmentNames.length > 0) && ( {labels.attachedDocuments || 'Attached Documents'} {otherAttachments.map((att, i) => ( {att.fileName} ))} {pdfAttachmentNames.map((name, i) => ( {labels.attached ? fillTemplate(labels.attached, { name }) : `${name} (attached)`} ))} )} {torqvoiceLogoDataUri ? ( {validDate ? (labels.validityFooterUntil ? fillTemplate(labels.validityFooterUntil, { date: validDate }) : `This quote is valid until ${validDate}`) : (labels.validityFooter30 || 'This quote is valid for 30 days')} ·{' '} {labels.poweredBy || 'Powered by'} Torqvoice ) : ( {validDate ? (labels.validityFooterUntil ? fillTemplate(labels.validityFooterUntil, { date: validDate }) : `This quote is valid until ${validDate}`) : (labels.validityFooter30 || 'This quote is valid for 30 days')} · {shopName} )} {imageAttachments.length > 0 && ( {labels.quoteImages || 'Quote Images'} {imageAttachments.map((img, i) => ( {img.description ? ( {img.description} ) : ( {img.fileName} )} ))} {quoteNum} · {shopName} )} ) }