From 43e6a92663dfba704d92a10adb96c03c4e12b3f9 Mon Sep 17 00:00:00 2001 From: DanielS Date: Fri, 10 Jul 2026 15:27:55 +0200 Subject: [PATCH] feat: implement category flags, drop global tables, fix sidebar sticky layout --- shop/app/api/orders/checkout/route.ts | 9 ++++ shop/components/order-wizard.tsx | 51 +++++++++++++----- shop/components/wizard/summary-sidebar.tsx | 10 ++-- shop/lib/actions/validation.ts | 16 ++++-- shop/lib/wizard-state.ts | 60 ---------------------- 5 files changed, 64 insertions(+), 82 deletions(-) diff --git a/shop/app/api/orders/checkout/route.ts b/shop/app/api/orders/checkout/route.ts index ed2be74..99d300c 100644 --- a/shop/app/api/orders/checkout/route.ts +++ b/shop/app/api/orders/checkout/route.ts @@ -8,6 +8,8 @@ import { renderToBuffer } from '@react-pdf/renderer'; import React from 'react'; import { InvoicePDF } from '@/components/invoice-pdf'; import { getOrderEmailTemplate, buildEmailItemsSection } from '@/lib/actions/email-templates'; +import { validateWizardSelections } from '@/lib/actions/validation'; + function generateOrderNumber(prefix: 'BE' | 'AE' = 'AE'): string { const year = new Date().getFullYear(); @@ -49,6 +51,13 @@ export async function POST(request: Request) { const products = await getProducts(); const categories = await getCategories(); + // --- Backend-Validierung: alle Kategorie-Flags für jede Kasse prüfen --- + for (const item of items) { + if (item.selections) { + validateWizardSelections(item.selections, products, categories); + } + } + // Split items into purchase and subscription const purchaseItems = items.filter( (item: any) => item.billingInterval === 'one_time' || item.billingType === 'purchase' diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index 0ee17cf..42c79cf 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -593,31 +593,54 @@ export function OrderWizard({ function selectProduct(catId: string, productId: string) { setSelections(prev => { const cat = categories.find(c => c.id === catId) + + // Build updated selection for this category + let updatedCatSel: CategorySelection if (cat?.allow_multiselect) { const currentIds = prev[catId]?.productIds || [] const nextIds = currentIds.includes(productId) ? currentIds.filter(id => id !== productId) : [...currentIds, productId] - return { - ...prev, - [catId]: { - productId: nextIds[0] || null, - productIds: nextIds, - moduleIds: prev[catId]?.moduleIds || [] - } + updatedCatSel = { + productId: nextIds[0] || null, + productIds: nextIds, + moduleIds: prev[catId]?.moduleIds || [] } } else { const isCurrentlyChecked = prev[catId]?.productId === productId const nextId = isCurrentlyChecked ? null : productId - return { - ...prev, - [catId]: { - productId: nextId, - productIds: nextId ? [nextId] : [], - moduleIds: [] - } + updatedCatSel = { + productId: nextId, + productIds: nextId ? [nextId] : [], + moduleIds: [] } } + + // If this category resets others, rebuild all other categories to their initial state + if (cat?.resets_others) { + const resetSels: Record = {} + categories.forEach(c => { + if (c.id === catId) { + resetSels[c.id] = updatedCatSel + } else { + const isVisible = selectedBillingInterval === 'one_time' ? c.show_in_kauf !== false : c.show_in_abo !== false + if (!isVisible || c.allow_multiselect || c.preselect === false) { + resetSels[c.id] = { productId: null, productIds: [], moduleIds: [] } + } else { + const first = products.find(p => + p.category_id === c.id && + (selectedBillingInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false) + ) + resetSels[c.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] } + } + } + }) + // Also reset module quantities for other categories + setModuleQuantities({}) + return resetSels + } + + return { ...prev, [catId]: updatedCatSel } }) } diff --git a/shop/components/wizard/summary-sidebar.tsx b/shop/components/wizard/summary-sidebar.tsx index 3ea8159..a0625ef 100644 --- a/shop/components/wizard/summary-sidebar.tsx +++ b/shop/components/wizard/summary-sidebar.tsx @@ -69,12 +69,12 @@ export function SummarySidebar({ prevStep, }: SummarySidebarProps) { return ( -
- - +
+ + Zusammenfassung - + {visibleCategories.map(cat => { const sel = selections[cat.id] const selectedProds: Product[] = [] @@ -261,7 +261,7 @@ export function SummarySidebar({
)} - +
0) || (!cat.allow_multiselect && sel.productId) @@ -17,6 +19,13 @@ export function validateWizardSelections( } if (sel) { + // --- allow_multiselect = false: keine doppelten IDs erlaubt --- + if (!cat.allow_multiselect && sel.productIds && sel.productIds.length > 1) { + throw new Error( + `Die Kategorie "${cat.name}" erlaubt keine Mehrfachauswahl. Bitte nur ein Produkt wählen.` + ) + } + const selectedProds: Product[] = [] if (cat.allow_multiselect && sel.productIds) { sel.productIds.forEach(pId => { @@ -32,8 +41,8 @@ export function validateWizardSelections( // Validate selected modules for (const mId of sel.moduleIds) { const mod = prod.modules?.find(m => m.id === mId) - if (!mod) continue // module might belong to another selected product in the same category, so skip error if it's not this one's - + if (!mod) continue + // Requirements check if (mod.requirements && mod.requirements.length > 0) { const hasMetRequirement = mod.requirements.some(reqId => sel.moduleIds.includes(reqId)) @@ -53,7 +62,7 @@ export function validateWizardSelections( } } - // Product-level constraints validation + // --- Product-level constraints validation --- const selectedProductIds: string[] = [] Object.values(selections).forEach(s => { if (s.productIds && s.productIds.length > 0) { @@ -92,3 +101,4 @@ export function validateWizardSelections( } } } + diff --git a/shop/lib/wizard-state.ts b/shop/lib/wizard-state.ts index e70dad8..bcdaab0 100644 --- a/shop/lib/wizard-state.ts +++ b/shop/lib/wizard-state.ts @@ -1,65 +1,5 @@ import { Product, ProductModule, Category, WizardSelections } from './types'; -export interface GlobalInclusion { - trigger_product_id: string; - included_module_id: string; -} - -export interface GlobalExclusion { - product_id: string; - excluded_product_id?: string | null; - excluded_module_id?: string | null; -} - -/** - * Handle changes to the base product. - * Resets selected modules that are excluded by global exclusions for the new product. - * Automatically adds globally included modules for the new product. - */ -export function handleBaseProductChange( - newProductId: string, - currentSelections: WizardSelections, - modules: ProductModule[], - inclusions: GlobalInclusion[], - exclusions: GlobalExclusion[] -): WizardSelections { - const updated: WizardSelections = JSON.parse(JSON.stringify(currentSelections)); - - // Find all modules that are excluded for the new product - const excludedModuleIds = exclusions - .filter(ex => ex.product_id === newProductId && ex.excluded_module_id) - .map(ex => ex.excluded_module_id as string); - - // Find all modules that are automatically included for the new product - const includedModuleIds = inclusions - .filter(inc => inc.trigger_product_id === newProductId) - .map(inc => inc.included_module_id); - - // Iterate over categories and filter modules - for (const categoryId in updated) { - const selection = updated[categoryId]; - - // Remove excluded modules - selection.moduleIds = selection.moduleIds.filter( - mid => !excludedModuleIds.includes(mid) - ); - - // Add automatically included modules if they belong to this category - for (const incId of includedModuleIds) { - const module = modules.find(m => m.id === incId); - if (module && module.product_id === newProductId) { - // Find category of module. In our schema, modules might belong to a category. - // Assuming we check if module belongs to current category: - // We'll append it if the selections is for that category (or we can resolve category from module) - // Let's assume we map it by checking module metadata or category_id if available. - // For simplicity: if the module is already in the DB and matches, we add it. - } - } - } - - return updated; -} - /** * Handle toggling of a module selection. * Handles category-specific allow_multiselect rules.