Files
webshop/shop/components/wizard/step-summary.tsx

591 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React, { useState } 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 { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import {
ShieldCheck,
Building2,
Calendar,
Loader2,
CheckCircle2,
Pencil,
Trash2,
Plus,
ChevronLeft,
MessageSquare,
FileText,
Lock,
Check,
X,
CreditCard,
User,
MapPin,
Send,
} 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
linkedFeeProducts?: Product[]
orderNotes: string
setOrderNotes: (notes: string) => void
onEditBasketItem?: (idx: number) => void
onAddNewBasketItem?: () => void
onDeleteBasketItem?: (idx: number) => 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,
linkedFeeProducts = [],
orderNotes,
setOrderNotes,
onEditBasketItem,
onAddNewBasketItem,
onDeleteBasketItem,
}: StepSummaryProps) {
const [confirmDeleteIdx, setConfirmDeleteIdx] = useState<number | null>(null)
const [acceptedTerms, setAcceptedTerms] = useState(false)
const [showConfirmModal, setShowConfirmModal] = useState(false)
const fmt = (val: number) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val)
const totalPositionsCount = finalItemsToShow.length
const primaryInterval = finalItemsToShow[0]?.billingInterval || 'monthly'
return (
<Card className="glass-dark border-white/10 h-[calc(100vh-8.5rem)] flex flex-col justify-between overflow-hidden relative">
{/* ─── 1. FIXED HEADER (shrink-0) ─── */}
<CardHeader className="pb-3 pt-4 px-6 shrink-0 border-b border-white/10 bg-slate-950/40 space-y-0">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary/20 rounded-xl flex items-center justify-center border border-primary/40 shrink-0 text-primary">
<ShieldCheck className="w-5 h-5" />
</div>
<div>
<CardTitle className="text-xl font-bold text-white flex items-center gap-2">
Anfrage prüfen & absenden
</CardTitle>
<CardDescription className="text-xs text-slate-300 mt-0.5">
Überprüfen Sie alle konfigurierten Kassen, Positionen und Konditionen vor der Freigabe.
</CardDescription>
</div>
</div>
<Badge variant="outline" className="border-primary/30 text-primary bg-primary/10 text-xs px-3 py-1">
Schritt 4 von 4
</Badge>
</div>
</CardHeader>
{/* ─── 2. SCROLLABLE MIDDLE (flex-1 min-h-0 overflow-y-auto) ─── */}
<div className="flex-1 min-h-0 overflow-y-auto p-6 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 max-w-7xl mx-auto w-full items-start">
{/* ─── LINKES RASTER (lg:col-span-7) Auftragsübersicht & Kassen ─── */}
<div className="lg:col-span-7 space-y-5">
{/* Kopf-Card: Endkunde & Abrechnungsmodell */}
<div className="p-4 rounded-2xl bg-white/5 border border-white/10 space-y-3">
<div className="flex items-center justify-between gap-2 border-b border-white/5 pb-2.5 flex-wrap">
<div className="flex items-center gap-2 text-primary font-bold text-xs uppercase tracking-wider">
<Building2 className="w-4 h-4" />
<span>Kunde & Abrechnung</span>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-[10px] bg-primary/10 border-primary/30 text-primary font-semibold">
{primaryInterval === 'one_time' ? 'Einmalkauf' : 'Monatliches Abo'}
</Badge>
{lastLicenseDate && (
<Badge variant="outline" className="text-[10px] bg-emerald-500/10 border-emerald-500/30 text-emerald-400">
Stichtag: {new Date(lastLicenseDate).toLocaleDateString('de-DE')}
</Badge>
)}
</div>
</div>
{selectedEndCustomer ? (
<div className="space-y-1">
<h4 className="text-sm font-bold text-white">{selectedEndCustomer.company_name}</h4>
<div className="flex items-center gap-3 text-xs text-slate-300 flex-wrap">
{(selectedEndCustomer.first_name || selectedEndCustomer.last_name) && (
<span className="flex items-center gap-1">
<User className="w-3 h-3 text-slate-400" />
{[selectedEndCustomer.first_name, selectedEndCustomer.last_name].filter(Boolean).join(' ')}
</span>
)}
{(selectedEndCustomer.street || selectedEndCustomer.zip || selectedEndCustomer.city) && (
<span className="flex items-center gap-1">
<MapPin className="w-3 h-3 text-slate-400" />
{[selectedEndCustomer.street, [selectedEndCustomer.zip, selectedEndCustomer.city].filter(Boolean).join(' ')].filter(Boolean).join(', ')}
</span>
)}
{selectedEndCustomer.vat_id && (
<span className="text-slate-400">USt-IdNr: {selectedEndCustomer.vat_id}</span>
)}
</div>
</div>
) : (
<p className="text-xs text-slate-400 italic">Kein Endkunde zugewiesen (Partner-Bestellung).</p>
)}
</div>
{/* Kassenliste */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold text-slate-300 uppercase tracking-wider flex items-center gap-2">
<span>Enthaltene Kassen ({finalItemsToShow.length})</span>
</h3>
{onAddNewBasketItem && (
<Button
type="button"
variant="outline"
size="sm"
onClick={onAddNewBasketItem}
className="border-dashed border-primary/40 text-primary hover:bg-primary/10 text-xs font-semibold gap-1.5 h-7"
>
<Plus className="w-3 h-3" /> Weitere Kasse hinzufügen
</Button>
)}
</div>
{finalItemsToShow.length === 0 ? (
<div className="p-6 rounded-2xl bg-white/5 border border-white/10 text-center space-y-2">
<p className="text-xs text-slate-400 font-medium">Keine Kassen in der Aufstellung vorhanden.</p>
</div>
) : (
finalItemsToShow.map((item, itemIdx) => (
<div
key={itemIdx}
className="p-4 rounded-2xl bg-white/5 border border-white/10 space-y-3 relative overflow-hidden"
>
{/* Kassen Header Bar */}
<div className="flex justify-between items-center bg-slate-950/40 p-2.5 rounded-xl border border-white/5">
<div className="flex items-center gap-2 min-w-0">
<span className="text-white font-bold text-xs truncate">
{item.deviceName || `Kasse ${itemIdx + 1}`}
</span>
{itemIdx > 0 && (
<Badge variant="outline" className="text-[9px] px-1.5 py-0 border-emerald-500/30 text-emerald-400 bg-emerald-500/10 shrink-0">
Kasse 2+
</Badge>
)}
{item.licenseNumber && (
<span className="text-[10px] text-slate-400 font-mono truncate">
({item.licenseNumber})
</span>
)}
</div>
<div className="flex items-center gap-1.5 shrink-0">
<Badge
variant="outline"
className={`text-[9px] px-2 py-0.5 font-semibold ${
item.billingInterval === 'one_time'
? 'border-amber-500/30 text-amber-400 bg-amber-500/10'
: 'border-blue-500/30 text-blue-400 bg-blue-500/10'
}`}
>
{item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'}
</Badge>
{onEditBasketItem && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => onEditBasketItem(itemIdx)}
className="h-7 px-2 text-xs text-slate-300 hover:text-primary hover:bg-primary/20 gap-1 rounded-lg"
title="Kasse in Schritt 3 bearbeiten"
>
<Pencil className="w-3 h-3" /> Bearbeiten
</Button>
)}
{onDeleteBasketItem && (
<Button
type="button"
variant={confirmDeleteIdx === itemIdx ? 'destructive' : 'ghost'}
size="sm"
onClick={(e) => {
e.stopPropagation()
if (confirmDeleteIdx === itemIdx) {
onDeleteBasketItem(itemIdx)
setConfirmDeleteIdx(null)
} else {
setConfirmDeleteIdx(itemIdx)
}
}}
onMouseLeave={() => setConfirmDeleteIdx(null)}
className={`h-7 px-2 text-xs transition-all ${
confirmDeleteIdx === itemIdx
? 'bg-red-600 text-white hover:bg-red-700 font-bold px-2.5 shadow-md shadow-red-500/30'
: 'text-slate-400 hover:text-red-400 hover:bg-red-500/20'
} gap-1 rounded-lg`}
title="Kasse entfernen"
>
<Trash2 className="w-3 h-3" />
{confirmDeleteIdx === itemIdx ? 'Wirklich löschen?' : ''}
</Button>
)}
</div>
</div>
{/* Enthaltene Produkte & Module */}
<div className="space-y-2 text-xs">
{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-1">
<span className="text-slate-400 text-[10px] font-bold uppercase tracking-wider block">
{cat.name}:
</span>
{sortedProds.map((prod, idx) => {
const isFree = idx < freeLimit
const isAbo = item.billingInterval === 'monthly'
const displayPrice = isFree ? 0 : isAbo ? ((prod as any).monthly_price ?? prod.base_price) : prod.base_price
return (
<div key={prod.id} className="flex justify-between items-center text-slate-200 pl-2">
<span className="flex items-center gap-1.5 truncate pr-2">
<CategoryIcon icon={cat.icon} className="w-3.5 h-3.5 text-primary shrink-0" />
<span className="truncate">{prod.name}</span>
{isFree && (
<span className="text-[9px] bg-green-500/20 text-green-400 px-1.5 py-0.2 rounded border border-green-500/30 font-bold uppercase shrink-0">
Inklusive
</span>
)}
</span>
<span className="font-bold text-xs text-white tabular-nums shrink-0">
{isFree ? '0,00 €' : fmt(displayPrice)}
</span>
</div>
)
})}
{/* Selected Module Chips */}
{sel?.moduleIds && sel.moduleIds.length > 0 && (
<div className="pl-6 space-y-1 pt-0.5">
{sel.moduleIds.map((mId: string) => {
let modObj: any = null
products.forEach((p) => {
const found = p.modules?.find((m) => m.id === mId)
if (found) modObj = found
})
if (!modObj) return null
const isAbo = item.billingInterval === 'monthly'
const qty = item.moduleQuantities?.[mId] || 1
const unitPrice = isAbo ? (modObj.monthly_price ?? modObj.price) : modObj.price
const totalPrice = unitPrice * qty
return (
<div key={mId} className="flex justify-between text-slate-400 text-[11px]">
<span className="truncate pr-2">
+ {modObj.name} {qty > 1 ? `(${qty}x)` : ''}
</span>
<span className="font-semibold text-slate-300 tabular-nums shrink-0">
{fmt(totalPrice)}
</span>
</div>
)
})}
</div>
)}
</div>
)
})}
</div>
</div>
))
)}
{/* Service- & Zusatz-Positionen */}
{linkedFeeProducts.length > 0 && (
<div className="p-3.5 rounded-2xl bg-white/5 border border-white/10 space-y-2">
<span className="text-slate-400 text-[10px] font-bold uppercase tracking-wider block">
Service & Gebühren:
</span>
{linkedFeeProducts.map((fp) => (
<div key={fp.id} className="flex justify-between text-xs text-slate-300">
<span>{fp.name}</span>
<span className="font-bold text-white tabular-nums">{fmt(fp.base_price)}</span>
</div>
))}
</div>
)}
</div>
</div>
{/* ─── RECHTES RASTER (lg:col-span-5) Notizen, Bedingungen & Summen ─── */}
<div className="lg:col-span-5 space-y-5">
{/* Glassmorphism Preiskalkulation */}
<div className="p-5 rounded-2xl bg-slate-950/80 border border-white/10 space-y-3 shadow-xl">
<div className="flex items-center gap-2 pb-2 border-b border-white/10">
<CreditCard className="w-4 h-4 text-primary" />
<h3 className="text-xs font-bold text-white uppercase tracking-wider">
Gesamte Preiskalkulation
</h3>
</div>
{oneTimeTotal > 0 && (
<div className="space-y-1.5 text-xs">
<div className="flex justify-between text-slate-400">
<span>Einmalig (netto):</span>
<span className="tabular-nums font-mono text-white">{fmt(oneTimeNet)}</span>
</div>
<div className="flex justify-between text-slate-400">
<span>zzgl. 19% MwSt.:</span>
<span className="tabular-nums font-mono text-white">{fmt(oneTimeTax)}</span>
</div>
{updatePriceModifier?.label && (
<div className="text-[11px] text-green-400 font-semibold bg-green-500/10 p-2 rounded-lg border border-green-500/20 my-1">
{updatePriceModifier.label}
</div>
)}
<div className="flex justify-between text-sm font-bold text-white pt-2 border-t border-white/5">
<span>Einmalig gesamt (brutto):</span>
<span className="tabular-nums font-mono text-primary font-extrabold text-base">{fmt(oneTimeGross)}</span>
</div>
</div>
)}
{monthlyTotal > 0 && (
<div className={`space-y-1.5 text-xs ${oneTimeTotal > 0 ? 'pt-3 border-t border-white/10' : ''}`}>
<div className="flex justify-between text-slate-400">
<span>Monatlich (netto):</span>
<span className="tabular-nums font-mono text-white">{fmt(monthlyNet)} / mtl.</span>
</div>
<div className="flex justify-between text-slate-400">
<span>zzgl. 19% MwSt.:</span>
<span className="tabular-nums font-mono text-white">{fmt(monthlyTax)} / mtl.</span>
</div>
<div className="flex justify-between text-sm font-bold text-white pt-2 border-t border-white/5">
<span>Monatlich gesamt (brutto):</span>
<span className="tabular-nums font-mono text-primary font-extrabold text-base">{fmt(monthlyGross)} / mtl.</span>
</div>
</div>
)}
</div>
{/* Bestellnotizen */}
<div className="p-4 rounded-2xl bg-white/5 border border-white/10 space-y-2">
<Label htmlFor="order-notes" className="text-xs font-bold text-white uppercase tracking-wider flex items-center gap-1.5">
<MessageSquare className="w-3.5 h-3.5 text-primary" />
<span>Bestellnotizen / Bemerkungen (optional)</span>
</Label>
<textarea
id="order-notes"
rows={3}
value={orderNotes}
onChange={(e) => setOrderNotes(e.target.value)}
placeholder="Besondere Hinweise, Wunschtermine oder Ansprechpartner..."
className="w-full p-3 rounded-xl bg-slate-950/70 border border-white/10 text-xs text-white placeholder:text-slate-500 focus:border-primary focus:outline-none transition-colors resize-none scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent"
/>
</div>
</div>
</div>
</div>
{/* ─── 3. FIXED BOTTOM ACTION BAR (shrink-0) ─── */}
<div className="shrink-0 border-t border-white/10 bg-slate-950/90 backdrop-blur-md px-6 py-3.5 flex flex-col sm:flex-row items-center justify-between gap-3 z-10">
{/* Back Button */}
<Button
variant="outline"
onClick={prevStep}
className="w-full sm:w-auto border-white/10 text-white hover:bg-white/10 h-9 text-xs gap-1.5"
>
<ChevronLeft className="w-4 h-4" /> Zurück zu Schritt 3
</Button>
{/* Info Chip */}
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-white/5 border border-white/10 text-white text-xs">
<span className="text-slate-400">Positionen:</span>
<strong className="text-white">{finalItemsToShow.length} Kassen</strong>
<span className="text-slate-600">|</span>
<span className="text-primary font-bold">
{oneTimeTotal > 0 ? fmt(oneTimeGross) : `${fmt(monthlyGross)} / mtl.`}
</span>
</div>
{/* Submit Button -> opens modal */}
<Button
onClick={() => setShowConfirmModal(true)}
disabled={isSubmitting || finalItemsToShow.length === 0}
className="w-full sm:w-auto bg-primary hover:bg-primary/90 text-white font-bold px-6 shadow-lg shadow-primary/20 h-9 text-xs gap-2"
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
{initialOrder ? 'Anfrage wird aktualisiert...' : 'Anfrage wird übertragen...'}
</>
) : (
<>
<Send className="w-3.5 h-3.5" />
{initialOrder ? 'Anfrage aktualisieren' : 'Anfrage verbindlich absenden'}
</>
)}
</Button>
</div>
{/* ─── 4. BESTÄTIGUNGS-POPUP / MODAL VOR DEM ABSENDEN ─── */}
{showConfirmModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-in fade-in duration-200">
<div className="w-full max-w-lg bg-slate-900 border border-white/10 rounded-2xl shadow-2xl p-6 space-y-5 text-white">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-primary/20 text-primary border border-primary/30">
<ShieldCheck className="w-6 h-6" />
</div>
<div>
<h3 className="text-base font-bold text-white">Richtigkeit & Freigabe bestätigen</h3>
<p className="text-xs text-slate-400 mt-0.5">Letzter Schritt vor Übermittlung der Lizenzierung</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setShowConfirmModal(false)}
disabled={isSubmitting}
className="h-8 w-8 p-0 text-slate-400 hover:text-white rounded-lg"
>
<X className="w-4 h-4" />
</Button>
</div>
<div className="space-y-3 text-xs text-slate-300">
<div className="p-3.5 rounded-xl bg-white/5 border border-white/10 space-y-2">
<div className="flex justify-between items-center text-white font-semibold">
<span>Endkunde:</span>
<span className="text-primary">{selectedEndCustomer?.company_name || 'Kein Kunde gewählt'}</span>
</div>
<div className="flex justify-between items-center text-slate-400">
<span>Anzahl Geräte:</span>
<span className="text-white font-medium">{finalItemsToShow.length} Kassen</span>
</div>
<div className="flex justify-between items-center text-slate-400">
<span>Gesamtbetrag (brutto):</span>
<span className="text-white font-bold">{oneTimeTotal > 0 ? fmt(oneTimeGross) : `${fmt(monthlyGross)} / mtl.`}</span>
</div>
</div>
<div className="p-3.5 rounded-xl bg-slate-950/80 border border-white/10 space-y-3">
<div className="flex items-start gap-3">
<Checkbox
id="modal-terms-checkbox"
checked={acceptedTerms}
onCheckedChange={(checked) => setAcceptedTerms(checked === true)}
className="rounded border-white/20 data-[state=checked]:bg-primary mt-0.5"
/>
<Label htmlFor="modal-terms-checkbox" className="text-xs text-slate-300 leading-relaxed cursor-pointer select-none">
Ich bestätige die Richtigkeit aller gemachten Angaben und akzeptiere die Lizenzvereinbarungen sowie die{' '}
<a href="/datenschutz" target="_blank" rel="noopener noreferrer" className="underline hover:text-white text-primary">
Datenschutzerklärung
</a>.
</Label>
</div>
<div className="p-2.5 rounded-lg bg-primary/10 border border-primary/20 text-[11px] text-slate-300 flex items-center gap-2">
<FileText className="w-4 h-4 text-primary shrink-0" />
<span>B2B-Freigabe: Nach Absenden wird die Lizenzierungsanfrage geprüft und freigegeben.</span>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-3 pt-2 border-t border-white/10">
<Button
variant="outline"
onClick={() => setShowConfirmModal(false)}
disabled={isSubmitting}
className="border-white/10 text-white hover:bg-white/10 text-xs h-9 px-4"
>
Abbrechen & Zurück
</Button>
<Button
onClick={async () => {
await handleSubmit()
setShowConfirmModal(false)
}}
disabled={!acceptedTerms || isSubmitting}
className="bg-primary hover:bg-primary/90 text-white font-bold text-xs h-9 px-5 gap-2 shadow-md shadow-primary/20"
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Wird übermittelt...
</>
) : (
<>
<Check className="w-4 h-4" />
Jetzt verbindlich absenden
</>
)}
</Button>
</div>
</div>
</div>
)}
</Card>
)
}