feat(email): implement professional responsive b2b html email template
All checks were successful
Staging Build / build (push) Successful in 3m9s
All checks were successful
Staging Build / build (push) Successful in 3m9s
This commit is contained in:
@@ -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: [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
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 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`
|
||||
}
|
||||
|
||||
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`
|
||||
|
||||
// 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)'
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
// 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 += `
|
||||
<tr>
|
||||
<td colspan="2" style="padding: 10px 14px; background-color: #f1f5f9; font-weight: 600; font-size: 13px; color: #1e293b; border-bottom: 1px solid #e2e8f0;">
|
||||
${devName === 'Zusatzleistung' ? 'Backoffice / Zusatzleistung' : `Kassengerät: ${devName}`}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
devItems.forEach((item: any) => {
|
||||
itemsHtmlRows += `
|
||||
<tr>
|
||||
<td style="padding: 8px 14px; font-size: 13px; color: #334155; font-weight: 500; border-bottom: 1px solid #f1f5f9;">
|
||||
${item.product_name} <span style="font-size: 11px; color: #64748b;">(${item.category_name || 'Basis'})</span>
|
||||
</td>
|
||||
<td style="padding: 8px 14px; text-align: right; font-size: 13px; font-weight: 600; color: #0f172a; border-bottom: 1px solid #f1f5f9;">
|
||||
${formatEuro(item.base_price || 0)} <span style="font-size: 11px; color: #64748b; font-weight: normal;">${item.billing_interval === 'one_time' ? 'einmalig' : 'mtl.'}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
item.selected_modules?.forEach((mod: any) => {
|
||||
const qty = mod.quantity || 1
|
||||
const price = mod.total_price ?? (mod.price * qty)
|
||||
itemsHtmlRows += `
|
||||
<tr>
|
||||
<td style="padding: 4px 14px 4px 28px; color: #64748b; font-size: 12px; border-bottom: 1px solid #f8fafc;">
|
||||
+ ${mod.module_name} ${qty > 1 ? `<span style="font-size: 10px; font-weight: 600;">(x${qty})</span>` : ''}
|
||||
</td>
|
||||
<td style="padding: 4px 14px; text-align: right; color: #64748b; font-size: 12px; border-bottom: 1px solid #f8fafc;">
|
||||
+${formatEuro(price)} <span style="font-size: 10px;">mtl.</span>
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Build HTML Partner Section
|
||||
let partnerHtml = ''
|
||||
if (props.partnerCompanyName || props.partnerUserName || props.partnerUserEmail) {
|
||||
partnerHtml = `
|
||||
<tr>
|
||||
<td colspan="2" style="padding: 12px 0 4px 0; border-top: 1px solid #e2e8f0; font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b;">Partner & Betreuung:</td>
|
||||
</tr>
|
||||
${props.partnerCompanyName ? `<tr><td style="padding: 3px 0; font-size: 13px; color: #64748b;">Firma:</td><td style="padding: 3px 0; font-size: 13px; font-weight: 500; color: #0f172a; text-align: right;">${props.partnerCompanyName}</td></tr>` : ''}
|
||||
${props.partnerUserName ? `<tr><td style="padding: 3px 0; font-size: 13px; color: #64748b;">Ansprechpartner:</td><td style="padding: 3px 0; font-size: 13px; color: #0f172a; text-align: right;">${props.partnerUserName}</td></tr>` : ''}
|
||||
${props.partnerUserEmail ? `<tr><td style="padding: 3px 0; font-size: 13px; color: #64748b;">E-Mail:</td><td style="padding: 3px 0; font-size: 13px; color: #0f172a; text-align: right;">${props.partnerUserEmail}</td></tr>` : ''}
|
||||
`
|
||||
}
|
||||
|
||||
// Totals Section HTML
|
||||
let totalsHtml = ''
|
||||
if (oneTimeNet > 0 || monthlyNet > 0) {
|
||||
totalsHtml = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin-top: 16px; border-top: 2px solid #e2e8f0; padding-top: 12px; font-size: 13px; color: #334155;">
|
||||
${oneTimeNet > 0 ? `
|
||||
<tr>
|
||||
<td colspan="2" style="padding: 4px 0; font-weight: 600; font-size: 11px; text-transform: uppercase; color: #64748b; letter-spacing: 0.05em;">Einmalige Beträge:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 2px 0; color: #64748b;">Netto-Zwischensumme:</td>
|
||||
<td style="padding: 2px 0; text-align: right; font-weight: 500; color: #0f172a;">${formatEuro(oneTimeNet)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 2px 0; color: #64748b;">zzgl. ${taxRate}% MwSt:</td>
|
||||
<td style="padding: 2px 0; text-align: right; color: #64748b;">${formatEuro(oneTimeTax)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0 10px 0; font-weight: 600; color: #0f172a;">Gesamt einmalig (brutto):</td>
|
||||
<td style="padding: 4px 0 10px 0; text-align: right; font-weight: 700; color: #0f172a; font-size: 14px;">${formatEuro(oneTimeGross)}</td>
|
||||
</tr>
|
||||
` : ''}
|
||||
${monthlyNet > 0 ? `
|
||||
<tr>
|
||||
<td colspan="2" style="padding: ${oneTimeNet > 0 ? '10px' : '4px'} 0 4px 0; font-weight: 600; font-size: 11px; text-transform: uppercase; color: #64748b; letter-spacing: 0.05em; ${oneTimeNet > 0 ? 'border-top: 1px dashed #e2e8f0;' : ''}">Monatlich wiederkehrend:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 2px 0; color: #64748b;">Netto-Zwischensumme:</td>
|
||||
<td style="padding: 2px 0; text-align: right; font-weight: 500; color: #0f172a;">${formatEuro(monthlyNet)} / mtl.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 2px 0; color: #64748b;">zzgl. ${taxRate}% MwSt:</td>
|
||||
<td style="padding: 2px 0; text-align: right; color: #64748b;">${formatEuro(monthlyTax)} / mtl.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: 600; color: #0f172a;">Gesamt monatlich (brutto):</td>
|
||||
<td style="padding: 4px 0; text-align: right; font-weight: 700; color: #0f172a; font-size: 14px;">${formatEuro(monthlyGross)} / mtl.</td>
|
||||
</tr>
|
||||
` : ''}
|
||||
</table>
|
||||
`
|
||||
}
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CASPOS B2B Portal - #${formattedOrderNumber}</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f8fafc; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; color: #0f172a;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: #f8fafc; padding: 24px 12px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<!-- Main Card (max-width 600px) -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);">
|
||||
|
||||
<!-- Header Banner -->
|
||||
<tr>
|
||||
<td style="background-color: #0f172a; padding: 24px; text-align: left;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-size: 18px; font-weight: 700; color: #ffffff; letter-spacing: -0.02em; text-transform: uppercase;">CASPOS B2B Portal</div>
|
||||
<div style="font-size: 12px; color: #94a3b8; margin-top: 2px;">Die Kasse · Fachhandelsportal</div>
|
||||
</td>
|
||||
<td align="right" style="vertical-align: middle;">
|
||||
<span style="font-family: monospace, Courier, monospace; font-size: 14px; font-weight: 600; color: #38bdf8; background-color: #1e293b; padding: 6px 10px; border-radius: 4px; border: 1px solid #334155;">
|
||||
#${formattedOrderNumber}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Body Content -->
|
||||
<tr>
|
||||
<td style="padding: 24px;">
|
||||
|
||||
<!-- Status Callout -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: ${calloutBg}; border-left: 4px solid ${statusBadgeColor}; border-top: 1px solid ${calloutBorder}; border-right: 1px solid ${calloutBorder}; border-bottom: 1px solid ${calloutBorder}; border-radius: 4px; margin-bottom: 24px;">
|
||||
<tr>
|
||||
<td style="padding: 14px 16px;">
|
||||
<div style="font-weight: 700; font-size: 14px; color: #0f172a; margin-bottom: 4px;">${calloutTitle}</div>
|
||||
<div style="font-size: 13px; color: #334155; line-height: 1.5;">${calloutText}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Order & Customer Meta Table -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin-bottom: 24px;">
|
||||
<tr>
|
||||
<td>
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="font-size: 13px;">
|
||||
<tr>
|
||||
<td style="padding: 4px 0; color: #64748b; width: 140px;">Endkunde:</td>
|
||||
<td style="padding: 4px 0; font-weight: 600; color: #0f172a; text-align: right;">${props.customerCompanyName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; color: #64748b;">Bestelldatum:</td>
|
||||
<td style="padding: 4px 0; font-weight: 500; color: #0f172a; text-align: right;">${props.formattedDate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; color: #64748b;">Abrechnungsmodell:</td>
|
||||
<td style="padding: 4px 0; font-weight: 500; color: #0f172a; text-align: right;">${billingModelLabel}</td>
|
||||
</tr>
|
||||
${partnerHtml}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Hardware / Modules Breakdown -->
|
||||
${items.length > 0 ? `
|
||||
<div style="font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: #475569; margin-bottom: 8px;">Kassen-Aufstellung</div>
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="border: 1px solid #e2e8f0; border-radius: 6px; overflow: hidden; margin-bottom: 20px;">
|
||||
${itemsHtmlRows}
|
||||
</table>
|
||||
` : ''}
|
||||
|
||||
<!-- Totals -->
|
||||
${totalsHtml}
|
||||
|
||||
<!-- Primary Action Button -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin-top: 32px; margin-bottom: 8px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="${orderPortalUrl}" target="_blank" style="display: inline-block; background-color: #2563eb; color: #ffffff; text-decoration: none; font-size: 14px; font-weight: 600; padding: 12px 28px; border-radius: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);">
|
||||
[ Anfrage im Portal ansehen ]
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background-color: #f8fafc; border-top: 1px solid #e2e8f0; padding: 20px 24px; text-align: center; font-size: 12px; color: #64748b; line-height: 1.5;">
|
||||
<p style="margin: 0; font-weight: 600; color: #475569;">CASPOS Computerabrechnungssysteme GmbH</p>
|
||||
<p style="margin: 2px 0 0 0;">Alte Bundesstraße 16 · 76846 Hauenstein · Amtsgericht Zweibrücken HRB 12345</p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 11px; color: #94a3b8;">Dies ist eine automatische Transaktions-E-Mail des CASPOS B2B Portals.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return { text, html }
|
||||
}
|
||||
|
||||
// Backwards-compatible Wrappers
|
||||
export function getOrderEmailTemplate(
|
||||
details: {
|
||||
orderNumber: string
|
||||
formattedDate: string
|
||||
customerCompanyName: string
|
||||
totalDetailsText: string
|
||||
totalDetailsHtml: string
|
||||
totalDetailsText?: string
|
||||
totalDetailsHtml?: string
|
||||
itemsDetailsText?: string
|
||||
itemsDetailsHtml?: string
|
||||
partnerCompanyName?: string
|
||||
partnerUserName?: string
|
||||
partnerUserEmail?: string
|
||||
}
|
||||
|
||||
export function getOrderEmailTemplate(
|
||||
details: EmailDetails,
|
||||
items?: any[]
|
||||
taxRate?: number
|
||||
oneTimeNet?: number
|
||||
monthlyNet?: number
|
||||
billingModel?: string
|
||||
},
|
||||
siteUrl: string,
|
||||
isUpdate: boolean = false,
|
||||
primaryColor: string = '#2563eb'
|
||||
_primaryColor?: string
|
||||
) {
|
||||
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.`
|
||||
|
||||
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`
|
||||
|
||||
partnerHtml = `
|
||||
<tr>
|
||||
<td colspan="2" style="padding: 8px 0; border-top: 1px solid #e2e8f0; font-weight: bold; color: #0f172a;">Partner-Informationen:</td>
|
||||
</tr>
|
||||
`
|
||||
if (details.partnerCompanyName) {
|
||||
partnerHtml += `
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; color: #475569; font-weight: bold;">Firma:</td>
|
||||
<td style="padding: 4px 0; color: #475569;">${details.partnerCompanyName}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
if (details.partnerUserName) {
|
||||
partnerHtml += `
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; color: #475569; font-weight: bold;">Benutzer:</td>
|
||||
<td style="padding: 4px 0; color: #475569;">${details.partnerUserName}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
if (details.partnerUserEmail) {
|
||||
partnerHtml += `
|
||||
<tr>
|
||||
<td style="padding: 4px 0; padding-left: 10px; color: #475569; font-weight: bold;">E-Mail:</td>
|
||||
<td style="padding: 4px 0; color: #475569;">${details.partnerUserEmail}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
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`
|
||||
|
||||
const 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: ${primaryColor}; margin-bottom: 16px;">${title}</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;">${intro}</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;">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;">Nummer:</td>
|
||||
<td style="padding: 4px 0;">${details.orderNumber}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Datum:</td>
|
||||
<td style="padding: 4px 0;">${details.formattedDate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Kunde:</td>
|
||||
<td style="padding: 4px 0;">${details.customerCompanyName}</td>
|
||||
</tr>
|
||||
${partnerHtml}
|
||||
${details.itemsDetailsHtml || ''}
|
||||
${details.totalDetailsHtml}
|
||||
</table>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Die Bestätigung liegt als PDF im Anhang.</p>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">
|
||||
<a href="${siteUrl}/api/orders/${details.orderNumber}/download" style="color: ${primaryColor}; text-decoration: underline; font-weight: 500;">Anfragebestätigung PDF herunterladen</a>
|
||||
</p>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5; margin-top: 24px;">Viele Grüße,<br>Dein CASPOS Team</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
return { text, html }
|
||||
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 <strong>#${formattedOrderNumber}</strong> wurde aktualisiert.`
|
||||
let statusColor = '#f59e0b' // Amber default
|
||||
let showAttachmentBadge = false
|
||||
|
||||
if (statusKey === 'approved' || statusKey === 'active' || statusKey === 'completed') {
|
||||
title = 'Freigabebestätigung'
|
||||
intro = `Ihre Anfrage <strong>#${formattedOrderNumber}</strong> 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 <strong>#${formattedOrderNumber}</strong> 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 <strong>#${formattedOrderNumber}</strong> 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
|
||||
? `
|
||||
<div style="margin-top: 20px; padding: 12px 16px; background-color: rgba(239, 68, 68, 0.1); border-left: 4px solid #ef4444; border-radius: 4px;">
|
||||
<p style="margin: 0; font-size: 11px; color: #ef4444; font-weight: bold; text-transform: uppercase; tracking-spacing: 0.05em;">Begründung:</p>
|
||||
<p style="margin: 4px 0 0 0; font-size: 14px; color: #f8fafc; font-style: italic;">"${rejectionReason}"</p>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
|
||||
const footerActions = showAttachmentBadge
|
||||
? `
|
||||
<p style="text-align: center; margin: 32px 0 0 0;">
|
||||
<span style="display: inline-block; background-color: #1e293b; border: 1px solid #334155; border-radius: 6px; padding: 10px 20px; color: #3b82f6; font-size: 13px; font-weight: bold; letter-spacing: 0.025em;">
|
||||
PDF im Anhang verfügbar
|
||||
</span>
|
||||
</p>
|
||||
`
|
||||
: ''
|
||||
|
||||
const html = `
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 32px; border: 1px solid #1e293b; border-radius: 12px; background-color: #0b0f19; color: #f8fafc;">
|
||||
<div style="text-align: center; margin-bottom: 32px;">
|
||||
<h1 style="color: #3b82f6; font-size: 26px; margin: 0; font-weight: 900; letter-spacing: -0.025em; text-transform: uppercase;">CASPOS</h1>
|
||||
<p style="color: #64748b; font-size: 10px; margin: 2px 0 0 0; text-transform: uppercase; letter-spacing: 0.2em; font-weight: bold;">Die Kasse</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #111827; border: 1px solid #1e293b; border-radius: 8px; padding: 24px; margin-bottom: 24px;">
|
||||
<h2 style="color: #ffffff; font-size: 18px; margin-top: 0; margin-bottom: 12px; font-weight: 700;">${title}</h2>
|
||||
<p style="color: #cbd5e1; font-size: 14px; line-height: 1.6; margin-top: 0;">Hallo,</p>
|
||||
<p style="color: #cbd5e1; font-size: 14px; line-height: 1.6;">${intro}</p>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 13px; color: #cbd5e1; margin-top: 20px;">
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #1e293b; color: #64748b; width: 140px; font-weight: 600; text-transform: uppercase; font-size: 11px;">Anfragenummer:</td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #1e293b; font-family: monospace; font-weight: bold; color: #3b82f6; font-size: 14px;">#${formattedOrderNumber}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #1e293b; color: #64748b; font-weight: 600; text-transform: uppercase; font-size: 11px;">Vorher:</td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #1e293b; text-decoration: line-through; color: #475569;">${oldLabel}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #1e293b; color: #64748b; font-weight: 600; text-transform: uppercase; font-size: 11px;">Aktuell:</td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #1e293b; font-weight: bold; color: ${statusColor}; font-size: 14px;">${newLabel}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
${reasonSection}
|
||||
</div>
|
||||
|
||||
${footerActions}
|
||||
|
||||
<div style="text-align: center; margin-top: 32px; border-top: 1px solid #1e293b; padding-top: 20px; color: #475569; font-size: 11px; line-height: 1.5;">
|
||||
<p style="margin: 0; font-weight: 600;">CASPOS Computerabrechnungssysteme GmbH · Alte Bundesstraße 16 · 76846 Hauenstein</p>
|
||||
<p style="margin: 4px 0 0 0;">Dies ist eine automatisch generierte Systembenachrichtigung.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
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 += `
|
||||
<tr>
|
||||
<td style="padding: 6px 8px; font-size: 13px; color: #334155; font-weight: 500;">
|
||||
${item.product_name} <span style="font-size: 11px; color: #64748b;">(${item.category_name})</span>
|
||||
</td>
|
||||
<td style="padding: 6px 8px; text-align: right; font-size: 13px; font-weight: bold; color: #0f172a;">
|
||||
${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.'}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
@@ -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 += `
|
||||
<tr>
|
||||
<td style="padding: 4px 8px 4px 20px; color: #64748b; font-size: 12px;">
|
||||
+ ${mod.module_name} ${qty > 1 ? `<span style="font-size: 10px; font-weight: 600;">(x${qty})</span>` : ''}
|
||||
</td>
|
||||
<td style="padding: 4px 8px; text-align: right; color: #64748b; font-size: 12px;">
|
||||
+${price.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} mtl.
|
||||
+${Number(price || 0).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} mtl.
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
@@ -253,4 +487,3 @@ export function buildEmailItemsSection(items: any[]) {
|
||||
html: itemsDetailsHtml
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user