From 1c3191876828d85a71bc96d8fb63ff5efd5304c8 Mon Sep 17 00:00:00 2001 From: DanielS Date: Wed, 22 Jul 2026 15:09:05 +0200 Subject: [PATCH] feat(shop): add dynamic linked fees to modules --- shop/app/admin/wysiwyg/wysiwyg-client.tsx | 27 +++- shop/app/api/orders/checkout/route.ts | 40 ++++++ shop/components/order-wizard.tsx | 115 ++++++++++++++++-- shop/components/wizard/step-summary.tsx | 26 ++++ shop/lib/actions/products.ts | 2 + shop/lib/types.ts | 1 + ...260722100000_add_linked_fee_to_modules.sql | 3 + shop/supabase/seed.sql | 15 ++- 8 files changed, 208 insertions(+), 21 deletions(-) create mode 100644 shop/supabase/migrations/20260722100000_add_linked_fee_to_modules.sql diff --git a/shop/app/admin/wysiwyg/wysiwyg-client.tsx b/shop/app/admin/wysiwyg/wysiwyg-client.tsx index 1a9c3b8..f403542 100644 --- a/shop/app/admin/wysiwyg/wysiwyg-client.tsx +++ b/shop/app/admin/wysiwyg/wysiwyg-client.tsx @@ -302,8 +302,31 @@ export function WysiwygAdminClient({
{(prod.modules || []).length > 0 ? ( (prod.modules || []).map((m) => ( - - {m.name} + + {m.name} +
))} + {linkedFeeProducts && linkedFeeProducts.length > 0 && ( +
+
+ Zusätzliche Dienste & Gebühren + Einmalig berechnet +
+ {linkedFeeProducts.map(p => ( +
+
+ {p.name} + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(p.base_price)} + {' '}{p.billing_interval === 'monthly' ? '/ mtl.' : 'einmalig'} + +
+ {p.description && ( +

+ {p.description} +

+ )} +
+ ))} +
+ )}
{selectedEndCustomer ? ( diff --git a/shop/lib/actions/products.ts b/shop/lib/actions/products.ts index 13ec6f5..f44f169 100644 --- a/shop/lib/actions/products.ts +++ b/shop/lib/actions/products.ts @@ -90,6 +90,7 @@ 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') @@ -127,6 +128,7 @@ export async function updateProduct(id: string, product: Partial, 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') diff --git a/shop/lib/types.ts b/shop/lib/types.ts index 8d5fe5c..f7d4971 100644 --- a/shop/lib/types.ts +++ b/shop/lib/types.ts @@ -46,6 +46,7 @@ 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 = { diff --git a/shop/supabase/migrations/20260722100000_add_linked_fee_to_modules.sql b/shop/supabase/migrations/20260722100000_add_linked_fee_to_modules.sql new file mode 100644 index 0000000..331de7d --- /dev/null +++ b/shop/supabase/migrations/20260722100000_add_linked_fee_to_modules.sql @@ -0,0 +1,3 @@ +-- Add linked_fee_product_id to product_modules table +ALTER TABLE public.product_modules +ADD COLUMN IF NOT EXISTS linked_fee_product_id UUID REFERENCES public.products(id) ON DELETE SET NULL; diff --git a/shop/supabase/seed.sql b/shop/supabase/seed.sql index 5637f91..42519f9 100644 --- a/shop/supabase/seed.sql +++ b/shop/supabase/seed.sql @@ -1,13 +1,16 @@ -- Seed Products -INSERT INTO public.products (id, name, description, base_price, tax_rate) +INSERT INTO public.products (id, name, description, base_price, tax_rate, billing_interval, show_in_abo, show_in_kauf) VALUES -('d1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'CASPOS Cloud', 'Die modulare Cloud-Lösung für Ihren Einzelhandel.', 49.00, 19.00), -('d2a2a2a2-a2a2-a2a2-a2a2-a2a2a2a2a2a2', 'CASPOS Gastro', 'Spezialisiert auf Gastronomie mit Tischplan und Funkbonieren.', 79.00, 19.00); +('d1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'CASPOS Cloud', 'Die modulare Cloud-Lösung für Ihren Einzelhandel.', 49.00, 19.00, 'monthly', true, true), +('d2a2a2a2-a2a2-a2a2-a2a2-a2a2-a2a2-a2a2a2a2', 'CASPOS Gastro', 'Spezialisiert auf Gastronomie mit Tischplan und Funkbonieren.', 79.00, 19.00, 'monthly', true, true), +('prod-poscloud-fee', 'POS Cloud Grundgebühr', 'Monatliche Grundgebühr für die POS Cloud Nutzung.', 19.00, 19.00, 'monthly', false, false); -- Seed Modules for CASPOS Cloud INSERT INTO public.product_modules (id, product_id, name, description, price, requirements, exclusions) VALUES ('m1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Bestandsführung Pro', 'Erweiterte Lagerverwaltung.', 15.00, '{}', '{}'), -('m2a2a2a2-a2a2-a2a2-a2a2-a2a2a2a2a2a2', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'DATEV Export', 'Direkte Schnittstelle zum Steuerberater.', 10.00, '{}', '{}'), -('m3a3a3a3-a3a3-a3a3-a3a3-a3a3a3a3a3a3', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Filialverwaltung', 'Zentrale Steuerung mehrerer Standorte.', 25.00, '{"m1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1"}', '{}'), -('m4a4a4a4-a4a4-a4a4-a4a4-a4a4a4a4a4a4', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Small Business Modus', 'Reduzierter Funktionsumfang für Kleinunternehmer.', 0.00, '{}', '{"m1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1", "m3a3a3a3-a3a3-a3a3-a3a3-a3a3a3a3a3a3"}'); +('m2a2a2a2-a2a2-a2a2-a2a2-a2a2-a2a2-a2a2a2a2', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'DATEV Export', 'Direkte Schnittstelle zum Steuerberater.', 10.00, '{}', '{}'), +('m3a3a3a3-a3a3-a3a3-a3a3-a3a3-a3a3-a3a3-a3a3', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Filialverwaltung', 'Zentrale Steuerung mehrerer Standorte.', 25.00, '{"m1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1"}', '{}'), +('m4a4a4a4-a4a4-a4a4-a4a4-a4a4-a4a4-a4a4-a4a4', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Small Business Modus', 'Reduzierter Funktionsumfang für Kleinunternehmer.', 0.00, '{}', '{"m1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1", "m3a3a3a3-a3a3-a3a3-a3a3-a3a3-a3a3-a3a3-a3a3"}'), +('m-poscloud-cloud', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Schnittstelle POS Cloud', 'Anbindung an die POS Cloud.', 0.00, '{}', '{}'), +('m-poscloud-gastro', 'd2a2a2a2-a2a2-a2a2-a2a2-a2a2-a2a2-a2a2a2a2', 'Schnittstelle POS Cloud', 'Anbindung an die POS Cloud.', 0.00, '{}', '{}');