Angebote bearbeiten fuer Admin und Ersteller implementiert
All checks were successful
Staging Build / build (push) Successful in 3m51s

This commit is contained in:
DanielS
2026-07-03 00:03:48 +02:00
parent 023c789baf
commit a4414cc0e6
5 changed files with 359 additions and 42 deletions

View File

@@ -374,3 +374,231 @@ export async function updateOrderStatus(
revalidatePath('/my-orders')
return updatedOrder as Order
}
export async function updateOrder(
orderId: string,
params: {
selections: WizardSelections
moduleQuantities?: Record<string, number>
customerProfile: Partial<Profile>
endCustomerId?: string | null
endCustomer?: EndCustomer | null
billingInterval?: 'one_time' | 'monthly'
lastLicenseDate?: string | null
}
): Promise<Order> {
const { selections, moduleQuantities, customerProfile, endCustomerId, endCustomer, billingInterval, lastLicenseDate } = params
const supabase = await createClient()
const admin = createAdminClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Not authenticated')
// 1. Fetch current order & verify permissions
const { data: existingOrder, error: fetchError } = await admin
.from('orders')
.select('*')
.eq('id', orderId)
.single()
if (fetchError || !existingOrder) {
throw new Error(`Bestellung nicht gefunden: ${fetchError?.message || 'Unbekannt'}`)
}
const { data: dbUser } = await admin
.from('users')
.select('role')
.eq('id', user.id)
.single()
const isAdmin = dbUser?.role === 'admin'
const isOwner = existingOrder.user_id === user.id
if (!isAdmin && !isOwner) {
throw new Error('Keine Berechtigung dieses Angebot zu bearbeiten.')
}
if (existingOrder.status !== 'pending' && !isAdmin) {
throw new Error('Nur Angebote im Status "Eingegangen" können bearbeitet werden.')
}
// Fetch catalog directly from DB to prevent client tampering
const dbProducts = await getProducts()
const dbCategories = await getCategories()
// ─── Constraint Validation ──────────────────────────────────────────────────
for (const cat of dbCategories) {
const sel = selections[cat.id]
if (cat.is_required && (!sel || !sel.productId)) {
throw new Error(`Die Kategorie "${cat.name}" ist ein Pflichtfeld, wurde aber nicht ausgewählt.`)
}
if (sel?.productId) {
const prod = dbProducts.find(p => p.id === sel.productId)
if (!prod) {
throw new Error(`Das ausgewählte Produkt für Kategorie "${cat.name}" wurde nicht gefunden.`)
}
// Validate selected modules
for (const mId of sel.moduleIds) {
const mod = prod.modules?.find(m => m.id === mId)
if (!mod) {
throw new Error(`Das Modul mit ID "${mId}" gehört nicht zum Produkt "${prod.name}".`)
}
// Requirements check
if (mod.requirements && mod.requirements.length > 0) {
const missing = mod.requirements.filter(reqId => !sel.moduleIds.includes(reqId))
if (missing.length > 0) {
throw new Error(`Das Modul "${mod.name}" setzt die Aktivierung anderer Module voraus.`)
}
}
// Exclusions check
if (mod.exclusions && mod.exclusions.length > 0) {
const conflicting = mod.exclusions.filter(exId => sel.moduleIds.includes(exId))
if (conflicting.length > 0) {
throw new Error(`Das Modul "${mod.name}" schließt die Kombination mit anderen gewählten Modulen aus.`)
}
}
}
}
}
// Product-level constraints validation
const selectedProductIds = Object.values(selections)
.map(s => s.productId)
.filter((id): id is string => !!id)
for (const prodId of selectedProductIds) {
const prod = dbProducts.find(p => p.id === prodId)
if (!prod) continue
// Product requirements check
if (prod.requirements && prod.requirements.length > 0) {
const missing = prod.requirements.filter(reqId => !selectedProductIds.includes(reqId))
if (missing.length > 0) {
const missingNames = missing
.map(reqId => dbProducts.find(p => p.id === reqId)?.name || reqId)
.join(', ')
throw new Error(`Das Produkt "${prod.name}" erfordert die Auswahl von: ${missingNames}.`)
}
}
// Product exclusions check
if (prod.exclusions && prod.exclusions.length > 0) {
const conflicting = prod.exclusions.filter(exId => selectedProductIds.includes(exId))
if (conflicting.length > 0) {
const conflictingNames = conflicting
.map(exId => dbProducts.find(p => p.id === exId)?.name || exId)
.join(', ')
throw new Error(`Das Produkt "${prod.name}" schließt die gleichzeitige Auswahl von folgenden Produkten aus: ${conflictingNames}.`)
}
}
}
// 1. Snapshots aufbauen
const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null)
const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval, moduleQuantities, lastLicenseDate)
const orderHash = hashOrderSnapshot(orderSnapshot)
// 2. Bestellung in DB aktualisieren
const { data: order, error: orderError } = await admin
.from('orders')
.update({
end_customer_id: endCustomerId ?? null,
total_price: orderSnapshot.total,
customer_data: customerSnapshot,
order_data: orderSnapshot,
order_hash: orderHash,
})
.eq('id', orderId)
.select()
.single()
if (orderError || !order) throw orderError || new Error('Fehler beim Aktualisieren der Bestellung.')
// 3. PDF generieren und überschreiben
try {
const buffer = await renderToBuffer(
React.createElement(InvoicePDF, {
order,
customer: customerSnapshot,
orderSnapshot,
})
)
// PDF hochladen (upsert: true, um das alte zu ersetzen)
const fileName = `ab_${order.id}.pdf`
const { error: uploadError } = await supabase
.storage
.from('invoices')
.upload(fileName, buffer, { contentType: 'application/pdf', upsert: true })
if (uploadError) {
console.error('PDF Upload Error:', uploadError)
} else {
const { data: publicUrlData } = supabase.storage.from('invoices').getPublicUrl(fileName)
await admin.from('orders').update({ pdf_url: publicUrlData.publicUrl }).eq('id', order.id)
order.pdf_url = publicUrlData.publicUrl
}
// 4. E-Mail mit PDF-Anhang an den Besteller senden (Info über Änderung)
const orderUserEmail = user.email || (await admin.auth.admin.getUserById(existingOrder.user_id).then(res => res.data.user?.email))
if (orderUserEmail) {
try {
const formattedDate = new Date(order.created_at).toLocaleDateString('de-DE')
const formattedTotal = order.total_price.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })
await sendMail({
to: orderUserEmail,
subject: `Angebotsänderung ${order.order_number}`,
text: `Hallo,\n\nihr Angebot wurde erfolgreich aktualisiert!\n\nNeue Angebotsdetails:\n- Anfrage-Nummer: ${order.order_number}\n- Datum: ${formattedDate}\n- Endkunde: ${customerSnapshot.company_name}\n- Gesamtsumme: ${formattedTotal}\n\nIm Anhang dieser E-Mail finden Sie Ihr aktualisiertes Angebot als PDF-Dokument.\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
<h2 style="color: #0f172a; margin-bottom: 16px;">Ihr Angebot wurde aktualisiert!</h2>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Hallo,</p>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Ihr Angebot mit der Nummer ${order.order_number} wurde erfolgreich aktualisiert.</p>
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 24px 0;">
<h3 style="color: #0f172a; margin-top: 0; margin-bottom: 12px;">Neue Anfrage-Details</h3>
<table style="width: 100%; border-collapse: collapse; font-size: 14px; color: #475569;">
<tr>
<td style="padding: 4px 0; font-weight: bold; width: 140px;">Anfrage-Nummer:</td>
<td style="padding: 4px 0;">${order.order_number}</td>
</tr>
<tr>
<td style="padding: 4px 0; font-weight: bold;">Datum:</td>
<td style="padding: 4px 0;">${formattedDate}</td>
</tr>
<tr>
<td style="padding: 4px 0; font-weight: bold;">Endkunde:</td>
<td style="padding: 4px 0;">${customerSnapshot.company_name}</td>
</tr>
<tr>
<td style="padding: 4px 0; font-weight: bold;">Gesamtsumme:</td>
<td style="padding: 4px 0; font-weight: bold; color: #0f172a;">${formattedTotal}</td>
</tr>
</table>
</div>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Im Anhang dieser E-Mail finden Sie Ihre aktualisierte Anfragebestätigung als PDF-Dokument.</p>
<p style="color: #475569; font-size: 16px; line-height: 1.5; margin-top: 24px;">Mit freundlichen Grüßen,<br>Ihr CASPOS Shop-Team</p>
</div>
`,
attachments: [
{
filename: `Angebotsbestaetigung_${order.order_number}.pdf`,
content: buffer,
contentType: 'application/pdf',
}
]
})
} catch (mailError) {
console.error('Failed to send order update confirmation mail:', mailError)
}
}
} catch (pdfError) {
console.error('PDF Re-generation Error:', pdfError)
}
revalidatePath('/my-customers')
revalidatePath('/my-orders')
revalidatePath('/admin/orders')
return order as Order
}