/* eslint-disable react/no-unknown-property */ import { Document, Image, Link, Page, StyleSheet, Text, View, } from "@react-pdf/renderer"; import React from "react"; import { formatDate, formatMoney, formatQuantity, formatTaxRate } from "../format"; import { getStrings, paymentStatusLabel as getPaymentStatusLabel } from "../i18n"; import type { InvoiceLanguage } from "../i18n"; import type { InvoiceViewModel, InvoiceLine, IssuerData, RecipientData } from "../types"; // Brand blue chosen to roughly match the reference invoice. This is not // pixel-perfect; merchants can tweak via a future setting if needed. const BRAND_BLUE = "#1E8FCD"; const TEXT_DARK = "#1F2933"; const TEXT_MUTED = "#6B7280"; const TABLE_BORDER = "#E5E7EB"; const styles = StyleSheet.create({ page: { paddingTop: 40, paddingBottom: 110, // leaves room for fixed footer paddingHorizontal: 40, fontSize: 9, fontFamily: "Helvetica", color: TEXT_DARK, lineHeight: 1.4, }, headerRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 24, }, logo: { maxHeight: 50, maxWidth: 180, objectFit: "contain", }, senderLine: { fontSize: 7, color: TEXT_MUTED, marginBottom: 4, textDecoration: "underline", }, recipientBlock: { width: "55%", }, recipientBlockFull: { flexDirection: "row", justifyContent: "space-between", marginBottom: 20, }, recipientName: { fontFamily: "Helvetica-Bold", fontSize: 10, }, shippingAddressBlock: { marginTop: 10, paddingTop: 6, borderTopWidth: 0.5, borderTopColor: TABLE_BORDER, }, shippingAddressHeading: { fontFamily: "Helvetica-Bold", color: BRAND_BLUE, fontSize: 8, marginBottom: 2, }, metaBlock: { width: "45%", }, metaBlockHeader: { width: "50%", }, metaTable: { flexDirection: "column", }, metaRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 2, }, metaLabel: { color: TEXT_MUTED, }, metaValue: { fontFamily: "Helvetica-Bold", }, unitOriginalStrike: { color: TEXT_MUTED, textDecoration: "line-through", fontSize: 7, }, invoiceNumberBig: { color: BRAND_BLUE, fontFamily: "Helvetica-Bold", fontSize: 14, }, title: { color: BRAND_BLUE, fontFamily: "Helvetica-Bold", fontSize: 18, marginTop: 20, marginBottom: 12, }, paragraph: { marginBottom: 6, }, table: { marginTop: 10, borderTopWidth: 0, }, tableHeader: { flexDirection: "row", backgroundColor: BRAND_BLUE, color: "#FFFFFF", fontFamily: "Helvetica-Bold", paddingVertical: 6, paddingHorizontal: 4, }, tableRow: { flexDirection: "row", borderBottomWidth: 0.5, borderBottomColor: TABLE_BORDER, paddingVertical: 6, paddingHorizontal: 4, }, colPos: { width: "8%" }, colDescription: { width: "44%" }, colQty: { width: "16%", textAlign: "right" }, colUnit: { width: "16%", textAlign: "right" }, colTotal: { width: "16%", textAlign: "right" }, descriptionCell: { flexDirection: "row", alignItems: "flex-start", gap: 6, }, productIcon: { width: 28, height: 28, objectFit: "contain", borderWidth: 0.5, borderColor: TABLE_BORDER, borderRadius: 2, }, productIconPlaceholder: { width: 28, height: 28, }, descriptionText: { flex: 1, }, itemTitle: { fontFamily: "Helvetica-Bold", }, itemSku: { color: TEXT_MUTED, fontSize: 7, marginTop: 1, }, totalsBlock: { marginTop: 10, alignSelf: "flex-end", width: "50%", // Match the table rows' horizontal padding so the right-aligned amounts // line up perfectly with the "Total" column above. paddingHorizontal: 4, }, totalRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 3, }, totalLabel: { color: TEXT_DARK, }, totalLabelBlue: { color: BRAND_BLUE, fontFamily: "Helvetica-Bold", }, totalValue: { textAlign: "right", }, totalValueBoldBlue: { textAlign: "right", color: BRAND_BLUE, fontFamily: "Helvetica-Bold", }, noticeBlock: { marginTop: 10, padding: 6, backgroundColor: "#F3F4F6", fontSize: 8, }, giroBlock: { marginTop: 20, flexDirection: "row", alignItems: "flex-start", gap: 10, }, giroImage: { width: 90, height: 90, }, giroCaption: { fontFamily: "Helvetica-Bold", color: BRAND_BLUE, fontSize: 9, marginBottom: 4, }, giroDetails: { fontSize: 8, color: TEXT_DARK, lineHeight: 1.4, }, closing: { marginTop: 24, }, footer: { position: "absolute", bottom: 30, left: 40, right: 40, borderTopWidth: 0.5, borderTopColor: BRAND_BLUE, paddingTop: 6, flexDirection: "row", justifyContent: "space-between", fontSize: 7, color: TEXT_DARK, }, footerCol: { width: "23%" }, footerHeading: { fontFamily: "Helvetica-Bold", color: BRAND_BLUE, fontSize: 7, marginBottom: 3, }, pageIndicator: { position: "absolute", bottom: 12, right: 40, fontSize: 7, color: TEXT_MUTED, }, stornoBanner: { backgroundColor: "#B91C1C", color: "#FFFFFF", fontFamily: "Helvetica-Bold", padding: 6, marginBottom: 10, fontSize: 11, textAlign: "center", }, }); interface DocProps { invoice: InvoiceViewModel; } export function InvoiceDocument({ invoice }: DocProps) { const t = getStrings(invoice.language); const cur = invoice.currency; return ( {invoice.kind === "storno" && ( {t.stornoInvoice} {invoice.cancelsNumber ? ` โ€” ${t.stornoReference(invoice.cancelsNumber)}` : ""} )} {invoice.issuer.logoDataUrl ? ( ) : ( )} {invoice.kind === "offer" ? t.offerDate : t.invoiceDate} {formatDate(invoice.invoiceDate, invoice.language)} {invoice.kind !== "offer" && ( {t.deliveryDate} {formatDate(invoice.deliveryDate, invoice.language)} )} {invoice.recipientVatId ? ( {t.customerVatId} {invoice.recipientVatId} ) : null} {invoice.kind === "invoice" && invoice.paymentGatewayNames.length > 0 && ( {t.paymentMethodLabel} {invoice.paymentGatewayNames.map(prettifyGatewayName).join(", ")} )} {invoice.kind === "invoice" && ( {t.paymentStatusLabel} {getPaymentStatusLabel(invoice.paymentStatus, t)} )} {invoice.kind === "invoice" && invoice.discountCodes.length > 0 ? ( {t.discountCodeLabel} {invoice.discountCodes.join(", ")} ) : null} {invoice.kind === "invoice" && invoice.isPickup ? ( {t.pickupLocationLabel} {invoice.pickupLocationName ?? t.pickupLabel} ) : invoice.kind === "invoice" && invoice.shippingMethod ? ( {t.shippingMethodLabel} {invoice.shippingMethod} ) : null} {invoice.kind === "invoice" && invoice.tracking.map((tr) => ( {t.trackingLabel} {tr.company ? ` (${tr.company})` : ""} {tr.url ? ( {tr.number} ) : ( {tr.number} )} ))} {senderInline(invoice.issuer)} {invoice.separateShippingAddress ? ( {t.shippingAddressHeading} ) : null} {invoice.kind === "storno" ? t.stornoInvoice : invoice.kind === "offer" ? t.offer : t.invoice}{" "} Nr. {invoice.number} {invoice.kind === "invoice" && invoice.orderName ? ` ยท ${t.orderNumberLabel}: ${invoice.orderName}` : ""} {t.salutationGeneric} {t.thankYouLine} {t.position} {t.description} {t.quantity} {t.unitPrice} {t.totalPrice} {invoice.lines.map((line) => ( ))} {t.netTotal} {formatMoney(invoice.totals.net, cur, invoice.language)} {invoice.totals.vatBreakdown.map((v) => ( {t.vatLine(formatTaxRate(v.ratePct, invoice.language))} {formatMoney(v.tax, cur, invoice.language)} ))} {t.grossTotal} {formatMoney(invoice.totals.gross, cur, invoice.language)} {invoice.notices.length > 0 && ( {invoice.notices.map((n) => ( {n.kind === "reverseCharge" && t.reverseChargeNotice} {n.kind === "export" && t.exportNotice} {n.kind === "kleinunternehmer" && t.kleinunternehmerNotice} ))} )} {invoice.kind === "offer" ? ( {invoice.dueDate ? t.offerValidUntil(formatDate(invoice.dueDate, invoice.language)) : null} ) : !invoice.paid && ( {invoice.dueDate ? t.paymentTerms( Math.max(0, Math.round((invoice.dueDate.getTime() - invoice.invoiceDate.getTime()) / 86400000)), formatDate(invoice.dueDate, invoice.language), ) : t.paymentTermsImmediate} )} {invoice.giroCodePngDataUrl && !invoice.paid && ( {t.giroCodeCaption} {t.recipientLabel}: {[invoice.issuer.companyName, invoice.issuer.legalForm].filter(Boolean).join(" ")} {invoice.issuer.bankName ? ( {t.bankLabel}: {invoice.issuer.bankName} ) : null} {t.ibanLabel}: {invoice.issuer.iban} {invoice.issuer.bic ? ( {t.bicLabel}: {invoice.issuer.bic} ) : null} {t.amountLabel}: {formatMoney(invoice.totals.gross, cur, invoice.language)} {t.referenceLabel}: {invoice.number} )} {t.closing}