From f947798bf6cf0bea9d607a53c42f877143c17c0e Mon Sep 17 00:00:00 2001 From: DanielS Date: Fri, 1 May 2026 02:35:22 +0200 Subject: [PATCH] refactor: remove yearly billing interval, keep only one_time and monthly --- shop/app/admin/categories/page.tsx | 6 +- shop/app/admin/layout.tsx | 4 + shop/app/admin/orders/page.tsx | 26 ++-- shop/app/admin/page.tsx | 34 ++--- shop/app/admin/products/page.tsx | 2 +- shop/app/admin/users/page.tsx | 42 +++++++ .../admin/create-product-dialog.tsx | 5 +- shop/components/admin/product-list.tsx | 3 - shop/components/admin/user-dialog.tsx | 113 +++++++++++++++++ shop/components/admin/user-list.tsx | 119 ++++++++++++++++++ shop/components/order-wizard.tsx | 4 +- shop/lib/actions/users.ts | 50 ++++++++ shop/lib/supabase/admin.ts | 16 +++ shop/lib/types.ts | 2 +- shop/supabase/.branches/_current_branch | 1 + shop/supabase/.temp/cli-latest | 1 + ...1003400_remove_yearly_billing_interval.sql | 7 ++ shop/supabase/snippets/Untitled query 657.sql | 1 + 18 files changed, 390 insertions(+), 46 deletions(-) create mode 100644 shop/app/admin/users/page.tsx create mode 100644 shop/components/admin/user-dialog.tsx create mode 100644 shop/components/admin/user-list.tsx create mode 100644 shop/lib/actions/users.ts create mode 100644 shop/lib/supabase/admin.ts create mode 100644 shop/supabase/.branches/_current_branch create mode 100644 shop/supabase/.temp/cli-latest create mode 100644 shop/supabase/migrations/20260501003400_remove_yearly_billing_interval.sql create mode 100644 shop/supabase/snippets/Untitled query 657.sql diff --git a/shop/app/admin/categories/page.tsx b/shop/app/admin/categories/page.tsx index 1814127..cf3afaa 100644 --- a/shop/app/admin/categories/page.tsx +++ b/shop/app/admin/categories/page.tsx @@ -37,10 +37,6 @@ async function CategoryDataWrapper() { async function CreateCategoryAction() { return ( - - - + ) } diff --git a/shop/app/admin/layout.tsx b/shop/app/admin/layout.tsx index bec2bea..44624a2 100644 --- a/shop/app/admin/layout.tsx +++ b/shop/app/admin/layout.tsx @@ -36,6 +36,10 @@ export default function AdminLayout({ Bestellungen + + + Benutzer + Einstellungen diff --git a/shop/app/admin/orders/page.tsx b/shop/app/admin/orders/page.tsx index d44f9e7..f4c02c8 100644 --- a/shop/app/admin/orders/page.tsx +++ b/shop/app/admin/orders/page.tsx @@ -13,8 +13,8 @@ export default async function AdminOrdersPage() {
-

Bestellungen & Rechnungen

-

Verwalten Sie alle Kundenbestellungen und greifen Sie auf generierte PDFs zu.

+

Bestellungen & Rechnungen

+

Verwalten Sie alle Kundenbestellungen und greifen Sie auf generierte PDFs zu.

@@ -43,12 +43,12 @@ async function OrdersData() { - ID - Datum - Kunde - Betrag - Status - Rechnung + ID + Datum + Kunde + Betrag + Status + Rechnung @@ -61,17 +61,17 @@ async function OrdersData() { ) : ( orders?.map((order) => ( - + {order.id.slice(0, 8)}... - + {new Date(order.created_at).toLocaleDateString('de-DE')} -
{order.customer_data?.company_name || 'Privatkunde'}
-
{order.customer_data?.first_name} {order.customer_data?.last_name}
+
{order.customer_data?.company_name || 'Privatkunde'}
+
{order.customer_data?.first_name} {order.customer_data?.last_name}
- + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_price)} diff --git a/shop/app/admin/page.tsx b/shop/app/admin/page.tsx index c97de7c..71c3e93 100644 --- a/shop/app/admin/page.tsx +++ b/shop/app/admin/page.tsx @@ -9,42 +9,42 @@ export default function AdminDashboard() {
- Umsatz gesamt + Umsatz gesamt -
€12,234.56
-

+20.1% zum Vormonat

+
€12,234.56
+

+20.1% zum Vormonat

- Bestellungen + Bestellungen -
+573
-

+12% zur Vorwoche

+
+573
+

+12% zur Vorwoche

- Produkte + Produkte -
12
-

+2 neue Module

+
12
+

+2 neue Module

- Kunden + Kunden -
2,450
-

+180 heute

+
2,450
+

+180 heute

@@ -55,8 +55,8 @@ export default function AdminDashboard() { Übersicht -
- [Chart Platzhalter] +
+ [Umsatz-Chart wird geladen...]
@@ -72,10 +72,10 @@ export default function AdminDashboard() { JD
-

John Doe

-

john.doe@example.com

+

John Doe

+

john.doe@example.com

-
+€450.00
+
+€450.00
))} diff --git a/shop/app/admin/products/page.tsx b/shop/app/admin/products/page.tsx index 5d3a12b..b0f28af 100644 --- a/shop/app/admin/products/page.tsx +++ b/shop/app/admin/products/page.tsx @@ -14,7 +14,7 @@ export default async function AdminProductsPage() { Produkte & Module -

+

Verwalte deine Hauptprodukte und deren zubuchbare Module.

diff --git a/shop/app/admin/users/page.tsx b/shop/app/admin/users/page.tsx new file mode 100644 index 0000000..f517bef --- /dev/null +++ b/shop/app/admin/users/page.tsx @@ -0,0 +1,42 @@ +import { getUsers } from '@/lib/actions/users' +import { UserList } from '@/components/admin/user-list' +import { UserDialog } from '@/components/admin/user-dialog' +import { Users } from 'lucide-react' +import { Suspense } from 'react' + +export default async function AdminUsersPage() { + return ( +
+
+
+

+ + Benutzerverwaltung +

+

+ Verwalten Sie Benutzerkonten, Berechtigungen und Sicherheit. +

+
+
+ +
+
+ }> + + +
+ ) +} + +async function UserDataWrapper() { + try { + const users = await getUsers() + return + } catch (error) { + return ( +
+ Fehler beim Laden der Benutzer. Stellen Sie sicher, dass der SERVICE_ROLE_KEY korrekt konfiguriert ist. +
+ ) + } +} diff --git a/shop/components/admin/create-product-dialog.tsx b/shop/components/admin/create-product-dialog.tsx index c01f933..a63e599 100644 --- a/shop/components/admin/create-product-dialog.tsx +++ b/shop/components/admin/create-product-dialog.tsx @@ -53,7 +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'), + billing_interval: z.enum(['one_time', 'monthly']).default('monthly'), modules: z.array(moduleSchema).default([]), }) @@ -190,11 +190,10 @@ export function CreateProductDialog({ children, categories, product }: { childre render={({ field }) => ( Abrechnungsmodell -
+
{[ { value: 'one_time', label: 'Einmalig', icon: '💳' }, { value: 'monthly', label: 'Monatlich', icon: '🔄' }, - { value: 'yearly', label: 'Jährlich', icon: '📅' }, ].map(opt => ( + + + + Neuen Benutzer anlegen + + Geben Sie die E-Mail und optional ein Passwort für den neuen Benutzer ein. + + + +
+ + ( + + E-Mail + + + + + + )} + /> + ( + + Passwort (optional) + + + + + + )} + /> + + + + + +
+ + ) +} diff --git a/shop/components/admin/user-list.tsx b/shop/components/admin/user-list.tsx new file mode 100644 index 0000000..f76d9e6 --- /dev/null +++ b/shop/components/admin/user-list.tsx @@ -0,0 +1,119 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { UserX, ShieldAlert, Key, Trash2, Mail } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { deleteUser, updateUserStatus, resetPassword } from '@/lib/actions/users' +import { Badge } from '@/components/ui/badge' + +export function UserList({ initialUsers }: { initialUsers: any[] }) { + const router = useRouter() + + const handleDelete = async (id: string) => { + if (confirm('Möchten Sie diesen Benutzer wirklich löschen?')) { + await deleteUser(id) + router.refresh() + } + } + + const handleToggleBan = async (id: string, isBanned: boolean) => { + await updateUserStatus(id, !isBanned) + router.refresh() + } + + const handleResetPassword = async (email: string) => { + try { + await resetPassword(email) + alert('Password-Reset Link generiert (siehe Mailpit/Supabase Log)') + } catch (error) { + alert('Fehler beim Passwort-Reset') + } + } + + return ( + + + Benutzerverwaltung + + Hier können Sie Benutzer neu anlegen, sperren oder Passwörter zurücksetzen. + + + +
+ + + E-Mail + Status + Zuletzt angemeldet + Aktionen + + + + {initialUsers.map((user) => { + const isBanned = !!user.banned_until + return ( + + +
+ + {user.email} +
+
+ + + {isBanned ? "Gesperrt" : "Aktiv"} + + + + {user.last_sign_in_at ? new Date(user.last_sign_in_at).toLocaleString('de-DE') : 'Noch nie'} + + +
+ + + +
+
+
+ ) + })} +
+
+ + + ) +} diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index e8dd73d..8b0861d 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -24,13 +24,11 @@ type CategorySelection = { // ─── 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' } @@ -283,7 +281,7 @@ export function OrderWizard({
{product.name} - {product.billing_interval === 'one_time' ? 'Einmalig' : product.billing_interval === 'yearly' ? 'Abo/Jahr' : 'Abo/Monat'} + {product.billing_interval === 'one_time' ? 'Einmalig' : 'Abo/Monat'}
diff --git a/shop/lib/actions/users.ts b/shop/lib/actions/users.ts new file mode 100644 index 0000000..1d18664 --- /dev/null +++ b/shop/lib/actions/users.ts @@ -0,0 +1,50 @@ +'use server' + +import { createAdminClient } from '@/lib/supabase/admin' +import { revalidatePath } from 'next/cache' + +export async function getUsers() { + const admin = createAdminClient() + const { data: { users }, error } = await admin.auth.admin.listUsers() + if (error) throw error + return users +} + +export async function createUser(data: { email: string; password?: string; email_confirm?: boolean }) { + const admin = createAdminClient() + const { data: { user }, error } = await admin.auth.admin.createUser({ + email: data.email, + password: data.password || Math.random().toString(36).slice(-10), + email_confirm: data.email_confirm ?? true, + }) + if (error) throw error + revalidatePath('/admin/users') + return user +} + +export async function deleteUser(id: string) { + const admin = createAdminClient() + const { error } = await admin.auth.admin.deleteUser(id) + if (error) throw error + revalidatePath('/admin/users') +} + +export async function updateUserStatus(id: string, ban: boolean) { + const admin = createAdminClient() + const { error } = await admin.auth.admin.updateUserById(id, { + ban_duration: ban ? '876000h' : '0h', // 100 years or none + }) + if (error) throw error + revalidatePath('/admin/users') +} + +export async function resetPassword(email: string) { + const admin = createAdminClient() + // Generate a reset link or send email + const { error } = await admin.auth.admin.generateLink({ + type: 'recovery', + email, + }) + if (error) throw error + // Usually you'd send this link or let Supabase handle it via regular auth.resetPasswordForEmail +} diff --git a/shop/lib/supabase/admin.ts b/shop/lib/supabase/admin.ts new file mode 100644 index 0000000..85013d3 --- /dev/null +++ b/shop/lib/supabase/admin.ts @@ -0,0 +1,16 @@ +import { createClient } from '@supabase/supabase-js' + +export function createAdminClient() { + const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU' + + return createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + serviceRoleKey, + { + auth: { + autoRefreshToken: false, + persistSession: false + } + } + ) +} diff --git a/shop/lib/types.ts b/shop/lib/types.ts index 510cccb..a3be56f 100644 --- a/shop/lib/types.ts +++ b/shop/lib/types.ts @@ -4,7 +4,7 @@ export type Product = { description?: string | null | undefined; base_price: number; is_active: boolean; - billing_interval: 'one_time' | 'monthly' | 'yearly'; + billing_interval: 'one_time' | 'monthly'; created_at: string; updated_at: string; category_id?: string | null; diff --git a/shop/supabase/.branches/_current_branch b/shop/supabase/.branches/_current_branch new file mode 100644 index 0000000..88d050b --- /dev/null +++ b/shop/supabase/.branches/_current_branch @@ -0,0 +1 @@ +main \ No newline at end of file diff --git a/shop/supabase/.temp/cli-latest b/shop/supabase/.temp/cli-latest new file mode 100644 index 0000000..4deee8a --- /dev/null +++ b/shop/supabase/.temp/cli-latest @@ -0,0 +1 @@ +v2.95.4 \ No newline at end of file diff --git a/shop/supabase/migrations/20260501003400_remove_yearly_billing_interval.sql b/shop/supabase/migrations/20260501003400_remove_yearly_billing_interval.sql new file mode 100644 index 0000000..79dd377 --- /dev/null +++ b/shop/supabase/migrations/20260501003400_remove_yearly_billing_interval.sql @@ -0,0 +1,7 @@ +-- Remove yearly from the billing_interval constraint, leaving only one_time and monthly +ALTER TABLE public.products DROP CONSTRAINT IF EXISTS products_billing_interval_check; +ALTER TABLE public.products ADD CONSTRAINT products_billing_interval_check + CHECK (billing_interval IN ('one_time', 'monthly')); + +-- Update any existing yearly products to monthly +UPDATE public.products SET billing_interval = 'monthly' WHERE billing_interval = 'yearly'; diff --git a/shop/supabase/snippets/Untitled query 657.sql b/shop/supabase/snippets/Untitled query 657.sql new file mode 100644 index 0000000..25e4f11 --- /dev/null +++ b/shop/supabase/snippets/Untitled query 657.sql @@ -0,0 +1 @@ +update auth.users set email_confirmed_at = now() where email = 'test@test.com'; \ No newline at end of file