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

473 lines
21 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React from 'react'
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Pencil, Trash2, UserPlus, ChevronRight, AlertCircle, Check } from 'lucide-react'
import { Category, Product, CategorySelection } from '@/lib/types'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
interface SummarySidebarProps {
visibleCategories: Category[]
selections: Record<string, CategorySelection>
products: Product[]
moduleQuantities: Record<string, number>
billingLabel: (interval: string) => string
oneTimeTotal: number
monthlyTotal: number
oneTimeNet: number
oneTimeTax: number
oneTimeGross: number
monthlyNet: number
monthlyTax: number
monthlyGross: number
updatePriceModifier: { label?: string }
allCategoriesFilled: boolean
productValidationErrors: string[]
basketItems: any[]
editingIdx: number | null
editBasketItem: (idx: number) => void
deleteBasketItem: (idx: number) => void
deviceName: string
setDeviceName: (name: string) => void
licenseNumber: string
setLicenseNumber: (num: string) => void
addToBasket: () => void
isNextStepDisabled: boolean
hasActiveSelection: boolean
nextStep: () => void
prevStep: () => void
}
export function SummarySidebar({
visibleCategories,
selections,
products,
moduleQuantities,
billingLabel,
oneTimeTotal,
monthlyTotal,
oneTimeNet,
oneTimeTax,
oneTimeGross,
monthlyNet,
monthlyTax,
monthlyGross,
updatePriceModifier,
allCategoriesFilled,
productValidationErrors,
basketItems,
editingIdx,
editBasketItem,
deleteBasketItem,
deviceName,
setDeviceName,
licenseNumber,
setLicenseNumber,
addToBasket,
isNextStepDisabled,
hasActiveSelection,
nextStep,
prevStep,
}: SummarySidebarProps) {
const [isOpen, setIsOpen] = React.useState(false)
const [isSavePromptOpen, setIsSavePromptOpen] = React.useState(false)
const handleNextClick = () => {
if (hasActiveSelection) {
setIsSavePromptOpen(true)
} else {
nextStep()
}
}
const handleSaveAndNext = async () => {
await addToBasket()
setIsSavePromptOpen(false)
nextStep()
}
const handleDiscardAndNext = () => {
setIsSavePromptOpen(false)
nextStep()
}
const [selectedBasketIdx, setSelectedBasketIdx] = React.useState<number | null>(null)
// When editing a basket item via pencil, open the modal for that item
const handleEditBasketItem = (idx: number) => {
editBasketItem(idx)
setIsOpen(true)
}
const fmt = (val: number) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val)
return (
<div className="h-full flex flex-col">
<Card className="bg-slate-900/80 backdrop-blur-xl border border-white/10 shadow-2xl rounded-2xl overflow-hidden flex flex-col h-full">
<CardHeader className="shrink-0 pb-3 border-b border-white/5">
<CardTitle className="text-white text-base font-bold flex items-center justify-between">
<span>Zusammenfassung</span>
{basketItems.length > 0 && (
<span className="text-xs bg-sky-500/20 text-sky-300 border border-sky-500/30 px-2 py-0.5 rounded-full font-semibold">
{basketItems.length} {basketItems.length === 1 ? 'Kasse' : 'Kassen'}
</span>
)}
</CardTitle>
</CardHeader>
<CardContent className="p-4 pt-3 flex-1 flex flex-col overflow-hidden gap-3">
{/* Active Selections & Price - scrollable */}
<div className="space-y-3 flex-1 overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
{visibleCategories.map(cat => {
const sel = selections[cat.id]
const selectedProds: Product[] = []
if (cat.allow_multiselect && sel?.productIds) {
sel.productIds.forEach(pId => {
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 (
<div key={cat.id} className="text-xs text-slate-500 italic flex items-center gap-1.5">
{cat.is_required ? (
<AlertCircle className="w-3 h-3 text-destructive shrink-0" />
) : (
<span className="w-3 h-3 inline-block shrink-0" />
)}
<span>{cat.name}: {cat.is_required ? 'nicht gewählt' : 'nicht gewählt (optional)'}</span>
</div>
)
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">
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">{cat.name}</p>
{sortedProds.map((prod, idx) => {
const isFree = idx < freeLimit
const actualPrice = isFree ? 0 : prod.base_price
return (
<div key={prod.id} className="space-y-0.5 pl-2">
<div className="flex justify-between text-sm">
<span className="text-white">{prod.name} {isFree && <span className="text-[10px] text-green-400">(Frei)</span>}</span>
<span className="text-white tabular-nums">
{fmt(actualPrice)}
{' '}<span className="text-slate-400 text-xs">{billingLabel(prod.billing_interval)}</span>
</span>
</div>
{sel.moduleIds.map(mId => {
const mod = prod.modules?.find(m => m.id === mId)
const qty = moduleQuantities[mId] || 1
return mod ? (
<div key={mId} className="flex justify-between text-xs pl-2">
<span className="text-slate-300">+ {mod.name} {mod.has_quantity ? `(x${qty})` : ''}</span>
<span className="text-slate-300 tabular-nums">
{fmt(mod.price * qty)}
</span>
</div>
) : null
})}
</div>
)
})}
</div>
)
})}
{/* Pricing block */}
<Separator className="bg-white/10" />
{oneTimeTotal > 0 && monthlyTotal > 0 ? (
<div className="space-y-3 bg-slate-900/90 border border-white/10 rounded-xl p-3 shadow-inner">
<div className="space-y-1">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Einmalig:</p>
<div className="flex justify-between text-xs text-slate-400">
<span>Netto:</span>
<span className="tabular-nums">{fmt(oneTimeNet)}</span>
</div>
<div className="flex justify-between text-xs text-slate-400">
<span>zzgl. 19% MwSt.:</span>
<span className="tabular-nums">{fmt(oneTimeTax)}</span>
</div>
<div className="flex justify-between text-sm font-extrabold text-white pt-1">
<span>Brutto:</span>
<span className="tabular-nums text-sky-400">{fmt(oneTimeGross)}</span>
</div>
</div>
<div className="space-y-1 pt-2 border-t border-white/10">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Monatlich:</p>
<div className="flex justify-between text-xs text-slate-400">
<span>Netto:</span>
<span className="tabular-nums">{fmt(monthlyNet)} / mtl.</span>
</div>
<div className="flex justify-between text-xs text-slate-400">
<span>zzgl. 19% MwSt.:</span>
<span className="tabular-nums">{fmt(monthlyTax)} / mtl.</span>
</div>
<div className="flex justify-between text-sm font-extrabold text-white pt-1">
<span>Brutto:</span>
<span className="tabular-nums text-sky-400">{fmt(monthlyGross)} / mtl.</span>
</div>
</div>
</div>
) : oneTimeTotal > 0 ? (
<div className="space-y-1 bg-slate-900/90 border border-white/10 rounded-xl p-3 shadow-inner">
<div className="flex justify-between text-xs text-slate-400">
<span>Netto:</span>
<span className="tabular-nums">{fmt(oneTimeNet)}</span>
</div>
<div className="flex justify-between text-xs text-slate-400">
<span>zzgl. 19% MwSt.:</span>
<span className="tabular-nums">{fmt(oneTimeTax)}</span>
</div>
<div className="flex justify-between text-base font-extrabold text-sky-400 pt-1">
<span>Gesamt (brutto):</span>
<span className="tabular-nums">{fmt(oneTimeGross)}</span>
</div>
</div>
) : (
<div className="space-y-1 bg-slate-900/90 border border-white/10 rounded-xl p-3 shadow-inner">
<div className="flex justify-between text-xs text-slate-400">
<span>Netto:</span>
<span className="tabular-nums">{fmt(monthlyNet)} / mtl.</span>
</div>
<div className="flex justify-between text-xs text-slate-400">
<span>zzgl. 19% MwSt.:</span>
<span className="tabular-nums">{fmt(monthlyTax)} / mtl.</span>
</div>
<div className="flex justify-between text-base font-extrabold text-sky-400 pt-1">
<span>Gesamt (brutto):</span>
<span className="tabular-nums">{fmt(monthlyGross)} / mtl.</span>
</div>
</div>
)}
{updatePriceModifier.label && (
<div className="text-xs text-green-400 bg-green-500/10 p-2.5 rounded-lg border border-green-500/20 space-y-1">
<p className="font-semibold flex items-center gap-1">
<Check className="w-3.5 h-3.5" />
Update-Rabatt aktiv
</p>
<p>{updatePriceModifier.label}</p>
</div>
)}
{!allCategoriesFilled && (
<p className="text-xs text-amber-400 flex items-center gap-1 bg-amber-500/10 p-2 rounded-lg border border-amber-500/20">
<AlertCircle className="w-3.5 h-3.5 shrink-0 text-amber-400" />
Bitte aus jeder Pflichtkategorie einen Artikel wählen.
</p>
)}
{productValidationErrors.map((err, errIdx) => (
<p key={errIdx} className="text-xs text-red-400 flex items-center gap-1 bg-red-500/10 p-2 rounded-lg border border-red-500/20">
<AlertCircle className="w-3.5 h-3.5 shrink-0 text-red-400" />
{err}
</p>
))}
</div> {/* Warenkorb List (Scrollable) */}
{basketItems.length > 0 && (
<div className="shrink-0 pt-2 pb-0 border-t border-white/10 space-y-1.5">
<div className="flex items-center justify-between text-xs text-slate-400">
<span className="font-semibold uppercase tracking-wider">Kassen ({basketItems.length}):</span>
<span className="text-[10px] text-slate-500">Zum Bearbeiten anklicken</span>
</div>
<div className="flex flex-col gap-2 max-h-36 overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
{basketItems.map((item, idx) => {
const isEditing = idx === editingIdx
return (
<div
key={idx}
onClick={() => editBasketItem(idx)}
className={`flex items-center justify-between p-2.5 rounded-xl border transition-all duration-200 text-left cursor-pointer group ${
isEditing
? 'border-primary bg-primary/15 text-white shadow-[0_0_12px_rgba(59,130,246,0.25)]'
: 'border-white/10 bg-slate-900/60 text-slate-300 hover:border-white/20 hover:bg-slate-900'
}`}
>
<div className="flex-1 min-w-0 pr-2">
<div className="flex items-center gap-1.5">
<span className="font-bold text-xs text-white truncate">{item.deviceName}</span>
<span className="text-[9px] px-1.5 py-0.2 rounded border border-white/10 text-slate-400 bg-white/5 uppercase">
{item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'}
</span>
{isEditing && (
<span className="text-[9px] px-1.5 py-0.2 rounded bg-primary text-slate-950 font-extrabold uppercase">
Aktiv
</span>
)}
</div>
{item.licenseNumber && (
<p className="text-[10px] text-slate-400 truncate mt-0.5">Lizenz: {item.licenseNumber}</p>
)}
</div>
<div className="flex items-center gap-1 shrink-0">
<Button
type="button"
variant="ghost"
size="icon"
className="w-6 h-6 hover:bg-primary/20 text-slate-400 hover:text-primary rounded-lg transition-colors"
onClick={(e) => {
e.stopPropagation()
editBasketItem(idx)
setIsOpen(true)
}}
title="Name / Lizenz bearbeiten"
>
<Pencil className="w-3 h-3" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="w-6 h-6 hover:bg-red-500/20 text-slate-400 hover:text-red-400 rounded-lg transition-colors"
onClick={(e) => {
e.stopPropagation()
deleteBasketItem(idx)
}}
title="Löschen"
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
)
})}
</div>
</div>
)}
</CardContent>
<CardFooter className="flex flex-col gap-2 shrink-0 p-4 pt-1.5 border-t border-white/10 bg-slate-950/80">
<Button
type="button"
variant="outline"
className="w-full border-dashed border-primary/50 text-primary hover:bg-primary/10 transition-all rounded-xl h-9 text-xs font-bold gap-1.5 shadow-sm"
onClick={() => setIsOpen(true)}
disabled={!hasActiveSelection && isNextStepDisabled}
>
<UserPlus className="w-3.5 h-3.5" /> {editingIdx !== null ? '💾 Kasse speichern' : '+ Weitere Kasse hinzufügen'}
</Button>
<Button
className="w-full h-10 text-sm font-bold bg-gradient-to-r from-blue-600 via-sky-500 to-cyan-400 hover:opacity-90 shadow-[0_0_20px_rgba(56,189,248,0.3)] transition-all"
onClick={handleNextClick}
disabled={basketItems.length === 0 && isNextStepDisabled}
>
Weiter zu Schritt 4 <ChevronRight className="ml-1 w-4 h-4" />
</Button>
<Button variant="ghost" className="w-full text-slate-400 hover:text-white h-7 text-xs" onClick={prevStep}>
Zurück zum Abrechnungsmodell
</Button>
</CardFooter>
</Card>
{/* Modal 1: Name & Lizenznummer beim manuellen Anlegen/Bearbeiten */}
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="bg-slate-900 border border-slate-800 text-white rounded-2xl max-w-md p-6">
<DialogHeader>
<DialogTitle className="text-lg font-bold text-white">
{editingIdx !== null ? 'Kasse bearbeiten' : 'Kasse anlegen'}
</DialogTitle>
<DialogDescription className="text-slate-400 text-xs mt-1">
Geben Sie den Namen und die Lizenznummer für diese Kasse ein.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="device-name-modal" className="text-xs text-slate-400">Kassenname (optional)</Label>
<Input
id="device-name-modal"
placeholder={`z.B. Kasse ${basketItems.length + 1}, Theke`}
value={deviceName}
onChange={e => setDeviceName(e.target.value)}
className="bg-white/5 border-white/10 text-white text-xs h-9 rounded-lg"
/>
</div>
<div className="space-y-2">
<Label htmlFor="license-number-modal" className="text-xs text-slate-400">Lizenznummer (optional)</Label>
<Input
id="license-number-modal"
placeholder="z.B. LIZ-12345 (eindeutig)"
value={licenseNumber}
onChange={e => setLicenseNumber(e.target.value)}
className="bg-white/5 border-white/10 text-white text-xs h-9 rounded-lg"
/>
</div>
</div>
<DialogFooter className="flex flex-row justify-end gap-2 mt-4">
<Button
type="button"
variant="ghost"
className="text-slate-400 hover:text-white"
onClick={() => setIsOpen(false)}
>
Abbrechen
</Button>
<Button
type="button"
className="h-10"
onClick={() => {
addToBasket()
setIsOpen(false)
}}
>
Speichern
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Modal 2: Abfrage beim Weiterklicken ohne gespeicherte Kasse */}
<Dialog open={isSavePromptOpen} onOpenChange={setIsSavePromptOpen}>
<DialogContent className="bg-slate-900 border border-slate-800 text-white rounded-2xl max-w-md p-6">
<DialogHeader>
<DialogTitle className="text-lg font-bold text-white flex items-center gap-2">
<span> Kasse noch nicht gespeichert</span>
</DialogTitle>
<DialogDescription className="text-slate-300 text-sm mt-2">
Sie haben aktuell eine Kassen-Konfiguration ausgewählt. Möchten Sie diese Kasse speichern und mit zur Bestellung hinzufügen?
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex flex-col sm:flex-row justify-end gap-2 mt-6">
<Button
type="button"
variant="ghost"
className="text-slate-400 hover:text-white text-xs"
onClick={() => setIsSavePromptOpen(false)}
>
Abbrechen
</Button>
{basketItems.length > 0 && (
<Button
type="button"
variant="outline"
className="border-white/20 text-slate-300 hover:bg-white/10 text-xs"
onClick={handleDiscardAndNext}
>
Nicht speichern & weiter
</Button>
)}
<Button
type="button"
className="bg-primary hover:bg-primary/90 text-white font-bold text-xs"
onClick={handleSaveAndNext}
>
💾 Kasse speichern & weiter
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}