'use client' import { useState, useMemo } from 'react' import { useRouter } from 'next/navigation' import { motion, AnimatePresence } from 'framer-motion' import { Product, ProductModule, Profile, EndCustomer, Order } from '@/lib/types' import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Separator } from '@/components/ui/separator' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { Check, ChevronRight, ChevronLeft, ShoppingCart, User, ShieldCheck, Cloud, Utensils, HardDrive, Package, AlertCircle, Loader2, UserPlus, Building2, CreditCard, Calendar, Search, Pencil, Trash2 } from 'lucide-react' import { submitOrder, updateOrder } from '@/lib/actions/orders' import { createEndCustomer } from '@/lib/actions/end-customers' import { Category } from '@/lib/types' import { Badge } from '@/components/ui/badge' 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 } // ─── Billing interval helpers ───────────────────────────────────────────────── 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' } // ─── Icon helper ────────────────────────────────────────────────────────────── function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) { const Icon = icon === 'Cloud' ? Cloud : icon === 'Utensils' ? Utensils : icon === 'HardDrive' ? HardDrive : Package return } // ─── Module toggle logic ─────────────────────────────────────────────────────── function toggleModuleInList( modules: ProductModule[], currentIds: string[], toggleId: string ): string[] { const isSelected = currentIds.includes(toggleId) if (isSelected) { // Recursive removal: repeatedly filter until no more dependent modules are removed 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 } // ─── Main Wizard ────────────────────────────────────────────────────────────── 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( initialOrder?.company_id ?? null ) const [basketItems, setBasketItems] = useState(() => { if (!initialOrder || !initialOrder.order_data?.items) return [] const groups: Record = {} 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 = {} const moduleQuantities: Record = {} 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('') // Partner-Profil (Fallback) const [customerData] = useState>(initialProfile || {}) // Endkunden-State const [endCustomers, setEndCustomers] = useState(initialEndCustomers) const [selectedEndCustomerId, setSelectedEndCustomerId] = useState( 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 filteredEndCustomers = useMemo(() => { const activeCustomers = endCustomers.filter(c => !c.is_anonymized) const term = searchTerm.toLowerCase().trim() if (!term) return activeCustomers return activeCustomers.filter(customer => { const company = customer.company_name?.toLowerCase() || '' const firstName = customer.first_name?.toLowerCase() || '' const lastName = customer.last_name?.toLowerCase() || '' const street = customer.street?.toLowerCase() || '' const zip = customer.zip?.toLowerCase() || '' const city = customer.city?.toLowerCase() || '' return ( company.includes(term) || firstName.includes(term) || lastName.includes(term) || street.includes(term) || zip.includes(term) || city.includes(term) ) }) }, [endCustomers, searchTerm]) // Modus: 'select' = Bestandskunde wählen | 'create' = Neuen anlegen const [customerMode, setCustomerMode] = useState<'select' | 'create'>('select') const [isCreatingCustomer, setIsCreatingCustomer] = useState(false) const [newCustomerForm, setNewCustomerForm] = useState({ company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '', }) // Abrechnungsmodell-Auswahl ('one_time' = Kauf, 'monthly' = Abo) const [selectedBillingInterval, setSelectedBillingInterval] = useState<'one_time' | 'monthly'>( initialOrder?.order_data?.billing_cycle ?? 'monthly' ) // Modulmengen (moduleId -> Menge) const [moduleQuantities, setModuleQuantities] = useState>(() => { if (!initialOrder) return {} const quantities: Record = {} initialOrder.order_data?.items?.forEach(item => { item.selected_modules?.forEach(mod => { quantities[mod.module_id] = mod.quantity ?? 1 }) }) return quantities }) // Datum der letzten Lizenz (Bestandskunden-Update-Logik) const [lastLicenseDate, setLastLicenseDate] = useState( initialOrder?.order_data?.last_license_date ?? "" ) const lastLicenseMonths = useMemo(() => { if (!lastLicenseDate || customerMode !== 'select' || !selectedEndCustomerId) return null const lastDate = new Date(lastLicenseDate) const today = new Date() const yearsDiff = today.getFullYear() - lastDate.getFullYear() const monthsDiff = today.getMonth() - lastDate.getMonth() return yearsDiff * 12 + monthsDiff }, [lastLicenseDate, customerMode, selectedEndCustomerId]) const updatePriceModifier = useMemo(() => { if (lastLicenseMonths === null || selectedBillingInterval === 'monthly') { return { multiplier: 1, discount: 0, label: null } } if (lastLicenseMonths <= 12) { return { multiplier: 0, discount: 1, label: "Update (0-12 Mon.): 100% Rabatt (inklusive)" } } else if (lastLicenseMonths <= 14) { return { multiplier: 0.15, discount: 0.85, label: "Update (13-14 Mon.): 15% vom Listenpreis" } } else if (lastLicenseMonths <= 25) { return { multiplier: 0.30, discount: 0.70, label: "Update (15-25 Mon.): 30% vom Listenpreis" } } else if (lastLicenseMonths <= 37) { return { multiplier: 0.45, discount: 0.55, label: "Update (26-37 Mon.): 45% vom Listenpreis" } } else if (lastLicenseMonths <= 49) { return { multiplier: 0.60, discount: 0.40, label: "Update (38-49 Mon.): 60% vom Listenpreis" } } else { return { multiplier: 0.90, discount: 0.10, label: "Neukauf (>= 50 Mon.): 10% Rabatt" } } }, [lastLicenseMonths, selectedBillingInterval]) const visibleCategories = useMemo(() => { return categories.filter(cat => selectedBillingInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false ) }, [categories, selectedBillingInterval]) const isProductDisabled = (product: Product, currentCategoryId: string) => { const baseCategoryId = visibleCategories[0]?.id if (!baseCategoryId || currentCategoryId === baseCategoryId) return false const baseProductId = selections[baseCategoryId]?.productId if (!baseProductId) return false const baseProduct = products.find(p => p.id === baseProductId) if (!baseProduct) return false return baseProduct.exclusions?.includes(product.id) || product.exclusions?.includes(baseProductId) } // Per-category selection map: categoryId -> { productId, productIds, moduleIds } const [selections, setSelections] = useState>(() => { const initInterval = initialOrder?.order_data?.billing_cycle ?? 'monthly' const init: Record = {} categories.forEach(cat => { const isVisible = initInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false if (!isVisible || cat.allow_multiselect || cat.preselect === false) { init[cat.id] = { productId: null, productIds: [], moduleIds: [] } } else { const first = products.find(p => p.category_id === cat.id && (initInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false)) init[cat.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] } } }) return init }) // Only REQUIRED categories must have a product selected (which must be filtered by selected branch) 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 // Ensure the selected product is actually allowed in the current branch return selectedBillingInterval === 'one_time' ? prod.show_in_kauf !== false : prod.show_in_abo !== false }) }, [visibleCategories, selections, products, selectedBillingInterval]) // Product validation errors (requirements and exclusions) const productValidationErrors = useMemo(() => { const errors: string[] = [] 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]) const isNextStepDisabled = !allCategoriesFilled || productValidationErrors.length > 0 // 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) } if (selectedProds.length === 0) continue // Apply free items limit: sort selected products by base_price ascending so that the cheapest are free const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 sortedProds.forEach((prod, sortedIndex) => { const base = sortedIndex < freeLimit ? 0 : Number(prod.base_price) let modulesSum = 0 sel.moduleIds.forEach(mId => { const mod = prod.modules?.find(m => m.id === mId) if (mod) { const qty = moduleQuantities[mId] || 1 modulesSum += Number(mod.price) * qty } }) const totalItem = base + modulesSum if (prod.billing_interval === 'one_time') { const allowDiscount = (prod as any).allow_update_discount !== false const finalItemTotal = allowDiscount ? (totalItem * updatePriceModifier.multiplier) : totalItem oneTime += finalItemTotal } else { monthly += totalItem } }) } return { monthlyTotal: monthly, oneTimeTotal: oneTime } }, [selections, products, categories, moduleQuantities, updatePriceModifier]) const oneTimeNet = oneTimeTotal const oneTimeTax = oneTimeNet * 0.19 const oneTimeGross = oneTimeNet + oneTimeTax const monthlyNet = monthlyTotal const monthlyTax = monthlyNet * 0.19 const monthlyGross = monthlyNet + monthlyTax // Helper: update product selection for a category function selectProduct(catId: string, productId: string) { const cat = categories.find(c => c.id === catId) setSelections(prev => { const sel = prev[catId] let nextProductIds = sel?.productIds || [] let nextProductId = sel?.productId || null if (cat?.allow_multiselect) { if (nextProductIds.includes(productId)) { nextProductIds = nextProductIds.filter(id => id !== productId) } else { nextProductIds = [...nextProductIds, productId] } nextProductId = nextProductIds[0] || null } else { nextProductIds = [productId] nextProductId = productId } const isSelectedNow = cat?.allow_multiselect ? nextProductIds.includes(productId) : nextProductId === productId const shouldReset = isSelectedNow && cat?.resets_others const next = { ...prev, [catId]: { productId: nextProductId, productIds: nextProductIds, moduleIds: [] }, } // Kaskaden-Bereinigung: Entferne inkompatible Module in anderen Kategorien const newProduct = products.find(p => p.id === nextProductId) categories.forEach(c => { if (c.id === catId) return const s = next[c.id] if (s && s.moduleIds.length > 0) { s.moduleIds = s.moduleIds.filter(mId => { // Prüfe ob das neue Basis-Produkt dieses Modul ausschließt const isExcluded = newProduct?.exclusions?.includes(mId) return !isExcluded }) } }) if (shouldReset) { // Reset all other categories categories.forEach(c => { if (c.id !== catId) { next[c.id] = { productId: null, productIds: [], moduleIds: [] } } }) } else { // Automatically deselect now conflicting products in other categories const selectedIds: string[] = [] Object.values(next).forEach(s => { if (s.productIds && s.productIds.length > 0) { s.productIds.forEach(id => { if (id) selectedIds.push(id) }) } else if (s.productId) { selectedIds.push(s.productId) } }) categories.forEach(c => { if (c.id === catId) return const s = next[c.id] if (c.allow_multiselect && s?.productIds) { const validProductIds = s.productIds.filter(pId => { const prod = products.find(p => p.id === pId) const otherSelectedProductIds = selectedIds.filter(id => id !== pId) const isExcluded = prod && ( otherSelectedProductIds.some(otherId => { const otherProd = products.find(p => p.id === otherId) return otherProd?.exclusions?.includes(prod.id) }) || prod.exclusions?.some(exId => otherSelectedProductIds.includes(exId)) ) return !isExcluded }) next[c.id] = { productId: validProductIds[0] || null, productIds: validProductIds, moduleIds: s.moduleIds } } else if (s?.productId) { const prod = products.find(p => p.id === s.productId) const otherSelectedProductIds = selectedIds.filter(id => id !== s.productId) const isExcluded = prod && ( otherSelectedProductIds.some(otherId => { const otherProd = products.find(p => p.id === otherId) return otherProd?.exclusions?.includes(prod.id) }) || prod.exclusions?.some(exId => otherSelectedProductIds.includes(exId)) ) if (isExcluded) { next[c.id] = { productId: null, productIds: [], moduleIds: [] } } } }) } return next }) } // Helper: toggle module for a category's selected product function toggleModule(catId: string, moduleId: string) { setSelections(prev => { const sel = prev[catId] // Pick first selected product to associate module with (or fall back to single select productId) 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) { const hasProduct = Object.values(selections).some(sel => sel.productId || (sel.productIds && sel.productIds.length > 0)) if (hasProduct) { addToBasket() } } setStep(s => s + 1) } const prevStep = () => setStep(s => s - 1) // Abrechnungsmodell wechseln (und Selektionen der Kategorien anpassen) const handleBillingIntervalChange = (val: 'one_time' | 'monthly') => { setSelectedBillingInterval(val) setModuleQuantities({}) if (val === 'monthly') { setLastLicenseDate("") } const newSelections: Record = {} 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) { alert(`Fehler beim Anlegen des Kunden: ${e.message || 'Unbekannter Fehler'}`) } finally { setIsCreatingCustomer(false) } } const addToBasket = () => { const currentItem = { deviceName: deviceName || `Kasse ${basketItems.length + 1}`, selections: JSON.parse(JSON.stringify(selections)), moduleQuantities: { ...moduleQuantities }, billingInterval: selectedBillingInterval, } setBasketItems(prev => [...prev, currentItem]) setModuleQuantities({}) setDeviceName('') // Reset selections const resetSels: Record = {} 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 const hasProduct = Object.values(selections).some(sel => sel.productId || (sel.productIds && sel.productIds.length > 0)) if (hasProduct) { const currentItem = { deviceName: deviceName || `Kasse ${basketItems.length + 1}`, selections: JSON.parse(JSON.stringify(selections)), moduleQuantities: { ...moduleQuantities }, billingInterval: selectedBillingInterval, } setBasketItems(prev => { const filtered = prev.filter((_, i) => i !== idx) return [...filtered, currentItem] }) } else { setBasketItems(prev => prev.filter((_, i) => i !== idx)) } setSelections(item.selections) setModuleQuantities(item.moduleQuantities) setDeviceName(item.deviceName) setSelectedBillingInterval(item.billingInterval) } const deleteBasketItem = (idx: number) => { setBasketItems(prev => prev.filter((_, i) => i !== idx)) } const handleSubmit = async () => { if (isSubmitting) return setIsSubmitting(true) try { let finalItems = [...basketItems] if (finalItems.length === 0 && allCategoriesFilled && productValidationErrors.length === 0) { finalItems.push({ deviceName: deviceName || 'Kasse 1', selections, moduleQuantities, billingInterval: selectedBillingInterval }) } 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) alert(error?.message || 'Fehler bei der Bestellung. Bitte versuchen Sie es erneut.') setIsSubmitting(false) } } const finalItemsToShow = basketItems.length > 0 ? basketItems : (allCategoriesFilled && productValidationErrors.length === 0 ? [{ deviceName: deviceName || 'Kasse 1', selections, moduleQuantities, billingInterval: selectedBillingInterval }] : []); return (
{/* Progress Stepper */} {step !== 3 && (
{[1, 2, 3, 4].map(s => (
= s ? 'bg-primary text-white scale-110 shadow-[0_0_15px_rgba(59,130,246,0.5)]' : 'bg-slate-800 text-slate-400' }`} > {step > s ? : s}
= s ? 'text-primary' : 'text-slate-500'}`}> {s === 1 ? 'Kunde' : s === 2 ? 'Modell' : s === 3 ? 'Software' : 'Abschluss'}
))}
)} {/* ───── Schritt 1: Endkunde wählen / anlegen ───── */} {step === 1 && ( Für wen bestellen Sie? Wählen Sie einen Endkunden aus oder legen Sie einen neuen an. {/* Admin Company Selector */} {isAdmin && (
)} {/* Modus-Toggle */}
{/* Modus A: Bestandskunde wählen */} {customerMode === 'select' && (
{endCustomers.filter(c => !c.is_anonymized).length === 0 ? (

Noch keine Endkunden angelegt.

) : (
setSearchTerm(e.target.value)} className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-500" />
{filteredEndCustomers.length === 0 ? (

Keine passenden Kunden gefunden.

) : (
{filteredEndCustomers.map(customer => ( ))}
)}
)}
)} {/* Modus B: Neuen Kunden anlegen */} {customerMode === 'create' && (
{([ { label: 'Firmenname *', key: 'company_name', span: true, placeholder: 'GmbH / Einzelunternehmen' }, { label: 'USt-IdNr.', key: 'vat_id', span: false, placeholder: 'DE123456789' }, { label: 'Vorname', key: 'first_name', span: false, placeholder: '' }, { label: 'Nachname', key: 'last_name', span: false, placeholder: '' }, { label: 'Straße & Hausnummer', key: 'street', span: true, placeholder: '' }, { label: 'PLZ', key: 'zip', span: false, placeholder: '' }, { label: 'Ort', key: 'city', span: false, placeholder: '' }, { label: 'Kontoinhaber', key: 'bank_owner', span: false, placeholder: 'Max Mustermann' }, { label: 'IBAN', key: 'bank_iban', span: false, placeholder: 'DE89370400440532013000' }, { label: 'BIC', key: 'bank_bic', span: false, placeholder: 'SOLADE21XXX' }, { label: 'Bankname', key: 'bank_name', span: false, placeholder: 'Musterbank' }, ] as const).map(({ label, key, span, placeholder }) => (
setNewCustomerForm(prev => ({ ...prev, [key]: e.target.value }))} placeholder={placeholder} className="bg-white/5 border-white/10 text-white placeholder:text-slate-500" />
))}
)}
)} {/* ───── Schritt 2: Abrechnungsmodell wählen ───── */} {step === 2 && ( Abrechnungsmodell wählen Möchten Sie Lizenzen dauerhaft kaufen oder monatlich mieten?
{/* Option 1: Kauf */}
handleBillingIntervalChange('one_time')} className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${selectedBillingInterval === 'one_time' ? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]' : 'border-white/5 bg-white/5 hover:border-white/20' }`} >

Einmaliger Kauf

Einmalige Anschaffungskosten für die Softwarelizenz. Keine monatlichen Mietgebühren.

{selectedBillingInterval === 'one_time' && (
)}
{/* Option 2: Abo */}
handleBillingIntervalChange('monthly')} className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${selectedBillingInterval === 'monthly' ? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]' : 'border-white/5 bg-white/5 hover:border-white/20' }`} >

