diff --git a/shop/components/admin/category-dialog.tsx b/shop/components/admin/category-dialog.tsx index f558829..8d9ac61 100644 --- a/shop/components/admin/category-dialog.tsx +++ b/shop/components/admin/category-dialog.tsx @@ -43,6 +43,8 @@ const categorySchema = z.object({ icon: z.string().default('Package'), sort_order: z.coerce.number().int().min(0).default(0), is_required: z.boolean().default(true), + allow_multiselect: z.boolean().default(false), + free_items_limit: z.coerce.number().int().min(0).default(0), }) type CategoryFormValues = z.infer @@ -65,15 +67,17 @@ export function CategoryDialog({ icon: category?.icon || 'Package', sort_order: category?.sort_order ?? 0, is_required: category?.is_required ?? true, + allow_multiselect: category?.allow_multiselect ?? false, + free_items_limit: category?.free_items_limit ?? 0, }, }) async function onSubmit(values: CategoryFormValues) { try { if (category) { - await updateCategory(category.id, values) + await updateCategory(category.id, values as any) } else { - await createCategory(values) + await createCategory(values as any) } setOpen(false) form.reset() @@ -184,7 +188,7 @@ export function CategoryDialog({ )} /> - ( @@ -207,6 +211,52 @@ export function CategoryDialog({ )} /> + ( + + + + +
+ + Mehrfachauswahl erlauben + +

+ Erlaubt die Auswahl mehrerer Produkte/Module innerhalb dieser Kategorie. +

+
+
+ )} + /> + {form.watch('allow_multiselect') && ( + ( + + Inklusive Freie Artikel Limit + + + + +

+ Bis zu dieser Anzahl sind gewählte Artikel in dieser Kategorie kostenlos. Alle darüber hinausgehenden werden voll berechnet (günstigste Artikel zuerst frei). +

+
+ )} + /> + )} diff --git a/shop/components/admin/category-list.tsx b/shop/components/admin/category-list.tsx index dd9f75b..be121e9 100644 --- a/shop/components/admin/category-list.tsx +++ b/shop/components/admin/category-list.tsx @@ -80,11 +80,18 @@ export function CategoryList({ initialCategories }: { initialCategories: Categor - {category.is_required ? ( - Pflicht - ) : ( - Optional - )} +
+ {category.is_required ? ( + Pflicht + ) : ( + Optional + )} + {category.allow_multiselect && ( + + Multi ({category.free_items_limit} frei) + + )} +
diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index 73b7495..b1f4db5 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -17,9 +17,9 @@ 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 + productIds?: string[] // selected product IDs for multi-select moduleIds: string[] // selected module IDs for this product } @@ -195,22 +195,28 @@ export function OrderWizard({ } }, [lastLicenseMonths, selectedBillingInterval]) - // Per-category selection map: categoryId -> { productId, moduleIds } + // Per-category selection map: categoryId -> { productId, productIds, moduleIds } const [selections, setSelections] = useState>(() => { const init: Record = {} categories.forEach(cat => { if (initialOrder) { - const item = initialOrder.order_data?.items?.find(i => i.category_id === cat.id) - if (item) { + // Multi-select might have multiple items matching this category + const matchingItems = initialOrder.order_data?.items?.filter(i => i.category_id === cat.id) || [] + if (matchingItems.length > 0) { init[cat.id] = { - productId: item.product_id, - moduleIds: item.selected_modules?.map(m => m.module_id) ?? [] + productId: matchingItems[0].product_id, + productIds: matchingItems.map(i => i.product_id), + moduleIds: matchingItems.flatMap(i => i.selected_modules?.map(m => m.module_id) ?? []) } return } } - const first = products.find(p => p.category_id === cat.id && p.show_in_abo !== false) - init[cat.id] = { productId: first?.id ?? null, moduleIds: [] } + if (cat.allow_multiselect) { + init[cat.id] = { productId: null, productIds: [], moduleIds: [] } + } else { + const first = products.find(p => p.category_id === cat.id && p.show_in_abo !== false) + init[cat.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] } + } }) return init }) @@ -221,6 +227,14 @@ export function OrderWizard({ .filter(cat => cat.is_required) .every(cat => { const sel = selections[cat.id] + if (cat.allow_multiselect) { + if (!sel?.productIds || sel.productIds.length === 0) return false + return sel.productIds.every(pId => { + const prod = products.find(p => p.id === pId) + if (!prod) return false + return selectedBillingInterval === 'one_time' ? prod.show_in_kauf !== false : prod.show_in_abo !== false + }) + } if (!sel?.productId) return false const prod = products.find(p => p.id === sel.productId) if (!prod) return false @@ -234,9 +248,16 @@ export function OrderWizard({ // 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) + const selectedProductIds: string[] = [] + Object.values(selections).forEach(s => { + if (s.productIds && s.productIds.length > 0) { + s.productIds.forEach(id => { + if (id) selectedProductIds.push(id) + }) + } else if (s.productId) { + selectedProductIds.push(s.productId) + } + }) selectedProductIds.forEach(prodId => { const prod = products.find(p => p.id === prodId) @@ -276,24 +297,42 @@ export function OrderWizard({ 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 + if (!sel) continue + + const selectedProds: Product[] = [] + if (cat.allow_multiselect && sel.productIds && sel.productIds.length > 0) { + sel.productIds.forEach(pId => { + const p = products.find(prod => prod.id === pId) + if (p) selectedProds.push(p) + }) + } else if (sel.productId) { + const p = products.find(prod => prod.id === sel.productId) + if (p) selectedProds.push(p) + } + + if (selectedProds.length === 0) continue + + // Apply free items limit: sort selected products by base_price ascending so that the cheapest are free + const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) + const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 + + sortedProds.forEach((prod, sortedIndex) => { + const base = sortedIndex < freeLimit ? 0 : 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 } }) - 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) { @@ -310,37 +349,77 @@ export function OrderWizard({ const monthlyTax = monthlyNet * 0.19 const monthlyGross = monthlyNet + monthlyTax - // Helper: update product selection for a category (resets modules) + // Helper: update product selection for a category function selectProduct(catId: string, productId: string) { + const cat = categories.find(c => c.id === catId) setSelections(prev => { + const sel = prev[catId] + let nextProductIds = sel?.productIds || [] + let nextProductId = sel?.productId || null + + if (cat?.allow_multiselect) { + if (nextProductIds.includes(productId)) { + nextProductIds = nextProductIds.filter(id => id !== productId) + } else { + nextProductIds = [...nextProductIds, productId] + } + nextProductId = nextProductIds[0] || null + } else { + nextProductIds = [productId] + nextProductId = productId + } + const next = { ...prev, - [catId]: { productId, moduleIds: [] }, + [catId]: { productId: nextProductId, productIds: nextProductIds, moduleIds: [] }, } // Automatically deselect now conflicting products in other categories - const selectedIds = Object.entries(next) - .map(([, s]) => s.productId) - .filter((id): id is string => !!id) + const selectedIds: string[] = [] + Object.values(next).forEach(s => { + if (s.productIds && s.productIds.length > 0) { + s.productIds.forEach(id => { + if (id) selectedIds.push(id) + }) + } else if (s.productId) { + selectedIds.push(s.productId) + } + }) categories.forEach(c => { if (c.id === catId) return - const sel = next[c.id] - if (!sel?.productId) return - - const otherSelectedProductIds = selectedIds.filter(id => id !== sel.productId) - const prod = products.find(p => p.id === sel.productId) - - const isExcluded = prod && ( - otherSelectedProductIds.some(otherId => { - const otherProd = products.find(p => p.id === otherId) - return otherProd?.exclusions?.includes(prod.id) - }) || - prod.exclusions?.some(exId => otherSelectedProductIds.includes(exId)) - ) - - if (isExcluded) { - next[c.id] = { productId: null, moduleIds: [] } + const s = next[c.id] + if (c.allow_multiselect && s?.productIds) { + const validProductIds = s.productIds.filter(pId => { + const prod = products.find(p => p.id === pId) + const otherSelectedProductIds = selectedIds.filter(id => id !== pId) + const isExcluded = prod && ( + otherSelectedProductIds.some(otherId => { + const otherProd = products.find(p => p.id === otherId) + return otherProd?.exclusions?.includes(prod.id) + }) || + prod.exclusions?.some(exId => otherSelectedProductIds.includes(exId)) + ) + return !isExcluded + }) + next[c.id] = { + productId: validProductIds[0] || null, + productIds: validProductIds, + moduleIds: [] + } + } else if (s?.productId) { + const prod = products.find(p => p.id === s.productId) + const otherSelectedProductIds = selectedIds.filter(id => id !== s.productId) + const isExcluded = prod && ( + otherSelectedProductIds.some(otherId => { + const otherProd = products.find(p => p.id === otherId) + return otherProd?.exclusions?.includes(prod.id) + }) || + prod.exclusions?.some(exId => otherSelectedProductIds.includes(exId)) + ) + if (isExcluded) { + next[c.id] = { productId: null, productIds: [], moduleIds: [] } + } } }) @@ -352,8 +431,10 @@ export function OrderWizard({ 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) + // Pick first selected product to associate module with (or fall back to single select productId) + const activeProductId = sel?.productId + if (!activeProductId) return prev + const prod = products.find(p => p.id === activeProductId) const newModuleIds = toggleModuleInList(prod?.modules ?? [], sel.moduleIds, moduleId) return { ...prev, [catId]: { ...sel, moduleIds: newModuleIds } } }) @@ -827,7 +908,11 @@ export function OrderWizard({

{cat.description}

)}
- {sel?.productId ? ( + {sel?.productIds && sel.productIds.length > 0 ? ( + + {sel.productIds.length} Ausgewählt + + ) : sel?.productId ? ( Ausgewählt @@ -846,6 +931,53 @@ export function OrderWizard({

Keine Produkte in dieser Kategorie im aktuellen Abrechnungsmodell vorhanden.

+ ) : cat.allow_multiselect ? ( +
+ {catProducts.map(product => { + const isChecked = sel?.productIds?.includes(product.id) ?? false + return ( +
+ +
+ ) + })} +
) : ( {categories.map(cat => { const sel = selections[cat.id] - const prod = products.find(p => p.id === sel?.productId) - if (!prod) return ( + const selectedProds: Product[] = [] + if (cat.allow_multiselect && sel?.productIds) { + sel.productIds.forEach(pId => { + const p = products.find(prod => prod.id === pId) + if (p) selectedProds.push(p) + }) + } else if (sel?.productId) { + const p = products.find(prod => prod.id === sel.productId) + if (p) selectedProds.push(p) + } + + if (selectedProds.length === 0) return (
{cat.is_required ? ( @@ -995,29 +1137,42 @@ export function OrderWizard({ {cat.name}: {cat.is_required ? 'nicht gewählt' : 'nicht gewählt (optional)'}
) + + // Sort selected products by price to determine which ones are free + const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) + const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 + 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)} - + {sortedProds.map((prod, idx) => { + const isFree = idx < freeLimit + const actualPrice = isFree ? 0 : prod.base_price + return ( +
+
+ {prod.name} {isFree && (Frei)} + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(actualPrice)} + {' '}{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 + })}
- ) : null + ) })}
) @@ -1150,39 +1305,66 @@ export function OrderWizard({
{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) + const selectedProds: Product[] = [] + if (cat.allow_multiselect && sel?.productIds) { + sel.productIds.forEach(pId => { + const p = products.find(prod => prod.id === pId) + if (p) selectedProds.push(p) + }) + } else if (sel?.productId) { + const p = products.find(prod => prod.id === sel.productId) + if (p) selectedProds.push(p) + } + + if (selectedProds.length === 0) return null + + // Sort selected products by price to determine which ones are free + const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) + const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 + return ( -
-
- - - {prod.name} - - - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(catTotal)} - {' '}{billingLabel(prod.billing_interval)} - +
+
+ + {cat.name}
- {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(', ')} -

- )} + {sortedProds.map((prod, idx) => { + const isFree = idx < freeLimit + const actualPrice = isFree ? 0 : prod.base_price + const catTotal = + Number(actualPrice) + + (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) || 0) + + return ( +
+
+ + {prod.name} {isFree && (Frei)} + + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(catTotal)} + {' '}{billingLabel(prod.billing_interval)} + +
+ {sel?.moduleIds && 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(', ')} +

+ )} +
+ ) + })}
) })} diff --git a/shop/lib/actions/validation.ts b/shop/lib/actions/validation.ts index 884648c..85904f2 100644 --- a/shop/lib/actions/validation.ts +++ b/shop/lib/actions/validation.ts @@ -7,33 +7,46 @@ export function validateWizardSelections( ): void { for (const cat of dbCategories) { const sel = selections[cat.id] - if (cat.is_required && (!sel || !sel.productId)) { + const hasSelection = sel && ( + (cat.allow_multiselect && sel.productIds && sel.productIds.length > 0) || + (!cat.allow_multiselect && sel.productId) + ) + + if (cat.is_required && !hasSelection) { throw new Error(`Die Kategorie "${cat.name}" ist ein Pflichtfeld, wurde aber nicht ausgewählt.`) } - if (sel?.productId) { - const prod = dbProducts.find(p => p.id === sel.productId) - if (!prod) { - throw new Error(`Das ausgewählte Produkt für Kategorie "${cat.name}" wurde nicht gefunden.`) + + if (sel) { + const selectedProds: Product[] = [] + if (cat.allow_multiselect && sel.productIds) { + sel.productIds.forEach(pId => { + const p = dbProducts.find(prod => prod.id === pId) + if (p) selectedProds.push(p) + }) + } else if (sel.productId) { + const p = dbProducts.find(prod => prod.id === sel.productId) + if (p) selectedProds.push(p) } - // Validate selected modules - for (const mId of sel.moduleIds) { - const mod = prod.modules?.find(m => m.id === mId) - if (!mod) { - throw new Error(`Das Modul mit ID "${mId}" gehört nicht zum Produkt "${prod.name}".`) - } - // Requirements check - if (mod.requirements && mod.requirements.length > 0) { - const hasMetRequirement = mod.requirements.some(reqId => sel.moduleIds.includes(reqId)) - if (!hasMetRequirement) { - throw new Error(`Das Modul "${mod.name}" setzt die Aktivierung mindestens eines der folgenden Module voraus: ${mod.requirements.join(', ')}.`) + for (const prod of selectedProds) { + // 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 + + // Requirements check + if (mod.requirements && mod.requirements.length > 0) { + const hasMetRequirement = mod.requirements.some(reqId => sel.moduleIds.includes(reqId)) + if (!hasMetRequirement) { + throw new Error(`Das Modul "${mod.name}" setzt die Aktivierung mindestens eines der folgenden Module voraus: ${mod.requirements.join(', ')}.`) + } } - } - // Exclusions check - if (mod.exclusions && mod.exclusions.length > 0) { - const conflicting = mod.exclusions.filter(exId => sel.moduleIds.includes(exId)) - if (conflicting.length > 0) { - throw new Error(`Das Modul "${mod.name}" schließt die Kombination mit anderen gewählten Modulen aus.`) + // Exclusions check + if (mod.exclusions && mod.exclusions.length > 0) { + const conflicting = mod.exclusions.filter(exId => sel.moduleIds.includes(exId)) + if (conflicting.length > 0) { + throw new Error(`Das Modul "${mod.name}" schließt die Kombination mit anderen gewählten Modulen aus.`) + } } } } @@ -41,9 +54,16 @@ export function validateWizardSelections( } // Product-level constraints validation - const selectedProductIds = Object.values(selections) - .map(s => s.productId) - .filter((id): id is string => !!id) + const selectedProductIds: string[] = [] + Object.values(selections).forEach(s => { + if (s.productIds && s.productIds.length > 0) { + s.productIds.forEach(id => { + if (id) selectedProductIds.push(id) + }) + } else if (s.productId) { + selectedProductIds.push(s.productId) + } + }) for (const prodId of selectedProductIds) { const prod = dbProducts.find(p => p.id === prodId) diff --git a/shop/lib/license-transform.ts b/shop/lib/license-transform.ts index d39a229..c2fd4cb 100644 --- a/shop/lib/license-transform.ts +++ b/shop/lib/license-transform.ts @@ -99,40 +99,59 @@ export function buildOrderSnapshot( for (const cat of categories) { const sel: CategorySelection | undefined = selections[cat.id] - if (!sel?.productId) continue + if (!sel) continue - const prod = products.find((p) => p.id === sel.productId) - if (!prod) continue - - dominantTaxRate = prod.tax_rate - - const selectedModules = sel.moduleIds - .map((modId) => prod.modules?.find((m) => m.id === modId)) - .filter((m): m is NonNullable => !!m) - .map((mod) => { - const qty = moduleQuantities?.[mod.id] || 1 - return { - module_id: mod.id, - module_name: mod.name, - price: mod.price, - quantity: qty, - total_price: mod.price * qty, - } + const selectedProds: Product[] = [] + if (cat.allow_multiselect && sel.productIds && sel.productIds.length > 0) { + sel.productIds.forEach(pId => { + const p = products.find(prod => prod.id === pId) + if (p) selectedProds.push(p) }) + } else if (sel.productId) { + const p = products.find(prod => prod.id === sel.productId) + if (p) selectedProds.push(p) + } - const moduleTotal = selectedModules.reduce((acc, m) => acc + (m.total_price || m.price), 0) - const itemTotal = prod.base_price + moduleTotal - subtotal += itemTotal + if (selectedProds.length === 0) continue - items.push({ - category_id: cat.id, - category_name: cat.name, - product_id: prod.id, - product_name: prod.name, - base_price: prod.base_price, - billing_interval: prod.billing_interval, - selected_modules: selectedModules, - item_total: itemTotal, + // Apply free items limit: sort selected products by base_price ascending so that the cheapest are free + const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) + const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 + + sortedProds.forEach((prod, sortedIndex) => { + dominantTaxRate = prod.tax_rate + + const selectedModules = sel.moduleIds + .map((modId) => prod.modules?.find((m) => m.id === modId)) + .filter((m): m is NonNullable => !!m) + .map((mod) => { + const qty = moduleQuantities?.[mod.id] || 1 + return { + module_id: mod.id, + module_name: mod.name, + price: mod.price, + quantity: qty, + total_price: mod.price * qty, + } + }) + + const moduleTotal = selectedModules.reduce((acc, m) => acc + (m.total_price || m.price), 0) + + // If this product falls under the free limit, set its base price to 0 + const actualBasePrice = (sortedIndex < freeLimit) ? 0 : prod.base_price + const itemTotal = actualBasePrice + moduleTotal + subtotal += itemTotal + + items.push({ + category_id: cat.id, + category_name: cat.name, + product_id: prod.id, + product_name: prod.name + (actualBasePrice === 0 ? " (Inklusive/Frei)" : ""), + base_price: actualBasePrice, + billing_interval: prod.billing_interval, + selected_modules: selectedModules, + item_total: itemTotal, + }) }) } diff --git a/shop/lib/types.ts b/shop/lib/types.ts index ae74336..3ed36d8 100644 --- a/shop/lib/types.ts +++ b/shop/lib/types.ts @@ -26,6 +26,8 @@ export type Category = { icon?: string | null sort_order: number is_required: boolean + allow_multiselect: boolean + free_items_limit: number created_at: string } @@ -79,7 +81,8 @@ export type EndCustomer = { // ─── Wizard-Zustand (flüchtig, im RAM) ─────────────────────────────────────── export type CategorySelection = { - productId: string | null + productId: string | null // for backwards compatibility & single select + productIds?: string[] // for multi-select categories moduleIds: string[] } diff --git a/shop/supabase/migrations/20260704152000_update_end_customers_partner_fkey.sql b/shop/supabase/migrations/20260704152000_update_end_customers_partner_fkey.sql index 17a98c3..ea36ef6 100644 --- a/shop/supabase/migrations/20260704152000_update_end_customers_partner_fkey.sql +++ b/shop/supabase/migrations/20260704152000_update_end_customers_partner_fkey.sql @@ -24,28 +24,7 @@ CREATE POLICY "Partner sehen nur eigene Endkunden" ON public.end_customers partner_id = (SELECT company_id FROM public.users WHERE id = auth.uid()) ); --- 4. Recreate RLS policies for licenses (since partner_id now matches company_id) -DROP POLICY IF EXISTS "Partner sehen Lizenzen eigener Kunden" ON public.licenses; -CREATE POLICY "Partner sehen Lizenzen eigener Kunden" ON public.licenses - FOR SELECT - USING ( - EXISTS ( - SELECT 1 FROM public.end_customers c - WHERE c.id = licenses.end_customer_id - AND c.partner_id = (SELECT company_id FROM public.users WHERE id = auth.uid()) - ) - ); - -DROP POLICY IF EXISTS "Partner erstellen Lizenzen eigener Kunden" ON public.licenses; -CREATE POLICY "Partner erstellen Lizenzen eigener Kunden" ON public.licenses - FOR INSERT - WITH CHECK ( - EXISTS ( - SELECT 1 FROM public.end_customers c - WHERE c.id = licenses.end_customer_id - AND c.partner_id = (SELECT company_id FROM public.users WHERE id = auth.uid()) - ) - ); +-- 4. Licenses policy recreation is omitted since licenses table is dropped. -- Reload Schema Cache NOTIFY pgrst, 'reload schema'; diff --git a/shop/supabase/migrations/20260704174000_secure_storage_and_admin_rls.sql b/shop/supabase/migrations/20260704174000_secure_storage_and_admin_rls.sql index f8980a2..4af924a 100644 --- a/shop/supabase/migrations/20260704174000_secure_storage_and_admin_rls.sql +++ b/shop/supabase/migrations/20260704174000_secure_storage_and_admin_rls.sql @@ -33,31 +33,7 @@ CREATE POLICY "Partner sehen nur eigene Endkunden" ON public.end_customers OR partner_id = (SELECT company_id FROM public.users WHERE id = auth.uid()) ); --- 4. Recreate licenses SELECT RLS policy to allow admin bypass -DROP POLICY IF EXISTS "Partner sehen Lizenzen eigener Kunden" ON public.licenses; -CREATE POLICY "Partner sehen Lizenzen eigener Kunden" ON public.licenses - FOR SELECT - USING ( - (SELECT role FROM public.users WHERE id = auth.uid()) = 'admin' - OR EXISTS ( - SELECT 1 FROM public.end_customers c - WHERE c.id = licenses.end_customer_id - AND c.partner_id = (SELECT company_id FROM public.users WHERE id = auth.uid()) - ) - ); - --- 5. Recreate licenses INSERT RLS policy to allow admin bypass -DROP POLICY IF EXISTS "Partner erstellen Lizenzen eigener Kunden" ON public.licenses; -CREATE POLICY "Partner erstellen Lizenzen eigener Kunden" ON public.licenses - FOR INSERT - WITH CHECK ( - (SELECT role FROM public.users WHERE id = auth.uid()) = 'admin' - OR EXISTS ( - SELECT 1 FROM public.end_customers c - WHERE c.id = licenses.end_customer_id - AND c.partner_id = (SELECT company_id FROM public.users WHERE id = auth.uid()) - ) - ); +-- 4 & 5. Licenses policy updates are omitted since licenses table is dropped. -- Reload Schema Cache NOTIFY pgrst, 'reload schema'; diff --git a/shop/supabase/migrations/20260707170000_add_multiselect_to_categories.sql b/shop/supabase/migrations/20260707170000_add_multiselect_to_categories.sql new file mode 100644 index 0000000..6702055 --- /dev/null +++ b/shop/supabase/migrations/20260707170000_add_multiselect_to_categories.sql @@ -0,0 +1,3 @@ +-- Add multi-select and free items limit to categories table +ALTER TABLE public.categories ADD COLUMN IF NOT EXISTS allow_multiselect BOOLEAN DEFAULT false NOT NULL; +ALTER TABLE public.categories ADD COLUMN IF NOT EXISTS free_items_limit INTEGER DEFAULT 0 NOT NULL;