diff --git a/shop/app/actions/checkout.ts b/shop/app/actions/checkout.ts index a83356e..ea8761a 100644 --- a/shop/app/actions/checkout.ts +++ b/shop/app/actions/checkout.ts @@ -8,7 +8,7 @@ import { sendMail } from '@/utils/mail'; import { renderToBuffer } from '@react-pdf/renderer'; import React from 'react'; import { InvoicePDF } from '@/components/invoice-pdf'; -import { getOrderEmailTemplate, buildEmailItemsSection } from '@/lib/actions/email-templates'; +import { generateOrderEmailHtml, generateOrderEmailSubject } from '@/lib/actions/email-templates'; function generateOrderNumber(prefix: 'BE' | 'AE' = 'AE'): string { const year = new Date().getFullYear(); @@ -147,21 +147,22 @@ async function triggerPostProcessing(orderId: string, supabase: any, customerSna `; } - const itemsSection = buildEmailItemsSection(items); - - const emailTemplate = getOrderEmailTemplate({ + const emailTemplate = generateOrderEmailHtml({ orderNumber: order.order_number, + status: 'pending', formattedDate, customerCompanyName: customerSnapshot.company_name, - totalDetailsText, - totalDetailsHtml, - itemsDetailsText: itemsSection.text, - itemsDetailsHtml: itemsSection.html - }, `${process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'}`, false); + items, + taxRate, + oneTimeNet, + monthlyNet, + }); + + const mailSubject = generateOrderEmailSubject(order.order_number, 'pending'); await sendMail({ to: email, - subject: `Anfragebestätigung ${order.order_number}`, + subject: mailSubject, text: emailTemplate.text, html: emailTemplate.html, attachments: [ diff --git a/shop/app/api/orders/checkout/route.ts b/shop/app/api/orders/checkout/route.ts index 7e5333d..9faca8e 100644 --- a/shop/app/api/orders/checkout/route.ts +++ b/shop/app/api/orders/checkout/route.ts @@ -7,7 +7,7 @@ import { createHash } from 'crypto'; import { renderToBuffer } from '@react-pdf/renderer'; import React from 'react'; import { InvoicePDF } from '@/components/invoice-pdf'; -import { getOrderEmailTemplate, buildEmailItemsSection } from '@/lib/actions/email-templates'; +import { generateOrderEmailHtml, generateOrderEmailSubject } from '@/lib/actions/email-templates'; import { validateWizardSelections } from '@/lib/actions/validation'; @@ -293,20 +293,18 @@ export async function POST(request: Request) { `; } - const itemsSection = buildEmailItemsSection(items); - - const { data: brandData } = await supabase.from('settings').select('primary_color').eq('id', 'branding').maybeSingle(); - const primaryColor = brandData?.primary_color || '#2563eb'; - - const emailTemplate = getOrderEmailTemplate({ + const emailTemplate = generateOrderEmailHtml({ orderNumber: order.order_number, + status: 'pending', formattedDate, customerCompanyName: customerSnapshot.company_name, - totalDetailsText, - totalDetailsHtml, - itemsDetailsText: itemsSection.text, - itemsDetailsHtml: itemsSection.html - }, `${process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'}`, false, primaryColor); + items, + taxRate, + oneTimeNet, + monthlyNet, + }); + + const mailSubject = generateOrderEmailSubject(order.order_number, 'pending'); const { data: bufferData, error: downloadError } = await supabase .storage @@ -325,7 +323,7 @@ export async function POST(request: Request) { await sendMail({ to: user.email, - subject: `Anfragebestätigung ${order.order_number}`, + subject: mailSubject, text: emailTemplate.text, html: emailTemplate.html, attachments diff --git a/shop/lib/actions/email-templates.ts b/shop/lib/actions/email-templates.ts index e6633e0..f1d107b 100644 --- a/shop/lib/actions/email-templates.ts +++ b/shop/lib/actions/email-templates.ts @@ -1,195 +1,429 @@ -interface EmailDetails { +export interface OrderEmailProps { orderNumber: string + status: 'pending' | 'pending_approval' | 'in_review' | 'approved' | 'active' | 'completed' | 'cancelled' | 'rejected' formattedDate: string customerCompanyName: string - totalDetailsText: string - totalDetailsHtml: string - itemsDetailsText?: string - itemsDetailsHtml?: string + billingModel?: string // e.g. "Kauf" | "Miete (monatlich)" | "Kauf / Miete" + rejectionReason?: string + siteUrl?: string + items?: any[] partnerCompanyName?: string partnerUserName?: string partnerUserEmail?: string + taxRate?: number + oneTimeNet?: number + monthlyNet?: number } -export function getOrderEmailTemplate( - details: EmailDetails, - siteUrl: string, - isUpdate: boolean = false, - primaryColor: string = '#2563eb' -) { - const title = isUpdate ? 'Anfrage geändert' : 'Anfrage erhalten' - const intro = isUpdate - ? `Deine Anfrage ${details.orderNumber} wurde aktualisiert.` - : `Deine Anfrage ${details.orderNumber} ist bei uns eingegangen und wird bearbeitet.` +export function generateOrderEmailSubject(orderNumber: string, status: string): string { + const formattedOrderNumber = (orderNumber || '').replace(/^BE-/, 'AE-') + if (status === 'approved' || status === 'active' || status === 'completed') { + return `Auftragsbestätigung: Anfrage #${formattedOrderNumber} freigegeben` + } + if (status === 'rejected' || status === 'cancelled') { + return `Status-Update: Anfrage #${formattedOrderNumber} abgelehnt` + } + return `Eingangsbestätigung: Anfrage #${formattedOrderNumber} eingegangen` +} - let partnerText = '' - let partnerHtml = '' - if (details.partnerCompanyName || details.partnerUserName || details.partnerUserEmail) { - partnerText = `\nPartner-Informationen:` - if (details.partnerCompanyName) partnerText += `\n- Firma: ${details.partnerCompanyName}` - if (details.partnerUserName) partnerText += `\n- Benutzer: ${details.partnerUserName}` - if (details.partnerUserEmail) partnerText += `\n- E-Mail: ${details.partnerUserEmail}` - partnerText += `\n` +export function generateOrderEmailHtml(props: OrderEmailProps): { text: string; html: string } { + const formattedOrderNumber = (props.orderNumber || '').replace(/^BE-/, 'AE-') + const siteUrl = props.siteUrl || process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de' + const taxRate = props.taxRate ?? 19 + const orderPortalUrl = `${siteUrl}/my-orders` - partnerHtml = ` - - Partner-Informationen: - - ` - if (details.partnerCompanyName) { - partnerHtml += ` - - Firma: - ${details.partnerCompanyName} - - ` - } - if (details.partnerUserName) { - partnerHtml += ` - - Benutzer: - ${details.partnerUserName} - - ` - } - if (details.partnerUserEmail) { - partnerHtml += ` - - E-Mail: - ${details.partnerUserEmail} - - ` + // Status Callout configuration + let calloutBg = '#f8fafc' + let calloutBorder = '#cbd5e1' + let calloutTitle = 'Status: Anfrage eingegangen und in Prüfung' + let calloutText = 'Ihre Anfrage ist erfolgreich in unserem System eingegangen und wird derzeit vom Support geprüft.' + let statusBadgeColor = '#2563eb' + + if (props.status === 'approved' || props.status === 'active' || props.status === 'completed') { + calloutBg = '#f0fdf4' + calloutBorder = '#86efac' + calloutTitle = 'Status: Freigegeben / Aktiv' + calloutText = 'Ihre Auftragsbestätigung / Rechnung finden Sie als PDF im Anhang dieser E-Mail.' + statusBadgeColor = '#16a34a' + } else if (props.status === 'rejected' || props.status === 'cancelled') { + calloutBg = '#fef2f2' + calloutBorder = '#fca5a5' + calloutTitle = 'Status: Anfrage abgelehnt' + calloutText = props.rejectionReason + ? `Begründung: "${props.rejectionReason}"` + : 'Ihre Anfrage wurde vom Support geprüft und konnte leider nicht freigegeben werden.' + statusBadgeColor = '#dc2626' + } + + // Calculate prices if items provided + let oneTimeNet = props.oneTimeNet ?? 0 + let monthlyNet = props.monthlyNet ?? 0 + const items = props.items || [] + + if (items.length > 0 && props.oneTimeNet === undefined && props.monthlyNet === undefined) { + items.forEach((item: any) => { + if (item.billing_interval === 'monthly') { + monthlyNet += item.base_price || 0 + } else { + oneTimeNet += item.base_price || 0 + } + + item.selected_modules?.forEach((mod: any) => { + const qty = mod.quantity || 1 + const price = mod.total_price ?? (mod.price * qty) + monthlyNet += price + }) + }) + } + + const oneTimeTax = Math.round(oneTimeNet * (taxRate / 100) * 100) / 100 + const oneTimeGross = Math.round((oneTimeNet + oneTimeTax) * 100) / 100 + + const monthlyTax = Math.round(monthlyNet * (taxRate / 100) * 100) / 100 + const monthlyGross = Math.round((monthlyNet + monthlyTax) * 100) / 100 + + const formatEuro = (val: number) => + new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val) + + // Determine Billing Model Label + let billingModelLabel = props.billingModel + if (!billingModelLabel) { + if (oneTimeNet > 0 && monthlyNet > 0) { + billingModelLabel = 'Kauf & Miete' + } else if (monthlyNet > 0) { + billingModelLabel = 'Miete (monatlich)' + } else { + billingModelLabel = 'Kauf (einmalig)' } } - const text = `Hallo,\n\n${isUpdate ? 'Deine Anfrage wurde aktualisiert.' : 'Deine Anfrage ist bei uns eingegangen.'}\n\nDetails:\n- Nummer: ${details.orderNumber}\n- Datum: ${details.formattedDate}\n- Kunde: ${details.customerCompanyName}\n${partnerText}${details.itemsDetailsText || ''}\n${details.totalDetailsText}\n\nDeine Anfragebestätigung findest du im Anhang als PDF.\n\nPDF-Link: ${siteUrl}/api/orders/${details.orderNumber}/download\n\nViele Grüße,\nDein CASPOS Team` + // Group items by Device + const groupedItems: { [key: string]: any[] } = {} + items.forEach((item: any) => { + const devName = item.device_name || 'Kasse 1' + if (!groupedItems[devName]) { + groupedItems[devName] = [] + } + groupedItems[devName].push(item) + }) - const html = ` -
-

${title}

-

Hallo,

-

${intro}

-
-

Details

- + // Build Text Breakdown + let itemsBreakdownText = '' + if (items.length > 0) { + itemsBreakdownText += '\nKassen-Aufstellung:\n' + Object.entries(groupedItems).forEach(([devName, devItems]) => { + itemsBreakdownText += `\n[ ${devName} ]\n` + devItems.forEach((item: any) => { + itemsBreakdownText += ` - ${item.product_name} (${item.category_name || 'Basis'}): ${formatEuro(item.base_price || 0)} ${item.billing_interval === 'one_time' ? 'einmalig' : 'mtl.'}\n` + item.selected_modules?.forEach((mod: any) => { + const qty = mod.quantity || 1 + const price = mod.total_price ?? (mod.price * qty) + itemsBreakdownText += ` + ${mod.module_name} ${qty > 1 ? `(x${qty})` : ''}: +${formatEuro(price)} mtl.\n` + }) + }) + }) + } + + let totalsText = '' + if (oneTimeNet > 0) { + totalsText += `\nEinmalige Beträge:\n- Netto: ${formatEuro(oneTimeNet)}\n- zzgl. ${taxRate}% MwSt: ${formatEuro(oneTimeTax)}\n- Brutto Gesamt: ${formatEuro(oneTimeGross)}\n` + } + if (monthlyNet > 0) { + totalsText += `\nMonatliche Beträge:\n- Netto: ${formatEuro(monthlyNet)} / mtl.\n- zzgl. ${taxRate}% MwSt: ${formatEuro(monthlyTax)} / mtl.\n- Brutto Gesamt: ${formatEuro(monthlyGross)} / mtl.\n` + } + + let partnerPlainText = '' + if (props.partnerCompanyName || props.partnerUserName || props.partnerUserEmail) { + partnerPlainText = `\nPartner / Betreuer:\n- Firma: ${props.partnerCompanyName || '-'}\n- Ansprechpartner: ${props.partnerUserName || '-'}\n- E-Mail: ${props.partnerUserEmail || '-'}\n` + } + + const text = `CASPOS B2B Portal\nAnfrage #${formattedOrderNumber}\n\n${calloutTitle}\n${calloutText}\n\nAuftragsdetails:\n- Endkunde: ${props.customerCompanyName}\n- Datum: ${props.formattedDate}\n- Abrechnung: ${billingModelLabel}\n${partnerPlainText}${itemsBreakdownText}${totalsText}\n\nAnfrage im Portal ansehen: ${orderPortalUrl}\n\nCASPOS Computerabrechnungssysteme GmbH\nAlte Bundesstraße 16 · 76846 Hauenstein\nAmtsgericht Zweibrücken HRB 12345\nAutomatische Systembenachrichtigung.` + + // Build HTML Items Rows + let itemsHtmlRows = '' + if (items.length > 0) { + Object.entries(groupedItems).forEach(([devName, devItems]) => { + itemsHtmlRows += ` + + + + ` + devItems.forEach((item: any) => { + itemsHtmlRows += ` - - + + + + ` + item.selected_modules?.forEach((mod: any) => { + const qty = mod.quantity || 1 + const price = mod.total_price ?? (mod.price * qty) + itemsHtmlRows += ` + + + + + ` + }) + }) + }) + } + + // Build HTML Partner Section + let partnerHtml = '' + if (props.partnerCompanyName || props.partnerUserName || props.partnerUserEmail) { + partnerHtml = ` + + + + ${props.partnerCompanyName ? `` : ''} + ${props.partnerUserName ? `` : ''} + ${props.partnerUserEmail ? `` : ''} + ` + } + + // Totals Section HTML + let totalsHtml = '' + if (oneTimeNet > 0 || monthlyNet > 0) { + totalsHtml = ` +
+ ${devName === 'Zusatzleistung' ? 'Backoffice / Zusatzleistung' : `Kassengerät: ${devName}`} +
Nummer:${details.orderNumber} + ${item.product_name} (${item.category_name || 'Basis'}) + + ${formatEuro(item.base_price || 0)} ${item.billing_interval === 'one_time' ? 'einmalig' : 'mtl.'} +
+ + ${mod.module_name} ${qty > 1 ? `(x${qty})` : ''} + + +${formatEuro(price)} mtl. +
Partner & Betreuung:
Firma:${props.partnerCompanyName}
Ansprechpartner:${props.partnerUserName}
E-Mail:${props.partnerUserEmail}
+ ${oneTimeNet > 0 ? ` + + - - + + - - + + - ${partnerHtml} - ${details.itemsDetailsHtml || ''} - ${details.totalDetailsHtml} + + + + + ` : ''} + ${monthlyNet > 0 ? ` + + + + + + + + + + + + + + + + ` : ''} +
Einmalige Beträge:
Datum:${details.formattedDate}Netto-Zwischensumme:${formatEuro(oneTimeNet)}
Kunde:${details.customerCompanyName}zzgl. ${taxRate}% MwSt:${formatEuro(oneTimeTax)}
Gesamt einmalig (brutto):${formatEuro(oneTimeGross)}
Monatlich wiederkehrend:
Netto-Zwischensumme:${formatEuro(monthlyNet)} / mtl.
zzgl. ${taxRate}% MwSt:${formatEuro(monthlyTax)} / mtl.
Gesamt monatlich (brutto):${formatEuro(monthlyGross)} / mtl.
+ ` + } + + const html = ` + + + + + CASPOS B2B Portal - #${formattedOrderNumber} + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + + + +
+
CASPOS B2B Portal
+
Die Kasse · Fachhandelsportal
+
+ + #${formattedOrderNumber} + +
+
+ + + + + + +
+
${calloutTitle}
+
${calloutText}
+
+ + + + + + +
+ + + + + + + + + + + + + + ${partnerHtml} +
Endkunde:${props.customerCompanyName}
Bestelldatum:${props.formattedDate}
Abrechnungsmodell:${billingModelLabel}
+
+ + + ${items.length > 0 ? ` +
Kassen-Aufstellung
+ + ${itemsHtmlRows} +
+ ` : ''} + + + ${totalsHtml} + + + + + + +
+ + [ Anfrage im Portal ansehen ] + +
+ +
+

CASPOS Computerabrechnungssysteme GmbH

+

Alte Bundesstraße 16 · 76846 Hauenstein · Amtsgericht Zweibrücken HRB 12345

+

Dies ist eine automatische Transaktions-E-Mail des CASPOS B2B Portals.

+
- -

Die Bestätigung liegt als PDF im Anhang.

-

- Anfragebestätigung PDF herunterladen -

-

Viele Grüße,
Dein CASPOS Team

- - ` +
+ +` return { text, html } } +// Backwards-compatible Wrappers +export function getOrderEmailTemplate( + details: { + orderNumber: string + formattedDate: string + customerCompanyName: string + totalDetailsText?: string + totalDetailsHtml?: string + itemsDetailsText?: string + itemsDetailsHtml?: string + partnerCompanyName?: string + partnerUserName?: string + partnerUserEmail?: string + items?: any[] + taxRate?: number + oneTimeNet?: number + monthlyNet?: number + billingModel?: string + }, + siteUrl: string, + isUpdate: boolean = false, + _primaryColor?: string +) { + return generateOrderEmailHtml({ + orderNumber: details.orderNumber, + status: isUpdate ? 'in_review' : 'pending', + formattedDate: details.formattedDate, + customerCompanyName: details.customerCompanyName, + billingModel: details.billingModel, + siteUrl, + items: details.items, + partnerCompanyName: details.partnerCompanyName, + partnerUserName: details.partnerUserName, + partnerUserEmail: details.partnerUserEmail, + taxRate: details.taxRate, + oneTimeNet: details.oneTimeNet, + monthlyNet: details.monthlyNet, + }) +} + export function getStatusEmailTemplate( orderNumber: string, - oldLabel: string, - newLabel: string, + _oldLabel: string, + _newLabel: string, statusKey?: string, - rejectionReason?: string -) { - const formattedOrderNumber = orderNumber.replace(/^BE-/, 'AE-') - - let title = 'Statusänderung' - let intro = `der Status deiner Anfrage #${formattedOrderNumber} wurde aktualisiert.` - let statusColor = '#f59e0b' // Amber default - let showAttachmentBadge = false - - if (statusKey === 'approved' || statusKey === 'active' || statusKey === 'completed') { - title = 'Freigabebestätigung' - intro = `Ihre Anfrage #${formattedOrderNumber} wurde erfolgreich freigegeben und ist nun aktiv. Die unterzeichnete Anfragebestätigung finden Sie im Anhang dieses Schreibens.` - statusColor = '#10b981' // Emerald - showAttachmentBadge = true - } else if (statusKey === 'rejected') { - title = 'Anfrage abgelehnt' - intro = `Ihre Anfrage #${formattedOrderNumber} wurde vom Support geprüft und abgelehnt.` - statusColor = '#ef4444' // Red - } else if (statusKey === 'pending_approval' || statusKey === 'in_review' || statusKey === 'pending') { - title = 'Eingangsbestätigung' - intro = `Ihre Anfrage #${formattedOrderNumber} ist eingegangen und wird derzeit von unserem Support geprüft.` - statusColor = '#f59e0b' + rejectionReason?: string, + extraDetails?: { + customerCompanyName?: string + formattedDate?: string + billingModel?: string + items?: any[] + partnerCompanyName?: string + partnerUserName?: string + partnerUserEmail?: string + taxRate?: number + oneTimeNet?: number + monthlyNet?: number } - - const text = `Hallo,\n\n${title}\n\n${intro.replace(/<[^>]*>/g, '')}\n\nStatus: ${newLabel} (vorher: ${oldLabel})\n${rejectionReason ? `Grund: ${rejectionReason}\n` : ''}\nViele Grüße,\nDein CASPOS Team` - - const reasonSection = rejectionReason - ? ` -
-

Begründung:

-

"${rejectionReason}"

-
- ` - : '' - - const footerActions = showAttachmentBadge - ? ` -

- - PDF im Anhang verfügbar - -

- ` - : '' - - const html = ` -
-
-

CASPOS

-

Die Kasse

-
- -
-

${title}

-

Hallo,

-

${intro}

- - - - - - - - - - - - - - -
Anfragenummer:#${formattedOrderNumber}
Vorher:${oldLabel}
Aktuell:${newLabel}
- - ${reasonSection} -
- - ${footerActions} - -
-

CASPOS Computerabrechnungssysteme GmbH · Alte Bundesstraße 16 · 76846 Hauenstein

-

Dies ist eine automatisch generierte Systembenachrichtigung.

-
-
- ` - - return { text, html } +) { + const status = (statusKey || 'pending') as any + return generateOrderEmailHtml({ + orderNumber, + status, + formattedDate: extraDetails?.formattedDate || new Date().toLocaleDateString('de-DE'), + customerCompanyName: extraDetails?.customerCompanyName || 'Endkunde', + billingModel: extraDetails?.billingModel, + rejectionReason, + items: extraDetails?.items, + partnerCompanyName: extraDetails?.partnerCompanyName, + partnerUserName: extraDetails?.partnerUserName, + partnerUserEmail: extraDetails?.partnerUserEmail, + taxRate: extraDetails?.taxRate, + oneTimeNet: extraDetails?.oneTimeNet, + monthlyNet: extraDetails?.monthlyNet, + }) } export function buildEmailItemsSection(items: any[]) { @@ -218,14 +452,14 @@ export function buildEmailItemsSection(items: any[]) { ` devItems.forEach((item: any) => { - itemsDetailsText += `\n * ${item.product_name} (${item.category_name}): ${item.base_price.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} ${item.billing_interval === 'one_time' ? 'einmalig' : 'mtl.'}` + itemsDetailsText += `\n * ${item.product_name} (${item.category_name}): ${Number(item.base_price || 0).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} ${item.billing_interval === 'one_time' ? 'einmalig' : 'mtl.'}` itemsDetailsHtml += ` ${item.product_name} (${item.category_name}) - ${item.base_price.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} ${item.billing_interval === 'one_time' ? 'einmalig' : 'mtl.'} + ${Number(item.base_price || 0).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} ${item.billing_interval === 'one_time' ? 'einmalig' : 'mtl.'} ` @@ -233,14 +467,14 @@ export function buildEmailItemsSection(items: any[]) { item.selected_modules?.forEach((mod: any) => { const qty = mod.quantity || 1 const price = mod.total_price ?? (mod.price * qty) - itemsDetailsText += `\n + ${mod.module_name} ${qty > 1 ? `(x${qty})` : ''}: +${price.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} mtl.` + itemsDetailsText += `\n + ${mod.module_name} ${qty > 1 ? `(x${qty})` : ''}: +${Number(price || 0).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} mtl.` itemsDetailsHtml += ` + ${mod.module_name} ${qty > 1 ? `(x${qty})` : ''} - +${price.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} mtl. + +${Number(price || 0).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} mtl. ` @@ -253,4 +487,3 @@ export function buildEmailItemsSection(items: any[]) { html: itemsDetailsHtml } } - diff --git a/shop/lib/actions/orders.ts b/shop/lib/actions/orders.ts index c9e6466..26ccbd1 100644 --- a/shop/lib/actions/orders.ts +++ b/shop/lib/actions/orders.ts @@ -12,7 +12,13 @@ import { buildCustomerSnapshot, buildOrderSnapshot } from '@/lib/license-transfo import type { Category, EndCustomer, Order, Product, Profile, WizardSelections } from '@/lib/types' import { getProducts, getCategories } from '@/lib/actions/products' import { validateWizardSelections } from '@/lib/actions/validation' -import { getOrderEmailTemplate, getStatusEmailTemplate, buildEmailItemsSection } from '@/lib/actions/email-templates' +import { + generateOrderEmailHtml, + generateOrderEmailSubject, + getOrderEmailTemplate, + getStatusEmailTemplate, + buildEmailItemsSection, +} from '@/lib/actions/email-templates' // ─── Hilfsfunktionen ───────────────────────────────────────────────────────── @@ -301,24 +307,25 @@ export async function submitOrder(params: { ` } - const itemsSection = buildEmailItemsSection(items) - - const emailTemplate = getOrderEmailTemplate({ + const emailTemplate = generateOrderEmailHtml({ orderNumber, + status: 'pending', formattedDate, customerCompanyName: customerSnapshot.company_name, - totalDetailsText, - totalDetailsHtml, - itemsDetailsText: itemsSection.text, - itemsDetailsHtml: itemsSection.html, + items, partnerCompanyName, partnerUserName, - partnerUserEmail - }, `${process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'}`, false) + partnerUserEmail, + taxRate, + oneTimeNet, + monthlyNet, + }) + + const mailSubject = generateOrderEmailSubject(orderNumber, 'pending') await sendMail({ to: user.email, - subject: `Anfragebestätigung ${orderNumber}`, + subject: mailSubject, text: emailTemplate.text, html: emailTemplate.html, attachments: [ @@ -343,7 +350,7 @@ export async function submitOrder(params: { if (adminEmail !== user.email) { await sendMail({ to: adminEmail, - subject: `[Admin-Kopie] Anfragebestätigung ${orderNumber}`, + subject: `[Admin-Kopie] ${mailSubject}`, text: emailTemplate.text, html: emailTemplate.html, attachments: [ @@ -455,24 +462,27 @@ export async function updateOrderStatus( if (!userError && user && user.email) { const orderNumber = order.order_number || order.id.slice(0, 8) - const statusLabelMap: Record = { - pending: 'Eingegangen', - pending_approval: 'Wartet auf Freigabe', - in_review: 'In Prüfung', - approved: 'Freigegeben', - active: 'Aktiviert', - completed: 'Abgeschlossen', - cancelled: 'Storniert', - rejected: 'Abgelehnt', - } - const oldLabel = statusLabelMap[oldStatus] || oldStatus - const newLabel = statusLabelMap[newStatus] || newStatus + const customerSnapshot = updatedOrder.customer_data || {} + const orderSnapshot = updatedOrder.order_data || {} + const formattedDate = new Date(updatedOrder.created_at || Date.now()).toLocaleDateString('de-DE') - const statusEmail = getStatusEmailTemplate(orderNumber, oldLabel, newLabel, newStatus) + const statusEmail = generateOrderEmailHtml({ + orderNumber, + status: newStatus, + formattedDate, + customerCompanyName: customerSnapshot.company_name || 'Endkunde', + items: orderSnapshot.items || [], + partnerCompanyName: partnerInfo.partnerCompanyName, + partnerUserName: partnerInfo.partnerUserName, + partnerUserEmail: partnerInfo.partnerUserEmail, + taxRate: orderSnapshot.tax_rate ?? 19, + }) + + const mailSubject = generateOrderEmailSubject(orderNumber, newStatus) const mailOptions: any = { to: user.email, - subject: `Statusänderung Ihrer Anfrage ${orderNumber}`, + subject: mailSubject, text: statusEmail.text, html: statusEmail.html, } @@ -504,7 +514,7 @@ export async function updateOrderStatus( await sendMail({ ...mailOptions, to: adminEmail, - subject: `[Admin-Kopie] Statusänderung Ihrer Anfrage ${orderNumber}` + subject: `[Admin-Kopie] ${mailSubject}` }) } } @@ -642,11 +652,29 @@ export async function rejectOrder( 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', 'rejected', reason) + const partnerInfo = await fetchOrderPartnerInfo(admin, updatedOrder.company_id, updatedOrder.user_id) + const customerSnapshot = updatedOrder.customer_data || {} + const orderSnapshot = updatedOrder.order_data || {} + const formattedDate = new Date(updatedOrder.created_at || Date.now()).toLocaleDateString('de-DE') + + const statusEmail = generateOrderEmailHtml({ + orderNumber, + status: 'rejected', + formattedDate, + customerCompanyName: customerSnapshot.company_name || 'Endkunde', + rejectionReason: reason, + items: orderSnapshot.items || [], + partnerCompanyName: partnerInfo.partnerCompanyName, + partnerUserName: partnerInfo.partnerUserName, + partnerUserEmail: partnerInfo.partnerUserEmail, + taxRate: orderSnapshot.tax_rate ?? 19, + }) + + const mailSubject = generateOrderEmailSubject(orderNumber, 'rejected') await sendMail({ to: orderUser.email, - subject: `Anfrage abgelehnt: ${orderNumber}`, + subject: mailSubject, text: statusEmail.text, html: statusEmail.html }) @@ -664,7 +692,7 @@ export async function rejectOrder( if (adminEmail !== orderUser.email) { await sendMail({ to: adminEmail, - subject: `[Admin-Kopie] Anfrage abgelehnt: ${orderNumber}`, + subject: `[Admin-Kopie] ${mailSubject}`, text: statusEmail.text, html: statusEmail.html }) @@ -672,7 +700,7 @@ export async function rejectOrder( } } } catch (adminMailError) { - console.error('Failed to notify admins of order rejection:', adminMailError) + console.error('Failed to notify admins of rejected order:', adminMailError) } } } catch (mailError) {