235 lines
7.8 KiB
TypeScript
235 lines
7.8 KiB
TypeScript
import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer';
|
|
|
|
const styles = StyleSheet.create({
|
|
page: {
|
|
padding: 50,
|
|
fontSize: 10,
|
|
color: '#333',
|
|
fontFamily: 'Helvetica',
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginBottom: 40,
|
|
},
|
|
companyInfo: {
|
|
textAlign: 'right',
|
|
},
|
|
title: {
|
|
fontSize: 24,
|
|
color: '#000',
|
|
marginBottom: 20,
|
|
},
|
|
section: {
|
|
marginBottom: 20,
|
|
},
|
|
label: {
|
|
fontSize: 8,
|
|
color: '#999',
|
|
textTransform: 'uppercase',
|
|
marginBottom: 2,
|
|
},
|
|
table: {
|
|
width: 'auto',
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderColor: '#eee',
|
|
borderRightWidth: 0,
|
|
borderBottomWidth: 0,
|
|
marginBottom: 20,
|
|
},
|
|
tableRow: {
|
|
margin: 'auto',
|
|
flexDirection: 'row',
|
|
},
|
|
tableCol: {
|
|
width: '60%',
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderColor: '#eee',
|
|
borderLeftWidth: 0,
|
|
borderTopWidth: 0,
|
|
padding: 8,
|
|
},
|
|
tableColPrice: {
|
|
width: '40%',
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderColor: '#eee',
|
|
borderLeftWidth: 0,
|
|
borderTopWidth: 0,
|
|
padding: 8,
|
|
textAlign: 'right',
|
|
},
|
|
total: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'flex-end',
|
|
marginTop: 20,
|
|
borderTopWidth: 1,
|
|
borderColor: '#000',
|
|
paddingTop: 10,
|
|
},
|
|
totalLabel: {
|
|
fontSize: 11,
|
|
fontWeight: 'bold',
|
|
},
|
|
footer: {
|
|
position: 'absolute',
|
|
bottom: 30,
|
|
left: 50,
|
|
right: 50,
|
|
textAlign: 'center',
|
|
color: '#999',
|
|
fontSize: 8,
|
|
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 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 formattedPrice = (val: number) =>
|
|
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val);
|
|
|
|
return (
|
|
<Document>
|
|
<Page size="A4" style={styles.page}>
|
|
<View style={styles.header}>
|
|
<View>
|
|
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>CASPOS</Text>
|
|
<Text>Die Kasse</Text>
|
|
</View>
|
|
<View style={styles.companyInfo}>
|
|
<Text>CASPOS Computerabrechnungssysteme GmbH</Text>
|
|
<Text>Alte Bundesstraße 16</Text>
|
|
<Text>76846 Hauenstein</Text>
|
|
<Text>Deutschland</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<Text style={styles.title}>
|
|
{isSubscription ? 'Vertragsbestätigung (Abonnement)' : 'B2B-Rechnung'}
|
|
</Text>
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.label}>Kunde</Text>
|
|
<Text>{customer.company_name}</Text>
|
|
<Text>{customer.first_name} {customer.last_name}</Text>
|
|
<Text>{customer.address}</Text>
|
|
<Text>{customer.zip_code} {customer.city}</Text>
|
|
<Text>USt-IdNr: {customer.vat_id}</Text>
|
|
</View>
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.label}>Bestelldetails</Text>
|
|
<Text>Bestellnummer: {order.order_number || order.id}</Text>
|
|
<Text>Zahlungsart: {isSubscription ? 'SEPA-Lastschrift' : 'Rechnung'}</Text>
|
|
<Text>Datum: {new Date(order.created_at || Date.now()).toLocaleDateString('de-DE')}</Text>
|
|
</View>
|
|
|
|
<View style={styles.table}>
|
|
<View style={[styles.tableRow, { backgroundColor: '#f9f9f9' }]}>
|
|
<View style={styles.tableCol}><Text style={{ fontWeight: 'bold' }}>Beschreibung</Text></View>
|
|
<View style={styles.tableColPrice}><Text style={{ fontWeight: 'bold' }}>Preis (netto)</Text></View>
|
|
</View>
|
|
|
|
{items.map((item: any, idx: number) => (
|
|
<View key={idx}>
|
|
<View style={styles.tableRow}>
|
|
<View style={styles.tableCol}>
|
|
<Text style={{ fontWeight: 'bold' }}>{item.product_name} ({item.category_name})</Text>
|
|
</View>
|
|
<View style={styles.tableColPrice}>
|
|
<Text>
|
|
{formattedPrice(item.base_price)} {isSubscription ? 'mtl.' : 'einmalig'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
{item.selected_modules?.map((mod: any, mIdx: number) => {
|
|
const qty = mod.quantity || 1;
|
|
const price = mod.total_price ?? (mod.price * qty);
|
|
return (
|
|
<View key={mIdx} style={styles.tableRow}>
|
|
<View style={[styles.tableCol, { paddingLeft: 20 }]}>
|
|
<Text style={{ color: '#666' }}>
|
|
+ {mod.module_name} {qty > 1 ? `(x${qty})` : ''}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.tableColPrice}>
|
|
<Text style={{ color: '#666' }}>
|
|
+{formattedPrice(price)}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
))}
|
|
</View>
|
|
|
|
<View style={styles.total}>
|
|
<View style={{ width: '70%' }}>
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 3 }}>
|
|
<Text>Netto-Zwischensumme:</Text>
|
|
<Text>{formattedPrice(totalNet)}{isSubscription ? ' / mtl.' : ''}</Text>
|
|
</View>
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 3 }}>
|
|
<Text>zzgl. {taxRate}% USt:</Text>
|
|
<Text>{formattedPrice(totalTax)}{isSubscription ? ' / mtl.' : ''}</Text>
|
|
</View>
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 5, borderTopWidth: 1, borderTopColor: '#000', paddingTop: 5 }}>
|
|
<Text style={styles.totalLabel}>
|
|
Gesamtbetrag (brutto):
|
|
</Text>
|
|
<Text style={styles.totalLabel}>
|
|
{formattedPrice(totalGross)}{isSubscription ? ' / mtl.' : ''}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{isSubscription ? (
|
|
<View style={styles.sepaBox}>
|
|
<Text style={{ fontWeight: 'bold', marginBottom: 4 }}>SEPA-Lastschriftmandat</Text>
|
|
<Text style={{ fontSize: 8, color: '#555' }}>
|
|
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.
|
|
</Text>
|
|
{customer.bank_iban && (
|
|
<Text style={{ marginTop: 4, fontSize: 8, fontWeight: 'bold' }}>
|
|
IBAN: {customer.bank_iban.replace(/(.{4})/g, '$1 ')} · Inhaber: {customer.bank_owner || 'Kunde'}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
) : (
|
|
<View style={{ marginTop: 20 }}>
|
|
<Text style={{ fontWeight: 'bold' }}>Zahlungsziel</Text>
|
|
<Text style={{ color: '#555' }}>
|
|
Zahlbar innerhalb von 14 Tagen nach Erhalt dieser Rechnung ohne Abzug.
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
<View style={styles.footer}>
|
|
<Text>CASPOS Computerabrechnungssysteme GmbH · Amtsgericht Zweibrücken HRB 12345</Text>
|
|
<Text>Dies ist ein automatisch erzeugtes Dokument.</Text>
|
|
</View>
|
|
</Page>
|
|
</Document>
|
|
);
|
|
};
|