feat(order): add net/tax/gross breakdown for one-time and monthly totals
All checks were successful
Staging Build / build (push) Successful in 3m28s
All checks were successful
Staging Build / build (push) Successful in 3m28s
This commit is contained in:
@@ -36,6 +36,18 @@ export default async function OrderSuccessPage({
|
||||
const o = order as Order
|
||||
const items = o.order_data?.items ?? []
|
||||
|
||||
const taxRate = o.order_data?.tax_rate ?? 19
|
||||
const oneTimeItems = items.filter((item) => item.billing_interval === 'one_time')
|
||||
const monthlyItems = items.filter((item) => item.billing_interval === 'monthly')
|
||||
|
||||
const oneTimeNet = oneTimeItems.reduce((sum, item) => 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, item) => sum + (item.item_total || 0), 0)
|
||||
const monthlyTax = Math.round(monthlyNet * (taxRate / 100) * 100) / 100
|
||||
const monthlyGross = Math.round((monthlyNet + monthlyTax) * 100) / 100
|
||||
|
||||
const statusLabel: Record<string, string> = {
|
||||
pending: 'Eingegangen',
|
||||
active: 'In Bearbeitung',
|
||||
@@ -104,12 +116,49 @@ export default async function OrderSuccessPage({
|
||||
<Separator className="bg-white/10" />
|
||||
|
||||
{/* Gesamtbetrag */}
|
||||
<div className="flex justify-between text-lg font-bold">
|
||||
<span className="text-slate-300">Gesamtbetrag</span>
|
||||
<span className="text-gradient">
|
||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(o.total_price)}
|
||||
</span>
|
||||
</div>
|
||||
{oneTimeNet > 0 && (
|
||||
<div className="space-y-1">
|
||||
{monthlyNet > 0 && <p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Einmaliger Betrag</p>}
|
||||
<div className="flex justify-between text-sm text-slate-400">
|
||||
<span>Netto:</span>
|
||||
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-400">
|
||||
<span>zzgl. {taxRate}% MwSt.:</span>
|
||||
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-base font-bold text-white">
|
||||
<span>Brutto Gesamtbetrag:</span>
|
||||
<span className={monthlyNet > 0 ? "text-white" : "text-gradient"}>
|
||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{oneTimeNet > 0 && monthlyNet > 0 && (
|
||||
<Separator className="bg-white/5 my-2" />
|
||||
)}
|
||||
|
||||
{monthlyNet > 0 && (
|
||||
<div className="space-y-1">
|
||||
{oneTimeNet > 0 && <p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Monatlicher Betrag</p>}
|
||||
<div className="flex justify-between text-sm text-slate-400">
|
||||
<span>Netto:</span>
|
||||
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / Monat</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-400">
|
||||
<span>zzgl. {taxRate}% MwSt.:</span>
|
||||
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / Monat</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-base font-bold text-white">
|
||||
<span>Brutto Gesamtbetrag:</span>
|
||||
<span className="text-gradient">
|
||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / Monat
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{o.order_data?.price_multiplier !== null && o.order_data?.price_multiplier !== undefined && (
|
||||
<div className="text-sm text-green-400 font-semibold bg-green-500/10 p-3 rounded-lg border border-green-500/20 space-y-1">
|
||||
|
||||
@@ -130,14 +130,13 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
||||
variants={itemVariants}
|
||||
className="text-4xl font-extrabold tracking-tight sm:text-5xl md:text-6xl lg:text-7xl/tight bg-clip-text text-transparent bg-gradient-to-b from-white via-slate-100 to-slate-400"
|
||||
>
|
||||
POS Software <span className="bg-clip-text text-transparent bg-gradient-to-r from-blue-400 via-indigo-400 to-purple-500 font-black">Lizenzen & Module</span>
|
||||
CASPOS Software <span className="bg-clip-text text-transparent bg-gradient-to-r from-blue-400 via-indigo-400 to-purple-500 font-black">Lizenzen & Module</span>
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
variants={itemVariants}
|
||||
className="mx-auto max-w-[750px] text-slate-400 text-base sm:text-lg md:text-xl font-normal leading-relaxed"
|
||||
>
|
||||
Erwerben und verwalten Sie digitale Lizenzen für CASPOS Kassensysteme.
|
||||
Sofortige Aktivierung und automatische Bereitstellung für Ihre Kunden.
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
@@ -347,46 +346,6 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Core values / Features list */}
|
||||
<section className="w-full py-16 md:py-24 border-t border-slate-900 bg-slate-950/50">
|
||||
<div className="container max-w-5xl mx-auto px-4">
|
||||
<div className="grid gap-8 md:grid-cols-2 items-center">
|
||||
<div className="space-y-6">
|
||||
<div className="inline-flex items-center gap-1.5 text-xs font-bold text-blue-400 uppercase tracking-widest bg-blue-500/10 border border-blue-500/20 px-3 py-1 rounded-full">
|
||||
<ShieldCheck className="w-3.5 h-3.5" /> Sicherheit & Stabilität
|
||||
</div>
|
||||
<h3 className="text-3xl font-extrabold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-white via-slate-100 to-slate-400">
|
||||
Verlässliche Plattform für Lizenz-Aktivierungen
|
||||
</h3>
|
||||
<p className="text-slate-400 text-sm sm:text-base leading-relaxed">
|
||||
Unsere Kassenlösungen sind GoBD-konform und offline-fähig. Über den Lizenz-Management-Server werden alle Lizenzen und Module verschlüsselt übertragen und sind innerhalb weniger Millisekunden auf der Kasse aktiv.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="p-5 rounded-xl border border-slate-900 bg-slate-950/70 space-y-2">
|
||||
<CheckCircle2 className="w-5 h-5 text-blue-500" />
|
||||
<h5 className="font-bold text-sm text-slate-200">GoBD Konformität</h5>
|
||||
<p className="text-xs text-slate-400">Konform nach allen aktuellen steuerrechtlichen Anforderungen.</p>
|
||||
</div>
|
||||
<div className="p-5 rounded-xl border border-slate-900 bg-slate-950/70 space-y-2">
|
||||
<CheckCircle2 className="w-5 h-5 text-blue-500" />
|
||||
<h5 className="font-bold text-sm text-slate-200">Same-Day-Bearbeitung</h5>
|
||||
<p className="text-xs text-slate-400">Bearbeitung und Erstellung Ihrer Lizenzen noch am selben Werktag.</p>
|
||||
</div>
|
||||
<div className="p-5 rounded-xl border border-slate-900 bg-slate-950/70 space-y-2">
|
||||
<CheckCircle2 className="w-5 h-5 text-blue-500" />
|
||||
<h5 className="font-bold text-sm text-slate-200">Verschlüsselte Keys</h5>
|
||||
<p className="text-xs text-slate-400">Sichere Lizenz-Authentifizierung direkt auf der Kasse.</p>
|
||||
</div>
|
||||
<div className="p-5 rounded-xl border border-slate-900 bg-slate-950/70 space-y-2">
|
||||
<CheckCircle2 className="w-5 h-5 text-blue-500" />
|
||||
<h5 className="font-bold text-sm text-slate-200">Partner Support</h5>
|
||||
<p className="text-xs text-slate-400">Schnelle Hilfe und kompetente Ansprechpartner für Händler.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="w-full border-t border-slate-900 bg-slate-950 py-8 px-4 md:px-6 relative z-10">
|
||||
|
||||
@@ -96,10 +96,18 @@ const styles = StyleSheet.create({
|
||||
|
||||
export const InvoicePDF = ({ order, orderSnapshot, customer }: any) => {
|
||||
const items = orderSnapshot?.items ?? [];
|
||||
const subtotal = orderSnapshot?.subtotal ?? 0;
|
||||
const taxAmount = orderSnapshot?.tax_amount ?? 0;
|
||||
const taxRate = orderSnapshot?.tax_rate ?? 19;
|
||||
const total = orderSnapshot?.total ?? 0;
|
||||
|
||||
const oneTimeItems = items.filter((item: any) => item.billing_interval === 'one_time');
|
||||
const monthlyItems = items.filter((item: any) => item.billing_interval === 'monthly');
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<Document>
|
||||
@@ -204,18 +212,57 @@ export const InvoicePDF = ({ order, orderSnapshot, customer }: any) => {
|
||||
|
||||
<View style={styles.total}>
|
||||
<View style={{ width: '50%' }}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 3 }}>
|
||||
<Text>Netto-Zwischensumme:</Text>
|
||||
<Text>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(subtotal)}</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 3 }}>
|
||||
<Text>zzgl. {taxRate}% USt:</Text>
|
||||
<Text>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(taxAmount)}</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}>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(total)}</Text>
|
||||
</View>
|
||||
{oneTimeNet > 0 && (
|
||||
<View style={{ marginBottom: monthlyNet > 0 ? 10 : 0 }}>
|
||||
{monthlyNet > 0 && (
|
||||
<Text style={{ fontWeight: 'bold', marginBottom: 4, fontSize: 11 }}>Einmaliger Betrag:</Text>
|
||||
)}
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 3 }}>
|
||||
<Text>Netto-Zwischensumme:</Text>
|
||||
<Text>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)}</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 3 }}>
|
||||
<Text>zzgl. {taxRate}% USt:</Text>
|
||||
<Text>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)}</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 5, borderTopWidth: 1, borderTopColor: '#000', paddingTop: 5 }}>
|
||||
<Text style={styles.totalLabel}>
|
||||
{monthlyNet > 0 ? 'Gesamtbetrag einmalig (brutto):' : 'Gesamtbetrag (brutto):'}
|
||||
</Text>
|
||||
<Text style={styles.totalLabel}>
|
||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{oneTimeNet > 0 && monthlyNet > 0 && (
|
||||
<View style={{ borderTopWidth: 1, borderTopColor: '#eee', marginVertical: 10 }} />
|
||||
)}
|
||||
|
||||
{monthlyNet > 0 && (
|
||||
<View>
|
||||
{oneTimeNet > 0 && (
|
||||
<Text style={{ fontWeight: 'bold', marginBottom: 4, fontSize: 11 }}>Monatlicher Betrag:</Text>
|
||||
)}
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 3 }}>
|
||||
<Text>Netto-Zwischensumme:</Text>
|
||||
<Text>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl.</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 3 }}>
|
||||
<Text>zzgl. {taxRate}% USt:</Text>
|
||||
<Text>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl.</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 5, borderTopWidth: 1, borderTopColor: '#000', paddingTop: 5 }}>
|
||||
<Text style={styles.totalLabel}>
|
||||
{oneTimeNet > 0 ? 'Gesamtbetrag monatlich (brutto):' : 'Gesamtbetrag (brutto):'}
|
||||
</Text>
|
||||
<Text style={styles.totalLabel}>
|
||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -442,11 +442,10 @@ export function OrderWizard({
|
||||
{[1, 2, 3, 4].map(s => (
|
||||
<div key={s} className="relative z-10 flex flex-col items-center gap-2">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-500 ${
|
||||
step >= s
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-500 ${step >= s
|
||||
? 'bg-primary text-white scale-110 shadow-[0_0_15px_rgba(59,130,246,0.5)]'
|
||||
: 'bg-slate-800 text-slate-400'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{step > s ? <Check className="w-6 h-6" /> : s}
|
||||
</div>
|
||||
@@ -530,11 +529,10 @@ export function OrderWizard({
|
||||
{filteredEndCustomers.map(customer => (
|
||||
<label
|
||||
key={customer.id}
|
||||
className={`flex items-start p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
||||
selectedEndCustomerId === customer.id
|
||||
className={`flex items-start p-4 rounded-xl border-2 cursor-pointer transition-all ${selectedEndCustomerId === customer.id
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-white/5 bg-white/5 hover:bg-white/10'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
@@ -641,11 +639,10 @@ export function OrderWizard({
|
||||
{/* Option 1: Kauf */}
|
||||
<div
|
||||
onClick={() => handleBillingIntervalChange('one_time')}
|
||||
className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${
|
||||
selectedBillingInterval === 'one_time'
|
||||
className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${selectedBillingInterval === 'one_time'
|
||||
? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]'
|
||||
: 'border-white/5 bg-white/5 hover:border-white/20'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
@@ -668,11 +665,10 @@ export function OrderWizard({
|
||||
{/* Option 2: Abo */}
|
||||
<div
|
||||
onClick={() => handleBillingIntervalChange('monthly')}
|
||||
className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${
|
||||
selectedBillingInterval === 'monthly'
|
||||
className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${selectedBillingInterval === 'monthly'
|
||||
? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]'
|
||||
: 'border-white/5 bg-white/5 hover:border-white/20'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
@@ -1106,7 +1102,7 @@ export function OrderWizard({
|
||||
<div className="w-20 h-20 bg-primary/20 rounded-full flex items-center justify-center mx-auto mb-4 border border-primary/50">
|
||||
<ShieldCheck className="w-10 h-10 text-primary" />
|
||||
</div>
|
||||
<CardTitle className="text-3xl text-white">Bestellung prüfen</CardTitle>
|
||||
<CardTitle className="text-3xl text-white">Anfrage prüfen</CardTitle>
|
||||
<CardDescription className="text-slate-300">
|
||||
Fast fertig! Bitte überprüfen Sie Ihre Auswahl.
|
||||
</CardDescription>
|
||||
@@ -1269,10 +1265,10 @@ export function OrderWizard({
|
||||
initialOrder ? (
|
||||
<><Loader2 className="mr-2 h-5 w-5 animate-spin" /> Angebot wird aktualisiert...</>
|
||||
) : (
|
||||
<><Loader2 className="mr-2 h-5 w-5 animate-spin" /> Bestellung wird verarbeitet...</>
|
||||
<><Loader2 className="mr-2 h-5 w-5 animate-spin" /> Anfrage wird versendet...</>
|
||||
)
|
||||
) : (
|
||||
initialOrder ? 'Angebot aktualisieren' : 'Kostenpflichtig bestellen'
|
||||
initialOrder ? 'Angebot aktualisieren' : 'Anfrage versenden'
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="ghost" className="w-full text-white" onClick={prevStep}>
|
||||
|
||||
@@ -223,12 +223,99 @@ export async function submitOrder(params: {
|
||||
if (user.email) {
|
||||
try {
|
||||
const formattedDate = new Date(order.created_at).toLocaleDateString('de-DE')
|
||||
const formattedTotal = order.total_price.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })
|
||||
|
||||
const taxRate = orderSnapshot.tax_rate ?? 19
|
||||
const items = orderSnapshot.items ?? []
|
||||
const oneTimeItems = items.filter((item: any) => item.billing_interval === 'one_time')
|
||||
const monthlyItems = items.filter((item: any) => item.billing_interval === 'monthly')
|
||||
|
||||
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
|
||||
|
||||
let totalDetailsText = ''
|
||||
if (oneTimeNet > 0 && monthlyNet > 0) {
|
||||
totalDetailsText = `\nEinmaliger Betrag:\n- Netto: ${oneTimeNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}\n- zzgl. ${taxRate}% MwSt: ${oneTimeTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}\n- Brutto Gesamtbetrag: ${oneTimeGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}\n\nMonatlicher Betrag:\n- Netto: ${monthlyNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat\n- zzgl. ${taxRate}% MwSt: ${monthlyTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat\n- Brutto Gesamtbetrag: ${monthlyGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat`
|
||||
} else if (oneTimeNet > 0) {
|
||||
totalDetailsText = `\nGesamtsumme:\n- Netto: ${oneTimeNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}\n- zzgl. ${taxRate}% MwSt: ${oneTimeTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}\n- Brutto Gesamtbetrag: ${oneTimeGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}`
|
||||
} else {
|
||||
totalDetailsText = `\nGesamtsumme:\n- Netto: ${monthlyNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat\n- zzgl. ${taxRate}% MwSt: ${monthlyTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat\n- Brutto Gesamtbetrag: ${monthlyGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat`
|
||||
}
|
||||
|
||||
let totalDetailsHtml = ''
|
||||
if (oneTimeNet > 0 && monthlyNet > 0) {
|
||||
totalDetailsHtml = `
|
||||
<tr>
|
||||
<td colspan="2" style="padding: 8px 0; border-top: 1px solid #e2e8f0; font-weight: bold; color: #0f172a;">Einmaliger Betrag:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; color: #475569;">Netto:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${oneTimeNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; color: #475569;">zzgl. ${taxRate}% MwSt:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${oneTimeTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; font-weight: bold; color: #0f172a;">Brutto Gesamtbetrag:</td>
|
||||
<td style="padding: 4px 0; text-align: right; font-weight: bold; color: #0f172a;">${oneTimeGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" style="padding: 8px 0; border-top: 1px solid #e2e8f0; font-weight: bold; color: #0f172a;">Monatlicher Betrag:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; color: #475569;">Netto:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${monthlyNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; color: #475569;">zzgl. ${taxRate}% MwSt:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${monthlyTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; font-weight: bold; color: #0f172a;">Brutto Gesamtbetrag:</td>
|
||||
<td style="padding: 4px 0; text-align: right; font-weight: bold; color: #0f172a;">${monthlyGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
`
|
||||
} else if (oneTimeNet > 0) {
|
||||
totalDetailsHtml = `
|
||||
<tr>
|
||||
<td style="padding: 4px 0; color: #475569;">Netto-Gesamtbetrag:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${oneTimeNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; color: #475569;">zzgl. ${taxRate}% MwSt:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${oneTimeTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold; color: #0f172a; border-top: 1px solid #e2e8f0; padding-top: 6px;">Gesamtbetrag (brutto):</td>
|
||||
<td style="padding: 4px 0; text-align: right; font-weight: bold; color: #0f172a; border-top: 1px solid #e2e8f0; padding-top: 6px;">${oneTimeGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
`
|
||||
} else {
|
||||
totalDetailsHtml = `
|
||||
<tr>
|
||||
<td style="padding: 4px 0; color: #475569;">Netto-Gesamtbetrag:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${monthlyNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; color: #475569;">zzgl. ${taxRate}% MwSt:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${monthlyTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold; color: #0f172a; border-top: 1px solid #e2e8f0; padding-top: 6px;">Gesamtbetrag (brutto):</td>
|
||||
<td style="padding: 4px 0; text-align: right; font-weight: bold; color: #0f172a; border-top: 1px solid #e2e8f0; padding-top: 6px;">${monthlyGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
await sendMail({
|
||||
to: user.email,
|
||||
subject: `Anfragebestätigung ${orderNumber}`,
|
||||
text: `Hallo,\n\nvielen Dank für Ihre Anfrage bei CASPOS Shop!\n\nAngebotsdetails:\n- Anfrage-Nummer: ${orderNumber}\n- Datum: ${formattedDate}\n- Endkunde: ${customerSnapshot.company_name}\n- Gesamtsumme: ${formattedTotal}\n\nIm Anhang dieser E-Mail finden Sie Ihre Anfragebestätigung als PDF-Dokument.\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
|
||||
text: `Hallo,\n\nvielen Dank für Ihre Anfrage bei CASPOS Shop!\n\nAngebotsdetails:\n- Anfrage-Nummer: ${orderNumber}\n- Datum: ${formattedDate}\n- Endkunde: ${customerSnapshot.company_name}\n${totalDetailsText}\n\nIm Anhang dieser E-Mail finden Sie Ihre Anfragebestätigung als PDF-Dokument.\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
|
||||
html: `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
||||
<h2 style="color: #0f172a; margin-bottom: 16px;">Vielen Dank für Ihre Anfrage!</h2>
|
||||
@@ -249,10 +336,7 @@ export async function submitOrder(params: {
|
||||
<td style="padding: 4px 0; font-weight: bold;">Endkunde:</td>
|
||||
<td style="padding: 4px 0;">${customerSnapshot.company_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Gesamtsumme:</td>
|
||||
<td style="padding: 4px 0; font-weight: bold; color: #0f172a;">${formattedTotal}</td>
|
||||
</tr>
|
||||
${totalDetailsHtml}
|
||||
</table>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Im Anhang dieser E-Mail finden Sie Ihre Anfragebestätigung als PDF-Dokument.</p>
|
||||
@@ -545,12 +629,99 @@ export async function updateOrder(
|
||||
if (orderUserEmail) {
|
||||
try {
|
||||
const formattedDate = new Date(order.created_at).toLocaleDateString('de-DE')
|
||||
const formattedTotal = order.total_price.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })
|
||||
|
||||
const taxRate = orderSnapshot.tax_rate ?? 19
|
||||
const items = orderSnapshot.items ?? []
|
||||
const oneTimeItems = items.filter((item: any) => item.billing_interval === 'one_time')
|
||||
const monthlyItems = items.filter((item: any) => item.billing_interval === 'monthly')
|
||||
|
||||
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
|
||||
|
||||
let totalDetailsText = ''
|
||||
if (oneTimeNet > 0 && monthlyNet > 0) {
|
||||
totalDetailsText = `\nEinmaliger Betrag:\n- Netto: ${oneTimeNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}\n- zzgl. ${taxRate}% MwSt: ${oneTimeTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}\n- Brutto Gesamtbetrag: ${oneTimeGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}\n\nMonatlicher Betrag:\n- Netto: ${monthlyNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat\n- zzgl. ${taxRate}% MwSt: ${monthlyTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat\n- Brutto Gesamtbetrag: ${monthlyGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat`
|
||||
} else if (oneTimeNet > 0) {
|
||||
totalDetailsText = `\nGesamtsumme:\n- Netto: ${oneTimeNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}\n- zzgl. ${taxRate}% MwSt: ${oneTimeTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}\n- Brutto Gesamtbetrag: ${oneTimeGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}`
|
||||
} else {
|
||||
totalDetailsText = `\nGesamtsumme:\n- Netto: ${monthlyNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat\n- zzgl. ${taxRate}% MwSt: ${monthlyTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat\n- Brutto Gesamtbetrag: ${monthlyGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat`
|
||||
}
|
||||
|
||||
let totalDetailsHtml = ''
|
||||
if (oneTimeNet > 0 && monthlyNet > 0) {
|
||||
totalDetailsHtml = `
|
||||
<tr>
|
||||
<td colspan="2" style="padding: 8px 0; border-top: 1px solid #e2e8f0; font-weight: bold; color: #0f172a;">Einmaliger Betrag:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; color: #475569;">Netto:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${oneTimeNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; color: #475569;">zzgl. ${taxRate}% MwSt:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${oneTimeTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; font-weight: bold; color: #0f172a;">Brutto Gesamtbetrag:</td>
|
||||
<td style="padding: 4px 0; text-align: right; font-weight: bold; color: #0f172a;">${oneTimeGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" style="padding: 8px 0; border-top: 1px solid #e2e8f0; font-weight: bold; color: #0f172a;">Monatlicher Betrag:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; color: #475569;">Netto:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${monthlyNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; color: #475569;">zzgl. ${taxRate}% MwSt:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${monthlyTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; font-weight: bold; color: #0f172a;">Brutto Gesamtbetrag:</td>
|
||||
<td style="padding: 4px 0; text-align: right; font-weight: bold; color: #0f172a;">${monthlyGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
`
|
||||
} else if (oneTimeNet > 0) {
|
||||
totalDetailsHtml = `
|
||||
<tr>
|
||||
<td style="padding: 4px 0; color: #475569;">Netto-Gesamtbetrag:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${oneTimeNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; color: #475569;">zzgl. ${taxRate}% MwSt:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${oneTimeTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold; color: #0f172a; border-top: 1px solid #e2e8f0; padding-top: 6px;">Gesamtbetrag (brutto):</td>
|
||||
<td style="padding: 4px 0; text-align: right; font-weight: bold; color: #0f172a; border-top: 1px solid #e2e8f0; padding-top: 6px;">${oneTimeGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}</td>
|
||||
</tr>
|
||||
`
|
||||
} else {
|
||||
totalDetailsHtml = `
|
||||
<tr>
|
||||
<td style="padding: 4px 0; color: #475569;">Netto-Gesamtbetrag:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${monthlyNet.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; color: #475569;">zzgl. ${taxRate}% MwSt:</td>
|
||||
<td style="padding: 4px 0; text-align: right; color: #475569;">${monthlyTax.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold; color: #0f172a; border-top: 1px solid #e2e8f0; padding-top: 6px;">Gesamtbetrag (brutto):</td>
|
||||
<td style="padding: 4px 0; text-align: right; font-weight: bold; color: #0f172a; border-top: 1px solid #e2e8f0; padding-top: 6px;">${monthlyGross.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} / Monat</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
await sendMail({
|
||||
to: orderUserEmail,
|
||||
subject: `Angebotsänderung ${order.order_number}`,
|
||||
text: `Hallo,\n\nihr Angebot wurde erfolgreich aktualisiert!\n\nNeue Angebotsdetails:\n- Anfrage-Nummer: ${order.order_number}\n- Datum: ${formattedDate}\n- Endkunde: ${customerSnapshot.company_name}\n- Gesamtsumme: ${formattedTotal}\n\nIm Anhang dieser E-Mail finden Sie Ihr aktualisiertes Angebot als PDF-Dokument.\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
|
||||
text: `Hallo,\n\nihr Angebot wurde erfolgreich aktualisiert!\n\nNeue Angebotsdetails:\n- Anfrage-Nummer: ${order.order_number}\n- Datum: ${formattedDate}\n- Endkunde: ${customerSnapshot.company_name}\n${totalDetailsText}\n\nIm Anhang dieser E-Mail finden Sie Ihr aktualisiertes Angebot als PDF-Dokument.\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
|
||||
html: `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
||||
<h2 style="color: #0f172a; margin-bottom: 16px;">Ihr Angebot wurde aktualisiert!</h2>
|
||||
@@ -571,10 +742,7 @@ export async function updateOrder(
|
||||
<td style="padding: 4px 0; font-weight: bold;">Endkunde:</td>
|
||||
<td style="padding: 4px 0;">${customerSnapshot.company_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Gesamtsumme:</td>
|
||||
<td style="padding: 4px 0; font-weight: bold; color: #0f172a;">${formattedTotal}</td>
|
||||
</tr>
|
||||
${totalDetailsHtml}
|
||||
</table>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Im Anhang dieser E-Mail finden Sie Ihre aktualisierte Anfragebestätigung als PDF-Dokument.</p>
|
||||
|
||||
Reference in New Issue
Block a user