From 86b63ce77e7cb9164fb47072c8d76657a29eefcb Mon Sep 17 00:00:00 2001 From: DanielS Date: Mon, 20 Jul 2026 23:18:40 +0200 Subject: [PATCH] feat(checkout): implement wizard and admin actions --- shop/app/actions/checkout.ts | 9 +- shop/app/api/orders/checkout/route.ts | 8 +- shop/components/order-wizard.tsx | 1 + shop/lib/actions/orders.ts | 147 +++++++++++++++++++++++++- shop/lib/actions/queries.ts | 36 +++++++ 5 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 shop/lib/actions/queries.ts diff --git a/shop/app/actions/checkout.ts b/shop/app/actions/checkout.ts index 985791a..d6d591e 100644 --- a/shop/app/actions/checkout.ts +++ b/shop/app/actions/checkout.ts @@ -182,8 +182,9 @@ export async function checkoutAction(params: { items: any[]; endCustomerId: string; customerProfile?: any; + lastLicenseDate?: string | null; }) { - const { items, endCustomerId, customerProfile } = params; + const { items, endCustomerId, customerProfile, lastLicenseDate } = params; const supabase = await createClient(); // 1. Session & Auth Check @@ -256,7 +257,8 @@ export async function checkoutAction(params: { products, categories, type === 'purchase' ? 'one_time' : 'monthly', - item.moduleQuantities + item.moduleQuantities, + type === 'purchase' ? lastLicenseDate : null ); const itemsWithDevice = itemSnapshot.items.map(i => ({ ...i, @@ -273,7 +275,8 @@ export async function checkoutAction(params: { total: total, tax_rate: 19, tax_amount: Math.round(total * 0.19 * 100) / 100, - subtotal: Math.round((total / 1.19) * 100) / 100 + subtotal: Math.round((total / 1.19) * 100) / 100, + last_license_date: type === 'purchase' ? lastLicenseDate : null }; const orderNumber = generateOrderNumber(type === 'purchase' ? 'AE' : 'BE'); diff --git a/shop/app/api/orders/checkout/route.ts b/shop/app/api/orders/checkout/route.ts index 99d300c..5eab220 100644 --- a/shop/app/api/orders/checkout/route.ts +++ b/shop/app/api/orders/checkout/route.ts @@ -42,7 +42,7 @@ export async function POST(request: Request) { } const body = await request.json(); - const { items, customerProfile, endCustomerId, endCustomer } = body; + const { items, customerProfile, endCustomerId, endCustomer, lastLicenseDate } = body; if (!items || !Array.isArray(items) || items.length === 0) { return NextResponse.json({ error: 'Warenkorb leer' }, { status: 400 }); @@ -84,7 +84,8 @@ export async function POST(request: Request) { products, categories, type === 'purchase' ? 'one_time' : 'monthly', - item.moduleQuantities + item.moduleQuantities, + type === 'purchase' ? lastLicenseDate : null ); const itemsWithDevice = itemSnapshot.items.map(i => ({ ...i, @@ -101,7 +102,8 @@ export async function POST(request: Request) { total: total, tax_rate: 19, // default tax_amount: Math.round(total * 0.19 * 100) / 100, - subtotal: Math.round((total / 1.19) * 100) / 100 + subtotal: Math.round((total / 1.19) * 100) / 100, + last_license_date: type === 'purchase' ? lastLicenseDate : null }; const orderHash = hashOrderSnapshot(orderSnapshot); diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index 59e2e5f..b92771f 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -567,6 +567,7 @@ export function OrderWizard({ customerProfile: customerData, endCustomerId: selectedEndCustomerId, endCustomer: selectedEndCustomer, + lastLicenseDate: lastLicenseDate || null, }) }) diff --git a/shop/lib/actions/orders.ts b/shop/lib/actions/orders.ts index 74f7ab8..6139c33 100644 --- a/shop/lib/actions/orders.ts +++ b/shop/lib/actions/orders.ts @@ -332,9 +332,9 @@ export async function updateOrderStatus( throw new Error(`Fehler beim Aktualisieren des Status: ${updateError?.message || 'Unbekannt'}`) } - // 3. Wenn Status auf 'active' wechselt, generiere PDF und lade es hoch (pdf-invoice-generator) + // 3. Wenn Status auf 'active' oder 'approved' wechselt, generiere PDF und lade es hoch (pdf-invoice-generator) let attachmentBuffer: Buffer | null = null - if (newStatus === 'active') { + if (newStatus === 'active' || newStatus === 'approved') { try { const customerSnapshot = updatedOrder.customer_data const orderSnapshot = updatedOrder.order_data @@ -362,7 +362,7 @@ export async function updateOrderStatus( updatedOrder.pdf_url = fileName } } catch (pdfError) { - console.error('PDF Generation Error on active status transition:', pdfError) + console.error('PDF Generation Error on status transition:', pdfError) } } @@ -395,8 +395,8 @@ export async function updateOrderStatus( html: statusEmail.html, } - // Bei Aktivierung hänge die PDF an - if (newStatus === 'active' && attachmentBuffer) { + // Bei Aktivierung oder Freigabe hänge die PDF an + if ((newStatus === 'active' || newStatus === 'approved') && attachmentBuffer) { mailOptions.attachments = [ { filename: `Anfragebestaetigung_${orderNumber}.pdf`, @@ -418,6 +418,143 @@ export async function updateOrderStatus( return updatedOrder as Order } +export async function updateOrderPayload( + orderId: string, + params: { + orderSnapshot: Partial; + } +) { + const admin = createAdminClient() + const supabase = await createClient() + + // Authentifizierung und Admin-Prüfung + const { data: { user } } = await supabase.auth.getUser() + if (!user) throw new Error('Not authenticated') + + const { data: dbUser } = await admin + .from('users') + .select('role') + .eq('id', user.id) + .single() + + if (dbUser?.role !== 'admin') { + throw new Error('Nur Administratoren können Bestellanfragen anpassen.') + } + + // Aktuelle Bestellung abrufen + const { data: order, error: fetchError } = await admin + .from('orders') + .select('*') + .eq('id', orderId) + .single() + + if (fetchError || !order) { + throw new Error(`Bestellung nicht gefunden: ${fetchError?.message || 'Unbekannt'}`) + } + + if (order.status !== 'in_review') { + throw new Error('Bestellungen können nur im Status "in_review" angepasst werden.') + } + + // Neue Snapshot-Daten mergen + const updatedSnapshot = { + ...order.order_data, + ...params.orderSnapshot + } + + const { data: updatedOrder, error: updateError } = await admin + .from('orders') + .update({ + order_data: updatedSnapshot, + total_price: updatedSnapshot.total || order.total_price + }) + .eq('id', orderId) + .select() + .single() + + if (updateError || !updatedOrder) { + throw new Error(`Fehler beim Aktualisieren der Bestelldaten: ${updateError?.message || 'Unbekannt'}`) + } + + revalidatePath('/admin/orders') + revalidatePath('/my-orders') + return updatedOrder as Order +} + +export async function rejectOrder( + orderId: string, + reason: string +) { + const admin = createAdminClient() + const supabase = await createClient() + + // Authentifizierung und Admin-Prüfung + const { data: { user } } = await supabase.auth.getUser() + if (!user) throw new Error('Not authenticated') + + const { data: dbUser } = await admin + .from('users') + .select('role') + .eq('id', user.id) + .single() + + if (dbUser?.role !== 'admin') { + throw new Error('Nur Administratoren können Bestellungen ablehnen.') + } + + const { data: order, error: fetchError } = await admin + .from('orders') + .select('*') + .eq('id', orderId) + .single() + + if (fetchError || !order) { + throw new Error(`Bestellung nicht gefunden: ${fetchError?.message || 'Unbekannt'}`) + } + + const updatedSnapshot = { + ...order.order_data, + rejection_reason: reason + } + + const { data: updatedOrder, error: updateError } = await admin + .from('orders') + .update({ + status: 'rejected', + order_data: updatedSnapshot + }) + .eq('id', orderId) + .select() + .single() + + if (updateError || !updatedOrder) { + throw new Error(`Fehler beim Ablehnen der Bestellung: ${updateError?.message || 'Unbekannt'}`) + } + + // Ablehnungs-Mail senden + if (order.user_id) { + try { + const { data: { user: orderUser }, error: userError } = await admin.auth.admin.getUserById(order.user_id) + if (!userError && orderUser && orderUser.email) { + const orderNumber = order.order_number || order.id.slice(0, 8) + const statusEmail = getStatusEmailTemplate(orderNumber, 'Wartet auf Freigabe', `Abgelehnt (Grund: ${reason})`) + + await sendMail({ + to: orderUser.email, + subject: `Anfrage abgelehnt: ${orderNumber}`, + text: statusEmail.text, + html: statusEmail.html + }) + } + } catch (mailError) { + console.error('Failed to send rejection email:', mailError) + } + } + + revalidatePath('/admin/orders') + revalidatePath('/my-orders') + return updatedOrder as Order +} export async function updateOrder( orderId: string, diff --git a/shop/lib/actions/queries.ts b/shop/lib/actions/queries.ts new file mode 100644 index 0000000..e304dcb --- /dev/null +++ b/shop/lib/actions/queries.ts @@ -0,0 +1,36 @@ +'use server' + +import { createClient } from '@/lib/supabase/server' +import type { Order, EndCustomer } from '@/lib/types' + +/** + * Holt alle Bestellanfragen. + * RLS filtert automatisch auf die eigene Firma (bzw. lässt Admins alle sehen). + */ +export async function getCompanyOrders(): Promise { + const supabase = await createClient() + + const { data, error } = await supabase + .from('orders') + .select('*') + .order('created_at', { ascending: false }) + + if (error) throw error + return data as Order[] +} + +/** + * Holt alle Endkunden. + * RLS filtert automatisch auf die eigene Firma (bzw. lässt Admins alle sehen). + */ +export async function getCompanyEndCustomers(): Promise { + const supabase = await createClient() + + const { data, error } = await supabase + .from('end_customers') + .select('*') + .order('company_name', { ascending: true }) + + if (error) throw error + return data as EndCustomer[] +}