diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index 85801e8..3146084 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -10,89 +10,154 @@ 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, CreditCard, ShieldCheck, Cloud, Utensils, HardDrive, Package } from 'lucide-react' +import { Check, ChevronRight, ChevronLeft, ShoppingCart, User, ShieldCheck, Cloud, Utensils, HardDrive, Package, AlertCircle } from 'lucide-react' import { submitOrder } from '@/lib/actions/orders' import { Category } from '@/lib/types' -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Badge } from '@/components/ui/badge' -export function OrderWizard({ products, categories, initialProfile }: { products: Product[], categories: Category[], initialProfile: Profile | null }) { +// ─── Types ──────────────────────────────────────────────────────────────────── +type CategorySelection = { + productId: string | null // selected product in this category + moduleIds: string[] // selected module IDs for this product +} + +// ─── 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) { + const next = currentIds.filter(id => id !== toggleId) + // cascade: remove modules that required this one + return next.filter(mId => { + const m = modules.find(mod => mod.id === mId) + return !m?.requirements || m.requirements.every(reqId => next.includes(reqId) || reqId === toggleId) + }).filter(id => id !== toggleId) + } 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, +}: { + products: Product[] + categories: Category[] + initialProfile: Profile | null +}) { const [step, setStep] = useState(1) - const [activeCategory, setActiveCategory] = useState(categories[0]?.id || 'all') - const [customerData, setCustomerData] = useState>(initialProfile || { - company_name: '', - vat_id: '', - first_name: '', - last_name: '', - address: '', - city: '', - zip_code: '' + const [customerData, setCustomerData] = useState>( + initialProfile || { + company_name: '', + vat_id: '', + first_name: '', + last_name: '', + address: '', + city: '', + zip_code: '', + } + ) + + // 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) + init[cat.id] = { productId: first?.id ?? null, moduleIds: [] } + }) + return init }) - const filteredProducts = useMemo(() => { - if (activeCategory === 'all') return products - return products.filter(p => p.category_id === activeCategory) - }, [products, activeCategory]) - - const [selectedProduct, setSelectedProduct] = useState(filteredProducts[0] || null) - const [selectedModules, setSelectedModules] = useState([]) + // All categories must have a product selected + const allCategoriesFilled = useMemo( + () => categories.every(cat => !!selections[cat.id]?.productId), + [categories, selections] + ) + // Total price across all selections const totalPrice = useMemo(() => { - if (!selectedProduct) return 0 - let total = Number(selectedProduct.base_price) - selectedModules.forEach(modId => { - const mod = selectedProduct.modules?.find(m => m.id === modId) - if (mod) total += Number(mod.price) - }) + let total = 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 + total += Number(prod.base_price) + sel.moduleIds.forEach(mId => { + const mod = prod.modules?.find(m => m.id === mId) + if (mod) total += Number(mod.price) + }) + } return total - }, [selectedProduct, selectedModules]) + }, [selections, products, categories]) + + // 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 } } + }) + } const nextStep = () => setStep(s => s + 1) const prevStep = () => setStep(s => s - 1) - const toggleModule = (id: string) => { - const module = selectedProduct?.modules?.find(m => m.id === id) - if (!module) return - - setSelectedModules(prev => { - const isSelected = prev.includes(id) - - if (isSelected) { - // When deselecting, we might need to deselect others that depend on this one - const newSelection = prev.filter(m => m !== id) - return newSelection.filter(mId => { - const m = selectedProduct?.modules?.find(mod => mod.id === mId) - return !m?.requirements || m.requirements.every(reqId => newSelection.includes(reqId) || reqId === id) - }).filter(mId => mId !== id) - } else { - // When selecting, check exclusions - if (module.exclusions && module.exclusions.some(exId => prev.includes(exId))) { - return prev // Should be disabled anyway, but safety check - } - return [...prev, id] - } - }) - } - - const isModuleDisabled = (module: ProductModule) => { - // 1. Check requirements: all required modules must be selected - const requirementsMet = !module.requirements || module.requirements.length === 0 || - module.requirements.every(reqId => selectedModules.includes(reqId)) - - // 2. Check exclusions: no excluded module must be selected - const isExcluded = module.exclusions && module.exclusions.some(exId => selectedModules.includes(exId)) - - return !requirementsMet || isExcluded - } - const handleSubmit = async () => { - if (!selectedProduct) return try { + // Build multi-product order payload – submit each category item + const items = categories + .map(cat => { + const sel = selections[cat.id] + if (!sel?.productId) return null + const prod = products.find(p => p.id === sel.productId)! + return { + category: cat.name, + product_id: sel.productId, + product_name: prod.name, + base_price: prod.base_price, + selected_modules: sel.moduleIds, + } + }) + .filter(Boolean) + await submitOrder({ - product_id: selectedProduct.id, - selected_modules: selectedModules, + product_id: items[0]!.product_id, // primary (first category) + selected_modules: items.flatMap(i => i!.selected_modules), total_amount: totalPrice, - customer_details: customerData + customer_details: customerData, + // @ts-ignore extended payload + items, }) alert('Bestellung erfolgreich aufgegeben!') window.location.href = '/' @@ -103,15 +168,17 @@ export function OrderWizard({ products, categories, initialProfile }: { products } return ( -
+
{/* Progress Stepper */}
- {[1, 2, 3].map((s) => ( -
( +
= s ? 'bg-primary text-white scale-110 shadow-[0_0_15px_rgba(var(--primary),0.5)]' : 'bg-slate-800 text-slate-400' + step >= 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} @@ -120,6 +187,7 @@ export function OrderWizard({ products, categories, initialProfile }: { products
+ {/* ───── Step 1: Product Selection per Category ───── */} {step === 1 && (
-
+ {/* Left: Category sections */} +
Software wählen - Wählen Sie Ihr Basispaket und optionale Erweiterungen. + + Wählen Sie pro Kategorie mindestens einen Artikel aus. + - - {/* Category Tabs */} -
- - { - setActiveCategory(val) - // Auto-select first product in new category - const firstInCat = products.find(p => val === 'all' || p.category_id === val) - if (firstInCat) { - setSelectedProduct(firstInCat) - setSelectedModules([]) - } - }}> - - - Alle - - {categories.map(cat => { - const Icon = cat.icon === 'Cloud' ? Cloud : cat.icon === 'Utensils' ? Utensils : cat.icon === 'HardDrive' ? HardDrive : Package - return ( - - {cat.name} - - ) - })} - - -
+ + {categories.map((cat, idx) => { + const catProducts = products.filter(p => p.category_id === cat.id) + const sel = selections[cat.id] + const selectedProduct = catProducts.find(p => p.id === sel?.productId) ?? null - + return ( +
+ {idx > 0 && } -
- - { - const p = products.find(prod => prod.id === id) - if (p) { - setSelectedProduct(p) - setSelectedModules([]) // Reset modules when product changes - } - }} - className="grid gap-4" - > - {filteredProducts.map(product => ( -
- - -
- ))} -
-
- - {selectedProduct && selectedProduct.modules && selectedProduct.modules.length > 0 && ( -
- -
- {selectedProduct.modules.map(module => ( -
- toggleModule(module.id)} - disabled={isModuleDisabled(module)} - /> -
- -

{module.description}

- {isModuleDisabled(module) && ( -

- {module.requirements && module.requirements.some(reqId => !selectedModules.includes(reqId)) - ? "Benötigt weitere Module" - : "Nicht kombinierbar mit aktueller Auswahl"} -

- )} -
+ {/* Category header */} +
+
+
- ))} +
+

{cat.name}

+ {cat.description && ( +

{cat.description}

+ )} +
+ {sel?.productId ? ( + + Ausgewählt + + ) : ( + + Pflichtfeld + + )} +
+ + {catProducts.length === 0 ? ( +

+ Keine Produkte in dieser Kategorie 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'} +

+ )} +
+
+ ) + })} +
+ )}
-
- )} + ) + })}
+ {/* Right: Summary */}
- Zusammenfassung + Zusammenfassung -
- Basispreis: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(selectedProduct?.base_price || 0)} -
- {selectedModules.map(modId => { - const mod = selectedProduct?.modules?.find(m => m.id === modId) + {categories.map(cat => { + const sel = selections[cat.id] + const prod = products.find(p => p.id === sel?.productId) + if (!prod) return ( +
+ + {cat.name}: nicht gewählt +
+ ) return ( -
- {mod?.name}: - +{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod?.price || 0)} +
+
+ {cat.name} +
+
+ {prod.name} + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(prod.base_price)} + +
+ {sel.moduleIds.map(mId => { + const mod = prod.modules?.find(m => m.id === mId) + return mod ? ( +
+ + {mod.name} + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price)} + +
+ ) : null + })}
) })}
Gesamt: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(totalPrice)} + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(totalPrice)} +
+ {!allCategoriesFilled && ( +

+ + Bitte aus jeder Kategorie einen Artikel wählen. +

+ )} - @@ -284,6 +411,7 @@ export function OrderWizard({ products, categories, initialProfile }: { products )} + {/* ───── Step 2: Customer Data ───── */} {step === 2 && ( Kundendaten - Wir benötigen Ihre Daten für die Rechnungsstellung. + + Wir benötigen Ihre Daten für die Rechnungsstellung. + -
- - setCustomerData({...customerData, company_name: e.target.value})} - placeholder="GmbH / Einzelunternehmen" - className="bg-white/5 border-white/10" - /> -
-
- - setCustomerData({...customerData, vat_id: e.target.value})} - placeholder="DE123456789" - className="bg-white/5 border-white/10" - /> -
-
- - setCustomerData({...customerData, first_name: e.target.value})} - className="bg-white/5 border-white/10" - /> -
-
- - setCustomerData({...customerData, last_name: e.target.value})} - className="bg-white/5 border-white/10" - /> -
-
- - setCustomerData({...customerData, address: e.target.value})} - className="bg-white/5 border-white/10" - /> -
-
- - setCustomerData({...customerData, zip_code: e.target.value})} - className="bg-white/5 border-white/10" - /> -
-
- - setCustomerData({...customerData, city: e.target.value})} - className="bg-white/5 border-white/10" - /> -
+ {[ + { label: 'Firmenname', key: 'company_name', placeholder: 'GmbH / Einzelunternehmen', span: false }, + { label: 'USt-IdNr.', key: 'vat_id', placeholder: 'DE123456789', span: false }, + { label: 'Vorname', key: 'first_name', placeholder: '', span: false }, + { label: 'Nachname', key: 'last_name', placeholder: '', span: false }, + { label: 'Straße & Hausnummer', key: 'address', placeholder: '', span: true }, + { label: 'Postleitzahl', key: 'zip_code', placeholder: '', span: false }, + { label: 'Ort', key: 'city', placeholder: '', span: false }, + ].map(({ label, key, placeholder, span }) => ( +
+ + setCustomerData({ ...customerData, [key]: e.target.value })} + placeholder={placeholder} + className="bg-white/5 border-white/10 text-white placeholder:text-slate-500" + /> +
+ ))}
- Bestellung prüfen - Fast fertig! Bitte überprüfen Sie Ihre Auswahl. + Bestellung prüfen + + Fast fertig! Bitte überprüfen Sie Ihre Auswahl. +
-
- {selectedProduct?.name} - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(totalPrice)} / Monat -
-
-

Inklusive: {selectedModules.length > 0 ? selectedModules.map(id => selectedProduct?.modules?.find(m => m.id === id)?.name).join(', ') : 'Keine Zusatzmodule'}

- -

{customerData.company_name}

-

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

+ {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) + return acc + Number(mod?.price ?? 0) + }, 0) + return ( +
+
+ + + {prod.name} + + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(catTotal)} + +
+ {sel.moduleIds.length > 0 && ( +

+ Module: {sel.moduleIds + .map(id => prod.modules?.find(m => m.id === id)?.name) + .filter(Boolean) + .join(', ')} +

+ )} +
+ ) + })} + +
+

{customerData.company_name}

+

+ {customerData.first_name} {customerData.last_name} +

+

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

- -

- Mit dem Klick auf "Kostenpflichtig bestellen" akzeptieren Sie unsere AGB und die Datenschutzerklärung. +

+ Gesamtbetrag: + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(totalPrice)} / Monat + +
+

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

-