Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Updates the invoice PDF generation to include richer store/shipping/payment metadata and a redesigned invoice layout, backed by expanded InvoiceData typing and updated order-to-invoice mapping.
Changes:
- Expanded
InvoiceData(store address fields + discount/shipping/customer note metadata). - Extended
OrderService.getInvoiceData()Prisma selection and mapping for the new invoice fields. - Revamped the React-PDF invoice template styling/layout, including shipment label, tracking grid, discount chip, and customer note section.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/lib/types/invoice.ts | Extends invoice data contract with store address fields and new shipment/discount/note metadata. |
| src/lib/services/order.service.ts | Adds store fields to invoice query and maps new discount/shipping/note fields into InvoiceData. |
| src/components/invoices/invoice-template.tsx | Redesigns PDF template layout/styles and renders the new metadata (ship-to fallback, discount code chip, tracking grid, customer note). |
| const parseAddress = (addr: Record<string, unknown> | 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; |
There was a problem hiding this comment.
parseAddress is typed as (addr: Record<string, unknown> | null), but it contains a typeof addr === 'string' branch. With the current InvoiceData type (billingAddress/shippingAddress are Record<string, unknown> | null), this condition is statically impossible and should fail TypeScript (and also hides that Order.billingAddress/shippingAddress are stored as strings in Prisma). Update the types to include string (or, preferably, JSON.parse the address strings in OrderService.getInvoiceData and keep the template working with a typed object).
| const shipToName = effectiveShipToAddr | ||
| ? [effectiveShipToAddr.firstName, effectiveShipToAddr.lastName].filter(Boolean).join(' ') | ||
| : data.customer | ||
| ? `${data.customer.firstName} ${data.customer.lastName}` | ||
| : 'N/A'; |
There was a problem hiding this comment.
shipToName can become an empty string when an address object exists but has no firstName/lastName (e.g. { address: "..." }). Because the ternary only checks effectiveShipToAddr truthiness, the customer fallback never runs and the recipient name renders blank. Consider falling back when the computed name is empty (e.g., compute the name string first and then || to customer name / "N/A").
| const shipToName = effectiveShipToAddr | |
| ? [effectiveShipToAddr.firstName, effectiveShipToAddr.lastName].filter(Boolean).join(' ') | |
| : data.customer | |
| ? `${data.customer.firstName} ${data.customer.lastName}` | |
| : 'N/A'; | |
| const shipToAddrName = effectiveShipToAddr | |
| ? [effectiveShipToAddr.firstName, effectiveShipToAddr.lastName].filter(Boolean).join(' ') | |
| : ''; | |
| const customerName = data.customer | |
| ? [data.customer.firstName, data.customer.lastName].filter(Boolean).join(' ') | |
| : ''; | |
| const shipToName = shipToAddrName || customerName || 'N/A'; |
| {(shipToLines.length > 0 ? shipToLines : billToLines).slice(2).map((line, i) => ( | ||
| <Text key={i} style={styles.shipmentAddress}>{line}</Text> | ||
| ))} |
There was a problem hiding this comment.
In the shipment label "Ship To" address, using .slice(2) on the chosen address lines can drop the actual street address/city lines when the parsed address doesn't include both a name and phone/email. This can result in an empty/partial address on the label. Instead of slicing by index, skip specific fields (name/phone/email) when building the lines for this label, or build a dedicated formatShippingLabelLines() that only returns address lines.
| {(shipToLines.length > 0 ? shipToLines : billToLines).slice(2).map((line, i) => ( | |
| <Text key={i} style={styles.shipmentAddress}>{line}</Text> | |
| ))} | |
| {(shipToLines.length > 0 ? shipToLines : billToLines) | |
| .filter((line) => { | |
| const trimmed = line.trim(); | |
| const lower = trimmed.toLowerCase(); | |
| const phone = shipToPhone?.trim(); | |
| return ( | |
| trimmed.length > 0 && | |
| trimmed !== shipToName?.trim() && | |
| (!phone || trimmed !== phone) && | |
| !lower.startsWith('tel:') && | |
| !lower.startsWith('phone:') | |
| ); | |
| }) | |
| .map((line, i) => ( | |
| <Text key={i} style={styles.shipmentAddress}>{line}</Text> | |
| ))} |
| {data.customer && ( | ||
| <Text style={styles.addressName}> | ||
| {data.customer.firstName} {data.customer.lastName} | ||
| </Text> | ||
| )} | ||
| {billToLines.length > 0 ? ( | ||
| <> | ||
| {billToLines.map((line, i) => ( | ||
| <Text key={i} style={i < 2 ? styles.addressHighlight : styles.addressText}> | ||
| {line} | ||
| </Text> |
There was a problem hiding this comment.
The Bill To block prints the customer name (data.customer.firstName/lastName) and then renders billToLines, which may also include the parsed firstName/lastName as its first line. This can duplicate the recipient name in the PDF. Consider either omitting the name line from formatAddressLines when you already render addressName, or only render addressName when the first formatted line is not a name.
| <Text style={styles.label}>Order Status</Text> | ||
| <View style={[styles.badge, orderBadgeStyle()]}> | ||
| <Text>{data.status}</Text> | ||
| </View> | ||
| </View> | ||
| <View style={styles.metadataBlock}> | ||
| <Text style={styles.label}>Payment Status</Text> | ||
| <View | ||
| style={[ | ||
| styles.badge, | ||
| data.paymentStatus === 'PAID' ? styles.badgePaid : styles.badgePending, | ||
| ]} | ||
| > | ||
| <View style={[styles.badge, paymentBadgeStyle()]}> | ||
| <Text>{data.paymentStatus}</Text> | ||
| </View> |
There was a problem hiding this comment.
Status badges apply fontSize, fontFamily, textAlign, and color via styles.badge/styles.badge* on a <View>, while the nested <Text> has no style. In @react-pdf/renderer these text-related styles generally need to be on the <Text> node (and color won't reliably affect the child text when set on a View), so the badge text may render with the wrong size/weight/color. Move typography/color to a dedicated badgeText style applied to the <Text>, and keep only layout/background/padding on the <View>.
| // ── Shipping info strip ──────────────────────────────────────────────────── | ||
| shippingStrip: { | ||
| flexDirection: 'row', | ||
| gap: 12, | ||
| marginBottom: 18, | ||
| padding: 10, | ||
| backgroundColor: '#eff6ff', | ||
| borderRadius: 4, | ||
| borderWidth: 1, | ||
| borderColor: '#bfdbfe', | ||
| }, | ||
| shippingStripBlock: { | ||
| flex: 1, | ||
| }, | ||
|
|
There was a problem hiding this comment.
shippingStrip / shippingStripBlock styles are defined but never used in the component. Consider removing them (or wiring them up) to avoid accumulating unused style code in the PDF template.
| // ── Shipping info strip ──────────────────────────────────────────────────── | |
| shippingStrip: { | |
| flexDirection: 'row', | |
| gap: 12, | |
| marginBottom: 18, | |
| padding: 10, | |
| backgroundColor: '#eff6ff', | |
| borderRadius: 4, | |
| borderWidth: 1, | |
| borderColor: '#bfdbfe', | |
| }, | |
| shippingStripBlock: { | |
| flex: 1, | |
| }, |
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.