import "../module-globals.js"; import React from "react"; const header = defineComponent({ name: "header", schema: z.object({ strings: z .object({ invoice: z.string().default(""), invoiceNumber: z.string().default(""), date: z.string().default(""), dueDate: z.string().default(""), }) .default({}), fromAddress: z.string().array().nullish(), invoice: z.object({ invoiceNumber: z.string().or(z.number()), date: z.string(), dueDate: z.string(), }), }), component: ({ spec, styles, getComponent }) => { const Markdown = getComponent(defaultComponents.markdown); return ( <> {spec.strings?.invoice} {spec.fromAddress?.join("\n")} {spec.strings?.invoiceNumber} {spec.invoice.invoiceNumber} {spec.strings?.date} {spec.invoice.date} {spec.strings?.dueDate} {spec.invoice.dueDate} ); }, defaultStyles: { container: { marginBottom: "20pt", }, headerRow: { display: "flex", flexDirection: "row", justifyContent: "space-between", }, fromSection: { flex: 1, }, title: { fontSize: "28pt", fontWeight: "bold", marginBottom: "10pt", }, fromAddress: { fontSize: "10pt", lineHeight: "1.2", }, invoiceDetails: { width: "200pt", }, detailRow: { display: "flex", flexDirection: "row", justifyContent: "space-between", marginBottom: "4pt", }, label: { fontSize: "10pt", fontWeight: "bold", }, value: { fontSize: "10pt", }, } as const, }); const toAddress = defineComponent({ name: "toAddress", schema: z.object({ strings: z .object({ billTo: z.string().default(""), shipTo: z.string().default(""), }) .default({}), toAddress: z.string().array().nullish(), shipAddress: z.string().array().nullish(), }), component: ({ spec, styles, getComponent }) => { const Markdown = getComponent(defaultComponents.markdown); const billTo = spec.toAddress; const shipTo = spec.shipAddress; return ( <> {spec.strings?.billTo} {billTo?.join("\n")} {shipTo && ( {spec.strings?.shipTo} {shipTo?.join("\n")} )} ); }, defaultStyles: { container: { marginBottom: "20pt", }, addressRow: { display: "flex", flexDirection: "row", gap: "40pt", }, addressSection: { flex: 1, }, addressLabel: { fontSize: "12pt", fontWeight: "bold", marginBottom: "6pt", }, address: { fontSize: "10pt", lineHeight: "1.2", }, } as const, }); const invoiceTableRow = defineComponent({ name: "invoiceTableRow", schema: z.object({}), additionalProps: z.object({ item: z.object({ definition: z.object({ description: z.string(), unitPrice: z.number(), }), quantity: z.number().nullish(), }), currency: z.string().nullish(), }), component: ({ item, currency = "USD", styles }) => { const lineTotal = item.definition.unitPrice * (item.quantity || 1); return ( <> {item.definition.description} {item.quantity || 1} {currency} {item.definition.unitPrice.toFixed(2)} {currency} {lineTotal.toFixed(2)} ); }, defaultStyles: { tableRow: { display: "flex", flexDirection: "row", borderBottom: "0.5pt solid #ccc", }, cell: { fontSize: "9pt", padding: "6pt", borderRight: "0.5pt solid #ccc", }, descriptionCol: { flex: 3, }, qtyCol: { flex: 1, }, priceCol: { flex: 2, textAlign: "right", }, totalCol: { flex: 2, textAlign: "right", borderRight: "none", }, } as const, }); const invoiceSummaryModifier = defineComponent({ name: "invoiceSummaryModifier", schema: z.object({}), additionalProps: z.object({ modifier: z.object({ description: z.string(), rate: z.number(), }), subtotal: z.number(), currency: z.string().nullish(), }), component: ({ modifier, subtotal, currency = "USD", styles }) => { const modifierAmount = subtotal * modifier.rate; return ( <> {modifier.description} ({(modifier.rate * 100).toFixed(0)}%): {currency} {modifierAmount.toFixed(2)} ); }, defaultStyles: { summaryRow: { display: "flex", flexDirection: "row", justifyContent: "space-between", marginBottom: "4pt", }, summaryLabel: { fontSize: "10pt", }, summaryValue: { fontSize: "10pt", textAlign: "right", }, } as const, }); const invoiceSummary = defineComponent({ name: "invoiceSummary", schema: z.object({}), additionalProps: z.object({ subtotal: z.number(), total: z.number(), modifiers: z .array( z.object({ description: z.string(), rate: z.number(), }), ) .nullish(), paymentMade: z.number().nullish(), currency: z.string().nullish(), targetCurrency: z.string().nullish(), conversionRate: z.number().nullish(), strings: z .object({ subtotal: z.string().default(""), totalLabel: z.string().default(""), paymentMade: z.string().default(""), balanceDue: z.string().default(""), exchangeRate: z.string().default(""), convertedTotal: z.string().default(""), }) .nullish(), }), component: ({ subtotal, total, modifiers, paymentMade = 0, currency = "USD", targetCurrency, conversionRate, strings, styles, getComponent, }) => { const balance = total - (paymentMade || 0); const InvoiceSummaryModifier = getComponent({ name: "invoiceSummaryModifier", }); const convertedTotal = targetCurrency && conversionRate ? total * conversionRate : null; // Payment made is assumed to already be in target currency if target currency is defined const convertedBalance = targetCurrency && conversionRate && convertedTotal !== null ? convertedTotal - (paymentMade || 0) : null; return ( <> {strings?.subtotal}: {currency} {subtotal.toFixed(2)} {/* Modifiers */} {(modifiers || []).map((modifier, index) => ( ))} {strings?.totalLabel}: {currency} {total.toFixed(2)} {/* Currency conversion section */} {targetCurrency && conversionRate && convertedTotal !== null && ( <> {strings?.exchangeRate}: {currency} 1 = {targetCurrency} {conversionRate.toFixed(4)} {strings?.convertedTotal}: {targetCurrency} {convertedTotal.toFixed(2)} )} {(paymentMade || 0) > 0 && ( <> {strings?.paymentMade}: {targetCurrency && conversionRate ? targetCurrency : currency}{" "} {(paymentMade || 0).toFixed(2)} {strings?.balanceDue}: {targetCurrency && conversionRate ? targetCurrency : currency}{" "} {targetCurrency && conversionRate ? convertedBalance?.toFixed(2) : balance.toFixed(2)} )} ); }, defaultStyles: { summaryRow: { display: "flex", flexDirection: "row", justifyContent: "space-between", marginBottom: "4pt", }, summaryLabel: { fontSize: "10pt", }, summaryValue: { fontSize: "10pt", textAlign: "right", }, totalRow: { borderTop: "1pt solid #000", paddingTop: "6pt", marginTop: "6pt", }, totalLabel: { fontWeight: "bold", fontSize: "12pt", }, totalValue: { fontWeight: "bold", fontSize: "12pt", }, convertedTotalRow: { borderTop: "0.5pt solid #999", paddingTop: "4pt", marginTop: "4pt", }, convertedTotalLabel: { fontWeight: "bold", fontSize: "11pt", fontStyle: "italic", }, convertedTotalValue: { fontWeight: "bold", fontSize: "11pt", fontStyle: "italic", }, balanceRow: { borderTop: "1pt solid #000", paddingTop: "6pt", marginTop: "6pt", }, balanceLabel: { fontWeight: "bold", fontSize: "12pt", }, balanceValue: { fontWeight: "bold", fontSize: "12pt", }, convertedBalanceText: { fontSize: "10pt", fontStyle: "italic", color: "#666", }, } as const, }); const invoiceTable = defineComponent({ name: "invoiceTable", schema: z.object({ strings: z .object({ description: z.string().default(""), qty: z.string().default(""), unitPrice: z.string().default(""), total: z.string().default(""), subtotal: z.string().default(""), totalLabel: z.string().default(""), paymentMade: z.string().default(""), balanceDue: z.string().default(""), }) .default({}), items: z.array( z.object({ definition: z.object({ description: z.string(), unitPrice: z.number(), }), quantity: z.number().nullish(), }), ), modifiers: z .array( z.object({ description: z.string(), rate: z.number(), }), ) .nullish(), invoice: z.object({ currency: z.string().nullish(), targetCurrency: z.string().nullish(), conversionRate: z.number().nullish(), paymentMade: z.number().nullish(), }), }), component: ({ spec, styles, getComponent }) => { const InvoiceTableRow = getComponent({ name: "invoiceTableRow" }); const InvoiceSummary = getComponent({ name: "invoiceSummary" }); const currency = spec.invoice.currency || "USD"; const paymentMade = spec.invoice.paymentMade || 0; const subtotal = spec.items.reduce( (sum, item) => sum + item.definition.unitPrice * (item.quantity || 1), 0, ); const modifierTotal = (spec.modifiers || []).reduce( (sum, modifier) => sum + subtotal * modifier.rate, 0, ); const total = subtotal + modifierTotal; return ( <> {/* Table Header */} {spec.strings?.description} {spec.strings?.qty} {spec.strings?.unitPrice} {spec.strings?.total} {/* Table Rows */} {spec.items.map((item, index) => ( ))} {/* Summary Section */} ); }, defaultStyles: { container: { marginTop: "20pt", marginBottom: "20pt", }, tableHeader: { display: "flex", flexDirection: "row", backgroundColor: "#f0f0f0", borderTop: "1pt solid #000", borderBottom: "1pt solid #000", }, headerCell: { fontSize: "10pt", fontWeight: "bold", padding: "6pt", borderRight: "0.5pt solid #ccc", }, descriptionCol: { flex: 3, }, qtyCol: { flex: 1, }, priceCol: { flex: 2, textAlign: "right", }, totalCol: { flex: 2, textAlign: "right", borderRight: "none", }, summaryContainer: { marginTop: "10pt", width: "50%", marginLeft: "auto", }, } as const, }); const body = defineComponent({ name: "body", schema: z.object({ body: z.string().or(z.string().array()).nullish(), }), component: ({ spec, styles, getComponent }) => { const Markdown = getComponent(defaultComponents.markdown); if (!spec.body) return null; return ( <> {spec.body} ); }, defaultStyles: { container: { marginTop: "20pt", }, text: { fontSize: "10pt" }, } as const, }); export default { config: { components: { header, toAddress, invoiceTable, invoiceTableRow, invoiceSummaryModifier, invoiceSummary, body, }, }, order: ["header", "toAddress", "invoiceTable", "body"], strings: { invoice: "INVOICE", invoiceNumber: "Invoice #:", date: "Date:", dueDate: "Due Date:", billTo: "Bill To:", shipTo: "Ship To:", description: "Description", qty: "Qty", unitPrice: "Unit Price", total: "Total", subtotal: "Subtotal", totalLabel: "Total", paymentMade: "Payment Made", balanceDue: "Balance Due", exchangeRate: "Exchange Rate", convertedTotal: "Total (Converted)", }, styles: { page: { container: { paddingHorizontal: "48pt", paddingVertical: "40pt", }, }, }, };