diff --git a/shop/components/admin/create-product-dialog.tsx b/shop/components/admin/create-product-dialog.tsx index 2cd870a..c01f933 100644 --- a/shop/components/admin/create-product-dialog.tsx +++ b/shop/components/admin/create-product-dialog.tsx @@ -53,6 +53,7 @@ const productSchema = z.object({ base_price: z.coerce.number().min(0, 'Preis darf nicht negativ sein'), category_id: z.string().min(1, 'Bitte wählen Sie eine Kategorie'), is_active: z.boolean().default(true), + billing_interval: z.enum(['one_time', 'monthly', 'yearly']).default('monthly'), modules: z.array(moduleSchema).default([]), }) @@ -69,6 +70,7 @@ export function CreateProductDialog({ children, categories, product }: { childre base_price: product?.base_price || 0, category_id: product?.category_id || '', is_active: product?.is_active ?? true, + billing_interval: product?.billing_interval || 'monthly', modules: product?.modules?.map(m => ({ name: m.name, description: m.description || '', @@ -121,7 +123,7 @@ export function CreateProductDialog({ children, categories, product }: { childre {product ? 'Produkt bearbeiten' : 'Neues Produkt erstellen'} - + Definieren Sie das Basisprodukt und fügen Sie optionale oder erforderliche Module hinzu. @@ -136,9 +138,9 @@ export function CreateProductDialog({ children, categories, product }: { childre name="name" render={({ field }) => ( - Produktname + Produktname - + @@ -149,9 +151,9 @@ export function CreateProductDialog({ children, categories, product }: { childre name="base_price" render={({ field }) => ( - Basispreis (€) + Basispreis (€) - + @@ -164,16 +166,16 @@ export function CreateProductDialog({ children, categories, product }: { childre name="category_id" render={({ field }) => ( - Kategorie + Kategorie @@ -182,14 +184,46 @@ export function CreateProductDialog({ children, categories, product }: { childre )} /> + ( + + Abrechnungsmodell +
+ {[ + { value: 'one_time', label: 'Einmalig', icon: '💳' }, + { value: 'monthly', label: 'Monatlich', icon: '🔄' }, + { value: 'yearly', label: 'Jährlich', icon: '📅' }, + ].map(opt => ( + + ))} +
+ +
+ )} + /> + ( - Beschreibung + Beschreibung - + @@ -233,9 +267,9 @@ export function CreateProductDialog({ children, categories, product }: { childre name={`modules.${index}.name`} render={({ field }) => ( - Modulname + Modulname - + @@ -246,9 +280,9 @@ export function CreateProductDialog({ children, categories, product }: { childre name={`modules.${index}.price`} render={({ field }) => ( - Aufpreis (€) + Aufpreis (€) - + @@ -262,9 +296,9 @@ export function CreateProductDialog({ children, categories, product }: { childre name={`modules.${index}.requirements`} render={({ field }) => ( - Benötigt (IDs, kommagetrennt) + Benötigt (IDs, kommagetrennt) - + @@ -275,9 +309,9 @@ export function CreateProductDialog({ children, categories, product }: { childre name={`modules.${index}.exclusions`} render={({ field }) => ( - Schließt aus (IDs, kommagetrennt) + Schließt aus (IDs, kommagetrennt) - + @@ -298,7 +332,7 @@ export function CreateProductDialog({ children, categories, product }: { childre />
- Erforderlich + Erforderlich
)} @@ -308,7 +342,7 @@ export function CreateProductDialog({ children, categories, product }: { childre ))} {fields.length === 0 && ( -
+
Noch keine Module hinzugefügt.
)} diff --git a/shop/components/admin/product-list.tsx b/shop/components/admin/product-list.tsx index 43bf2c7..49f8158 100644 --- a/shop/components/admin/product-list.tsx +++ b/shop/components/admin/product-list.tsx @@ -32,7 +32,7 @@ export function ProductList({ initialProducts, categories }: { initialProducts: Produktübersicht - + Alle konfigurierten Software-Lösungen und Erweiterungen. @@ -40,17 +40,17 @@ export function ProductList({ initialProducts, categories }: { initialProducts: - Name - Basispreis - Module - Status - Aktionen + Name + Basispreis + Module + Status + Aktionen {products.map((product) => ( - +
{product.name} @@ -59,15 +59,24 @@ export function ProductList({ initialProducts, categories }: { initialProducts: {product.category.name} )} + {product.billing_interval === 'monthly' && ( + Abo/Monat + )} + {product.billing_interval === 'yearly' && ( + Abo/Jahr + )} + {product.billing_interval === 'one_time' && ( + Einmalig + )}
-

{product.description}

+

{product.description}

- {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(product.base_price)} + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(product.base_price)}
- {product.modules?.length || 0} Module + {product.modules?.length || 0} Module
@@ -96,7 +105,7 @@ export function ProductList({ initialProducts, categories }: { initialProducts: ))} {products.length === 0 && ( - + Keine Produkte gefunden. Erstellen Sie Ihr erstes Produkt! diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index a2b84f5..e8dd73d 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -21,6 +21,19 @@ type CategorySelection = { moduleIds: string[] // selected module IDs for this product } +// ─── Billing interval helpers ───────────────────────────────────────────────── +function billingLabel(interval?: string) { + if (interval === 'one_time') return 'einmalig' + if (interval === 'yearly') return '/ Jahr' + return '/ Monat' +} + +function billingBadgeClass(interval?: string) { + if (interval === 'one_time') return 'bg-slate-500/20 text-slate-300 border-slate-500/30' + if (interval === 'yearly') return 'bg-purple-500/20 text-purple-400 border-purple-500/30' + return 'bg-blue-500/20 text-blue-400 border-blue-500/30' +} + // ─── Icon helper ────────────────────────────────────────────────────────────── function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) { const Icon = icon === 'Cloud' ? Cloud : icon === 'Utensils' ? Utensils : icon === 'HardDrive' ? HardDrive : Package @@ -267,12 +280,18 @@ export function OrderWizard({ className="flex flex-col items-start p-4 rounded-xl border-2 border-white/5 bg-white/5 hover:bg-white/10 peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary/5 cursor-pointer transition-all" >
- {product.name} +
+ {product.name} + + {product.billing_interval === 'one_time' ? 'Einmalig' : product.billing_interval === 'yearly' ? 'Abo/Jahr' : 'Abo/Monat'} + +
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', - }).format(product.base_price)} + }).format(product.base_price)}{' '} + {billingLabel(product.billing_interval)}
{product.description && ( @@ -374,6 +393,7 @@ export function OrderWizard({ {prod.name} {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(prod.base_price)} + {' '}{billingLabel(prod.billing_interval)} {sel.moduleIds.map(mId => { @@ -511,6 +531,7 @@ export function OrderWizard({ {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(catTotal)} + {' '}{billingLabel(prod.billing_interval)} {sel.moduleIds.length > 0 && ( diff --git a/shop/lib/types.ts b/shop/lib/types.ts index b5acaae..510cccb 100644 --- a/shop/lib/types.ts +++ b/shop/lib/types.ts @@ -4,6 +4,7 @@ export type Product = { description?: string | null | undefined; base_price: number; is_active: boolean; + billing_interval: 'one_time' | 'monthly' | 'yearly'; created_at: string; updated_at: string; category_id?: string | null; diff --git a/shop/supabase/migrations/20260501003000_add_billing_interval_to_products.sql b/shop/supabase/migrations/20260501003000_add_billing_interval_to_products.sql new file mode 100644 index 0000000..62ef514 --- /dev/null +++ b/shop/supabase/migrations/20260501003000_add_billing_interval_to_products.sql @@ -0,0 +1,4 @@ +-- Add billing_interval to products to support subscription and one-time products +-- Values: 'one_time', 'monthly', 'yearly' +ALTER TABLE public.products ADD COLUMN IF NOT EXISTS billing_interval TEXT DEFAULT 'monthly' NOT NULL + CHECK (billing_interval IN ('one_time', 'monthly', 'yearly'));