Monatliches Abo (Miete)

Laufende monatliche Gebühren. Inklusive aller Updates und flexibler Laufzeit.

{selectedBillingInterval === 'monthly' && (
)}
{selectedBillingInterval === 'one_time' && customerMode === 'select' && selectedEndCustomerId && (
setLastLicenseDate(e.target.value)} className="bg-[#0b1329] border-white/10 text-white focus:border-primary" />

Falls dieser Kunde bereits Lizenzen besitzt, tragen Sie das Datum der letzten Lizenzierung ein. Damit werden die korrekten Update-Gebühren ermittelt.

)}
)} {/* ───── Schritt 3: Software wählen ───── */} {step === 3 && (
{/* Left: Category sections */}
{/* Progress Stepper docked above Software wählen */}
{[1, 2, 3, 4].map(s => (
= s ? 'bg-primary text-white scale-110 shadow-[0_0_15px_rgba(59,130,246,0.5)]' : 'bg-slate-800 text-slate-400' }`} > {step > s ? : s}
= s ? 'text-primary' : 'text-slate-500'}`}> {s === 1 ? 'Kunde' : s === 2 ? 'Modell' : s === 3 ? 'Software' : 'Abschluss'}
))}
Software wählen Wählen Sie pro Kategorie mindestens einen Artikel aus. {visibleCategories.map((cat, idx) => { const otherSelectedProductIds = Object.entries(selections) .filter(([cId]) => cId !== cat.id) .map(([, s]) => s.productId) .filter((id): id is string => !!id) // Filter products based on display flags (do not hide due to exclusions to prevent UI deadlocks) const catProducts = products.filter(p => { if (p.category_id !== cat.id) return false const matchesInterval = selectedBillingInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false return matchesInterval }) const sel = selections[cat.id] const selectedProduct = catProducts.find(p => p.id === sel?.productId) ?? null return (
{idx > 0 && } {/* Category header */}

{cat.name}

{cat.description && (

{cat.description}

)}
{sel?.productIds && sel.productIds.length > 0 ? ( {sel.productIds.length} Ausgewählt ) : sel?.productId ? ( Ausgewählt ) : cat.is_required ? ( Pflichtfeld ) : ( Optional )}
{catProducts.length === 0 ? (

Keine Produkte in dieser Kategorie im aktuellen Abrechnungsmodell vorhanden.

) : cat.allow_multiselect ? (
{catProducts.map(product => { const isChecked = sel?.productIds?.includes(product.id) ?? false const disabled = isProductDisabled(product, cat.id) return (
) })}
) : ( selectProduct(cat.id, id)} className="grid gap-3" > {catProducts.map(product => { const disabled = isProductDisabled(product, cat.id) return (
) })}
)} {/* Modules for selected product in this category */} {selectedProduct?.modules && selectedProduct.modules.length > 0 && (

Zusatzmodule:

{selectedProduct.modules.map(module => { const disabled = isModuleDisabled(module, sel?.moduleIds ?? []) const checked = sel?.moduleIds.includes(module.id) ?? false return (
toggleModule(cat.id, module.id)} disabled={disabled} />
{module.description && (

{module.description}

)} {disabled && (

{module.requirements?.length && !module.requirements.some( reqId => sel?.moduleIds.includes(reqId) ) ? 'Benötigt weitere Module' : 'Nicht kombinierbar mit aktueller Auswahl'}

)}
{/* Scalable Quantity Input */} {checked && module.has_quantity && (
{ const val = Math.max(1, parseInt(e.target.value) || 1) setModuleQuantities(prev => ({ ...prev, [module.id]: val })) }} className="w-20 h-8 bg-white/5 border-white/10 text-white text-xs text-center rounded-lg" /> Gesamt: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', }).format(module.price * (moduleQuantities[module.id] || 1))}
)}
) })}
)}
) })}
{/* Right: Summary */}
Zusammenfassung {visibleCategories.map(cat => { const sel = selections[cat.id] const selectedProds: Product[] = [] if (cat.allow_multiselect && sel?.productIds) { sel.productIds.forEach(pId => { const p = products.find(prod => prod.id === pId) if (p) selectedProds.push(p) }) } else if (sel?.productId) { const p = products.find(prod => prod.id === sel.productId) if (p) selectedProds.push(p) } if (selectedProds.length === 0) return (
{cat.is_required ? ( ) : ( )} {cat.name}: {cat.is_required ? 'nicht gewählt' : 'nicht gewählt (optional)'}
) // Sort selected products by price to determine which ones are free const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 return (
{cat.name}
{sortedProds.map((prod, idx) => { const isFree = idx < freeLimit const actualPrice = isFree ? 0 : prod.base_price return (
{prod.name} {isFree && (Frei)} {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(actualPrice)} {' '}{billingLabel(prod.billing_interval)}
{sel.moduleIds.map(mId => { const mod = prod.modules?.find(m => m.id === mId) const qty = moduleQuantities[mId] || 1 return mod ? (
+ {mod.name} {mod.has_quantity ? `(x${qty})` : ''} {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * qty)}
) : null })}
) })}
) })} {oneTimeTotal > 0 && monthlyTotal > 0 ? (

Einmalig:

Netto: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)}
zzgl. 19% MwSt.: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)}
Gesamt (brutto): {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}

