1112 lines
43 KiB
TypeScript
1112 lines
43 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 { isLicenseNumberTaken } from '@/lib/actions/queries'
|
||
import { Category } from '@/lib/types'
|
||
import { Check } from 'lucide-react'
|
||
|
||
// 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, CategoryIcon } 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 = [],
|
||
upgradeMode = false,
|
||
initialEndCustomerId = null,
|
||
lockedDeviceId = null,
|
||
}: {
|
||
products: Product[]
|
||
categories: Category[]
|
||
initialProfile: Profile | null
|
||
initialEndCustomers: EndCustomer[]
|
||
initialOrder?: Order | null
|
||
isAdmin?: boolean
|
||
companies?: any[]
|
||
upgradeMode?: boolean
|
||
initialEndCustomerId?: string | null
|
||
lockedDeviceId?: string | null
|
||
}) {
|
||
const router = useRouter()
|
||
// Upgrade-Mode: starte direkt bei Schritt 3 (Software)
|
||
const [step, setStep] = useState(upgradeMode ? 3 : (initialOrder ? 3 : 1))
|
||
const [direction, setDirection] = useState(1)
|
||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||
const [selectedCompanyId, setSelectedCompanyId] = useState<string | null>(
|
||
initialOrder?.company_id ?? (isAdmin ? 'all' : 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'
|
||
}
|
||
})
|
||
})
|
||
// Upgrade-Mode: Ermittle bereits lizenzierte Modul-IDs für die Ziel-Kasse
|
||
const existingModuleIds = useMemo<string[]>(() => {
|
||
if (!upgradeMode || !initialOrder || !lockedDeviceId) return []
|
||
const items = initialOrder.order_data?.items || []
|
||
const deviceItems = items.filter(
|
||
(item: any) => (item.device_name || 'Kasse 1') === lockedDeviceId
|
||
)
|
||
const moduleIds: string[] = []
|
||
deviceItems.forEach((item: any) => {
|
||
item.selected_modules?.forEach((mod: any) => {
|
||
if (mod.module_id) moduleIds.push(mod.module_id)
|
||
})
|
||
})
|
||
return moduleIds
|
||
}, [upgradeMode, initialOrder, lockedDeviceId])
|
||
|
||
const [deviceName, setDeviceName] = useState<string>(() => {
|
||
if (upgradeMode && lockedDeviceId) return `${lockedDeviceId} – Upgrade`
|
||
return ''
|
||
})
|
||
const [licenseNumber, setLicenseNumber] = useState<string>('')
|
||
const [editingIdx, setEditingIdx] = useState<number | null>(null)
|
||
const [orderNotes, setOrderNotes] = useState<string>('')
|
||
const [toast, setToast] = useState<{ message: string; type: 'error' | 'success' } | null>(null)
|
||
const [activeCategoryId, setActiveCategoryId] = useState<string | 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>(() => {
|
||
// Upgrade-Mode: Kundenauswahl aus URL-Param
|
||
if (upgradeMode && initialEndCustomerId) return initialEndCustomerId
|
||
if (initialOrder) return initialOrder.end_customer_id
|
||
return 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 CASPOS-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])
|
||
|
||
useEffect(() => {
|
||
if (visibleCategories.length > 0 && !visibleCategories.some(c => c.id === activeCategoryId)) {
|
||
setActiveCategoryId(visibleCategories[0].id)
|
||
}
|
||
}, [visibleCategories, activeCategoryId])
|
||
|
||
// 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 finalItemsToShow = basketItems.length > 0
|
||
? basketItems
|
||
: (allCategoriesFilled && productValidationErrors.length === 0
|
||
? [{ deviceName: deviceName || 'Kasse 1', selections, moduleQuantities, billingInterval: selectedBillingInterval }]
|
||
: [])
|
||
|
||
// Gesamtsummen für alle Items in der Bestellung (finalItemsToShow)
|
||
const { overallMonthlyTotal, overallOneTimeTotal, linkedFeeProducts } = useMemo(() => {
|
||
let monthly = 0
|
||
let oneTime = 0
|
||
const feeProductIds = new Set<string>()
|
||
|
||
finalItemsToShow.forEach(item => {
|
||
categories.forEach(cat => {
|
||
const sel = item.selections[cat.id]
|
||
if (!sel) return
|
||
|
||
const selectedProds: Product[] = []
|
||
if (cat.allow_multiselect && sel.productIds && sel.productIds.length > 0) {
|
||
sel.productIds.forEach((pId: string) => {
|
||
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
|
||
}
|
||
|
||
if (prod.linked_fee_product_id) {
|
||
feeProductIds.add(prod.linked_fee_product_id)
|
||
}
|
||
|
||
// Module
|
||
sel.moduleIds?.forEach((mId: string) => {
|
||
const mod = prod.modules?.find(m => m.id === mId)
|
||
if (mod) {
|
||
const qty = item.moduleQuantities?.[mId] || 1
|
||
if (prod.billing_interval === 'monthly') {
|
||
monthly += mod.price * qty
|
||
} else {
|
||
oneTime += mod.price * qty
|
||
}
|
||
}
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
const feeProducts: Product[] = []
|
||
feeProductIds.forEach(id => {
|
||
const p = products.find(prod => prod.id === id)
|
||
if (p) {
|
||
feeProducts.push(p)
|
||
if (p.billing_interval === 'monthly') {
|
||
monthly += p.base_price
|
||
} else {
|
||
oneTime += p.base_price
|
||
}
|
||
}
|
||
})
|
||
|
||
return {
|
||
overallMonthlyTotal: monthly,
|
||
overallOneTimeTotal: oneTime,
|
||
linkedFeeProducts: feeProducts
|
||
}
|
||
}, [finalItemsToShow, products, categories])
|
||
|
||
// Gesamte Endbeträge ermitteln
|
||
const overallOneTimeNet = overallOneTimeTotal * discountFactor
|
||
const overallOneTimeTax = overallOneTimeNet * 0.19
|
||
const overallOneTimeGross = overallOneTimeNet + overallOneTimeTax
|
||
|
||
const overallMonthlyNet = overallMonthlyTotal
|
||
const overallMonthlyTax = overallMonthlyNet * 0.19
|
||
const overallMonthlyGross = overallMonthlyNet + overallMonthlyTax
|
||
|
||
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()
|
||
}
|
||
}
|
||
setDirection(1)
|
||
setStep(s => s + 1)
|
||
}
|
||
|
||
const prevStep = () => {
|
||
setDirection(-1)
|
||
setStep(s => s - 1)
|
||
}
|
||
|
||
const goToStep = (targetStep: number) => {
|
||
if (targetStep < step) {
|
||
setDirection(-1)
|
||
setStep(targetStep)
|
||
}
|
||
}
|
||
|
||
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 = async () => {
|
||
const normLicense = licenseNumber.trim().toUpperCase()
|
||
if (normLicense) {
|
||
const isDuplicateInBasket = basketItems.some(
|
||
(item, idx) => idx !== editingIdx && item.licenseNumber?.toUpperCase().trim() === normLicense
|
||
)
|
||
if (isDuplicateInBasket) {
|
||
setToast({ message: `Lizenznummer "${licenseNumber}" ist bereits im aktuellen Warenkorb vorhanden.`, type: 'error' })
|
||
return
|
||
}
|
||
|
||
const isTaken = await isLicenseNumberTaken(normLicense, initialOrder?.id)
|
||
if (isTaken) {
|
||
setToast({ message: `Lizenznummer "${licenseNumber}" wird bereits von einer anderen Bestellung verwendet.`, type: 'error' })
|
||
return
|
||
}
|
||
}
|
||
|
||
const currentItem = {
|
||
deviceName: deviceName || (editingIdx !== null ? basketItems[editingIdx]?.deviceName : `Kasse ${basketItems.length + 1}`),
|
||
licenseNumber: normLicense,
|
||
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('')
|
||
setLicenseNumber('')
|
||
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)
|
||
setLicenseNumber(item.licenseNumber || '')
|
||
setSelectedBillingInterval(item.billingInterval)
|
||
}
|
||
|
||
const deleteBasketItem = (idx: number) => {
|
||
if (editingIdx === idx) {
|
||
setEditingIdx(null)
|
||
setModuleQuantities({})
|
||
setDeviceName('')
|
||
setLicenseNumber('')
|
||
|
||
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 normLicense = licenseNumber.trim().toUpperCase()
|
||
if (normLicense) {
|
||
const isDuplicateInBasket = basketItems.some(
|
||
(item, idx) => idx !== editingIdx && item.licenseNumber?.toUpperCase().trim() === normLicense
|
||
)
|
||
if (isDuplicateInBasket) {
|
||
throw new Error(`Lizenznummer "${licenseNumber}" ist bereits im aktuellen Warenkorb vorhanden.`)
|
||
}
|
||
|
||
const isTaken = await isLicenseNumberTaken(normLicense, initialOrder?.id)
|
||
if (isTaken) {
|
||
throw new Error(`Lizenznummer "${licenseNumber}" wird bereits von einer anderen Bestellung verwendet.`)
|
||
}
|
||
}
|
||
|
||
const currentItem = {
|
||
deviceName: deviceName || (editingIdx !== null ? basketItems[editingIdx]?.deviceName : `Kasse ${basketItems.length + 1}`),
|
||
licenseNumber: normLicense,
|
||
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.')
|
||
}
|
||
|
||
// Check all license numbers in finalItems for duplicates
|
||
const licenseNumbers = finalItems.map(item => item.licenseNumber?.toUpperCase().trim()).filter(Boolean)
|
||
const uniqueLicenses = new Set(licenseNumbers)
|
||
if (uniqueLicenses.size !== licenseNumbers.length) {
|
||
throw new Error('Eine Lizenznummer darf im Warenkorb nicht mehrfach verwendet werden.')
|
||
}
|
||
|
||
for (const lic of uniqueLicenses) {
|
||
const isTaken = await isLicenseNumberTaken(lic, initialOrder?.id)
|
||
if (isTaken) {
|
||
throw new Error(`Die Lizenznummer "${lic}" wird bereits von einer anderen Bestellung verwendet.`)
|
||
}
|
||
}
|
||
|
||
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,
|
||
lastLicenseDate: lastLicenseDate || null,
|
||
notes: orderNotes || null,
|
||
})
|
||
})
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
|
||
|
||
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-7xl mx-auto px-4 py-6">
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
||
{/* Left Side Status Stepper & Categories for ALL steps */}
|
||
<div className="lg:col-span-3 bg-slate-900/50 backdrop-blur-md border border-white/10 rounded-2xl p-6 space-y-6 sticky top-6">
|
||
<div>
|
||
<h2 className="text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">
|
||
Fortschritt
|
||
</h2>
|
||
<p className="text-lg font-extrabold text-white">
|
||
Bestell-Wizard
|
||
</p>
|
||
</div>
|
||
<ProgressStepper step={step} basketItemsCount={basketItems.length} orientation="vertical" onStepClick={goToStep} />
|
||
|
||
{/* Step 3 Categories inside left sidebar */}
|
||
{step === 3 && (
|
||
<div className="pt-4 border-t border-white/10 space-y-3">
|
||
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400">
|
||
Kategorien
|
||
</h3>
|
||
<div className="flex flex-col gap-2">
|
||
{visibleCategories.map((cat) => {
|
||
const sel = selections[cat.id]
|
||
const hasSelection = (sel?.productIds && sel.productIds.length > 0) || !!sel?.productId
|
||
const isRequired = cat.is_required
|
||
const isActive = activeCategoryId === cat.id
|
||
|
||
return (
|
||
<button
|
||
key={cat.id}
|
||
onClick={() => setActiveCategoryId(cat.id)}
|
||
className={`flex items-center gap-2.5 p-2.5 rounded-xl border transition-all duration-300 text-left relative overflow-hidden group ${isActive
|
||
? 'bg-primary/10 border-primary text-white shadow-[0_0_15px_rgba(59,130,246,0.2)]'
|
||
: 'bg-slate-900/50 border-white/5 text-slate-400 hover:bg-slate-900 hover:border-white/20 hover:text-white'
|
||
}`}
|
||
>
|
||
{isActive && (
|
||
<div className="absolute left-0 top-0 bottom-0 w-1 bg-primary" />
|
||
)}
|
||
|
||
<div className={`p-1.5 rounded-lg transition-colors ${isActive ? 'bg-primary/20 text-primary' : 'bg-white/5 text-slate-400 group-hover:text-white'}`}>
|
||
<CategoryIcon icon={cat.icon} className="w-3.5 h-3.5" />
|
||
</div>
|
||
|
||
<div className="flex-1 min-w-0">
|
||
<p className={`text-xs font-semibold truncate ${isActive ? 'text-white' : 'text-slate-300 group-hover:text-white'}`}>
|
||
{cat.name}
|
||
</p>
|
||
</div>
|
||
|
||
{hasSelection ? (
|
||
<span className="text-[9px] px-1.5 py-0.2 rounded-full font-semibold shrink-0 border border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
|
||
✓
|
||
</span>
|
||
) : isRequired ? (
|
||
<span className="text-[9px] px-1.5 py-0.2 rounded-full font-semibold shrink-0 border border-amber-500/30 text-amber-400 bg-amber-500/10">
|
||
!
|
||
</span>
|
||
) : null}
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Right Main Content Column */}
|
||
<div className="lg:col-span-9 space-y-6">
|
||
|
||
<AnimatePresence mode="wait">
|
||
{/* Step 1: Customer */}
|
||
{step === 1 && (
|
||
<motion.div
|
||
key="step1"
|
||
initial={{ opacity: 0, y: direction * 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: direction * -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, y: direction * 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: direction * -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 && (
|
||
<motion.div
|
||
key="step3"
|
||
initial={{ opacity: 0, y: direction * 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: direction * -20 }}
|
||
className="h-[calc(100vh-160px)] overflow-hidden bg-slate-950/70 backdrop-blur-xl border border-slate-800/80 rounded-2xl shadow-2xl p-2"
|
||
>
|
||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-4 h-full items-stretch">
|
||
|
||
{/* LEFT: Scrollable Category Selection */}
|
||
<div className="lg:col-span-3 flex flex-col h-full overflow-hidden p-2 px-3">
|
||
<div className="flex-1 overflow-y-auto pr-2 subpixel-antialiased scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
|
||
{/* Upgrade-Modus Hinweis-Banner */}
|
||
{upgradeMode && lockedDeviceId && (
|
||
<div className="mb-4 p-3 rounded-xl bg-primary/10 border border-primary/20 text-xs text-primary/90 flex items-start gap-2">
|
||
<span className="text-primary mt-0.5">⚡</span>
|
||
<div>
|
||
<p className="font-semibold">Upgrade-Modus: {lockedDeviceId}</p>
|
||
<p className="text-primary/70 mt-0.5">Bereits lizenzierte Module sind ausgegraut und können nicht doppelt gebucht werden.</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<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}
|
||
existingModuleIds={existingModuleIds}
|
||
activeCategoryId={activeCategoryId}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* RIGHT: Summary sidebar */}
|
||
<div className="lg:col-span-2 flex flex-col h-full overflow-hidden p-2 pl-2">
|
||
<div className="flex-1 overflow-hidden">
|
||
<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}
|
||
licenseNumber={licenseNumber}
|
||
setLicenseNumber={setLicenseNumber}
|
||
addToBasket={addToBasket}
|
||
isNextStepDisabled={isNextStepDisabled}
|
||
hasActiveSelection={hasActiveSelection}
|
||
nextStep={nextStep}
|
||
prevStep={prevStep}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
|
||
|
||
|
||
|
||
|
||
{/* Step 4: Verification & Submit */}
|
||
{step === 4 && (
|
||
<motion.div
|
||
key="step4"
|
||
initial={{ opacity: 0, y: direction * 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: direction * -20 }}
|
||
className="h-[calc(100vh-140px)] overflow-hidden w-full"
|
||
>
|
||
<StepSummary
|
||
finalItemsToShow={finalItemsToShow}
|
||
visibleCategories={visibleCategories}
|
||
products={products}
|
||
oneTimeTotal={overallOneTimeTotal}
|
||
monthlyTotal={overallMonthlyTotal}
|
||
oneTimeNet={overallOneTimeNet}
|
||
oneTimeTax={overallOneTimeTax}
|
||
oneTimeGross={overallOneTimeGross}
|
||
monthlyNet={overallMonthlyNet}
|
||
monthlyTax={overallMonthlyTax}
|
||
monthlyGross={overallMonthlyGross}
|
||
updatePriceModifier={updatePriceModifier}
|
||
selectedEndCustomer={selectedEndCustomer}
|
||
customerData={customerData}
|
||
lastLicenseDate={lastLicenseDate}
|
||
handleSubmit={handleSubmit}
|
||
isSubmitting={isSubmitting}
|
||
initialOrder={initialOrder}
|
||
prevStep={prevStep}
|
||
linkedFeeProducts={linkedFeeProducts}
|
||
orderNotes={orderNotes}
|
||
setOrderNotes={setOrderNotes}
|
||
/>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* Toast Notification Container */}
|
||
<ToastNotification toast={toast} onClose={() => setToast(null)} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|