Files
webshop/shop/components/order-wizard.tsx
DanielS 72b906779c
All checks were successful
Staging Build / build (push) Successful in 2m55s
feat(wizard): add license lookup panel (UI mockup)
Add collapsible LicenseLookupPanel component to step 3
of the order wizard (left column). Panel queries mock
LicServer data by license key and displays product,
customer, dates, status badge and licensed modules.
Real API endpoint can replace mock in lookupLicense().

- New: components/wizard/license-lookup-panel.tsx
- Modified: components/order-wizard.tsx (4-col grid)
2026-07-14 12:57:46 +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="max-w-5xl mx-auto px-4">
{/* 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-4 gap-6 items-start">
{/* FAR LEFT: License Lookup Panel (sticky) */}
<div className="lg:col-span-1 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-1 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>
)
}