490 lines
22 KiB
TypeScript
490 lines
22 KiB
TypeScript
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
|
|
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,
|
|
statusKey?: string,
|
|
rejectionReason?: string,
|
|
extraDetails?: {
|
|
customerCompanyName?: string
|
|
formattedDate?: string
|
|
billingModel?: string
|
|
items?: any[]
|
|
partnerCompanyName?: string
|
|
partnerUserName?: string
|
|
partnerUserEmail?: string
|
|
taxRate?: number
|
|
oneTimeNet?: number
|
|
monthlyNet?: number
|
|
}
|
|
) {
|
|
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[]) {
|
|
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)
|
|
})
|
|
|
|
let itemsDetailsText = '\nKassen-Konfigurationen:'
|
|
let itemsDetailsHtml = `
|
|
<tr>
|
|
<td colspan="2" style="padding: 12px 0 6px 0; border-top: 1px solid #e2e8f0; font-weight: bold; color: #0f172a;">Kassen-Konfigurationen:</td>
|
|
</tr>
|
|
`
|
|
|
|
Object.entries(groupedItems).forEach(([deviceName, devItems]) => {
|
|
itemsDetailsText += `\n- Kasse: ${deviceName}`
|
|
itemsDetailsHtml += `
|
|
<tr style="background-color: #f1f5f9;">
|
|
<td colspan="2" style="padding: 6px 8px; font-weight: bold; color: #1e3a8a; font-size: 13px;">Kasse: ${deviceName}</td>
|
|
</tr>
|
|
`
|
|
|
|
devItems.forEach((item: any) => {
|
|
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;">
|
|
${Number(item.base_price || 0).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} ${item.billing_interval === 'one_time' ? 'einmalig' : 'mtl.'}
|
|
</td>
|
|
</tr>
|
|
`
|
|
|
|
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})` : ''}: +${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;">
|
|
+${Number(price || 0).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} mtl.
|
|
</td>
|
|
</tr>
|
|
`
|
|
})
|
|
})
|
|
})
|
|
|
|
return {
|
|
text: itemsDetailsText + '\n',
|
|
html: itemsDetailsHtml
|
|
}
|
|
}
|