Files
webshop/shop/components/order-wizard.tsx

424 lines
20 KiB
TypeScript

'use client'
import { useState, useMemo } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Product, ProductModule, Profile } 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, CreditCard, ShieldCheck, Cloud, Utensils, HardDrive, Package } 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 }) {
const [step, setStep] = useState(1)
const [activeCategory, setActiveCategory] = useState<string>(categories[0]?.id || 'all')
const [customerData, setCustomerData] = useState<Partial<Profile>>(initialProfile || {
company_name: '',
vat_id: '',
first_name: '',
last_name: '',
address: '',
city: '',
zip_code: ''
})
const filteredProducts = useMemo(() => {
if (activeCategory === 'all') return products
return products.filter(p => p.category_id === activeCategory)
}, [products, activeCategory])
const [selectedProduct, setSelectedProduct] = useState<Product | null>(filteredProducts[0] || null)
const [selectedModules, setSelectedModules] = useState<string[]>([])
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)
})
return total
}, [selectedProduct, selectedModules])
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 {
await submitOrder({
product_id: selectedProduct.id,
selected_modules: selectedModules,
total_amount: totalPrice,
customer_details: customerData
})
alert('Bestellung erfolgreich aufgegeben!')
window.location.href = '/'
} catch (error) {
console.error(error)
alert('Fehler bei der Bestellung.')
}
}
return (
<div className="max-w-4xl mx-auto py-12 px-4">
{/* Progress Stepper */}
<div className="flex justify-between mb-12 relative">
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-white/10 -translate-y-1/2 z-0" />
{[1, 2, 3].map((s) => (
<div
key={s}
className={`relative z-10 w-10 h-10 rounded-full flex items-center justify-center transition-all duration-500 ${
step >= s ? 'bg-primary text-white scale-110 shadow-[0_0_15px_rgba(var(--primary),0.5)]' : 'bg-slate-800 text-slate-400'
}`}
>
{step > s ? <Check className="w-6 h-6" /> : s}
</div>
))}
</div>
<AnimatePresence mode="wait">
{step === 1 && (
<motion.div
key="step1"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-6"
>
<div className="grid md:grid-cols-3 gap-6">
<div className="md:col-span-2 space-y-6">
<Card className="glass-dark border-white/10">
<CardHeader>
<CardTitle className="text-2xl flex items-center gap-2">
<ShoppingCart className="w-6 h-6 text-primary" />
Software wählen
</CardTitle>
<CardDescription>Wählen Sie Ihr Basispaket und optionale Erweiterungen.</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Category Tabs */}
<div className="space-y-4">
<Label className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Kategorie</Label>
<Tabs value={activeCategory} onValueChange={(val) => {
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([])
}
}}>
<TabsList className="bg-white/5 border border-white/10 p-1 h-auto grid grid-cols-2 md:grid-cols-4 gap-2">
<TabsTrigger value="all" className="data-[state=active]:bg-primary flex items-center gap-2 py-2">
<Package className="w-4 h-4" /> Alle
</TabsTrigger>
{categories.map(cat => {
const Icon = cat.icon === 'Cloud' ? Cloud : cat.icon === 'Utensils' ? Utensils : cat.icon === 'HardDrive' ? HardDrive : Package
return (
<TabsTrigger key={cat.id} value={cat.id} className="data-[state=active]:bg-primary flex items-center gap-2 py-2">
<Icon className="w-4 h-4" /> {cat.name}
</TabsTrigger>
)
})}
</TabsList>
</Tabs>
</div>
<Separator className="bg-white/10" />
<div className="space-y-4">
<Label className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Lösung</Label>
<RadioGroup
value={selectedProduct?.id}
onValueChange={(id) => {
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 => (
<div key={product.id} className="relative">
<RadioGroupItem value={product.id} id={product.id} className="peer sr-only" />
<Label
htmlFor={product.id}
className="flex flex-col items-start p-4 rounded-xl border-2 border-white/5 bg-white/5 hover:bg-white/10 peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary/5 cursor-pointer transition-all"
>
<div className="flex justify-between w-full items-center">
<span className="font-bold text-lg">{product.name}</span>
{product.category && (
<Badge variant="outline" className="text-[10px] uppercase border-primary/30 text-primary">
{product.category.name}
</Badge>
)}
</div>
<span className="text-sm text-muted-foreground">{product.description}</span>
<span className="mt-2 text-primary font-semibold">
Ab {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(product.base_price)}
</span>
</Label>
</div>
))}
</RadioGroup>
</div>
{selectedProduct && selectedProduct.modules && selectedProduct.modules.length > 0 && (
<div className="space-y-4 pt-4">
<Label className="text-lg font-semibold">Zusatzmodule</Label>
<div className="grid gap-3">
{selectedProduct.modules.map(module => (
<div
key={module.id}
className="flex items-center space-x-3 p-3 rounded-lg border border-white/5 bg-white/5 hover:bg-white/10 transition-colors"
>
<Checkbox
id={module.id}
checked={selectedModules.includes(module.id)}
onCheckedChange={() => toggleModule(module.id)}
disabled={isModuleDisabled(module)}
/>
<div className="flex-1">
<Label
htmlFor={module.id}
className={`font-medium cursor-pointer flex justify-between ${isModuleDisabled(module) ? 'opacity-50' : ''}`}
>
<span>{module.name}</span>
<span className="text-primary">+{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(module.price)}</span>
</Label>
<p className="text-xs text-muted-foreground">{module.description}</p>
{isModuleDisabled(module) && (
<p className="text-[10px] text-destructive mt-1">
{module.requirements && module.requirements.some(reqId => !selectedModules.includes(reqId))
? "Benötigt weitere Module"
: "Nicht kombinierbar mit aktueller Auswahl"}
</p>
)}
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card className="glass-dark border-primary/20 sticky top-6">
<CardHeader>
<CardTitle>Zusammenfassung</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Basispreis:</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(selectedProduct?.base_price || 0)}</span>
</div>
{selectedModules.map(modId => {
const mod = selectedProduct?.modules?.find(m => m.id === modId)
return (
<div key={modId} className="flex justify-between text-sm">
<span className="text-muted-foreground">{mod?.name}:</span>
<span>+{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod?.price || 0)}</span>
</div>
)
})}
<Separator className="bg-white/10" />
<div className="flex justify-between text-xl font-bold text-gradient">
<span>Gesamt:</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(totalPrice)}</span>
</div>
</CardContent>
<CardFooter>
<Button className="w-full h-12 text-lg" onClick={nextStep} disabled={!selectedProduct}>
Weiter <ChevronRight className="ml-2 w-5 h-5" />
</Button>
</CardFooter>
</Card>
</div>
</div>
</motion.div>
)}
{step === 2 && (
<motion.div
key="step2"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-6"
>
<Card className="glass-dark border-white/10">
<CardHeader>
<CardTitle className="text-2xl flex items-center gap-2">
<User className="w-6 h-6 text-primary" />
Kundendaten
</CardTitle>
<CardDescription>Wir benötigen Ihre Daten für die Rechnungsstellung.</CardDescription>
</CardHeader>
<CardContent className="grid md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label>Firmenname</Label>
<Input
value={customerData.company_name || ''}
onChange={(e) => setCustomerData({...customerData, company_name: e.target.value})}
placeholder="GmbH / Einzelunternehmen"
className="bg-white/5 border-white/10"
/>
</div>
<div className="space-y-2">
<Label>USt-IdNr.</Label>
<Input
value={customerData.vat_id || ''}
onChange={(e) => setCustomerData({...customerData, vat_id: e.target.value})}
placeholder="DE123456789"
className="bg-white/5 border-white/10"
/>
</div>
<div className="space-y-2">
<Label>Vorname</Label>
<Input
value={customerData.first_name || ''}
onChange={(e) => setCustomerData({...customerData, first_name: e.target.value})}
className="bg-white/5 border-white/10"
/>
</div>
<div className="space-y-2">
<Label>Nachname</Label>
<Input
value={customerData.last_name || ''}
onChange={(e) => setCustomerData({...customerData, last_name: e.target.value})}
className="bg-white/5 border-white/10"
/>
</div>
<div className="md:col-span-2 space-y-2">
<Label>Straße & Hausnummer</Label>
<Input
value={customerData.address || ''}
onChange={(e) => setCustomerData({...customerData, address: e.target.value})}
className="bg-white/5 border-white/10"
/>
</div>
<div className="space-y-2">
<Label>Postleitzahl</Label>
<Input
value={customerData.zip_code || ''}
onChange={(e) => setCustomerData({...customerData, zip_code: e.target.value})}
className="bg-white/5 border-white/10"
/>
</div>
<div className="space-y-2">
<Label>Ort</Label>
<Input
value={customerData.city || ''}
onChange={(e) => setCustomerData({...customerData, city: e.target.value})}
className="bg-white/5 border-white/10"
/>
</div>
</CardContent>
<CardFooter className="flex justify-between border-t border-white/10 pt-6">
<Button variant="ghost" onClick={prevStep}>
<ChevronLeft className="mr-2 w-4 h-4" /> Zurück
</Button>
<Button onClick={nextStep}>
Prüfung & Abschluss <ChevronRight className="ml-2 w-4 h-4" />
</Button>
</CardFooter>
</Card>
</motion.div>
)}
{step === 3 && (
<motion.div
key="step3"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="space-y-6 text-center"
>
<Card className="glass-dark border-primary/30 max-w-2xl mx-auto shadow-primary/10 shadow-2xl">
<CardHeader>
<div className="w-20 h-20 bg-primary/20 rounded-full flex items-center justify-center mx-auto mb-4 border border-primary/50">
<ShieldCheck className="w-10 h-10 text-primary" />
</div>
<CardTitle className="text-3xl">Bestellung prüfen</CardTitle>
<CardDescription>Fast fertig! Bitte überprüfen Sie Ihre Auswahl.</CardDescription>
</CardHeader>
<CardContent className="space-y-6 text-left">
<div className="p-4 rounded-lg bg-white/5 space-y-4">
<div className="flex justify-between font-bold text-lg">
<span>{selectedProduct?.name}</span>
<span>{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(totalPrice)} / Monat</span>
</div>
<div className="text-sm text-muted-foreground space-y-1">
<p>Inklusive: {selectedModules.length > 0 ? selectedModules.map(id => selectedProduct?.modules?.find(m => m.id === id)?.name).join(', ') : 'Keine Zusatzmodule'}</p>
<Separator className="bg-white/10 my-2" />
<p>{customerData.company_name}</p>
<p>{customerData.address}, {customerData.zip_code} {customerData.city}</p>
</div>
</div>
<p className="text-xs text-muted-foreground italic">
Mit dem Klick auf "Kostenpflichtig bestellen" akzeptieren Sie unsere AGB und die Datenschutzerklärung.
</p>
</CardContent>
<CardFooter className="flex flex-col gap-4">
<Button className="w-full h-14 text-xl font-bold bg-primary hover:bg-primary/90" onClick={handleSubmit}>
Kostenpflichtig bestellen
</Button>
<Button variant="ghost" onClick={prevStep} className="w-full">
Noch etwas ändern
</Button>
</CardFooter>
</Card>
</motion.div>
)}
</AnimatePresence>
</div>
)
}