250 lines
8.7 KiB
TypeScript
250 lines
8.7 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);
|
|
|
|
const groupedItems: { [key: string]: any[] } = {};
|
|
items.forEach((item: any) => {
|
|
const devName = item.device_name || 'Kasse 1';
|
|
if (!groupedItems[devName]) {
|
|
groupedItems[devName] = [];
|
|
}
|
|
groupedItems[devName].push(item);
|
|
});
|
|
|
|
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 ? 'Anfragebestätigung: Software-Abonnement' : 'Anfragebestätigung: Einmalige Lizenzen'}
|
|
</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>
|
|
|
|
{Object.entries(groupedItems).map(([deviceName, devItems], groupIdx) => (
|
|
<View key={groupIdx} style={{ marginBottom: 6 }}>
|
|
<View style={{ backgroundColor: '#f1f5f9', padding: '4 8', marginTop: 6, borderBottomWidth: 1, borderBottomColor: '#cbd5e1' }}>
|
|
<Text style={{ fontSize: 9, fontWeight: 'bold', color: '#1e293b' }}>Gerät: {deviceName}</Text>
|
|
</View>
|
|
{devItems.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>
|
|
|
|
<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. Das Mandat wird erst mit der finalen Aktivierung der Lizenzen durch den CASPOS-Support wirksam.
|
|
</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={{ fontSize: 9, color: '#555', fontStyle: 'italic' }}>
|
|
Dies ist eine unverbindliche Bestätigung Ihrer Anfrage. Ein verbindliches Angebot wird Ihnen in Kürze von unserem Vertriebsteam zugesendet.
|
|
</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>
|
|
);
|
|
};
|