feat: split step-3 into independent left/right grid columns with sticky local stepper
All checks were successful
Staging Build / build (push) Successful in 2m51s

This commit is contained in:
DanielS
2026-07-10 16:10:25 +02:00
parent d37a6b4348
commit 0b4755d438
3 changed files with 81 additions and 150 deletions

View File

@@ -15,6 +15,7 @@ import { StepCustomer } from './wizard/step-customer'
import { StepBilling } from './wizard/step-billing' import { StepBilling } from './wizard/step-billing'
import { StepSoftware } from './wizard/step-software' import { StepSoftware } from './wizard/step-software'
import { StepSummary } from './wizard/step-summary' import { StepSummary } from './wizard/step-summary'
import { SummarySidebar } from './wizard/summary-sidebar'
type CategorySelection = { type CategorySelection = {
productId: string | null // selected product in this category productId: string | null // selected product in this category
@@ -664,16 +665,9 @@ export function OrderWizard({
return ( return (
<div className="max-w-5xl mx-auto px-4"> <div className="max-w-5xl mx-auto px-4">
{/* Sticky Header — Stepper für Step 3, normaler Stepper für andere Steps */} {/* Global Stepper for Steps 1, 2, 4 */}
{step === 3 ? ( {step !== 3 && (
<div className="sticky top-0 z-30 w-full bg-slate-950/80 backdrop-blur-md pt-6 pb-4 border-b border-white/5 mb-6"> <div className="pt-12 pb-6">
<div className="text-center mb-4">
<p className="text-slate-400 text-sm">Schritt 3 von 4 Software konfigurieren</p>
</div>
<ProgressStepper step={step} basketItemsCount={basketItems.length} />
</div>
) : (
<div className="pt-12">
<ProgressStepper step={step} basketItemsCount={basketItems.length} /> <ProgressStepper step={step} basketItemsCount={basketItems.length} />
</div> </div>
)} )}
@@ -739,44 +733,66 @@ export function OrderWizard({
initial={{ opacity: 0, x: 20 }} initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }} exit={{ opacity: 0, x: -20 }}
className="space-y-6"
> >
<StepSoftware <div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-start">
visibleCategories={visibleCategories} {/* LEFT: Sticky header (stepper) + scrollable categories */}
products={products} <div className="lg:col-span-2 space-y-0">
selections={selections} {/* Sticky stepper header — only covers left column */}
selectProduct={selectProduct} <div className="sticky top-0 z-20 bg-slate-950/80 backdrop-blur-md pb-4 pt-6 border-b border-white/5 mb-6">
isProductDisabled={isProductDisabled} <p className="text-center text-slate-400 text-sm mb-4">Schritt 3 von 4 Software konfigurieren</p>
isModuleDisabled={isModuleDisabled} <ProgressStepper step={step} basketItemsCount={basketItems.length} />
toggleModule={toggleModule} </div>
moduleQuantities={moduleQuantities}
setModuleQuantities={setModuleQuantities} {/* Scrollable category content */}
selectedBillingInterval={selectedBillingInterval} <StepSoftware
billingLabel={billingLabel} visibleCategories={visibleCategories}
billingBadgeClass={billingBadgeClass} products={products}
oneTimeTotal={oneTimeTotal} selections={selections}
monthlyTotal={monthlyTotal} selectProduct={selectProduct}
oneTimeNet={oneTimeNet} isProductDisabled={isProductDisabled}
oneTimeTax={oneTimeTax} isModuleDisabled={isModuleDisabled}
oneTimeGross={oneTimeGross} toggleModule={toggleModule}
monthlyNet={monthlyNet} moduleQuantities={moduleQuantities}
monthlyTax={monthlyTax} setModuleQuantities={setModuleQuantities}
monthlyGross={monthlyGross} selectedBillingInterval={selectedBillingInterval}
updatePriceModifier={updatePriceModifier} billingLabel={billingLabel}
allCategoriesFilled={allCategoriesFilled} billingBadgeClass={billingBadgeClass}
productValidationErrors={productValidationErrors} />
basketItems={basketItems} </div>
editingIdx={editingIdx}
editBasketItem={editBasketItem} {/* RIGHT: Independent full-height sidebar column */}
deleteBasketItem={deleteBasketItem} <div className="lg:col-span-1 sticky top-6">
deviceName={deviceName} <SummarySidebar
setDeviceName={setDeviceName} visibleCategories={visibleCategories}
addToBasket={addToBasket} selections={selections}
isNextStepDisabled={isNextStepDisabled} products={products}
hasActiveSelection={hasActiveSelection} moduleQuantities={moduleQuantities}
nextStep={nextStep} billingLabel={billingLabel}
prevStep={prevStep} oneTimeTotal={oneTimeTotal}
/> monthlyTotal={monthlyTotal}
oneTimeNet={oneTimeNet}
oneTimeTax={oneTimeTax}
oneTimeGross={oneTimeGross}
monthlyNet={monthlyNet}
monthlyTax={monthlyTax}
monthlyGross={monthlyGross}
updatePriceModifier={updatePriceModifier}
allCategoriesFilled={allCategoriesFilled}
productValidationErrors={productValidationErrors}
basketItems={basketItems}
editingIdx={editingIdx}
editBasketItem={editBasketItem}
deleteBasketItem={deleteBasketItem}
deviceName={deviceName}
setDeviceName={setDeviceName}
addToBasket={addToBasket}
isNextStepDisabled={isNextStepDisabled}
hasActiveSelection={hasActiveSelection}
nextStep={nextStep}
prevStep={prevStep}
/>
</div>
</div>
</motion.div> </motion.div>
)} )}

View File

@@ -11,7 +11,6 @@ import { Badge } from '@/components/ui/badge'
import { ShoppingCart, Check, AlertCircle } from 'lucide-react' import { ShoppingCart, Check, AlertCircle } from 'lucide-react'
import * as Icons from 'lucide-react' import * as Icons from 'lucide-react'
import { Category, Product, CategorySelection } from '@/lib/types' import { Category, Product, CategorySelection } from '@/lib/types'
import { SummarySidebar } from './summary-sidebar'
interface StepSoftwareProps { interface StepSoftwareProps {
visibleCategories: Category[] visibleCategories: Category[]
@@ -26,30 +25,6 @@ interface StepSoftwareProps {
selectedBillingInterval: 'one_time' | 'monthly' selectedBillingInterval: 'one_time' | 'monthly'
billingLabel: (interval: string) => string billingLabel: (interval: string) => string
billingBadgeClass: (interval: string) => string billingBadgeClass: (interval: string) => string
// Sidebar props
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
addToBasket: () => void
isNextStepDisabled: boolean
hasActiveSelection: boolean
nextStep: () => void
prevStep: () => void
} }
function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) { function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) {
@@ -71,44 +46,19 @@ export function StepSoftware({
selectedBillingInterval, selectedBillingInterval,
billingLabel, billingLabel,
billingBadgeClass, billingBadgeClass,
oneTimeTotal,
monthlyTotal,
oneTimeNet,
oneTimeTax,
oneTimeGross,
monthlyNet,
monthlyTax,
monthlyGross,
updatePriceModifier,
allCategoriesFilled,
productValidationErrors,
basketItems,
editingIdx,
editBasketItem,
deleteBasketItem,
deviceName,
setDeviceName,
addToBasket,
isNextStepDisabled,
hasActiveSelection,
nextStep,
prevStep,
}: StepSoftwareProps) { }: StepSoftwareProps) {
return ( return (
<div className="grid md:grid-cols-3 gap-6"> <Card className="glass-dark border-white/10">
{/* Left: Category sections */} <CardHeader>
<div className="md:col-span-2 space-y-8"> <CardTitle className="text-2xl flex items-center gap-2 text-white">
<Card className="glass-dark border-white/10"> <ShoppingCart className="w-6 h-6 text-blue-400" />
<CardHeader> Software wählen
<CardTitle className="text-2xl flex items-center gap-2 text-white"> </CardTitle>
<ShoppingCart className="w-6 h-6 text-primary" /> <CardDescription className="text-slate-300">
Software wählen Wählen Sie pro Kategorie mindestens einen Artikel aus.
</CardTitle> </CardDescription>
<CardDescription className="text-slate-300"> </CardHeader>
Wählen Sie pro Kategorie mindestens einen Artikel aus. <CardContent className="space-y-8">
</CardDescription>
</CardHeader>
<CardContent className="space-y-8">
{visibleCategories.map((cat, idx) => { {visibleCategories.map((cat, idx) => {
const catProducts = products.filter(p => { const catProducts = products.filter(p => {
if (p.category_id !== cat.id) return false if (p.category_id !== cat.id) return false
@@ -339,40 +289,7 @@ export function StepSoftware({
</div> </div>
) )
})} })}
</CardContent> </CardContent>
</Card> </Card>
</div>
{/* Right: Summary sidebar */}
<SummarySidebar
visibleCategories={visibleCategories}
selections={selections}
products={products}
moduleQuantities={moduleQuantities}
billingLabel={billingLabel}
oneTimeTotal={oneTimeTotal}
monthlyTotal={monthlyTotal}
oneTimeNet={oneTimeNet}
oneTimeTax={oneTimeTax}
oneTimeGross={oneTimeGross}
monthlyNet={monthlyNet}
monthlyTax={monthlyTax}
monthlyGross={monthlyGross}
updatePriceModifier={updatePriceModifier}
allCategoriesFilled={allCategoriesFilled}
productValidationErrors={productValidationErrors}
basketItems={basketItems}
editingIdx={editingIdx}
editBasketItem={editBasketItem}
deleteBasketItem={deleteBasketItem}
deviceName={deviceName}
setDeviceName={setDeviceName}
addToBasket={addToBasket}
isNextStepDisabled={isNextStepDisabled}
hasActiveSelection={hasActiveSelection}
nextStep={nextStep}
prevStep={prevStep}
/>
</div>
) )
} }

View File

@@ -69,11 +69,10 @@ export function SummarySidebar({
prevStep, prevStep,
}: SummarySidebarProps) { }: SummarySidebarProps) {
return ( return (
<div className="sticky top-32 max-h-[70vh] flex flex-col"> <Card className="glass-dark border-primary/20 backdrop-blur-lg bg-slate-950/75 flex flex-col max-h-[80vh]">
<Card className="glass-dark border-primary/20 backdrop-blur-lg bg-slate-950/75 flex flex-col max-h-[70vh]"> <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="space-y-4 flex-1 overflow-y-auto subpixel-antialiased scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent">
{visibleCategories.map(cat => { {visibleCategories.map(cat => {
const sel = selections[cat.id] const sel = selections[cat.id]
@@ -293,7 +292,6 @@ export function SummarySidebar({
Zurück zum Abrechnungsmodell Zurück zum Abrechnungsmodell
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
</div>
) )
} }