265 lines
10 KiB
TypeScript
265 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import {
|
|
ChevronRight,
|
|
ChevronLeft,
|
|
AlertCircle,
|
|
Check,
|
|
ShoppingCart,
|
|
Save,
|
|
Plus,
|
|
} from 'lucide-react'
|
|
import { Category, Product, CategorySelection } from '@/lib/types'
|
|
|
|
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[]
|
|
editingIdx: number | null
|
|
deviceName: string
|
|
setDeviceName: (name: string) => void
|
|
licenseNumber: string
|
|
setLicenseNumber: (num: string) => void
|
|
onSaveAndProceed: () => void
|
|
isNextStepDisabled: boolean
|
|
hasActiveSelection: boolean
|
|
showValidationWarning?: boolean
|
|
prevStep: () => void
|
|
}
|
|
|
|
export function SummarySidebar({
|
|
visibleCategories,
|
|
selections,
|
|
products,
|
|
moduleQuantities,
|
|
billingLabel,
|
|
oneTimeTotal,
|
|
monthlyTotal,
|
|
oneTimeNet,
|
|
oneTimeTax,
|
|
oneTimeGross,
|
|
monthlyNet,
|
|
monthlyTax,
|
|
monthlyGross,
|
|
updatePriceModifier,
|
|
allCategoriesFilled,
|
|
productValidationErrors,
|
|
editingIdx,
|
|
deviceName,
|
|
setDeviceName,
|
|
licenseNumber,
|
|
setLicenseNumber,
|
|
onSaveAndProceed,
|
|
isNextStepDisabled,
|
|
prevStep,
|
|
}: SummarySidebarProps) {
|
|
const fmt = (val: number) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val)
|
|
|
|
const isEditing = editingIdx !== null
|
|
const currentDeviceTitle = isEditing
|
|
? `Kasse ${editingIdx + 1}`
|
|
: (deviceName.trim() || 'Neue Kasse')
|
|
|
|
const isSaveDisabled = !allCategoriesFilled || productValidationErrors.length > 0
|
|
|
|
return (
|
|
<Card className="glass-dark border-white/10 shadow-2xl rounded-2xl overflow-hidden flex flex-col h-full bg-slate-900/90 backdrop-blur-xl">
|
|
{/* ─── 1. TOP HEADER (shrink-0) ─── */}
|
|
<CardHeader className="shrink-0 pb-3 pt-4 px-4 border-b border-white/10 bg-slate-950/60">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<CardTitle className="text-white text-sm font-bold flex items-center gap-2">
|
|
<ShoppingCart className="w-4 h-4 text-primary" />
|
|
<span>Kassen-Konfiguration</span>
|
|
</CardTitle>
|
|
<Badge
|
|
variant="outline"
|
|
className={`text-[10px] px-2 py-0.5 font-semibold ${
|
|
isEditing
|
|
? 'border-amber-500/40 bg-amber-500/10 text-amber-400'
|
|
: 'border-primary/40 bg-primary/10 text-primary'
|
|
}`}
|
|
>
|
|
{isEditing ? `Bearbeite #${editingIdx + 1}` : 'Neuer Eintrag'}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
{/* ─── 2. SCROLLABLE MIDDLE (flex-1 min-h-0 overflow-y-auto) ─── */}
|
|
<CardContent className="p-4 flex-1 min-h-0 overflow-y-auto space-y-4 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
|
|
{/* Aktuelle Kassen-Auswahl */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between text-xs font-bold text-slate-300 uppercase tracking-wider">
|
|
<span>Gewählte Positionen</span>
|
|
</div>
|
|
|
|
{/* Selected Categories List */}
|
|
<div className="space-y-2">
|
|
{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-[11px] 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 (Pflicht)' : '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-0.5 text-xs">
|
|
<p className="text-[10px] font-semibold text-slate-400 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="pl-1.5 space-y-0.5">
|
|
<div className="flex justify-between text-white text-xs">
|
|
<span className="truncate pr-2">{prod.name} {isFree && <span className="text-[10px] text-green-400">(Inkl.)</span>}</span>
|
|
<span className="tabular-nums shrink-0 font-medium">
|
|
{fmt(actualPrice)}
|
|
</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-[11px] text-slate-400 pl-2">
|
|
<span className="truncate pr-2">+ {mod.name} {mod.has_quantity ? `(x${qty})` : ''}</span>
|
|
<span className="tabular-nums shrink-0 font-medium text-slate-300">
|
|
{fmt(mod.price * qty)}
|
|
</span>
|
|
</div>
|
|
) : null
|
|
})}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Pricing Box */}
|
|
<div className="p-3 rounded-xl bg-slate-950/80 border border-white/10 space-y-2 text-xs">
|
|
{oneTimeTotal > 0 && (
|
|
<div className="space-y-1">
|
|
<div className="flex justify-between text-slate-400">
|
|
<span>Kaufpreis Kasse (netto):</span>
|
|
<span className="tabular-nums font-mono">{fmt(oneTimeNet)}</span>
|
|
</div>
|
|
<div className="flex justify-between font-bold text-white">
|
|
<span>Kaufpreis Kasse (brutto):</span>
|
|
<span className="tabular-nums font-mono text-primary">{fmt(oneTimeGross)}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{monthlyTotal > 0 && (
|
|
<div className={`space-y-1 ${oneTimeTotal > 0 ? 'pt-2 border-t border-white/10' : ''}`}>
|
|
<div className="flex justify-between text-slate-400">
|
|
<span>Mietpreis Kasse (netto):</span>
|
|
<span className="tabular-nums font-mono">{fmt(monthlyNet)} / mtl.</span>
|
|
</div>
|
|
<div className="flex justify-between font-bold text-white">
|
|
<span>Mietpreis Kasse (brutto):</span>
|
|
<span className="tabular-nums font-mono text-primary">{fmt(monthlyGross)} / mtl.</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Gerätename & Lizenznummer Eingabe */}
|
|
<div className="pt-2 border-t border-white/10 space-y-2.5">
|
|
<p className="text-xs font-bold text-white uppercase tracking-wider">
|
|
{isEditing ? `Kasse anpassen` : `Kassenbezeichnung`}
|
|
</p>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="device-name-sidebar" className="text-[11px] text-slate-400">Kassenname (optional)</Label>
|
|
<Input
|
|
id="device-name-sidebar"
|
|
placeholder="z. B. Kasse 1, Hauptkasse, Theke"
|
|
value={deviceName}
|
|
onChange={e => setDeviceName(e.target.value)}
|
|
className="bg-white/5 border-white/10 text-white text-xs h-8 rounded-lg"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="license-number-sidebar" className="text-[11px] text-slate-400">Lizenznummer (optional)</Label>
|
|
<Input
|
|
id="license-number-sidebar"
|
|
placeholder="z. B. LIZ-12345"
|
|
value={licenseNumber}
|
|
onChange={e => setLicenseNumber(e.target.value)}
|
|
className="bg-white/5 border-white/10 text-white text-xs h-8 rounded-lg font-mono uppercase"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
|
|
{/* ─── 3. FIXED BOTTOM ACTIONS (shrink-0) ─── */}
|
|
<CardFooter className="flex flex-col gap-2 shrink-0 p-4 border-t border-white/10 bg-slate-950/90">
|
|
{/* Primary Action Button: Save & Proceed immediately to Step 4 */}
|
|
<Button
|
|
className="w-full h-11 text-xs font-bold bg-primary hover:bg-primary/90 text-white shadow-lg shadow-primary/20 gap-2"
|
|
onClick={onSaveAndProceed}
|
|
disabled={isSaveDisabled}
|
|
>
|
|
{isEditing ? (
|
|
<>
|
|
<Save className="w-4 h-4" /> Änderungen speichern & zur Übersicht <ChevronRight className="w-4 h-4 ml-0.5" />
|
|
</>
|
|
) : (
|
|
<>
|
|
<Plus className="w-4 h-4" /> Kasse zum Auftrag hinzufügen <ChevronRight className="w-4 h-4 ml-0.5" />
|
|
</>
|
|
)}
|
|
</Button>
|
|
|
|
{/* Back Button */}
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full text-slate-400 hover:text-white h-7 text-xs"
|
|
onClick={prevStep}
|
|
>
|
|
<ChevronLeft className="w-3 h-3 mr-1" /> Zurück zum Abrechnungsmodell
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
)
|
|
}
|