Files
webshop/shop/components/order-wizard.tsx
DanielS 2e30974aed
All checks were successful
Staging Build / build (push) Successful in 2m34s
fix(wizard): give summary sidebar equal 2/6 col width
2026-07-14 16:55:45 +02:00

845 lines
32 KiB
TypeScript

'use client'
import React, { useState, useEffect, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { AnimatePresence, motion } from 'framer-motion'
import { Product, ProductModule, Profile, EndCustomer, Order } from '@/lib/types'
import { submitOrder, updateOrder } from '@/lib/actions/orders'
import { createEndCustomer } from '@/lib/actions/end-customers'
import { Category } from '@/lib/types'
// Modular Step Components
import { ProgressStepper } from './wizard/progress-stepper'
import { ToastNotification } from './wizard/toast-notification'
import { StepCustomer } from './wizard/step-customer'
import { StepBilling } from './wizard/step-billing'
import { StepSoftware } from './wizard/step-software'
import { StepSummary } from './wizard/step-summary'
import { SummarySidebar } from './wizard/summary-sidebar'
import { LicenseLookupPanel } from './wizard/license-lookup-panel'
type CategorySelection = {
productId: string | null // selected product in this category
productIds?: string[] // selected product IDs for multi-select
moduleIds: string[] // selected module IDs for this product
}
function billingLabel(interval?: string) {
if (interval === 'one_time') return 'einmalig'
return '/ Monat'
}
function billingBadgeClass(interval?: string) {
if (interval === 'one_time') return 'bg-slate-500/20 text-slate-300 border-slate-500/30'
return 'bg-blue-500/20 text-blue-400 border-blue-500/30'
}
function toggleModuleInList(
modules: ProductModule[],
currentIds: string[],
toggleId: string
): string[] {
const isSelected = currentIds.includes(toggleId)
if (isSelected) {
let next = currentIds.filter(id => id !== toggleId)
let changed = true
while (changed) {
const beforeLength = next.length
next = next.filter(mId => {
const m = modules.find(mod => mod.id === mId)
return !m?.requirements || m.requirements.length === 0 || m.requirements.some(reqId => next.includes(reqId))
})
changed = next.length !== beforeLength
}
return next
} else {
const module = modules.find(m => m.id === toggleId)
if (module?.exclusions?.some(exId => currentIds.includes(exId))) return currentIds
return [...currentIds, toggleId]
}
}
function isModuleDisabled(module: ProductModule, selectedModules: string[]): boolean {
const requirementsMet = !module.requirements?.length ||
module.requirements.some(reqId => selectedModules.includes(reqId))
const isExcluded = module.exclusions?.some(exId => selectedModules.includes(exId))
return !requirementsMet || !!isExcluded
}
export function OrderWizard({
products,
categories,
initialProfile,
initialEndCustomers,
initialOrder,
isAdmin = false,
companies = [],
}: {
products: Product[]
categories: Category[]
initialProfile: Profile | null
initialEndCustomers: EndCustomer[]
initialOrder?: Order | null
isAdmin?: boolean
companies?: any[]
}) {
const router = useRouter()
const [step, setStep] = useState(initialOrder ? 3 : 1)
const [isSubmitting, setIsSubmitting] = useState(false)
const [selectedCompanyId, setSelectedCompanyId] = useState<string | null>(
initialOrder?.company_id ?? null
)
const [basketItems, setBasketItems] = useState<any[]>(() => {
if (!initialOrder || !initialOrder.order_data?.items) return []
const groups: Record<string, any[]> = {}
initialOrder.order_data.items.forEach((item: any) => {
const devName = item.device_name || 'Kasse 1'
if (!groups[devName]) {
groups[devName] = []
}
groups[devName].push(item)
})
return Object.entries(groups).map(([deviceName, devItems]) => {
const selections: Record<string, CategorySelection> = {}
const moduleQuantities: Record<string, number> = {}
categories.forEach(cat => {
selections[cat.id] = { productId: null, productIds: [], moduleIds: [] }
})
devItems.forEach((item: any) => {
const catId = item.category_id
if (!selections[catId]) {
selections[catId] = { productId: null, productIds: [], moduleIds: [] }
}
const cat = categories.find(c => c.id === catId)
if (cat?.allow_multiselect) {
selections[catId].productIds = [...(selections[catId].productIds || []), item.product_id]
selections[catId].productId = selections[catId].productIds[0] || null
} else {
selections[catId].productId = item.product_id
selections[catId].productIds = [item.product_id]
}
item.selected_modules?.forEach((mod: any) => {
selections[catId].moduleIds.push(mod.module_id)
moduleQuantities[mod.module_id] = mod.quantity || 1
})
})
return {
deviceName,
selections,
moduleQuantities,
billingInterval: devItems[0]?.billing_interval || 'monthly'
}
})
})
const [deviceName, setDeviceName] = useState<string>('')
const [editingIdx, setEditingIdx] = useState<number | null>(null)
const [toast, setToast] = useState<{ message: string; type: 'error' | 'success' } | null>(null)
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 5000)
return () => clearTimeout(timer)
}
}, [toast])
// Partner-Profil (Fallback)
const [customerData] = useState<Partial<Profile>>(initialProfile || {})
// Endkunden-State
const [endCustomers, setEndCustomers] = useState<EndCustomer[]>(initialEndCustomers)
const [selectedEndCustomerId, setSelectedEndCustomerId] = useState<string | null>(
initialOrder
? initialOrder.end_customer_id
: (initialEndCustomers.length > 0 ? initialEndCustomers[0].id : null)
)
const selectedEndCustomer = endCustomers.find(c => c.id === selectedEndCustomerId) ?? null
const [searchTerm, setSearchTerm] = useState('')
const [customerMode, setCustomerMode] = useState<'select' | 'create'>('select')
// Formular-State für neuen Endkunden
const [newCustomerForm, setNewCustomerForm] = useState({
company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '',
bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '',
})
const [isCreatingCustomer, setIsCreatingCustomer] = useState(false)
// Standard-Abrechnungsmodell: falls initialOrder vorhanden, dessen Intervall nehmen
const [selectedBillingInterval, setSelectedBillingInterval] = useState<'one_time' | 'monthly'>(() => {
if (initialOrder?.order_data?.items?.[0]?.billing_interval) {
return initialOrder.order_data.items[0].billing_interval as 'one_time' | 'monthly'
}
return 'one_time'
})
// Falls "one_time" (Kauf) gewählt: optionales Datum der letzten CAS-Lizenzierung
const [lastLicenseDate, setLastLicenseDate] = useState<string>(
initialOrder?.order_data?.last_license_date || ''
)
// Aktive Auswahllisten initialisieren
const [selections, setSelections] = useState<Record<string, CategorySelection>>(() => {
const initialSels: Record<string, CategorySelection> = {}
categories.forEach(cat => {
const isVisible = selectedBillingInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false
if (!isVisible || cat.allow_multiselect || cat.preselect === false) {
initialSels[cat.id] = { productId: null, productIds: [], moduleIds: [] }
} else {
const first = products.find(p => p.category_id === cat.id && (selectedBillingInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false))
initialSels[cat.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] }
}
})
return initialSels
})
const [moduleQuantities, setModuleQuantities] = useState<Record<string, number>>({})
// Filtere sichtbare Kategorien nach Abrechnungsmodell
const visibleCategories = useMemo(() => {
return categories.filter(cat => {
return selectedBillingInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false
})
}, [categories, selectedBillingInterval])
// Endkunden Filterung
const filteredEndCustomers = useMemo(() => {
const term = searchTerm.toLowerCase().trim()
return endCustomers
.filter(c => !c.is_anonymized)
.filter(c => {
if (!term) return true
return (
c.company_name?.toLowerCase().includes(term) ||
c.city?.toLowerCase().includes(term) ||
c.zip?.toLowerCase().includes(term) ||
`${c.first_name} ${c.last_name}`.toLowerCase().includes(term)
)
})
}, [endCustomers, searchTerm])
const hasActiveSelection = useMemo(() => {
return Object.values(selections).some(sel => sel.productId || (sel.productIds && sel.productIds.length > 0))
}, [selections])
// Product validation errors (requirements and exclusions)
const productValidationErrors = useMemo(() => {
const errors: string[] = []
// Check for duplicate device names
const currentName = deviceName.trim() || (editingIdx !== null ? basketItems[editingIdx]?.deviceName : `Kasse ${basketItems.length + 1}`)
const isDuplicate = basketItems.some((item, idx) => {
if (editingIdx !== null && idx === editingIdx) return false
return item.deviceName.toLowerCase().trim() === currentName.toLowerCase().trim()
})
if (isDuplicate) {
errors.push(`Gerätename "${currentName}" existiert bereits im Warenkorb. Bitte einen anderen Namen wählen.`)
}
const selectedProductIds: string[] = []
Object.values(selections).forEach(s => {
if (s.productIds && s.productIds.length > 0) {
s.productIds.forEach(id => {
if (id) selectedProductIds.push(id)
})
} else if (s.productId) {
selectedProductIds.push(s.productId)
}
})
selectedProductIds.forEach(prodId => {
const prod = products.find(p => p.id === prodId)
if (!prod) return
// Requirements check
if (prod.requirements && prod.requirements.length > 0) {
const hasMetRequirement = prod.requirements.some(reqId => selectedProductIds.includes(reqId))
if (!hasMetRequirement) {
const names = prod.requirements
.map(reqId => products.find(p => p.id === reqId)?.name || reqId)
.join(' oder ')
errors.push(`"${prod.name}" benötigt mindestens eines dieser Produkte: ${names}`)
}
}
// Exclusions check
if (prod.exclusions && prod.exclusions.length > 0) {
const conflicting = prod.exclusions.filter(exId => selectedProductIds.includes(exId))
if (conflicting.length > 0) {
const names = conflicting
.map(exId => products.find(p => p.id === exId)?.name || exId)
.join(', ')
errors.push(`"${prod.name}" schließt aus: ${names}`)
}
}
})
return errors
}, [selections, products, deviceName, basketItems, editingIdx])
// Total price calculations (split by billing interval)
const { monthlyTotal, oneTimeTotal } = useMemo(() => {
let monthly = 0
let oneTime = 0
for (const cat of categories) {
const sel = selections[cat.id]
if (!sel) continue
const selectedProds: Product[] = []
if (cat.allow_multiselect && sel.productIds && sel.productIds.length > 0) {
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)
}
const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0
const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price)
sortedProds.forEach((prod, idx) => {
const isFree = idx < freeLimit
const basePrice = isFree ? 0 : prod.base_price
if (prod.billing_interval === 'monthly') {
monthly += basePrice
} else {
oneTime += basePrice
}
// Add module prices
sel.moduleIds.forEach(mId => {
const mod = prod.modules?.find(m => m.id === mId)
if (mod) {
const qty = moduleQuantities[mId] || 1
if (prod.billing_interval === 'monthly') {
monthly += mod.price * qty
} else {
oneTime += mod.price * qty
}
}
})
})
}
return { monthlyTotal: monthly, oneTimeTotal: oneTime }
}, [selections, products, categories, moduleQuantities])
// Ermittlung der Update-Faktoren basierend auf dem Lizenzdatum (nur für Kauf/einmalig relevant)
const updatePriceModifier = useMemo(() => {
if (selectedBillingInterval !== 'one_time' || !lastLicenseDate) return { factor: 1 }
const lastDate = new Date(lastLicenseDate)
const now = new Date()
const diffTime = Math.abs(now.getTime() - lastDate.getTime())
const diffMonths = Math.ceil(diffTime / (1000 * 60 * 60 * 24 * 30))
if (diffMonths <= 12) {
return { factor: 0.15, label: 'Update innerhalb 1 Jahr (15% des Neupreises)', diffMonths }
} else if (diffMonths <= 24) {
return { factor: 0.30, label: 'Update innerhalb 2 Jahre (30% des Neupreises)', diffMonths }
} else if (diffMonths <= 36) {
return { factor: 0.50, label: 'Update innerhalb 3 Jahre (50% des Neupreises)', diffMonths }
} else {
return { factor: 1.0, label: 'Älter als 3 Jahre: Voller Neupreis', diffMonths }
}
}, [selectedBillingInterval, lastLicenseDate])
// Endbeträge ermitteln (Netto / Steuer / Brutto)
const discountFactor = updatePriceModifier.factor ?? 1
const oneTimeNet = oneTimeTotal * discountFactor
const oneTimeTax = oneTimeNet * 0.19
const oneTimeGross = oneTimeNet + oneTimeTax
const monthlyNet = monthlyTotal
const monthlyTax = monthlyNet * 0.19
const monthlyGross = monthlyNet + monthlyTax
// Only REQUIRED categories must have a product selected
const allCategoriesFilled = useMemo(() => {
return visibleCategories
.filter(cat => cat.is_required)
.every(cat => {
const sel = selections[cat.id]
if (cat.allow_multiselect) {
if (!sel?.productIds || sel.productIds.length === 0) return false
return sel.productIds.every(pId => {
const prod = products.find(p => p.id === pId)
if (!prod) return false
return selectedBillingInterval === 'one_time' ? prod.show_in_kauf !== false : prod.show_in_abo !== false
})
}
if (!sel?.productId) return false
const prod = products.find(p => p.id === sel.productId)
if (!prod) return false
return selectedBillingInterval === 'one_time'
? prod.show_in_kauf !== false
: prod.show_in_abo !== false
})
}, [visibleCategories, selections, products, selectedBillingInterval])
const isNextStepDisabled = !allCategoriesFilled || productValidationErrors.length > 0
// Helper: toggle module for a category's selected product
function toggleModule(catId: string, moduleId: string) {
setSelections(prev => {
const sel = prev[catId]
const activeProductId = sel?.productId
if (!activeProductId) return prev
const prod = products.find(p => p.id === activeProductId)
const newModuleIds = toggleModuleInList(prod?.modules ?? [], sel.moduleIds, moduleId)
return { ...prev, [catId]: { ...sel, moduleIds: newModuleIds } }
})
setModuleQuantities(prev => {
if (prev[moduleId] !== undefined) {
const { [moduleId]: _, ...rest } = prev
return rest
}
return { ...prev, [moduleId]: 1 }
})
}
const nextStep = () => {
if (step === 3 && allCategoriesFilled && productValidationErrors.length === 0) {
if (hasActiveSelection) {
addToBasket()
}
}
setStep(s => s + 1)
}
const prevStep = () => {
setStep(s => s - 1)
}
function handleBillingIntervalChange(val: 'one_time' | 'monthly') {
setSelectedBillingInterval(val)
if (val === 'monthly') {
setLastLicenseDate('')
}
const newSelections: Record<string, CategorySelection> = {}
categories.forEach(cat => {
const isVisible = val === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false
if (!isVisible || cat.allow_multiselect || cat.preselect === false) {
newSelections[cat.id] = { productId: null, productIds: [], moduleIds: [] }
} else {
const matchingProd = products.find(p =>
p.category_id === cat.id &&
(val === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false)
)
newSelections[cat.id] = {
productId: matchingProd?.id ?? null,
productIds: matchingProd ? [matchingProd.id] : [],
moduleIds: []
}
}
})
setSelections(newSelections)
}
// Neuen Endkunden inline anlegen
const handleCreateCustomer = async () => {
if (!newCustomerForm.company_name.trim()) return
setIsCreatingCustomer(true)
try {
const created = await createEndCustomer(newCustomerForm, selectedCompanyId || undefined)
setEndCustomers(prev => [...prev, created])
setSelectedEndCustomerId(created.id)
setCustomerMode('select')
setNewCustomerForm({
company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '',
bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '',
})
} catch (e: any) {
setToast({ message: `Fehler beim Anlegen des Kunden: ${e.message || 'Unbekannter Fehler'}`, type: 'error' })
} finally {
setIsCreatingCustomer(false)
}
}
const addToBasket = () => {
const currentItem = {
deviceName: deviceName || (editingIdx !== null ? basketItems[editingIdx]?.deviceName : `Kasse ${basketItems.length + 1}`),
selections: JSON.parse(JSON.stringify(selections)),
moduleQuantities: { ...moduleQuantities },
billingInterval: selectedBillingInterval,
}
if (editingIdx !== null) {
setBasketItems(prev => {
const next = [...prev]
next[editingIdx] = currentItem
return next
})
setEditingIdx(null)
} else {
setBasketItems(prev => [...prev, currentItem])
}
// Reset current form config
setDeviceName('')
setModuleQuantities({})
const resetSels: Record<string, CategorySelection> = {}
categories.forEach(cat => {
const isVisible = selectedBillingInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false
if (!isVisible || cat.allow_multiselect || cat.preselect === false) {
resetSels[cat.id] = { productId: null, productIds: [], moduleIds: [] }
} else {
const first = products.find(p => p.category_id === cat.id && (selectedBillingInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false))
resetSels[cat.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] }
}
})
setSelections(resetSels)
}
const editBasketItem = (idx: number) => {
const item = basketItems[idx]
if (!item) return
setEditingIdx(idx)
setSelections(item.selections)
setModuleQuantities(item.moduleQuantities)
setDeviceName(item.deviceName)
setSelectedBillingInterval(item.billingInterval)
}
const deleteBasketItem = (idx: number) => {
if (editingIdx === idx) {
setEditingIdx(null)
setModuleQuantities({})
setDeviceName('')
const resetSels: Record<string, CategorySelection> = {}
categories.forEach(cat => {
const isVisible = selectedBillingInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false
if (!isVisible || cat.allow_multiselect || cat.preselect === false) {
resetSels[cat.id] = { productId: null, productIds: [], moduleIds: [] }
} else {
const first = products.find(p => p.category_id === cat.id && (selectedBillingInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false))
resetSels[cat.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] }
}
})
setSelections(resetSels)
} else if (editingIdx !== null && idx < editingIdx) {
setEditingIdx(editingIdx - 1)
}
setBasketItems(prev => prev.filter((_, i) => i !== idx))
}
const handleSubmit = async () => {
if (isSubmitting) return
setIsSubmitting(true)
try {
let finalItems = [...basketItems]
if (hasActiveSelection && allCategoriesFilled && productValidationErrors.length === 0) {
const currentItem = {
deviceName: deviceName || (editingIdx !== null ? basketItems[editingIdx]?.deviceName : `Kasse ${basketItems.length + 1}`),
selections: JSON.parse(JSON.stringify(selections)),
moduleQuantities: { ...moduleQuantities },
billingInterval: selectedBillingInterval,
}
if (editingIdx !== null) {
finalItems[editingIdx] = currentItem
} else {
finalItems.push(currentItem)
}
}
if (finalItems.length === 0) {
throw new Error('Bitte konfigurieren Sie mindestens eine Kasse.')
}
const res = await fetch('/api/orders/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: finalItems,
customerProfile: customerData,
endCustomerId: selectedEndCustomerId,
endCustomer: selectedEndCustomer,
})
})
const result = await res.json()
if (result.error) throw new Error(result.error)
const firstOrderId = result.orders?.[0]?.id
if (firstOrderId) {
router.push(`/order/success?id=${firstOrderId}`)
} else {
router.push('/my-orders')
}
} catch (error: any) {
console.error(error)
setToast({ message: error?.message || 'Fehler bei der Bestellung. Bitte versuchen Sie es erneut.', type: 'error' })
setIsSubmitting(false)
}
}
const finalItemsToShow = basketItems.length > 0
? basketItems
: (allCategoriesFilled && productValidationErrors.length === 0
? [{ deviceName: deviceName || 'Kasse 1', selections, moduleQuantities, billingInterval: selectedBillingInterval }]
: [])
function selectProduct(catId: string, productId: string) {
setSelections(prev => {
const cat = categories.find(c => c.id === catId)
// Build updated selection for this category
let updatedCatSel: CategorySelection
if (cat?.allow_multiselect) {
const currentIds = prev[catId]?.productIds || []
const nextIds = currentIds.includes(productId)
? currentIds.filter(id => id !== productId)
: [...currentIds, productId]
updatedCatSel = {
productId: nextIds[0] || null,
productIds: nextIds,
moduleIds: prev[catId]?.moduleIds || []
}
} else {
const isCurrentlyChecked = prev[catId]?.productId === productId
const nextId = isCurrentlyChecked ? null : productId
updatedCatSel = {
productId: nextId,
productIds: nextId ? [nextId] : [],
moduleIds: []
}
}
// If this category resets others, rebuild all other categories to their initial state
if (cat?.resets_others) {
const resetSels: Record<string, CategorySelection> = {}
categories.forEach(c => {
if (c.id === catId) {
resetSels[c.id] = updatedCatSel
} else {
const isVisible = selectedBillingInterval === 'one_time' ? c.show_in_kauf !== false : c.show_in_abo !== false
if (!isVisible || c.allow_multiselect || c.preselect === false) {
resetSels[c.id] = { productId: null, productIds: [], moduleIds: [] }
} else {
const first = products.find(p =>
p.category_id === c.id &&
(selectedBillingInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false)
)
resetSels[c.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] }
}
}
})
// Also reset module quantities for other categories
setModuleQuantities({})
return resetSels
}
return { ...prev, [catId]: updatedCatSel }
})
}
function isProductDisabled(product: Product, catId: string): boolean {
const otherSelectedProductIds: string[] = []
Object.entries(selections).forEach(([cId, sel]) => {
if (cId === catId) return
if (sel.productIds && sel.productIds.length > 0) {
otherSelectedProductIds.push(...sel.productIds)
} else if (sel.productId) {
otherSelectedProductIds.push(sel.productId)
}
})
const hasExclusions = product.exclusions?.some(exId => otherSelectedProductIds.includes(exId))
const meetsRequirements = !product.requirements?.length ||
product.requirements.some(reqId => otherSelectedProductIds.includes(reqId))
return !!hasExclusions || !meetsRequirements
}
return (
<div className={`mx-auto px-4 ${step === 3 ? 'max-w-screen-2xl' : 'max-w-5xl'}`}>
{/* Global Stepper for Steps 1, 2, 4 */}
{step !== 3 && (
<div className="pt-12 pb-6">
<ProgressStepper step={step} basketItemsCount={basketItems.length} />
</div>
)}
<AnimatePresence mode="wait">
{/* Step 1: Customer */}
{step === 1 && (
<motion.div
key="step1"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-6"
>
<StepCustomer
isAdmin={isAdmin}
companies={companies}
selectedCompanyId={selectedCompanyId}
setSelectedCompanyId={setSelectedCompanyId}
customerMode={customerMode}
setCustomerMode={setCustomerMode}
endCustomers={endCustomers}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
filteredEndCustomers={filteredEndCustomers}
selectedEndCustomerId={selectedEndCustomerId}
setSelectedEndCustomerId={setSelectedEndCustomerId}
newCustomerForm={newCustomerForm}
setNewCustomerForm={setNewCustomerForm}
handleCreateCustomer={handleCreateCustomer}
isCreatingCustomer={isCreatingCustomer}
nextStep={nextStep}
/>
</motion.div>
)}
{/* Step 2: Billing Model */}
{step === 2 && (
<motion.div
key="step2"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-6"
>
<StepBilling
selectedBillingInterval={selectedBillingInterval}
handleBillingIntervalChange={handleBillingIntervalChange}
customerMode={customerMode}
selectedEndCustomerId={selectedEndCustomerId}
lastLicenseDate={lastLicenseDate}
setLastLicenseDate={setLastLicenseDate}
prevStep={prevStep}
nextStep={nextStep}
/>
</motion.div>
)}
{/* Step 3: Software Selector */}
{step === 3 && (
<motion.div
key="step3"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
>
<div className="grid grid-cols-1 lg:grid-cols-6 gap-6 items-start">
{/* FAR LEFT: License Lookup Panel (sticky) */}
<div className="lg:col-span-2 sticky top-6">
<LicenseLookupPanel />
</div>
{/* CENTER: Sticky stepper + scrollable categories */}
<div className="lg:col-span-2 space-y-0">
{/* Sticky stepper header — only covers center column */}
<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">
<p className="text-center text-slate-400 text-sm mb-4">Schritt 3 von 4 Software konfigurieren</p>
<ProgressStepper step={step} basketItemsCount={basketItems.length} />
</div>
{/* Scrollable category content */}
<StepSoftware
visibleCategories={visibleCategories}
products={products}
selections={selections}
selectProduct={selectProduct}
isProductDisabled={isProductDisabled}
isModuleDisabled={isModuleDisabled}
toggleModule={toggleModule}
moduleQuantities={moduleQuantities}
setModuleQuantities={setModuleQuantities}
selectedBillingInterval={selectedBillingInterval}
billingLabel={billingLabel}
billingBadgeClass={billingBadgeClass}
/>
</div>
{/* RIGHT: Summary sidebar (sticky) */}
<div className="lg:col-span-2 sticky top-6">
<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>
</div>
</motion.div>
)}
{/* Step 4: Verification & Submit */}
{step === 4 && (
<motion.div
key="step4"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="space-y-6 text-center"
>
<StepSummary
finalItemsToShow={finalItemsToShow}
visibleCategories={visibleCategories}
products={products}
oneTimeTotal={oneTimeTotal}
monthlyTotal={monthlyTotal}
oneTimeNet={oneTimeNet}
oneTimeTax={oneTimeTax}
oneTimeGross={oneTimeGross}
monthlyNet={monthlyNet}
monthlyTax={monthlyTax}
monthlyGross={monthlyGross}
updatePriceModifier={updatePriceModifier}
selectedEndCustomer={selectedEndCustomer}
customerData={customerData}
lastLicenseDate={lastLicenseDate}
handleSubmit={handleSubmit}
isSubmitting={isSubmitting}
initialOrder={initialOrder}
prevStep={prevStep}
/>
</motion.div>
)}
</AnimatePresence>
{/* Toast Notification Container */}
<ToastNotification toast={toast} onClose={() => setToast(null)} />
</div>
)
}