import React from "react"; import { Document, Page, Text, View, StyleSheet, } from "@react-pdf/renderer"; const styles = StyleSheet.create({ page: { fontFamily: "Helvetica", fontSize: 9, color: "#111827", paddingTop: 50, paddingBottom: 60, paddingLeft: 55, paddingRight: 55, }, header: { flexDirection: "row", justifyContent: "space-between", marginBottom: 30, }, companyName: { fontSize: 16, fontFamily: "Helvetica-Bold", color: "#1e1b4b", marginBottom: 3, }, companyInfo: { fontSize: 8, color: "#6b7280", lineHeight: 1.5, }, invoiceTitle: { fontSize: 22, fontFamily: "Helvetica-Bold", color: "#1e1b4b", marginBottom: 20, }, metaGrid: { flexDirection: "row", gap: 0, marginBottom: 20, backgroundColor: "#f9fafb", padding: 10, borderRadius: 4, }, metaItem: { flex: 1, }, metaLabel: { fontSize: 7, color: "#9ca3af", textTransform: "uppercase", marginBottom: 2, }, metaValue: { fontSize: 9, fontFamily: "Helvetica-Bold", color: "#111827", }, addressSection: { flexDirection: "row", justifyContent: "space-between", marginBottom: 25, }, addressBlock: { flex: 1, }, addressLabel: { fontSize: 7, color: "#9ca3af", textTransform: "uppercase", marginBottom: 4, }, addressName: { fontSize: 10, fontFamily: "Helvetica-Bold", color: "#111827", marginBottom: 2, }, addressLine: { fontSize: 8.5, color: "#374151", lineHeight: 1.4, }, tableHeader: { flexDirection: "row", backgroundColor: "#1e1b4b", paddingVertical: 6, paddingHorizontal: 8, borderRadius: 3, marginBottom: 0, }, tableHeaderText: { color: "#ffffff", fontSize: 8, fontFamily: "Helvetica-Bold", }, tableRow: { flexDirection: "row", paddingVertical: 5, paddingHorizontal: 8, borderBottomColor: "#e5e7eb", borderBottomWidth: 0.5, }, tableRowAlt: { backgroundColor: "#f9fafb", }, col_pos: { width: "5%" }, col_desc: { width: "40%" }, col_qty: { width: "10%", textAlign: "right" }, col_unit: { width: "8%", textAlign: "center" }, col_price: { width: "14%", textAlign: "right" }, col_tax: { width: "8%", textAlign: "center" }, col_total: { width: "15%", textAlign: "right" }, totalsSection: { marginTop: 15, flexDirection: "row", justifyContent: "flex-end", }, totalsTable: { width: 220, }, totalsRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 3, }, totalsLabel: { fontSize: 9, color: "#6b7280", }, totalsValue: { fontSize: 9, color: "#374151", }, totalsFinalRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 4, borderTopColor: "#1e1b4b", borderTopWidth: 1.5, marginTop: 3, }, totalsFinalLabel: { fontSize: 10, fontFamily: "Helvetica-Bold", color: "#1e1b4b", }, totalsFinalValue: { fontSize: 10, fontFamily: "Helvetica-Bold", color: "#1e1b4b", }, notes: { marginTop: 20, padding: 10, backgroundColor: "#f9fafb", borderRadius: 4, }, notesLabel: { fontSize: 7, color: "#9ca3af", textTransform: "uppercase", marginBottom: 3, }, notesText: { fontSize: 8.5, color: "#374151", lineHeight: 1.5, }, footer: { position: "absolute", bottom: 30, left: 55, right: 55, borderTopColor: "#e5e7eb", borderTopWidth: 0.5, paddingTop: 8, flexDirection: "row", justifyContent: "space-between", }, footerText: { fontSize: 7, color: "#9ca3af", }, }); function formatMoney(amount: number) { return new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(amount); } function formatDate(date: Date | string) { return new Intl.DateTimeFormat("de-DE").format(new Date(date)); } interface InvoicePDFProps { invoice: { number: string | null; issueDate: Date | string; deliveryDate?: Date | string | null; dueDate: Date | string; notes?: string | null; kleinunternehmer?: boolean; netTotal: number | string | { toString(): string }; taxTotal: number | string | { toString(): string }; grossTotal: number | string | { toString(): string }; company: { name: string; legalForm?: string | null; taxId?: string | null; vatId?: string | null; address: string; zip: string; city: string; email?: string | null; phone?: string | null; bankIban?: string | null; bankBic?: string | null; bankName?: string | null; }; customer: { name: string; address: string; zip: string; city: string; country: string; }; items: Array<{ position: number; description: string; quantity: number | string | { toString(): string }; unit?: string | null; unitPrice: number | string | { toString(): string }; taxRate: number | string | { toString(): string }; netAmount: number | string | { toString(): string }; taxAmount: number | string | { toString(): string }; grossAmount: number | string | { toString(): string }; }>; }; } export function InvoicePDFDocument({ invoice }: InvoicePDFProps) { const n = (v: unknown) => Number(v); const taxGroups = invoice.items.reduce( (acc, item) => { const rate = n(item.taxRate); if (!acc[rate]) acc[rate] = { net: 0, tax: 0 }; acc[rate].net += n(item.netAmount); acc[rate].tax += n(item.taxAmount); return acc; }, {} as Record ); return ( {invoice.company.name} {invoice.company.legalForm && ( {invoice.company.legalForm} )} {invoice.company.address} {invoice.company.zip} {invoice.company.city} {invoice.company.email && ( {invoice.company.email} )} {invoice.company.phone && ( {invoice.company.phone} )} {invoice.company.taxId && ( St.-Nr.: {invoice.company.taxId} )} {invoice.company.vatId && ( USt-IdNr.: {invoice.company.vatId} )} Rechnung Rechnungsempfänger {invoice.customer.name} {invoice.customer.address} {invoice.customer.zip} {invoice.customer.city} {invoice.customer.country !== "DE" && ( {invoice.customer.country} )} Rechnungsnummer {invoice.number ?? "-"} Rechnungsdatum {formatDate(invoice.issueDate)} {invoice.deliveryDate && ( Leistungsdatum {formatDate(invoice.deliveryDate)} )} Fällig am {formatDate(invoice.dueDate)} # Beschreibung Menge Einh. {invoice.kleinunternehmer ? "EP (brutto)" : "EP (netto)"} {!invoice.kleinunternehmer && ( MwSt. )} Gesamt {invoice.items.map((item, idx) => ( {item.position} {item.description} {n(item.quantity)} {item.unit ?? ""} {formatMoney(n(item.unitPrice))} {!invoice.kleinunternehmer && ( {n(item.taxRate)}% )} {formatMoney(n(item.grossAmount))} ))} {invoice.kleinunternehmer ? ( <> Gesamtbetrag {formatMoney(n(invoice.grossTotal))} Dieser Rechnungsbetrag enthält nach §19 Abs. 1 UStG keine USt. ) : ( <> Nettobetrag {formatMoney(n(invoice.netTotal))} {Object.entries(taxGroups).map(([rate, { net, tax }]) => ( MwSt. {rate}% auf {formatMoney(net)} {formatMoney(tax)} ))} Gesamtbetrag (inkl. MwSt.) {formatMoney(n(invoice.grossTotal))} )} {invoice.notes && ( Hinweise {invoice.notes} )} {invoice.company.name} {invoice.company.taxId ? ` · St.-Nr.: ${invoice.company.taxId}` : ""} {invoice.company.vatId ? ` · USt-IdNr.: ${invoice.company.vatId}` : ""} {invoice.company.bankIban && ( {invoice.company.bankName ? `${invoice.company.bankName} · ` : ""} IBAN: {invoice.company.bankIban} {invoice.company.bankBic ? ` · BIC: ${invoice.company.bankBic}` : ""} )} ); }