276 lines
13 KiB
TypeScript
276 lines
13 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
|
|
import { Separator } from '@/components/ui/separator'
|
|
import { Button } from '@/components/ui/button'
|
|
import { ShieldCheck, Building2, Calendar, Loader2 } from 'lucide-react'
|
|
import * as Icons from 'lucide-react'
|
|
import { Category, Product, EndCustomer, Profile } from '@/lib/types'
|
|
|
|
interface StepSummaryProps {
|
|
finalItemsToShow: any[]
|
|
visibleCategories: Category[]
|
|
products: Product[]
|
|
oneTimeTotal: number
|
|
monthlyTotal: number
|
|
oneTimeNet: number
|
|
oneTimeTax: number
|
|
oneTimeGross: number
|
|
monthlyNet: number
|
|
monthlyTax: number
|
|
monthlyGross: number
|
|
updatePriceModifier: { label?: string }
|
|
selectedEndCustomer: EndCustomer | null
|
|
customerData: Partial<Profile>
|
|
lastLicenseDate: string
|
|
handleSubmit: () => Promise<void>
|
|
isSubmitting: boolean
|
|
initialOrder: any
|
|
prevStep: () => void
|
|
}
|
|
|
|
function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) {
|
|
const LucideIcon = icon ? (Icons as any)[icon] : null
|
|
if (!LucideIcon) return <Icons.HelpCircle className={className} />
|
|
return <LucideIcon className={className} />
|
|
}
|
|
|
|
export function StepSummary({
|
|
finalItemsToShow,
|
|
visibleCategories,
|
|
products,
|
|
oneTimeTotal,
|
|
monthlyTotal,
|
|
oneTimeNet,
|
|
oneTimeTax,
|
|
oneTimeGross,
|
|
monthlyNet,
|
|
monthlyTax,
|
|
monthlyGross,
|
|
updatePriceModifier,
|
|
selectedEndCustomer,
|
|
customerData,
|
|
lastLicenseDate,
|
|
handleSubmit,
|
|
isSubmitting,
|
|
initialOrder,
|
|
prevStep,
|
|
}: StepSummaryProps) {
|
|
return (
|
|
<Card className="glass-dark border-primary/30 max-w-2xl mx-auto shadow-primary/10 shadow-2xl">
|
|
<CardHeader>
|
|
<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">Anfrage prüfen</CardTitle>
|
|
<CardDescription className="text-slate-300">
|
|
Fast fertig! Bitte überprüfen Sie Ihre Auswahl.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6 text-left">
|
|
<div className="p-4 rounded-lg bg-white/5 space-y-6">
|
|
{finalItemsToShow.map((item, itemIdx) => (
|
|
<div key={itemIdx} className="space-y-3 border-b border-white/10 pb-4 last:border-0 last:pb-0">
|
|
<div className="flex justify-between items-center bg-white/5 p-2 rounded">
|
|
<span className="text-white font-bold text-sm">Kasse: {item.deviceName}</span>
|
|
<span className="text-xs text-slate-400 capitalize">{item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'}</span>
|
|
</div>
|
|
|
|
{visibleCategories.map(cat => {
|
|
const sel = item.selections[cat.id]
|
|
const selectedProds: Product[] = []
|
|
if (cat.allow_multiselect && sel?.productIds) {
|
|
sel.productIds.forEach((pId: string) => {
|
|
const p = products.find(prod => prod.id === pId)
|
|
if (p) selectedProds.push(p)
|
|
})
|
|
} else if (sel?.productId) {
|
|
const p = products.find(prod => prod.id === sel.productId)
|
|
if (p) selectedProds.push(p)
|
|
}
|
|
|
|
if (selectedProds.length === 0) return null
|
|
|
|
const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price)
|
|
const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0
|
|
|
|
return (
|
|
<div key={cat.id} className="space-y-1 pl-2">
|
|
<div className="text-slate-400 font-semibold text-xs flex items-center gap-1">
|
|
<CategoryIcon icon={cat.icon} className="w-3.5 h-3.5 text-primary" />
|
|
<span>{cat.name}</span>
|
|
</div>
|
|
{sortedProds.map((prod, idx) => {
|
|
const isFree = idx < freeLimit
|
|
const actualPrice = isFree ? 0 : prod.base_price
|
|
const catTotal =
|
|
Number(actualPrice) +
|
|
(sel?.moduleIds?.reduce((acc: number, mId: string) => {
|
|
const mod = prod.modules?.find(m => m.id === mId)
|
|
const qty = item.moduleQuantities[mId] || 1
|
|
return acc + (Number(mod?.price ?? 0) * qty)
|
|
}, 0) || 0)
|
|
|
|
return (
|
|
<div key={prod.id} className="pl-3">
|
|
<div className="flex justify-between font-semibold text-sm text-white">
|
|
<span>
|
|
{prod.name} {isFree && <span className="text-[10px] text-green-400 font-semibold">(Frei)</span>}
|
|
</span>
|
|
<span>
|
|
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(catTotal)}
|
|
</span>
|
|
</div>
|
|
{sel?.moduleIds && sel.moduleIds.length > 0 && (
|
|
<p className="text-xs text-slate-400 mt-0.5">
|
|
Module: {sel.moduleIds
|
|
.map((id: string) => {
|
|
const mod = prod.modules?.find(m => m.id === id)
|
|
const qty = item.moduleQuantities[id] || 1
|
|
return mod ? `${mod.name}${mod.has_quantity ? ` (x${qty})` : ''}` : ''
|
|
})
|
|
.filter(Boolean)
|
|
.join(', ')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
))}
|
|
<Separator className="bg-white/10" />
|
|
<div className="text-sm text-slate-300 space-y-1">
|
|
{selectedEndCustomer ? (
|
|
<>
|
|
<p className="font-semibold text-white flex items-center gap-2">
|
|
<Building2 className="w-3.5 h-3.5 text-primary" />
|
|
{selectedEndCustomer.company_name}
|
|
</p>
|
|
<p>{[selectedEndCustomer.first_name, selectedEndCustomer.last_name].filter(Boolean).join(' ')}</p>
|
|
<p>{[selectedEndCustomer.street, selectedEndCustomer.zip, selectedEndCustomer.city].filter(Boolean).join(', ')}</p>
|
|
<p className="text-xs text-slate-500 mt-1">Endkunde Ihres Partners</p>
|
|
{lastLicenseDate && (
|
|
<p className="text-xs text-slate-400 flex items-center gap-1 mt-1 font-mono">
|
|
<Calendar className="w-3.5 h-3.5 text-primary" />
|
|
Letzte Lizenz vom: {new Date(lastLicenseDate).toLocaleDateString('de-DE')}
|
|
</p>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
<p className="font-semibold text-white">{customerData.company_name}</p>
|
|
<p>{customerData.first_name} {customerData.last_name}</p>
|
|
<p>{customerData.address}, {customerData.zip_code} {customerData.city}</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{oneTimeTotal > 0 && monthlyTotal > 0 ? (
|
|
<div className="space-y-4 border-t border-white/10 pt-4">
|
|
<div className="space-y-1">
|
|
<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. 19% 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:</span>
|
|
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}</span>
|
|
</div>
|
|
</div>
|
|
{updatePriceModifier.label && (
|
|
<div className="text-sm text-green-400 font-semibold bg-green-500/10 p-2 rounded border border-green-500/20 my-1">
|
|
{updatePriceModifier.label}
|
|
</div>
|
|
)}
|
|
<div className="space-y-1 pt-2 border-t border-white/10">
|
|
<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)} / mtl.</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm text-slate-400">
|
|
<span>zzgl. 19% MwSt.:</span>
|
|
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl.</span>
|
|
</div>
|
|
<div className="flex justify-between text-base font-bold text-white">
|
|
<span>Brutto:</span>
|
|
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : oneTimeTotal > 0 ? (
|
|
<div className="space-y-1 border-t border-white/10 pt-4">
|
|
<div className="flex justify-between text-sm text-slate-400">
|
|
<span>Netto-Gesamtbetrag:</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. 19% MwSt.:</span>
|
|
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)}</span>
|
|
</div>
|
|
{updatePriceModifier.label && (
|
|
<div className="text-sm text-green-400 font-semibold bg-green-500/10 p-2 rounded border border-green-500/20 my-1">
|
|
{updatePriceModifier.label}
|
|
</div>
|
|
)}
|
|
<div className="flex justify-between text-xl font-bold text-blue-500 dark:text-blue-400 pt-1 border-t border-white/5">
|
|
<span>Gesamtbetrag (brutto):</span>
|
|
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}</span>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1 border-t border-white/10 pt-4">
|
|
<div className="flex justify-between text-sm text-slate-400">
|
|
<span>Netto-Gesamtbetrag:</span>
|
|
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl.</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm text-slate-400">
|
|
<span>zzgl. 19% MwSt.:</span>
|
|
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl.</span>
|
|
</div>
|
|
<div className="flex justify-between text-xl font-bold text-blue-500 dark:text-blue-400 pt-1 border-t border-white/5">
|
|
<span>Gesamtbetrag (brutto):</span>
|
|
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl.</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<p className="text-xs text-slate-500 italic">
|
|
Mit dem Klick auf „Kostenpflichtig bestellen" akzeptieren Sie unsere AGB und die{' '}
|
|
<a href="/datenschutz" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-400">
|
|
Datenschutzerklärung
|
|
</a>.
|
|
</p>
|
|
</CardContent>
|
|
<CardFooter className="flex flex-col gap-4">
|
|
<Button
|
|
className="w-full h-14 text-xl font-bold bg-primary hover:bg-primary/90"
|
|
onClick={handleSubmit}
|
|
disabled={isSubmitting}
|
|
>
|
|
{isSubmitting ? (
|
|
initialOrder ? (
|
|
<><Loader2 className="mr-2 h-5 w-5 animate-spin" /> Anfrage wird aktualisiert...</>
|
|
) : (
|
|
<><Loader2 className="mr-2 h-5 w-5 animate-spin" /> Anfrage wird versendet...</>
|
|
)
|
|
) : (
|
|
initialOrder ? 'Anfrage aktualisieren' : 'Anfrage versenden'
|
|
)}
|
|
</Button>
|
|
<Button variant="ghost" className="w-full text-white" onClick={prevStep}>
|
|
Noch etwas ändern
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
)
|
|
}
|