280 lines
10 KiB
TypeScript
280 lines
10 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';
|
|
|
|
let oneTimeNet = 0;
|
|
let monthlyNet = 0;
|
|
|
|
items.forEach((item: any) => {
|
|
if (item.billing_interval === 'monthly') {
|
|
monthlyNet += item.base_price || 0;
|
|
} else {
|
|
oneTimeNet += item.base_price || 0;
|
|
}
|
|
|
|
item.selected_modules?.forEach((mod: any) => {
|
|
const qty = mod.quantity || 1;
|
|
const price = mod.total_price ?? (mod.price * qty);
|
|
// Module sind monatliche wiederkehrende Beträge (Abo/Miete)
|
|
monthlyNet += price;
|
|
});
|
|
});
|
|
|
|
const oneTimeTax = Math.round(oneTimeNet * (taxRate / 100) * 100) / 100;
|
|
const oneTimeGross = Math.round((oneTimeNet + oneTimeTax) * 100) / 100;
|
|
|
|
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);
|
|
|
|
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={{ marginTop: 10, marginBottom: 15 }}>
|
|
{Object.entries(groupedItems).map(([deviceName, devItems], groupIdx) => (
|
|
<View key={groupIdx} style={{ marginBottom: 12, borderWidth: 1, borderColor: '#e2e8f0', borderRadius: 4, padding: 8, backgroundColor: '#f8fafc' }}>
|
|
<View style={{ borderBottomWidth: 1, borderBottomColor: '#cbd5e1', paddingBottom: 4, marginBottom: 6 }}>
|
|
<Text style={{ fontSize: 10, fontWeight: 'bold', color: '#1e3a8a' }}>
|
|
Gerät: {deviceName}
|
|
</Text>
|
|
</View>
|
|
{devItems.map((item: any, idx: number) => (
|
|
<View key={idx} style={{ marginBottom: 6 }}>
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 2 }}>
|
|
<Text style={{ fontSize: 9, fontWeight: 'bold', color: '#334155' }}>
|
|
{item.product_name} ({item.category_name})
|
|
</Text>
|
|
<Text style={{ fontSize: 9, fontWeight: 'bold', color: '#0f172a' }}>
|
|
{formattedPrice(item.base_price)} {item.billing_interval === 'one_time' ? 'einmalig' : 'mtl.'}
|
|
</Text>
|
|
</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={{ flexDirection: 'row', justifyContent: 'space-between', paddingLeft: 12, marginBottom: 1 }}>
|
|
<Text style={{ fontSize: 8, color: '#64748b' }}>
|
|
+ {mod.module_name} {qty > 1 ? `(x${qty})` : ''}
|
|
</Text>
|
|
<Text style={{ fontSize: 8, color: '#64748b' }}>
|
|
+{formattedPrice(price)} mtl.
|
|
</Text>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
))}
|
|
</View>
|
|
))}
|
|
</View>
|
|
|
|
<View style={styles.total}>
|
|
<View style={{ width: '75%' }}>
|
|
{oneTimeNet > 0 && (
|
|
<View style={{ marginBottom: 6 }}>
|
|
<Text style={{ fontSize: 9, fontWeight: 'bold', color: '#1e293b', marginBottom: 2 }}>Einmalige Beträge:</Text>
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 2 }}>
|
|
<Text>Netto-Zwischensumme:</Text>
|
|
<Text>{formattedPrice(oneTimeNet)}</Text>
|
|
</View>
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 2 }}>
|
|
<Text>zzgl. {taxRate}% USt:</Text>
|
|
<Text>{formattedPrice(oneTimeTax)}</Text>
|
|
</View>
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', fontWeight: 'bold' }}>
|
|
<Text>Gesamt einmalig (brutto):</Text>
|
|
<Text>{formattedPrice(oneTimeGross)}</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{monthlyNet > 0 && (
|
|
<View style={{ marginTop: 4, paddingTop: 4, borderTopWidth: oneTimeNet > 0 ? 1 : 0, borderTopColor: '#e2e8f0' }}>
|
|
<Text style={{ fontSize: 9, fontWeight: 'bold', color: '#1e293b', marginBottom: 2 }}>Monatlich wiederkehrende Beträge:</Text>
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 2 }}>
|
|
<Text>Netto-Zwischensumme:</Text>
|
|
<Text>{formattedPrice(monthlyNet)} / mtl.</Text>
|
|
</View>
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 2 }}>
|
|
<Text>zzgl. {taxRate}% USt:</Text>
|
|
<Text>{formattedPrice(monthlyTax)} / mtl.</Text>
|
|
</View>
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', fontWeight: 'bold' }}>
|
|
<Text>Gesamt monatlich (brutto):</Text>
|
|
<Text>{formattedPrice(monthlyGross)} / mtl.</Text>
|
|
</View>
|
|
</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>
|
|
);
|
|
};
|