Monatlich:

Netto: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl.
zzgl. 19% MwSt.: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl.
Gesamt (brutto): {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl.
) : oneTimeTotal > 0 ? (
Netto-Gesamtbetrag: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)}
zzgl. 19% MwSt.: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)}
Gesamtbetrag (brutto): {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}
) : (
Netto-Gesamtbetrag: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl.
zzgl. 19% MwSt.: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl.
Gesamtbetrag (brutto): {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl.
)} {updatePriceModifier.label && (

Update-Rabatt aktiv

{updatePriceModifier.label}

)} {!allCategoriesFilled && (

Bitte aus jeder Pflichtkategorie einen Artikel wählen.

)} {productValidationErrors.map((err, errIdx) => (

{err}

))} {basketItems.length > 0 && (

Warenkorb ({basketItems.length}):

{basketItems.map((item, idx) => (
{item.deviceName} {item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'}
))}
)}
setDeviceName(e.target.value)} className="bg-white/5 border-white/10 text-white text-xs h-9 rounded-lg" />
)} {/* ───── Schritt 4: Prüfung & Abschluss ───── */} {step === 4 && (
Anfrage prüfen Fast fertig! Bitte überprüfen Sie Ihre Auswahl.
{finalItemsToShow.map((item, itemIdx) => (
Kasse: {item.deviceName} {item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'}
{visibleCategories.map(cat => { const sel = item.selections[cat.id] const selectedProds: Product[] = [] if (cat.allow_multiselect && sel?.productIds) { 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) } if (selectedProds.length === 0) return null const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 return (
{cat.name}
{sortedProds.map((prod, idx) => { const isFree = idx < freeLimit const actualPrice = isFree ? 0 : prod.base_price const catTotal = Number(actualPrice) + (sel?.moduleIds?.reduce((acc: number, mId: string) => { const mod = prod.modules?.find(m => m.id === mId) const qty = item.moduleQuantities[mId] || 1 return acc + (Number(mod?.price ?? 0) * qty) }, 0) || 0) return (
{prod.name} {isFree && (Frei)} {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(catTotal)}
{sel?.moduleIds && sel.moduleIds.length > 0 && (

Module: {sel.moduleIds .map((id: string) => { const mod = prod.modules?.find(m => m.id === id) const qty = item.moduleQuantities[id] || 1 return mod ? `${mod.name}${mod.has_quantity ? ` (x${qty})` : ''}` : '' }) .filter(Boolean) .join(', ')}

)}
) })}
) })}
))}
{selectedEndCustomer ? ( <>

{selectedEndCustomer.company_name}

{[selectedEndCustomer.first_name, selectedEndCustomer.last_name].filter(Boolean).join(' ')}

{[selectedEndCustomer.street, selectedEndCustomer.zip, selectedEndCustomer.city].filter(Boolean).join(', ')}

Endkunde Ihres Partners

{lastLicenseDate && (

Letzte Lizenz vom: {new Date(lastLicenseDate).toLocaleDateString('de-DE')}

)} ) : ( <>

{customerData.company_name}

{customerData.first_name} {customerData.last_name}

{customerData.address}, {customerData.zip_code} {customerData.city}

)}
{oneTimeTotal > 0 && monthlyTotal > 0 ? (

Einmaliger Betrag:

Netto: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)}
zzgl. 19% MwSt.: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)}
Brutto: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}
{updatePriceModifier.label && (
{updatePriceModifier.label}
)}

Monatlicher Betrag:

Netto: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl.
zzgl. 19% MwSt.: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl.
Brutto: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl.
) : oneTimeTotal > 0 ? (
Netto-Gesamtbetrag: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)}
zzgl. 19% MwSt.: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)}
{updatePriceModifier.label && (
{updatePriceModifier.label}
)}
Gesamtbetrag (brutto): {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}
) : (
Netto-Gesamtbetrag: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl.
zzgl. 19% MwSt.: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl.
Gesamtbetrag (brutto): {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl.
)}

Mit dem Klick auf „Kostenpflichtig bestellen" akzeptieren Sie unsere AGB und die{' '} Datenschutzerklärung .

)}
) }