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 += `
+
+ |
+ ${devName === 'Zusatzleistung' ? 'Backoffice / Zusatzleistung' : `Kassengerät: ${devName}`}
+ |
+
+ `
+ devItems.forEach((item: any) => {
+ itemsHtmlRows += `
- | Nummer: |
- ${details.orderNumber} |
+
+ ${item.product_name} (${item.category_name || 'Basis'})
+ |
+
+ ${formatEuro(item.base_price || 0)} ${item.billing_interval === 'one_time' ? 'einmalig' : 'mtl.'}
+ |
+
+ `
+ item.selected_modules?.forEach((mod: any) => {
+ const qty = mod.quantity || 1
+ const price = mod.total_price ?? (mod.price * qty)
+ itemsHtmlRows += `
+
+ |
+ + ${mod.module_name} ${qty > 1 ? `(x${qty})` : ''}
+ |
+
+ +${formatEuro(price)} mtl.
+ |
+
+ `
+ })
+ })
+ })
+ }
+
+ // Build HTML Partner Section
+ let partnerHtml = ''
+ if (props.partnerCompanyName || props.partnerUserName || props.partnerUserEmail) {
+ partnerHtml = `
+
+ | Partner & Betreuung: |
+
+ ${props.partnerCompanyName ? `| Firma: | ${props.partnerCompanyName} |
` : ''}
+ ${props.partnerUserName ? `| Ansprechpartner: | ${props.partnerUserName} |
` : ''}
+ ${props.partnerUserEmail ? `| E-Mail: | ${props.partnerUserEmail} |
` : ''}
+ `
+ }
+
+ // Totals Section HTML
+ let totalsHtml = ''
+ if (oneTimeNet > 0 || monthlyNet > 0) {
+ totalsHtml = `
+
+ ${oneTimeNet > 0 ? `
+
+ | Einmalige Beträge: |
- | Datum: |
- ${details.formattedDate} |
+ Netto-Zwischensumme: |
+ ${formatEuro(oneTimeNet)} |
- | Kunde: |
- ${details.customerCompanyName} |
+ zzgl. ${taxRate}% MwSt: |
+ ${formatEuro(oneTimeTax)} |
- ${partnerHtml}
- ${details.itemsDetailsHtml || ''}
- ${details.totalDetailsHtml}
+
+ | Gesamt einmalig (brutto): |
+ ${formatEuro(oneTimeGross)} |
+
+ ` : ''}
+ ${monthlyNet > 0 ? `
+
+ | 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}
+ |
+
+
+
+
+
+
+
+
+
+ | Endkunde: |
+ ${props.customerCompanyName} |
+
+
+ | Bestelldatum: |
+ ${props.formattedDate} |
+
+
+ | Abrechnungsmodell: |
+ ${billingModelLabel} |
+
+ ${partnerHtml}
+
+ |
+
+
+
+
+ ${items.length > 0 ? `
+ Kassen-Aufstellung
+
+ ` : ''}
+
+
+ ${totalsHtml}
+
+
+
+
+ |
+
+
+
+
+ |
+ 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 = `
-
-
-
-
-
${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) {