style: limit step 3 sidebar scroll to basket items
All checks were successful
Staging Build / build (push) Successful in 2m59s
All checks were successful
Staging Build / build (push) Successful in 2m59s
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user