feat(shop): move linked fee configuration to product level
All checks were successful
Staging Build / build (push) Successful in 2m52s
All checks were successful
Staging Build / build (push) Successful in 2m52s
This commit is contained in:
@@ -302,31 +302,8 @@ export function WysiwygAdminClient({
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(prod.modules || []).length > 0 ? (
|
||||
(prod.modules || []).map((m) => (
|
||||
<span key={m.id} className="inline-flex items-center gap-2 px-2.5 py-1 rounded text-[10px] bg-white/10 text-slate-300 font-medium">
|
||||
<span>{m.name}</span>
|
||||
<select
|
||||
value={m.linked_fee_product_id || ''}
|
||||
onChange={async (e) => {
|
||||
const val = e.target.value || null
|
||||
const updatedModules = (prod.modules || []).map(mod =>
|
||||
mod.id === m.id ? { ...mod, linked_fee_product_id: val } : mod
|
||||
)
|
||||
setProducts(products.map(p => p.id === prod.id ? { ...p, modules: updatedModules } : p))
|
||||
await updateProduct(prod.id, {}, updatedModules)
|
||||
}}
|
||||
className="bg-slate-900 border border-white/10 text-[9px] text-slate-400 focus:ring-0 cursor-pointer rounded px-1 py-0.5 outline-none max-w-[120px]"
|
||||
title="Verknüpfte Gebühr"
|
||||
>
|
||||
<option value="">Keine Gebühr</option>
|
||||
{products
|
||||
.filter(p => p.billing_interval === 'monthly')
|
||||
.map(p => (
|
||||
<option key={p.id} value={p.id}>
|
||||
+ {p.name} ({p.base_price} €)
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
<span key={m.id} className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] bg-white/10 text-slate-300 font-medium">
|
||||
{m.name}
|
||||
<button
|
||||
onClick={async () => {
|
||||
const updatedModules = (prod.modules || []).filter(mod => mod.id !== m.id)
|
||||
@@ -425,6 +402,30 @@ export function WysiwygAdminClient({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verknüpfte monatliche Gebühr */}
|
||||
<div className="space-y-1 mt-2">
|
||||
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold block">Verknüpfte Gebühr:</span>
|
||||
<select
|
||||
value={prod.linked_fee_product_id || ''}
|
||||
onChange={async (e) => {
|
||||
const val = e.target.value || null
|
||||
setProducts(products.map(p => p.id === prod.id ? { ...p, linked_fee_product_id: val } : p))
|
||||
await updateProduct(prod.id, { linked_fee_product_id: val }, prod.modules || [])
|
||||
}}
|
||||
className="w-full bg-slate-950 border border-white/10 text-[10px] text-slate-300 focus:ring-0 cursor-pointer rounded px-2 py-1 outline-none"
|
||||
>
|
||||
<option value="">Keine Gebühr verknüpft</option>
|
||||
{products
|
||||
.filter(p => p.billing_interval === 'monthly' && p.id !== prod.id)
|
||||
.map(p => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name} ({new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(p.base_price)} / mtl.)
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Modul hinzufügen Popover */}
|
||||
<div className="mt-3 pt-2 border-t border-white/5">
|
||||
<ModulePopover
|
||||
|
||||
@@ -99,12 +99,18 @@ export async function POST(request: Request) {
|
||||
if (item.selections) {
|
||||
for (const catId in item.selections) {
|
||||
const sel = item.selections[catId];
|
||||
sel.moduleIds?.forEach((mId: string) => {
|
||||
for (const p of products) {
|
||||
const mod = p.modules?.find((m: any) => m.id === mId);
|
||||
if (mod && mod.linked_fee_product_id) {
|
||||
feeProductIds.add(mod.linked_fee_product_id);
|
||||
}
|
||||
|
||||
if (sel.productId) {
|
||||
const p = products.find((prod: any) => prod.id === sel.productId);
|
||||
if (p && p.linked_fee_product_id) {
|
||||
feeProductIds.add(p.linked_fee_product_id);
|
||||
}
|
||||
}
|
||||
|
||||
sel.productIds?.forEach((pId: string) => {
|
||||
const p = products.find((prod: any) => prod.id === pId);
|
||||
if (p && p.linked_fee_product_id) {
|
||||
feeProductIds.add(p.linked_fee_product_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -429,6 +429,10 @@ export function OrderWizard({
|
||||
oneTime += basePrice
|
||||
}
|
||||
|
||||
if (prod.linked_fee_product_id) {
|
||||
feeProductIds.add(prod.linked_fee_product_id)
|
||||
}
|
||||
|
||||
// Module
|
||||
sel.moduleIds?.forEach((mId: string) => {
|
||||
const mod = prod.modules?.find(m => m.id === mId)
|
||||
@@ -439,10 +443,6 @@ export function OrderWizard({
|
||||
} else {
|
||||
oneTime += mod.price * qty
|
||||
}
|
||||
|
||||
if (mod.linked_fee_product_id) {
|
||||
feeProductIds.add(mod.linked_fee_product_id)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function getProducts() {
|
||||
const supabase = await createClient()
|
||||
const { data, error } = await supabase
|
||||
.from('products')
|
||||
.select('*, category:categories(*), modules:product_modules!product_modules_product_id_fkey(*)')
|
||||
.select('*, category:categories(*), modules:product_modules(*)')
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
if (error) throw error
|
||||
@@ -90,7 +90,6 @@ export async function createProduct(
|
||||
requirements: m.requirements || [],
|
||||
exclusions: m.exclusions || [],
|
||||
has_quantity: m.has_quantity ?? false,
|
||||
linked_fee_product_id: m.linked_fee_product_id || null,
|
||||
}))
|
||||
const { error: modulesError } = await supabase
|
||||
.from('product_modules')
|
||||
@@ -128,7 +127,6 @@ export async function updateProduct(id: string, product: Partial<Product>, modul
|
||||
requirements: m.requirements || [],
|
||||
exclusions: m.exclusions || [],
|
||||
has_quantity: m.has_quantity ?? false,
|
||||
linked_fee_product_id: m.linked_fee_product_id || null,
|
||||
}))
|
||||
const { error: modulesError } = await supabase
|
||||
.from('product_modules')
|
||||
|
||||
@@ -18,6 +18,7 @@ export type Product = {
|
||||
requirements?: string[]
|
||||
exclusions?: string[]
|
||||
allow_update_discount?: boolean
|
||||
linked_fee_product_id?: string | null
|
||||
}
|
||||
|
||||
export type Category = {
|
||||
@@ -46,7 +47,6 @@ export type ProductModule = {
|
||||
exclusions: string[] // UUID[] von Modulen, die nicht gleichzeitig aktiv sein dürfen
|
||||
created_at: string
|
||||
has_quantity?: boolean
|
||||
linked_fee_product_id?: string | null
|
||||
}
|
||||
|
||||
export type Profile = {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Remove linked_fee_product_id from product_modules
|
||||
ALTER TABLE public.product_modules
|
||||
DROP COLUMN IF EXISTS linked_fee_product_id;
|
||||
|
||||
-- Add linked_fee_product_id to products
|
||||
ALTER TABLE public.products
|
||||
ADD COLUMN IF NOT EXISTS linked_fee_product_id UUID REFERENCES public.products(id) ON DELETE SET NULL;
|
||||
Reference in New Issue
Block a user