From d57971ab52f352d3aaaf740f94a0462a3c9b5acb Mon Sep 17 00:00:00 2001 From: Rafiqul Islam Date: Wed, 11 Mar 2026 16:52:24 +0600 Subject: [PATCH] Enhance invoice template and shipping data Revamp the invoice PDF template with improved styling and richer shipment/payment metadata. Changes include redesigned styles (header, metadata, badges, table, summary, shipment label, tracking grid, note box, footer), better address parsing/formatting, Ship To fallback to billing, discount chip, and clearer tracking + customer note sections. To support these display changes the invoice data shape and service mapping were extended: address now includes city/state/postalCode/country/website, and InvoiceData/OrderService now expose discountCode, shippingMethod, estimatedDelivery, shippingStatus, and customerNote (plus small null/coalescing fixes). These updates make invoices more informative and production-ready. --- src/components/invoices/invoice-template.tsx | 782 +++++++++++++------ src/lib/services/order.service.ts | 10 + src/lib/types/invoice.ts | 10 + 3 files changed, 570 insertions(+), 232 deletions(-) diff --git a/src/components/invoices/invoice-template.tsx b/src/components/invoices/invoice-template.tsx index eb8b47f2..6c5a9b13 100644 --- a/src/components/invoices/invoice-template.tsx +++ b/src/components/invoices/invoice-template.tsx @@ -1,8 +1,10 @@ /** * Invoice PDF Template Component - * - * Production-grade invoice template using @react-pdf/renderer - * Includes all order details, tracking information, and branding + * + * Production-grade invoice template using @react-pdf/renderer. + * Includes: full seller/store address, Bill To, explicit Ship To (with + * fallback to billing address), order status, shipping method, estimated + * delivery, discount code, customer note, and complete tracking info. */ import React from 'react'; @@ -17,10 +19,7 @@ import { formatMoney } from '@/lib/money'; import type { InvoiceData } from '@/lib/types/invoice'; export type { InvoiceData } from '@/lib/types/invoice'; -// Register fonts (optional - using built-in Helvetica for now) -// You can register custom fonts here if needed using Font from @react-pdf/renderer - -// Define styles +// ─── Styles ─────────────────────────────────────────────────────────────────── const styles = StyleSheet.create({ page: { padding: 40, @@ -28,86 +27,169 @@ const styles = StyleSheet.create({ fontFamily: 'Helvetica', backgroundColor: '#ffffff', }, + + // ── Header (Seller / From) ────────────────────────────────────────────────── header: { - marginBottom: 30, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 24, + paddingBottom: 16, + borderBottomWidth: 2, + borderBottomColor: '#1a1a1a', + }, + headerLeft: { + flex: 1, + }, + headerRight: { + alignItems: 'flex-end', }, companyName: { - fontSize: 24, - fontWeight: 'bold', + fontSize: 20, + fontFamily: 'Helvetica-Bold', marginBottom: 4, color: '#1a1a1a', }, - companyInfo: { + companyAddress: { fontSize: 9, - color: '#666666', - lineHeight: 1.4, + color: '#555555', + lineHeight: 1.5, }, invoiceTitle: { - fontSize: 18, - fontWeight: 'bold', - marginTop: 20, - marginBottom: 10, + fontSize: 28, + fontFamily: 'Helvetica-Bold', color: '#1a1a1a', + letterSpacing: 2, + }, + invoiceSubTitle: { + fontSize: 9, + color: '#888888', + marginTop: 4, + textAlign: 'right', }, - metadataRow: { + + // ── Metadata row ─────────────────────────────────────────────────────────── + metadataSection: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20, + backgroundColor: '#f8f8f8', + padding: 12, + borderRadius: 4, }, metadataBlock: { flex: 1, }, label: { - fontSize: 9, - color: '#666666', - marginBottom: 4, + fontSize: 8, + color: '#888888', + marginBottom: 3, + textTransform: 'uppercase', }, value: { fontSize: 10, - fontWeight: 'bold', + fontFamily: 'Helvetica-Bold', color: '#1a1a1a', }, + + // ── Status badges ────────────────────────────────────────────────────────── + badge: { + paddingHorizontal: 6, + paddingVertical: 3, + borderRadius: 3, + fontSize: 8, + fontFamily: 'Helvetica-Bold', + textAlign: 'center', + marginTop: 3, + alignSelf: 'flex-start', + }, + badgePaid: { backgroundColor: '#dcfce7', color: '#166534' }, + badgePending: { backgroundColor: '#fef3c7', color: '#92400e' }, + badgeShipped: { backgroundColor: '#dbeafe', color: '#1e40af' }, + badgeCanceled: { backgroundColor: '#fee2e2', color: '#991b1b' }, + badgeDefault: { backgroundColor: '#f3f4f6', color: '#374151' }, + + // ── Generic section ──────────────────────────────────────────────────────── section: { - marginBottom: 20, + marginBottom: 18, }, sectionTitle: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 10, + fontSize: 11, + fontFamily: 'Helvetica-Bold', + marginBottom: 8, color: '#1a1a1a', borderBottomWidth: 1, - borderBottomColor: '#e5e5e5', - paddingBottom: 5, + borderBottomColor: '#e0e0e0', + paddingBottom: 4, }, + + // ── Addresses ───────────────────────────────────────────────────────────── addressRow: { flexDirection: 'row', - gap: 20, + gap: 16, }, addressBlock: { flex: 1, + backgroundColor: '#fafafa', + padding: 10, + borderRadius: 4, + borderWidth: 1, + borderColor: '#e8e8e8', }, addressTitle: { + fontSize: 9, + fontFamily: 'Helvetica-Bold', + marginBottom: 5, + color: '#1a1a1a', + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + addressName: { fontSize: 10, - fontWeight: 'bold', - marginBottom: 6, + fontFamily: 'Helvetica-Bold', color: '#1a1a1a', + marginBottom: 3, }, addressText: { fontSize: 9, - lineHeight: 1.4, + lineHeight: 1.5, color: '#4a4a4a', }, - table: { - marginTop: 10, + addressHighlight: { + fontSize: 9, + lineHeight: 1.5, + color: '#1a1a1a', + fontFamily: 'Helvetica-Bold', + }, + + // ── Shipping info strip ──────────────────────────────────────────────────── + shippingStrip: { + flexDirection: 'row', + gap: 12, + marginBottom: 18, + padding: 10, + backgroundColor: '#eff6ff', + borderRadius: 4, + borderWidth: 1, + borderColor: '#bfdbfe', + }, + shippingStripBlock: { + flex: 1, }, + + // ── Items table ──────────────────────────────────────────────────────────── + table: { marginTop: 6 }, tableHeader: { flexDirection: 'row', - backgroundColor: '#f5f5f5', + backgroundColor: '#1a1a1a', + color: '#ffffff', padding: 8, - fontWeight: 'bold', fontSize: 9, - borderBottomWidth: 1, - borderBottomColor: '#d5d5d5', + }, + tableHeaderText: { + color: '#ffffff', + fontFamily: 'Helvetica-Bold', + fontSize: 8, }, tableRow: { flexDirection: 'row', @@ -116,15 +198,23 @@ const styles = StyleSheet.create({ borderBottomColor: '#efefef', fontSize: 9, }, + tableRowAlt: { + backgroundColor: '#fafafa', + }, col1: { flex: 3 }, col2: { flex: 2, textAlign: 'left' }, col3: { flex: 1, textAlign: 'right' }, - col4: { flex: 1, textAlign: 'right' }, + col4: { flex: 1.2, textAlign: 'right' }, col5: { flex: 1.5, textAlign: 'right' }, + + // ── Summary ──────────────────────────────────────────────────────────────── + summaryOuter: { + flexDirection: 'row', + justifyContent: 'flex-end', + marginTop: 8, + }, summary: { - marginTop: 20, - marginLeft: 'auto', - width: '40%', + width: '42%', }, summaryRow: { flexDirection: 'row', @@ -132,13 +222,9 @@ const styles = StyleSheet.create({ paddingVertical: 4, fontSize: 10, }, - summaryLabel: { - color: '#666666', - }, - summaryValue: { - fontWeight: 'bold', - color: '#1a1a1a', - }, + summaryLabel: { color: '#666666' }, + summaryValue: { fontFamily: 'Helvetica-Bold', color: '#1a1a1a' }, + discountValue: { color: '#16a34a', fontFamily: 'Helvetica-Bold' }, totalRow: { flexDirection: 'row', justifyContent: 'space-between', @@ -146,35 +232,27 @@ const styles = StyleSheet.create({ marginTop: 8, borderTopWidth: 2, borderTopColor: '#1a1a1a', - fontSize: 12, - }, - totalLabel: { - fontWeight: 'bold', - color: '#1a1a1a', - }, - totalValue: { - fontWeight: 'bold', - color: '#1a1a1a', + fontSize: 13, }, - badge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 4, + totalLabel: { fontFamily: 'Helvetica-Bold', color: '#1a1a1a' }, + totalValue: { fontFamily: 'Helvetica-Bold', color: '#1a1a1a' }, + + // ── Discount code chip ───────────────────────────────────────────────────── + discountChip: { + alignSelf: 'flex-start', + backgroundColor: '#f0fdf4', + borderWidth: 1, + borderColor: '#86efac', + borderRadius: 3, + paddingHorizontal: 6, + paddingVertical: 2, fontSize: 8, - fontWeight: 'bold', - textAlign: 'center', - marginTop: 4, - }, - badgePaid: { - backgroundColor: '#dcfce7', color: '#166534', + fontFamily: 'Helvetica-Bold', }, - badgePending: { - backgroundColor: '#fef3c7', - color: '#92400e', - }, + + // ── Tracking ─────────────────────────────────────────────────────────────── trackingInfo: { - marginTop: 10, padding: 12, backgroundColor: '#f9fafb', borderRadius: 4, @@ -183,190 +261,398 @@ const styles = StyleSheet.create({ }, trackingTitle: { fontSize: 10, - fontWeight: 'bold', + fontFamily: 'Helvetica-Bold', marginBottom: 6, color: '#1a1a1a', }, - trackingText: { + trackingGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + }, + trackingItem: { + flex: 1, + minWidth: '45%', + }, + trackingLabel: { + fontSize: 8, + color: '#888888', + textTransform: 'uppercase', + marginBottom: 2, + }, + trackingValue: { + fontSize: 9, + color: '#1a1a1a', + fontFamily: 'Helvetica-Bold', + }, + + // ── Customer note ───────────────────────────────────────────────────────── + noteBox: { + padding: 10, + backgroundColor: '#fffbeb', + borderRadius: 4, + borderWidth: 1, + borderColor: '#fde68a', + }, + noteText: { fontSize: 9, - lineHeight: 1.4, - color: '#4a4a4a', + color: '#78350f', + lineHeight: 1.5, + fontFamily: 'Helvetica-Oblique', + }, + + // ── Shipment label strip ────────────────────────────────────────────────── + shipmentLabel: { + marginBottom: 18, + padding: 14, + borderWidth: 2, + borderColor: '#1a1a1a', + borderRadius: 4, + }, + shipmentLabelHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 10, + paddingBottom: 8, + borderBottomWidth: 1, + borderBottomColor: '#cccccc', + }, + shipmentFrom: { + flex: 1, + }, + shipmentTo: { + flex: 1, + paddingLeft: 16, + borderLeftWidth: 1, + borderLeftColor: '#cccccc', + }, + shipmentSectionLabel: { + fontSize: 8, + color: '#888888', + textTransform: 'uppercase', + letterSpacing: 0.5, + marginBottom: 3, + }, + shipmentName: { + fontSize: 12, + fontFamily: 'Helvetica-Bold', + color: '#1a1a1a', + marginBottom: 2, + }, + shipmentAddress: { + fontSize: 9, + color: '#333333', + lineHeight: 1.5, + }, + shipmentMeta: { + flexDirection: 'row', + gap: 16, + }, + shipmentMetaItem: { + flex: 1, }, + + // ── Footer ──────────────────────────────────────────────────────────────── footer: { marginTop: 'auto', - paddingTop: 20, + paddingTop: 16, borderTopWidth: 1, borderTopColor: '#e5e5e5', - }, - footerText: { - fontSize: 8, - color: '#999999', - textAlign: 'center', - lineHeight: 1.4, + alignItems: 'center', }, thankyou: { fontSize: 10, - fontWeight: 'bold', + fontFamily: 'Helvetica-Bold', textAlign: 'center', - marginTop: 20, + marginBottom: 4, color: '#1a1a1a', }, + footerText: { + fontSize: 8, + color: '#aaaaaa', + textAlign: 'center', + lineHeight: 1.4, + }, }); +// ─── Helper types ───────────────────────────────────────────────────────────── +interface ParsedAddress { + firstName?: string; + lastName?: string; + address?: string; + address2?: string; + city?: string; + state?: string; + postalCode?: string; + country?: string; + phone?: string; + email?: string; +} + +// ─── Component ──────────────────────────────────────────────────────────────── interface InvoiceTemplateProps { data: InvoiceData; } export const InvoiceTemplate: React.FC = ({ data }) => { - // Format currency const formatCurrency = formatMoney; - // Format date - const formatDate = (date: Date | string) => { - const d = typeof date === 'string' ? new Date(date) : date; - return d.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }); + /** Parse stored JSON address blob into a typed object */ + const parseAddress = (addr: Record | null): ParsedAddress | null => { + if (!addr) return null; + // Handle case where the JSON was double-encoded (stored as a string) + if (typeof addr === 'string') { + try { return JSON.parse(addr as string) as ParsedAddress; } catch { return null; } + } + return addr as ParsedAddress; + }; + + /** Build full lines for an address block */ + const formatAddressLines = (parsed: ParsedAddress | null): string[] => { + if (!parsed) return []; + const lines: string[] = []; + if (parsed.firstName || parsed.lastName) + lines.push([parsed.firstName, parsed.lastName].filter(Boolean).join(' ')); + if (parsed.phone) lines.push(`Tel: ${parsed.phone}`); + if (parsed.email) lines.push(parsed.email); + if (parsed.address) lines.push(parsed.address); + if (parsed.address2) lines.push(parsed.address2); + const cityLine = [parsed.city, parsed.state, parsed.postalCode].filter(Boolean).join(', '); + if (cityLine) lines.push(cityLine); + if (parsed.country) lines.push(parsed.country); + return lines; }; - // Parse address - const parseAddress = (addr: Record | null) => { - if (!addr) return null; - return addr as { - firstName?: string; - lastName?: string; - address?: string; - address2?: string; - city?: string; - state?: string; - postalCode?: string; - country?: string; - phone?: string; - email?: string; - }; + /** Build full store address lines */ + const formatStoreAddress = (): string[] => { + const s = data.store; + const lines: string[] = []; + if (s.address) lines.push(s.address); + const cityLine = [s.city, s.state, s.postalCode].filter(Boolean).join(', '); + if (cityLine) lines.push(cityLine); + if (s.country) lines.push(s.country); + if (s.phone) lines.push(`Tel: ${s.phone}`); + if (s.email) lines.push(s.email); + if (s.website) lines.push(s.website); + return lines; + }; + + /** Determine badge style by payment status */ + const paymentBadgeStyle = () => { + if (data.paymentStatus === 'PAID') return styles.badgePaid; + if (data.paymentStatus === 'PENDING') return styles.badgePending; + return styles.badgeDefault; + }; + + /** Determine badge style by order status */ + const orderBadgeStyle = () => { + if (data.status === 'DELIVERED') return styles.badgePaid; + if (data.status === 'SHIPPED' || data.status === 'PROCESSING') return styles.badgeShipped; + if (data.status === 'CANCELED' || data.status === 'REFUNDED') return styles.badgeCanceled; + return styles.badgePending; }; const billingAddr = parseAddress(data.billingAddress); const shippingAddr = parseAddress(data.shippingAddress); + // Ship To: prefer explicit shipping address, fall back to billing address, then customer info + const effectiveShipToAddr = shippingAddr ?? billingAddr; + const shipToLines = formatAddressLines(effectiveShipToAddr); + const billToLines = formatAddressLines(billingAddr); + const storeLines = formatStoreAddress(); + + // Recipient name for shipment label + const shipToName = effectiveShipToAddr + ? [effectiveShipToAddr.firstName, effectiveShipToAddr.lastName].filter(Boolean).join(' ') + : data.customer + ? `${data.customer.firstName} ${data.customer.lastName}` + : 'N/A'; + + // Recipient phone (shipping addr phone or customer phone) + const shipToPhone = effectiveShipToAddr?.phone ?? data.customer?.phone ?? null; + + const hasTracking = !!( + data.trackingNumber ?? + data.pathaoConsignmentId ?? + data.pathaoTrackingCode + ); + return ( - {/* Header */} + + {/* ── Header: Store info + INVOICE title ────────────────────────── */} - {data.store.name} - - {data.store.address && `${data.store.address}\n`} - {data.store.phone && `Phone: ${data.store.phone}\n`} - {data.store.email && `Email: ${data.store.email}`} - + + {data.store.name} + {storeLines.map((line, i) => ( + {line} + ))} + + + INVOICE + #{data.orderNumber} + - {/* Invoice Title */} - INVOICE - - {/* Metadata */} - + {/* ── Metadata row ─────────────────────────────────────────────── */} + - Invoice Number - {data.orderNumber} + Invoice Date + {data.createdAt} - Invoice Date - {formatDate(data.createdAt)} + Order Status + + {data.status} + Payment Status - + {data.paymentStatus} Payment Method - {data.paymentMethod || 'N/A'} + {data.paymentMethod ?? 'N/A'} - {/* Addresses */} + {/* ── Shipment Label Strip (From → To) ─────────────────────────── */} + + + {/* FROM */} + + From (Seller) + {data.store.name} + {storeLines.slice(0, 4).map((line, i) => ( + {line} + ))} + + {/* TO */} + + Ship To (Recipient) + {shipToName} + {shipToPhone && ( + Tel: {shipToPhone} + )} + {(shipToLines.length > 0 ? shipToLines : billToLines).slice(2).map((line, i) => ( + {line} + ))} + + + {/* Shipping meta */} + + {data.shippingMethod && ( + + Shipping Method + {data.shippingMethod} + + )} + {data.shippingStatus && ( + + Shipping Status + {data.shippingStatus} + + )} + {data.estimatedDelivery && ( + + Est. Delivery + {data.estimatedDelivery} + + )} + {(data.trackingNumber ?? data.pathaoTrackingCode) && ( + + Tracking No. + + {data.trackingNumber ?? data.pathaoTrackingCode} + + + )} + + + + {/* ── Bill To / Ship To (detailed) ─────────────────────────────── */} Billing & Shipping Information {/* Bill To */} Bill To - {data.customer ? ( - - {`${data.customer.firstName} ${data.customer.lastName}\n`} - {data.customer.email && `${data.customer.email}\n`} - {data.customer.phone && `${data.customer.phone}\n`} - {billingAddr?.address && `${billingAddr.address}\n`} - {billingAddr?.address2 && `${billingAddr.address2}\n`} - {billingAddr?.city && billingAddr?.state && billingAddr?.postalCode && - `${billingAddr.city}, ${billingAddr.state} ${billingAddr.postalCode}\n`} - {billingAddr?.country || ''} - - ) : billingAddr ? ( - - {billingAddr.firstName && billingAddr.lastName && - `${billingAddr.firstName} ${billingAddr.lastName}\n`} - {billingAddr.email && `${billingAddr.email}\n`} - {billingAddr.phone && `${billingAddr.phone}\n`} - {billingAddr.address && `${billingAddr.address}\n`} - {billingAddr.address2 && `${billingAddr.address2}\n`} - {billingAddr.city && billingAddr.state && billingAddr.postalCode && - `${billingAddr.city}, ${billingAddr.state} ${billingAddr.postalCode}\n`} - {billingAddr.country || ''} + {data.customer && ( + + {data.customer.firstName} {data.customer.lastName} + )} + {billToLines.length > 0 ? ( + <> + {billToLines.map((line, i) => ( + + {line} + + ))} + + ) : data.customer ? ( + <> + {data.customer.email && ( + {data.customer.email} + )} + {data.customer.phone && ( + {data.customer.phone} + )} + ) : ( N/A )} - {/* Ship To */} + {/* Ship To — explicit address or fallback to billing address */} - Ship To - {shippingAddr ? ( - - {shippingAddr.firstName && shippingAddr.lastName && - `${shippingAddr.firstName} ${shippingAddr.lastName}\n`} - {shippingAddr.email && `${shippingAddr.email}\n`} - {shippingAddr.phone && `${shippingAddr.phone}\n`} - {shippingAddr.address && `${shippingAddr.address}\n`} - {shippingAddr.address2 && `${shippingAddr.address2}\n`} - {shippingAddr.city && shippingAddr.state && shippingAddr.postalCode && - `${shippingAddr.city}, ${shippingAddr.state} ${shippingAddr.postalCode}\n`} - {shippingAddr.country || ''} - + + Ship To{!shippingAddr && billingAddr ? ' (Same as Billing)' : ''} + + {shipToName} + {shipToLines.length > 0 ? ( + shipToLines.map((line, i) => ( + + {line} + + )) + ) : data.customer ? ( + <> + {data.customer.email && ( + {data.customer.email} + )} + {data.customer.phone && ( + {data.customer.phone} + )} + ) : ( - Same as billing address + N/A )} - {/* Items Table */} + {/* ── Order Items ──────────────────────────────────────────────── */} Order Items - {/* Table Header */} + {/* Header */} - Product - SKU - Qty - Price - Total + Product + SKU + Qty + Unit Price + Total - - {/* Table Rows */} + {/* Rows */} {data.items.map((item, index) => ( - + {item.productName} {item.variantName && ( @@ -384,75 +670,107 @@ export const InvoiceTemplate: React.FC = ({ data }) => { - {/* Summary */} - - - Subtotal - {formatCurrency(data.subtotal)} - - - Shipping - {formatCurrency(data.shippingAmount)} - - {data.taxAmount > 0 && ( + {/* ── Summary ──────────────────────────────────────────────────── */} + + - Tax - {formatCurrency(data.taxAmount)} + Subtotal + {formatCurrency(data.subtotal)} - )} - {data.discountAmount > 0 && ( - - Discount - - -{formatCurrency(data.discountAmount)} - + {data.shippingAmount > 0 && ( + + + Shipping{data.shippingMethod ? ` (${data.shippingMethod})` : ''} + + {formatCurrency(data.shippingAmount)} + + )} + {data.taxAmount > 0 && ( + + Tax + {formatCurrency(data.taxAmount)} + + )} + {data.discountAmount > 0 && ( + + + Discount + {data.discountCode && ( + CODE: {data.discountCode} + )} + + –{formatCurrency(data.discountAmount)} + + )} + + Total + {formatCurrency(data.totalAmount)} - )} - - Total - {formatCurrency(data.totalAmount)} - {/* Tracking Information */} - {(data.trackingNumber || data.pathaoTrackingCode || data.pathaoConsignmentId) && ( + {/* ── Tracking Information ─────────────────────────────────────── */} + {hasTracking && ( Shipment Tracking - 📦 Tracking Information - {data.trackingNumber && ( - - {`Tracking Number: ${data.trackingNumber}\n`} - - )} - {data.trackingUrl && ( - {`Tracking URL: ${data.trackingUrl}\n`} - )} - {data.pathaoConsignmentId && ( - - {`Pathao Consignment ID: ${data.pathaoConsignmentId}\n`} - - )} - {data.pathaoTrackingCode && ( - - {`Pathao Tracking Code: ${data.pathaoTrackingCode}\n`} - - )} - {data.pathaoStatus && ( - {`Pathao Status: ${data.pathaoStatus}`} - )} + Tracking Details + + {data.trackingNumber && ( + + Tracking Number + {data.trackingNumber} + + )} + {data.trackingUrl && ( + + Tracking URL + {data.trackingUrl} + + )} + {data.pathaoConsignmentId && ( + + Pathao Consignment ID + {data.pathaoConsignmentId} + + )} + {data.pathaoTrackingCode && ( + + Pathao Tracking Code + {data.pathaoTrackingCode} + + )} + {data.pathaoStatus && ( + + Pathao Status + {data.pathaoStatus} + + )} + )} - {/* Footer */} + {/* ── Customer Note ────────────────────────────────────────────── */} + {data.customerNote && ( + + Customer Note + + {data.customerNote} + + + )} + + {/* ── Footer ───────────────────────────────────────────────────── */} Thank you for your business! - {`This is a computer-generated invoice.\n`} - For any queries, please contact ${data.store.email || 'support'}. + This is a computer-generated invoice — no signature required.{'\n'} + For inquiries please contact {data.store.email ?? data.store.name}. + {data.store.website ? ` | ${data.store.website}` : ''} + ); diff --git a/src/lib/services/order.service.ts b/src/lib/services/order.service.ts index f19e15ae..2d4088e3 100644 --- a/src/lib/services/order.service.ts +++ b/src/lib/services/order.service.ts @@ -680,6 +680,11 @@ export class OrderService { email: true, address: true, phone: true, + city: true, + state: true, + postalCode: true, + country: true, + website: true, }, }, }, @@ -721,7 +726,12 @@ export class OrderService { taxAmount: order.taxAmount || 0, shippingAmount: order.shippingAmount || 0, discountAmount: order.discountAmount || 0, + discountCode: order.discountCode ?? null, totalAmount: order.totalAmount || 0, + shippingMethod: order.shippingMethod ?? null, + estimatedDelivery: order.estimatedDelivery ? formatDate(order.estimatedDelivery) : null, + shippingStatus: order.shippingStatus ?? null, + customerNote: order.customerNote ?? null, trackingNumber: order.trackingNumber, trackingUrl: order.trackingUrl, pathaoConsignmentId: order.pathaoConsignmentId ?? null, diff --git a/src/lib/types/invoice.ts b/src/lib/types/invoice.ts index 75d127e4..98be05af 100644 --- a/src/lib/types/invoice.ts +++ b/src/lib/types/invoice.ts @@ -13,6 +13,11 @@ export interface InvoiceData { email: string | null; address: string | null; phone: string | null; + city: string | null; + state: string | null; + postalCode: string | null; + country: string; + website: string | null; }; customer: { id: string; @@ -35,7 +40,12 @@ export interface InvoiceData { taxAmount: number; shippingAmount: number; discountAmount: number; + discountCode: string | null; totalAmount: number; + shippingMethod: string | null; + estimatedDelivery: string | null; + shippingStatus: string | null; + customerNote: string | null; trackingNumber: string | null; trackingUrl: string | null; pathaoConsignmentId?: string | null;