feat(checkout): implement wizard and admin actions
All checks were successful
Staging Build / build (push) Successful in 3m11s

This commit is contained in:
DanielS
2026-07-20 23:18:40 +02:00
parent 2b372be228
commit 86b63ce77e
5 changed files with 190 additions and 11 deletions

View File

@@ -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');

View File

@@ -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);

View File

@@ -567,6 +567,7 @@ export function OrderWizard({
customerProfile: customerData,
endCustomerId: selectedEndCustomerId,
endCustomer: selectedEndCustomer,
lastLicenseDate: lastLicenseDate || null,
})
})

View File

@@ -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<any>;
}
) {
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,

View File

@@ -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<Order[]> {
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<EndCustomer[]> {
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[]
}