'use client' import { useState, useMemo } from 'react' import { useRouter } from 'next/navigation' import { motion, AnimatePresence } from 'framer-motion' import { Product, ProductModule, Profile, EndCustomer } 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 } from 'lucide-react' import { submitOrder } from '@/lib/actions/orders' import { createEndCustomer } from '@/lib/actions/end-customers' import { Category } from '@/lib/types' import { Badge } from '@/components/ui/badge' // ─── Types ──────────────────────────────────────────────────────────────────── type CategorySelection = { productId: string | null // selected product in this category 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.every(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.every(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, }: { products: Product[] categories: Category[] initialProfile: Profile | null initialEndCustomers: EndCustomer[] }) { const router = useRouter() const [step, setStep] = useState(1) const [isSubmitting, setIsSubmitting] = useState(false) // Partner-Profil (Fallback) const [customerData] = useState>(initialProfile || {}) // Endkunden-State const [endCustomers, setEndCustomers] = useState(initialEndCustomers) const [selectedEndCustomerId, setSelectedEndCustomerId] = useState( initialEndCustomers.length > 0 ? initialEndCustomers[0].id : null ) const selectedEndCustomer = endCustomers.find(c => c.id === selectedEndCustomerId) ?? null // 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: '', }) // Abrechnungsmodell-Auswahl ('one_time' = Kauf, 'monthly' = Abo) const [selectedBillingInterval, setSelectedBillingInterval] = useState<'one_time' | 'monthly'>('monthly') // Modulmengen (moduleId -> Menge) const [moduleQuantities, setModuleQuantities] = useState>({}) // Datum der letzten Lizenz (Bestandskunden-Update-Logik) const [lastLicenseDate, setLastLicenseDate] = useState("") 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]) // Per-category selection map: categoryId -> { productId, moduleIds } const [selections, setSelections] = useState>(() => { const init: Record = {} categories.forEach(cat => { const first = products.find(p => p.category_id === cat.id && p.show_in_abo !== false) init[cat.id] = { productId: first?.id ?? null, moduleIds: [] } }) return init }) // Only REQUIRED categories must have a product selected (which must be filtered by selected branch) const allCategoriesFilled = useMemo(() => { return categories .filter(cat => cat.is_required) .every(cat => { const sel = selections[cat.id] 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 }) }, [categories, selections, products, selectedBillingInterval]) // Product validation errors (requirements and exclusions) const productValidationErrors = useMemo(() => { const errors: string[] = [] const selectedProductIds = Object.values(selections) .map(s => s.productId) .filter((id): id is string => !!id) selectedProductIds.forEach(prodId => { const prod = products.find(p => p.id === prodId) if (!prod) return // Requirements check if (prod.requirements && prod.requirements.length > 0) { const missing = prod.requirements.filter(reqId => !selectedProductIds.includes(reqId)) if (missing.length > 0) { const names = missing .map(reqId => products.find(p => p.id === reqId)?.name || reqId) .join(', ') errors.push(`"${prod.name}" benötigt das Produkt: ${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?.productId) continue const prod = products.find(p => p.id === sel.productId) if (!prod) continue const base = 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') { oneTime += totalItem } else { monthly += totalItem } } // Apply update modifier to oneTime total if (updatePriceModifier.multiplier !== 1) { oneTime = oneTime * updatePriceModifier.multiplier } return { monthlyTotal: monthly, oneTimeTotal: oneTime } }, [selections, products, categories, moduleQuantities, updatePriceModifier]) // Helper: update product selection for a category (resets modules) function selectProduct(catId: string, productId: string) { setSelections(prev => ({ ...prev, [catId]: { productId, moduleIds: [] }, })) } // Helper: toggle module for a category's selected product function toggleModule(catId: string, moduleId: string) { setSelections(prev => { const sel = prev[catId] if (!sel?.productId) return prev const prod = products.find(p => p.id === sel.productId) 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 = () => 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 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, moduleIds: [] } }) setSelections(newSelections) } // Neuen Endkunden inline anlegen const handleCreateCustomer = async () => { if (!newCustomerForm.company_name.trim()) return setIsCreatingCustomer(true) try { const created = await createEndCustomer(newCustomerForm) setEndCustomers(prev => [...prev, created]) setSelectedEndCustomerId(created.id) setCustomerMode('select') setNewCustomerForm({ company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '' }) } catch (e) { alert('Fehler beim Anlegen des Kunden.') } finally { setIsCreatingCustomer(false) } } const handleSubmit = async () => { if (isSubmitting) return // Doppelklick-Guard setIsSubmitting(true) try { const order = await submitOrder({ selections, moduleQuantities, products, categories, customerProfile: customerData, endCustomerId: selectedEndCustomerId, endCustomer: selectedEndCustomer, billingInterval: selectedBillingInterval, lastLicenseDate: lastLicenseDate || null, }) router.push(`/order/success?id=${order.id}`) } catch (error) { console.error(error) alert('Fehler bei der Bestellung. Bitte versuchen Sie es erneut.') setIsSubmitting(false) // Nur bei Fehler wieder entsperren } } return (
{/* Progress Stepper */}
{[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 Ihrem CRM oder legen Sie einen neuen an. {/* Modus-Toggle */}
{/* Modus A: Bestandskunde wählen */} {customerMode === 'select' && (
{endCustomers.length === 0 ? (

Noch keine Endkunden angelegt.

) : (
{endCustomers.filter(c => !c.is_anonymized).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: '' }, ] 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 */}
Software wählen Wählen Sie pro Kategorie mindestens einen Artikel aus. {categories.map((cat, idx) => { // Filter products based on display flags show_in_kauf / show_in_abo const catProducts = products.filter(p => { if (p.category_id !== cat.id) return false return selectedBillingInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false }) 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?.productId ? ( Ausgewählt ) : cat.is_required ? ( Pflichtfeld ) : ( Optional )}
{catProducts.length === 0 ? (

Keine Produkte in dieser Kategorie im aktuellen Abrechnungsmodell vorhanden.

) : ( selectProduct(cat.id, id)} className="grid gap-3" > {catProducts.map(product => (
))}
)} {/* 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?.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 {categories.map(cat => { const sel = selections[cat.id] const prod = products.find(p => p.id === sel?.productId) if (!prod) return (
{cat.is_required ? ( ) : ( )} {cat.name}: {cat.is_required ? 'nicht gewählt' : 'nicht gewählt (optional)'}
) return (
{cat.name}
{prod.name} {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(prod.base_price)} {' '}{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 gesamt: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTotal)}
Monatlich gesamt: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTotal)} {' '}/ Monat
) : oneTimeTotal > 0 ? (
Gesamt (einmalig): {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTotal)}
) : (
Gesamt: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTotal)} {' '}/ Monat
)} {updatePriceModifier.label && (

Update-Rabatt aktiv

{updatePriceModifier.label}

)} {!allCategoriesFilled && (

Bitte aus jeder Pflichtkategorie einen Artikel wählen.

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

{err}

))}
)} {/* ───── Schritt 4: Prüfung & Abschluss ───── */} {step === 4 && (
Bestellung prüfen Fast fertig! Bitte überprüfen Sie Ihre Auswahl.
{categories.map(cat => { const sel = selections[cat.id] const prod = products.find(p => p.id === sel?.productId) if (!prod) return null const catTotal = Number(prod.base_price) + sel.moduleIds.reduce((acc, mId) => { const mod = prod.modules?.find(m => m.id === mId) const qty = moduleQuantities[mId] || 1 return acc + (Number(mod?.price ?? 0) * qty) }, 0) return (
{prod.name} {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(catTotal)} {' '}{billingLabel(prod.billing_interval)}
{sel.moduleIds.length > 0 && (

Module: {sel.moduleIds .map(id => { const mod = prod.modules?.find(m => m.id === id) const qty = 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 Gesamtbetrag: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTotal)}
{updatePriceModifier.label && (
{updatePriceModifier.label}
)}
Monatlicher Gesamtbetrag: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTotal)} {' '}/ Monat
) : oneTimeTotal > 0 ? (
Gesamtbetrag: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTotal)}
{updatePriceModifier.label && (
{updatePriceModifier.label}
)}
) : (
Gesamtbetrag: {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTotal)} {' '}/ Monat
)}

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

)}
) }