style: limit step 3 sidebar scroll to basket items
All checks were successful
Staging Build / build (push) Successful in 2m59s

This commit is contained in:
DanielS
2026-08-11 00:28:13 +02:00
parent 3ba7c91e1f
commit 73e046e959
2 changed files with 133 additions and 127 deletions

View File

@@ -1064,7 +1064,7 @@ export function OrderWizard({
<ProgressStepper step={step} basketItemsCount={basketItems.length} /> <ProgressStepper step={step} basketItemsCount={basketItems.length} />
</div> </div>
<div className="flex-1 overflow-y-auto pr-1"> <div className="flex-1 overflow-hidden">
<SummarySidebar <SummarySidebar
visibleCategories={visibleCategories} visibleCategories={visibleCategories}
selections={selections} selections={selections}

View File

@@ -73,164 +73,170 @@ export function SummarySidebar({
prevStep, prevStep,
}: SummarySidebarProps) { }: SummarySidebarProps) {
return ( return (
<div className="sticky top-6"> <div className="h-full flex flex-col">
<Card className="bg-[oklch(0.145_0.01_148)]/90 backdrop-blur-md border border-slate-800 shadow-[0_0_30px_rgba(0,0,0,0.5)] rounded-2xl overflow-hidden flex flex-col max-h-[80vh]"> <Card className="bg-[oklch(0.145_0.01_148)]/90 backdrop-blur-md border border-slate-800 shadow-[0_0_30px_rgba(0,0,0,0.5)] rounded-2xl overflow-hidden flex flex-col h-full">
<CardHeader className="shrink-0"> <CardHeader className="shrink-0">
<CardTitle className="text-white">Zusammenfassung</CardTitle> <CardTitle className="text-white">Zusammenfassung</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 flex-1 overflow-y-auto subpixel-antialiased scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent"> <CardContent className="p-5 flex-1 flex flex-col overflow-hidden space-y-4">
{visibleCategories.map(cat => {
const sel = selections[cat.id] {/* Active Selections & Price Calculation (Fixed) */}
const selectedProds: Product[] = [] <div className="space-y-4 shrink-0 overflow-y-auto max-h-[40%] pr-1 scrollbar-thin">
if (cat.allow_multiselect && sel?.productIds) { {visibleCategories.map(cat => {
sel.productIds.forEach(pId => { const sel = selections[cat.id]
const p = products.find(prod => prod.id === pId) 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 (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 ( if (selectedProds.length === 0) return (
<div key={cat.id} className="text-xs text-slate-500 italic flex items-center gap-1"> <div key={cat.id} className="text-xs text-slate-500 italic flex items-center gap-1">
{cat.is_required ? ( {cat.is_required ? (
<AlertCircle className="w-3 h-3 text-destructive" /> <AlertCircle className="w-3 h-3 text-destructive" />
) : ( ) : (
<span className="w-3 h-3 inline-block" /> <span className="w-3 h-3 inline-block" />
)} )}
{cat.name}: {cat.is_required ? 'nicht gewählt' : 'nicht gewählt (optional)'} {cat.name}: {cat.is_required ? 'nicht gewählt' : 'nicht gewählt (optional)'}
</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">
<div className="flex justify-between text-sm">
<span className="text-slate-400 font-medium">{cat.name}</span>
</div> </div>
{sortedProds.map((prod, idx) => { )
const isFree = idx < freeLimit
const actualPrice = isFree ? 0 : prod.base_price const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price)
return ( const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0
<div key={prod.id} className="space-y-0.5 pl-2">
<div className="flex justify-between text-sm"> return (
<span className="text-white">{prod.name} {isFree && <span className="text-[10px] text-green-400">(Frei)</span>}</span> <div key={cat.id} className="space-y-1">
<span className="text-white"> <div className="flex justify-between text-sm">
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(actualPrice)} <span className="text-slate-400 font-medium">{cat.name}</span>
{' '}<span className="text-slate-400 text-xs">{billingLabel(prod.billing_interval)}</span> </div>
</span> {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">
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(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">
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * qty)}
</span>
</div>
) : null
})}
</div> </div>
{sel.moduleIds.map(mId => { )
const mod = prod.modules?.find(m => m.id === mId) })}
const qty = moduleQuantities[mId] || 1 </div>
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> <Separator className="bg-white/10" />
<span className="text-slate-300"> {oneTimeTotal > 0 && monthlyTotal > 0 ? (
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * qty)} <div className="space-y-3">
</span> <div className="space-y-1">
</div> <p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Einmalig:</p>
) : null <div className="flex justify-between text-xs text-slate-400">
})} <span>Netto:</span>
</div> <span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)}</span>
) </div>
})} <div className="flex justify-between text-xs 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-sm font-bold text-white">
<span>Gesamt (brutto):</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}</span>
</div>
</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">Monatlich:</p>
<div className="flex justify-between text-xs 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-xs 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-sm font-bold text-white">
<span>Gesamt (brutto):</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl.</span>
</div>
</div>
</div> </div>
) ) : oneTimeTotal > 0 ? (
})}
<Separator className="bg-white/10" />
{oneTimeTotal > 0 && monthlyTotal > 0 ? (
<div className="space-y-3">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Einmalig:</p>
<div className="flex justify-between text-xs text-slate-400"> <div className="flex justify-between text-xs text-slate-400">
<span>Netto:</span> <span>Netto-Gesamtbetrag:</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)}</span> <span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)}</span>
</div> </div>
<div className="flex justify-between text-xs text-slate-400"> <div className="flex justify-between text-xs text-slate-400">
<span>zzgl. 19% MwSt.:</span> <span>zzgl. 19% MwSt.:</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)}</span> <span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)}</span>
</div> </div>
<div className="flex justify-between text-sm font-bold text-white"> <div className="flex justify-between text-base font-bold text-blue-500 dark:text-blue-400">
<span>Gesamt (brutto):</span> <span>Gesamtbetrag (brutto):</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}</span> <span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}</span>
</div> </div>
</div> </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">Monatlich:</p> <div className="space-y-1">
<div className="flex justify-between text-xs text-slate-400"> <div className="flex justify-between text-xs text-slate-400">
<span>Netto:</span> <span>Netto-Gesamtbetrag:</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl.</span> <span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl.</span>
</div> </div>
<div className="flex justify-between text-xs text-slate-400"> <div className="flex justify-between text-xs text-slate-400">
<span>zzgl. 19% MwSt.:</span> <span>zzgl. 19% MwSt.:</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl.</span> <span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl.</span>
</div> </div>
<div className="flex justify-between text-sm font-bold text-white"> <div className="flex justify-between text-base font-bold text-blue-500 dark:text-blue-400">
<span>Gesamt (brutto):</span> <span>Gesamtbetrag (brutto):</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl.</span> <span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl.</span>
</div> </div>
</div> </div>
</div> )}
) : oneTimeTotal > 0 ? ( {updatePriceModifier.label && (
<div className="space-y-1"> <div className="text-xs text-green-400 bg-green-500/10 p-2.5 rounded-lg border border-green-500/20 space-y-1">
<div className="flex justify-between text-xs text-slate-400"> <p className="font-semibold flex items-center gap-1">
<span>Netto-Gesamtbetrag:</span> <Check className="w-3.5 h-3.5" />
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)}</span> Update-Rabatt aktiv
</p>
<p>{updatePriceModifier.label}</p>
</div> </div>
<div className="flex justify-between text-xs text-slate-400"> )}
<span>zzgl. 19% MwSt.:</span> {!allCategoriesFilled && (
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)}</span> <p className="text-xs text-destructive flex items-center gap-1">
</div> <AlertCircle className="w-3 h-3" />
<div className="flex justify-between text-base font-bold text-blue-500 dark:text-blue-400"> Bitte aus jeder Pflichtkategorie einen Artikel wählen.
<span>Gesamtbetrag (brutto):</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}</span>
</div>
</div>
) : (
<div className="space-y-1">
<div className="flex justify-between text-xs 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-xs 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-blue-500 dark:text-blue-400">
<span>Gesamtbetrag (brutto):</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(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>
<p>{updatePriceModifier.label}</p> )}
</div> {productValidationErrors.map((err, errIdx) => (
)} <p key={errIdx} className="text-xs text-destructive flex items-center gap-1">
{!allCategoriesFilled && ( <AlertCircle className="w-3 h-3 text-destructive" />
<p className="text-xs text-destructive flex items-center gap-1"> {err}
<AlertCircle className="w-3 h-3" /> </p>
Bitte aus jeder Pflichtkategorie einen Artikel wählen. ))}
</p> </div>
)}
{productValidationErrors.map((err, errIdx) => ( {/* Warenkorb List (Scrollable) */}
<p key={errIdx} className="text-xs text-destructive flex items-center gap-1">
<AlertCircle className="w-3 h-3 text-destructive" />
{err}
</p>
))}
{basketItems.length > 0 && ( {basketItems.length > 0 && (
<div className="pt-4 border-t border-white/10 space-y-2"> <div className="flex-1 flex flex-col min-h-0 pt-3 border-t border-white/10 space-y-2 overflow-hidden">
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Warenkorb ({basketItems.length}):</p> <p className="text-xs font-semibold text-slate-500 uppercase tracking-wider shrink-0">Warenkorb ({basketItems.length}):</p>
<div className="space-y-1.5"> <div className="flex-1 overflow-y-auto pr-1 space-y-1.5 subpixel-antialiased scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent">
{basketItems.map((item, idx) => ( {basketItems.map((item, idx) => (
<div key={idx} className={`flex justify-between items-center text-xs bg-white/5 p-2 rounded border transition-all duration-300 ${idx === editingIdx ? 'border-blue-500/50 shadow-[0_0_10px_rgba(59,130,246,0.3)]' : 'border-white/10'}`}> <div key={idx} className={`flex justify-between items-center text-xs bg-white/5 p-2 rounded border transition-all duration-300 ${idx === editingIdx ? 'border-blue-500/50 shadow-[0_0_10px_rgba(59,130,246,0.3)]' : 'border-white/10'}`}>
<div className="flex flex-col"> <div className="flex flex-col">