diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index e08bcd1..85801e8 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -10,13 +10,15 @@ 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 } from 'lucide-react' +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, initialProfile }: { products: Product[], initialProfile: Profile | null }) { +export function OrderWizard({ products, categories, initialProfile }: { products: Product[], categories: Category[], initialProfile: Profile | null }) { const [step, setStep] = useState(1) - const [selectedProduct, setSelectedProduct] = useState(products[0] || null) - const [selectedModules, setSelectedModules] = useState([]) + const [activeCategory, setActiveCategory] = useState(categories[0]?.id || 'all') const [customerData, setCustomerData] = useState>(initialProfile || { company_name: '', vat_id: '', @@ -27,12 +29,20 @@ export function OrderWizard({ products, initialProfile }: { products: Product[], zip_code: '' }) + 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([]) + 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.additional_price) + if (mod) total += Number(mod.price) }) return total }, [selectedProduct, selectedModules]) @@ -41,9 +51,38 @@ export function OrderWizard({ products, initialProfile }: { products: Product[], const prevStep = () => setStep(s => s - 1) const toggleModule = (id: string) => { - setSelectedModules(prev => - prev.includes(id) ? prev.filter(m => m !== id) : [...prev, id] - ) + 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 () => { @@ -100,33 +139,73 @@ export function OrderWizard({ products, initialProfile }: { products: Product[], Wählen Sie Ihr Basispaket und optionale Erweiterungen. - { - const p = products.find(prod => prod.id === id) - if (p) { - setSelectedProduct(p) - setSelectedModules([]) // Reset modules when product changes + {/* 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([]) } - }} - className="grid gap-4" - > - {products.map(product => ( -
- - -
- ))} - + }}> + + + Alle + + {categories.map(cat => { + const Icon = cat.icon === 'Cloud' ? Cloud : cat.icon === 'Utensils' ? Utensils : cat.icon === 'HardDrive' ? HardDrive : Package + return ( + + {cat.name} + + ) + })} + +
+
+ + + +
+ + { + 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 && (
@@ -141,13 +220,24 @@ export function OrderWizard({ products, initialProfile }: { products: Product[], id={module.id} checked={selectedModules.includes(module.id)} onCheckedChange={() => toggleModule(module.id)} + disabled={isModuleDisabled(module)} />
-
))} @@ -173,7 +263,7 @@ export function OrderWizard({ products, initialProfile }: { products: Product[], return (
{mod?.name}: - +{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod?.additional_price || 0)} + +{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod?.price || 0)}
) })} diff --git a/shop/lib/types.ts b/shop/lib/types.ts index 6057ba5..174d3cb 100644 --- a/shop/lib/types.ts +++ b/shop/lib/types.ts @@ -6,17 +6,29 @@ export type Product = { is_active: boolean; created_at: string; updated_at: string; + category_id?: string | null; + category?: Category | null; modules?: ProductModule[]; }; +export type Category = { + id: string; + name: string; + description?: string | null; + icon?: string | null; + created_at: string; +}; + export type ProductModule = { id: string; product_id: string; name: string; description?: string | null | undefined; - additional_price: number; + price: number; is_required: boolean; is_active?: boolean; + requirements: string[]; + exclusions: string[]; created_at: string; }; diff --git a/shop/supabase/migrations/20240501000000_initial_schema.sql b/shop/supabase/migrations/20240501000000_initial_schema.sql new file mode 100644 index 0000000..e350986 --- /dev/null +++ b/shop/supabase/migrations/20240501000000_initial_schema.sql @@ -0,0 +1,50 @@ +-- Enable UUID extension +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Products Table +CREATE TABLE IF NOT EXISTS public.products ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + description TEXT, + base_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + tax_rate DECIMAL(5, 2) NOT NULL DEFAULT 19.00, + created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL +); + +-- Product Modules Table +CREATE TABLE IF NOT EXISTS public.product_modules ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + product_id UUID REFERENCES public.products(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT, + price DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + requirements UUID[] DEFAULT '{}', + exclusions UUID[] DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL +); + +-- Orders Table +CREATE TABLE IF NOT EXISTS public.orders ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES auth.users(id), + customer_data JSONB NOT NULL, + order_data JSONB NOT NULL, + total_price DECIMAL(10, 2) NOT NULL, + pdf_url TEXT, + status TEXT NOT NULL DEFAULT 'pending', + created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL +); + +-- Enable Row Level Security +ALTER TABLE public.products ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.product_modules ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY; + +-- Policies +-- Products: Read for everyone, Write for authenticated admins (simplified for now) +CREATE POLICY "Allow public read access on products" ON public.products FOR SELECT USING (true); +CREATE POLICY "Allow public read access on product_modules" ON public.product_modules FOR SELECT USING (true); + +-- Orders: Users can only see their own orders +CREATE POLICY "Users can view their own orders" ON public.orders FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY "Users can create their own orders" ON public.orders FOR INSERT WITH CHECK (auth.uid() = user_id);