From 66039eb4cf3765afa704230e6b6b55d2b99c0acd Mon Sep 17 00:00:00 2001 From: DanielS Date: Thu, 9 Jul 2026 21:51:23 +0200 Subject: [PATCH] feat: implement pdf generation service based on jsonb snapshots --- shop/components/invoice-pdf.tsx | 177 ++++++++++++-------------------- 1 file changed, 65 insertions(+), 112 deletions(-) diff --git a/shop/components/invoice-pdf.tsx b/shop/components/invoice-pdf.tsx index 8f640b9..7713967 100644 --- a/shop/components/invoice-pdf.tsx +++ b/shop/components/invoice-pdf.tsx @@ -1,6 +1,5 @@ -import { Document, Page, Text, View, StyleSheet, Font } from '@react-pdf/renderer'; +import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer'; -// Create styles const styles = StyleSheet.create({ page: { padding: 50, @@ -31,7 +30,6 @@ const styles = StyleSheet.create({ marginBottom: 2, }, table: { - width: 'auto', borderStyle: 'solid', borderWidth: 1, @@ -63,16 +61,11 @@ const styles = StyleSheet.create({ padding: 8, textAlign: 'right', }, - tableCell: { - margin: 'auto', - marginTop: 5, - fontSize: 10, - }, total: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 20, - borderTop: 1, + borderTopWidth: 1, borderColor: '#000', paddingTop: 10, }, @@ -88,26 +81,30 @@ const styles = StyleSheet.create({ textAlign: 'center', color: '#999', fontSize: 8, - borderTop: 1, + borderTopWidth: 1, borderColor: '#eee', paddingTop: 10, }, + sepaBox: { + marginTop: 15, + padding: 10, + backgroundColor: '#f9f9f9', + borderWidth: 1, + borderColor: '#ddd', + } }); export const InvoicePDF = ({ order, orderSnapshot, customer }: any) => { const items = orderSnapshot?.items ?? []; const taxRate = orderSnapshot?.tax_rate ?? 19; + const isSubscription = order?.type === 'subscription' || orderSnapshot?.billing_cycle === 'monthly'; - const oneTimeItems = items.filter((item: any) => item.billing_interval === 'one_time'); - const monthlyItems = items.filter((item: any) => item.billing_interval === 'monthly'); + const totalNet = items.reduce((sum: number, item: any) => sum + (item.item_total || 0), 0); + const totalTax = Math.round(totalNet * (taxRate / 100) * 100) / 100; + const totalGross = Math.round((totalNet + totalTax) * 100) / 100; - const oneTimeNet = oneTimeItems.reduce((sum: number, item: any) => sum + (item.item_total || 0), 0); - const oneTimeTax = Math.round(oneTimeNet * (taxRate / 100) * 100) / 100; - const oneTimeGross = Math.round((oneTimeNet + oneTimeTax) * 100) / 100; - - const monthlyNet = monthlyItems.reduce((sum: number, item: any) => sum + (item.item_total || 0), 0); - const monthlyTax = Math.round(monthlyNet * (taxRate / 100) * 100) / 100; - const monthlyGross = Math.round((monthlyNet + monthlyTax) * 100) / 100; + const formattedPrice = (val: number) => + new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val); return ( @@ -125,54 +122,26 @@ export const InvoicePDF = ({ order, orderSnapshot, customer }: any) => { - Anfragebestätigung + + {isSubscription ? 'Vertragsbestätigung (Abonnement)' : 'B2B-Rechnung'} + - Anfragender Kunde + Kunde {customer.company_name} {customer.first_name} {customer.last_name} {customer.address} {customer.zip_code} {customer.city} USt-IdNr: {customer.vat_id} - {customer.bank_iban && ( - - Bank: {customer.bank_name || '-'} · IBAN: {customer.bank_iban} · BIC: {customer.bank_bic || '-'} · Inhaber: {customer.bank_owner || '-'} - - )} - Anfragedetails - Anfrage-Nummer: {order.order_number || order.id} + Bestelldetails + Bestellnummer: {order.order_number || order.id} + Zahlungsart: {isSubscription ? 'SEPA-Lastschrift' : 'Rechnung'} Datum: {new Date(order.created_at || Date.now()).toLocaleDateString('de-DE')} - {orderSnapshot?.price_multiplier !== null && orderSnapshot?.price_multiplier !== undefined && ( - - Update-Konditionen - - {orderSnapshot.price_multiplier === 0 - ? "Update (0-12 Monate): 100% Rabatt (inklusive)" - : orderSnapshot.price_multiplier === 0.15 - ? "Update (13-14 Monate): 15% vom Listenpreis" - : orderSnapshot.price_multiplier === 0.30 - ? "Update (15-25 Monate): 30% vom Listenpreis" - : orderSnapshot.price_multiplier === 0.45 - ? "Update (26-37 Monate): 45% vom Listenpreis" - : orderSnapshot.price_multiplier === 0.60 - ? "Update (38-49 Monate): 60% vom Listenpreis" - : orderSnapshot.price_multiplier === 0.90 - ? "Neukauf (>= 50 Monate): 10% Rabatt auf Neukauf" - : `Gebühr: ${orderSnapshot.price_multiplier * 100}%`} - - {orderSnapshot.last_license_date && ( - - Letzte Lizenz vom: {new Date(orderSnapshot.last_license_date).toLocaleDateString('de-DE')} - - )} - - )} - Beschreibung @@ -187,25 +156,23 @@ export const InvoicePDF = ({ order, orderSnapshot, customer }: any) => { - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(item.base_price)} - {' '}{item.billing_interval === 'one_time' ? 'einmalig' : 'mtl.'} + {formattedPrice(item.base_price)} {isSubscription ? 'mtl.' : 'einmalig'} {item.selected_modules?.map((mod: any, mIdx: number) => { const qty = mod.quantity || 1; const price = mod.total_price ?? (mod.price * qty); - const hasQty = qty > 1; return ( - + {mod.module_name} {hasQty ? `(x${qty})` : ''} + + {mod.module_name} {qty > 1 ? `(x${qty})` : ''} - +{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price)} + +{formattedPrice(price)} @@ -217,63 +184,49 @@ export const InvoicePDF = ({ order, orderSnapshot, customer }: any) => { - {oneTimeNet > 0 && ( - 0 ? 10 : 0 }}> - {monthlyNet > 0 && ( - Einmaliger Betrag: - )} - - Netto-Zwischensumme: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)} - - - zzgl. {taxRate}% USt: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)} - - - - {monthlyNet > 0 ? 'Gesamtbetrag einmalig (brutto):' : 'Gesamtbetrag (brutto):'} - - - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)} - - - - )} - - {oneTimeNet > 0 && monthlyNet > 0 && ( - - )} - - {monthlyNet > 0 && ( - - {oneTimeNet > 0 && ( - Monatlicher Betrag: - )} - - Netto-Zwischensumme: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl. - - - zzgl. {taxRate}% USt: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl. - - - - {oneTimeNet > 0 ? 'Gesamtbetrag monatlich (brutto):' : 'Gesamtbetrag (brutto):'} - - - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl. - - - - )} + + Netto-Zwischensumme: + {formattedPrice(totalNet)}{isSubscription ? ' / mtl.' : ''} + + + zzgl. {taxRate}% USt: + {formattedPrice(totalTax)}{isSubscription ? ' / mtl.' : ''} + + + + Gesamtbetrag (brutto): + + + {formattedPrice(totalGross)}{isSubscription ? ' / mtl.' : ''} + + + {isSubscription ? ( + + SEPA-Lastschriftmandat + + Ich ermächtige die CASPOS GmbH, Zahlungen von meinem Konto mittels Lastschrift einzuziehen. Zugleich weise ich mein Kreditinstitut an, die von der CASPOS GmbH auf mein Konto gezogenen Lastschriften einzulösen. + + {customer.bank_iban && ( + + IBAN: {customer.bank_iban.replace(/(.{4})/g, '$1 ')} · Inhaber: {customer.bank_owner || 'Kunde'} + + )} + + ) : ( + + Zahlungsziel + + Zahlbar innerhalb von 14 Tagen nach Erhalt dieser Rechnung ohne Abzug. + + + )} + - Vielen Dank für Ihre Bestellung! - Dies ist ein automatisch generiertes Dokument. + CASPOS Computerabrechnungssysteme GmbH · Amtsgericht Zweibrücken HRB 12345 + Dies ist ein automatisch erzeugtes Dokument.