import nodemailer from 'nodemailer' import { PDFDocument, StandardFonts, rgb } from 'pdf-lib' import refreshTokenHelper from "../../utils/refreshTokenHelper" import forceLogoutHelper from "../../utils/forceLogoutHelper" import errorHandlingHelper from "../../utils/errorHandlingHelper" const formatCurrency = (value: number) => { return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value || 0) } const formatPct = (value: number) => { return new Intl.NumberFormat('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 }).format(value || 0) + '%' } const formatDateStr = (dateStr: string) => { if (!dateStr) return '' return new Date(dateStr).toLocaleDateString('de-DE') } const generatePdfReport = async (records: any[], summary: any, orgName: string, dateRange: string, lang: string) => { const isGerman = lang.startsWith('de') const pdfDoc = await PDFDocument.create() const font = await pdfDoc.embedFont(StandardFonts.Helvetica) const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold) const pageWidth = 841.89 // A4 landscape const pageHeight = 595.28 const margin = 14 const addNewPage = () => pdfDoc.addPage([pageWidth, pageHeight]) let page = addNewPage() let yPos = pageHeight - 25 // Title const title = isGerman ? `Margenanalyse: ${orgName}` : `Margin Analysis: ${orgName}` page.drawText(title, { x: margin, y: yPos, size: 16, font: fontBold }) yPos -= 14 if (dateRange) { page.drawText(`${isGerman ? 'Zeitraum' : 'Period'}: ${dateRange}`, { x: margin, y: yPos, size: 10, font }) yPos -= 10 } const now = new Date() page.drawText(`${isGerman ? 'Erstellt' : 'Generated'}: ${now.toLocaleDateString('de-DE')} ${now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`, { x: margin, y: yPos, size: 9, font, color: rgb(0.4, 0.4, 0.4) }) yPos -= 18 // Summary cards const cardW = 120 const cardH = 32 const cardGap = 10 const drawCard = (x: number, value: string, label: string, bgColor: { r: number, g: number, b: number }) => { page.drawRectangle({ x, y: yPos - cardH, width: cardW, height: cardH, color: rgb(bgColor.r, bgColor.g, bgColor.b), borderColor: rgb(bgColor.r, bgColor.g, bgColor.b), borderWidth: 0 }) // Round corners simulated by just using rectangle page.drawText(value, { x: x + 6, y: yPos - 13, size: 11, font: fontBold, color: rgb(1, 1, 1) }) page.drawText(label, { x: x + 6, y: yPos - 26, size: 7, font, color: rgb(1, 1, 1) }) } const orders = summary.uniqueOrders || 0 const lines = summary.recordCount || records.length const grandTotal = summary.totalGrandTotal || 0 const salesNet = summary.totalSalesNet || 0 const totalShipping = summary.totalShipping || 0 const totalFees = summary.totalReferralFees || 0 const totalMargin = summary.totalMargin || 0 const marginPct = summary.avgMarginPct || 0 drawCard(margin, `${orders} (${lines} lines)`, isGerman ? 'Bestellungen' : 'Orders', { r: 0.196, g: 0.451, b: 0.863 }) drawCard(margin + cardW + cardGap, formatCurrency(grandTotal), isGerman ? 'Gesamtbetrag (brutto)' : 'Grand Total (gross)', { r: 0.125, g: 0.612, b: 0.933 }) drawCard(margin + 2 * (cardW + cardGap), formatCurrency(salesNet), isGerman ? 'Umsatz (netto)' : 'Sales Net', { r: 0.3, g: 0.3, b: 0.3 }) drawCard(margin + 3 * (cardW + cardGap), formatCurrency(totalShipping), isGerman ? 'Versandkosten' : 'Shipping Cost', { r: 0.906, g: 0.298, b: 0.235 }) drawCard(margin + 4 * (cardW + cardGap), formatCurrency(totalFees), isGerman ? 'Gebuehren' : 'Referral Fees', { r: 1, g: 0.651, b: 0 }) // Margin card - green if positive, red if negative const marginColor = totalMargin >= 0 ? { r: 0.153, g: 0.682, b: 0.376 } : { r: 0.906, g: 0.298, b: 0.235 } drawCard(margin + 5 * (cardW + cardGap), `${formatCurrency(totalMargin)} (${formatPct(marginPct)})`, isGerman ? 'Gesamtmarge' : 'Total Margin', marginColor) yPos -= cardH + 14 // Table headers const colX = [14, 80, 140, 230, 315, 345, 395, 445, 495, 535, 585, 640, 690, 740, 790] const headers = ['Order', 'Date', isGerman ? 'Partner' : 'Partner', 'Product', 'Qty', isGerman ? 'Preis(n)' : 'Price(n)', isGerman ? 'Preis(b)' : 'Price(g)', 'Tax%', 'Line Net', isGerman ? 'Einkauf' : 'Purch.', isGerman ? 'Versand' : 'Ship.', 'Ref%', 'Ref.Fee', 'Margin', 'M%'] const drawTableHeader = (p: any, y: number) => { p.drawRectangle({ x: margin, y: y - 3, width: pageWidth - 2 * margin, height: 12, color: rgb(0.94, 0.95, 0.96) }) headers.forEach((h, i) => { p.drawText(h, { x: colX[i], y: y, size: 6.5, font: fontBold, color: rgb(0.2, 0.2, 0.2) }) }) return y - 14 } yPos = drawTableHeader(page, yPos) // Table rows const bottomMargin = 30 for (const row of records) { if (yPos < bottomMargin) { page = addNewPage() yPos = pageHeight - 20 yPos = drawTableHeader(page, yPos) } const fs = 6 const marginVal = row.margin || 0 const marginClr = marginVal > 0 ? rgb(0.18, 0.49, 0.2) : marginVal < 0 ? rgb(0.78, 0.16, 0.16) : rgb(0, 0, 0) page.drawText(String(row.documentNo || ''), { x: colX[0], y: yPos, size: fs, font }) page.drawText(formatDateStr(row.dateOrdered), { x: colX[1], y: yPos, size: fs, font }) let partner = row.partner || '' if (partner.length > 22) partner = partner.substring(0, 19) + '...' page.drawText(partner, { x: colX[2], y: yPos, size: fs, font }) let product = row.product || '' if (product.length > 22) product = product.substring(0, 19) + '...' page.drawText(product, { x: colX[3], y: yPos, size: fs, font }) page.drawText(String(row.qty || 0), { x: colX[4], y: yPos, size: fs, font }) page.drawText(formatCurrency(row.salesPriceNet || 0), { x: colX[5], y: yPos, size: fs, font }) page.drawText(formatCurrency(row.salesPriceGross || 0), { x: colX[6], y: yPos, size: fs, font }) page.drawText((row.taxRate || 0) + '%', { x: colX[7], y: yPos, size: fs, font }) page.drawText(formatCurrency(row.lineNetAmt || 0), { x: colX[8], y: yPos, size: fs, font: fontBold }) page.drawText(formatCurrency(row.purchasePrice || 0), { x: colX[9], y: yPos, size: fs, font }) page.drawText(formatCurrency(row.lineShippingCost || 0), { x: colX[10], y: yPos, size: fs, font, color: rgb(0.9, 0.22, 0.15) }) page.drawText(row.referralPct ? row.referralPct + '%' : '', { x: colX[11], y: yPos, size: fs, font }) page.drawText(formatCurrency(row.referralFee || 0), { x: colX[12], y: yPos, size: fs, font, color: row.referralFee > 0 ? rgb(0.9, 0.22, 0.15) : rgb(0, 0, 0) }) page.drawText(formatCurrency(marginVal), { x: colX[13], y: yPos, size: fs, font: fontBold, color: marginClr }) page.drawText(formatPct(row.marginPct || 0), { x: colX[14], y: yPos, size: fs, font, color: marginClr }) yPos -= 9 } return Buffer.from(await pdfDoc.save()) } const generateHtmlEmail = (orgName: string, dateRange: string, summary: any, lang: string) => { const isGerman = lang.startsWith('de') const title = isGerman ? 'Margenanalyse' : 'Margin Analysis' return `${title} - ${orgName}

${title}

${orgName}${dateRange ? ` — ${dateRange}` : ''}

${isGerman ? 'Bestellungen' : 'Orders'}${summary.uniqueOrders || 0} (${summary.recordCount || 0} ${isGerman ? 'Zeilen' : 'lines'})
${isGerman ? 'Gesamtbetrag (brutto)' : 'Grand Total (gross)'}${formatCurrency(summary.totalGrandTotal || 0)}
${isGerman ? 'Umsatz (netto)' : 'Sales Net'}${formatCurrency(summary.totalSalesNet || 0)}
${isGerman ? 'Versandkosten' : 'Shipping'}${formatCurrency(summary.totalShipping || 0)}
${isGerman ? 'Gebuehren' : 'Referral Fees'}${formatCurrency(summary.totalReferralFees || 0)}
${isGerman ? 'Gesamtmarge' : 'Total Margin'}${formatCurrency(summary.totalMargin || 0)} (${formatPct(summary.avgMarginPct || 0)})

${isGerman ? 'Diese E-Mail wurde automatisch versendet.' : 'This email was sent automatically.'}

` } const generateCsvReport = (records: any[], summary: any, orgName: string, dateRange: string, lang: string) => { const isGerman = lang.startsWith('de') const sep = ';' const escape = (val: any) => { const str = String(val ?? '') if (str.includes(sep) || str.includes('"') || str.includes('\n')) return '"' + str.replace(/"/g, '""') + '"' return str } const headers = [ isGerman ? 'Bestellung' : 'Order', isGerman ? 'Datum' : 'Date', 'Partner', isGerman ? 'Produkt' : 'Product', 'SKU', isGerman ? 'Menge' : 'Qty', isGerman ? 'Preis (netto)' : 'Price (net)', isGerman ? 'Preis (brutto)' : 'Price (gross)', 'Tax%', 'Line Net', isGerman ? 'Einkaufspreis' : 'Purch. Price', isGerman ? 'Einkauf Gesamt' : 'Purch. Total', isGerman ? 'Versandkosten' : 'Ship. Cost', 'Ref%', 'Ref. Fee', 'Margin', 'M%', 'Marketplace', isGerman ? 'Steuer inkl.' : 'Tax Incl.' ] const lines = [headers.map(escape).join(sep)] for (const row of records) { lines.push([ escape(row.documentNo || ''), escape(formatDateStr(row.dateOrdered)), escape(row.partner || ''), escape(row.product || ''), escape(row.productSKU || ''), escape(row.qty || 0), escape((row.salesPriceNet || 0).toFixed(2)), escape((row.salesPriceGross || 0).toFixed(2)), escape((row.taxRate || 0) + '%'), escape((row.lineNetAmt || 0).toFixed(2)), escape((row.purchasePrice || 0).toFixed(2)), escape((row.totalPurchaseCost || 0).toFixed(2)), escape((row.lineShippingCost || 0).toFixed(2)), escape(row.referralPct ? row.referralPct + '%' : ''), escape((row.referralFee || 0).toFixed(2)), escape((row.margin || 0).toFixed(2)), escape((row.marginPct || 0).toFixed(1) + '%'), escape(row.marketplace || ''), escape(row.isTaxIncluded ? (isGerman ? 'Ja' : 'Yes') : (isGerman ? 'Nein' : 'No')) ].join(sep)) } // Summary row if (summary) { lines.push('') lines.push([escape(isGerman ? 'Zusammenfassung' : 'Summary')].join(sep)) lines.push([escape(isGerman ? 'Bestellungen' : 'Orders'), escape(summary.uniqueOrders || 0)].join(sep)) lines.push([escape(isGerman ? 'Zeilen' : 'Lines'), escape(summary.recordCount || 0)].join(sep)) lines.push([escape(isGerman ? 'Gesamtbetrag (brutto)' : 'Grand Total (gross)'), escape((summary.totalGrandTotal || 0).toFixed(2))].join(sep)) lines.push([escape(isGerman ? 'Umsatz (netto)' : 'Sales Net'), escape((summary.totalSalesNet || 0).toFixed(2))].join(sep)) lines.push([escape(isGerman ? 'Versandkosten' : 'Shipping'), escape((summary.totalShipping || 0).toFixed(2))].join(sep)) lines.push([escape(isGerman ? 'Gebuehren' : 'Referral Fees'), escape((summary.totalReferralFees || 0).toFixed(2))].join(sep)) lines.push([escape(isGerman ? 'Gesamtmarge' : 'Total Margin'), escape((summary.totalMargin || 0).toFixed(2))].join(sep)) lines.push([escape(isGerman ? 'Margen %' : 'Margin %'), escape((summary.avgMarginPct || 0).toFixed(1) + '%')].join(sep)) } // BOM (Byte Order Mark) for Excel UTF-8 compatibility return Buffer.from('\uFEFF' + lines.join('\r\n'), 'utf-8') } const handleFunc = async (event: any, authToken: any = null) => { const body = await readBody(event) const { toEmail, ccEmail, subject, records, summary, orgName, dateRange, language = 'de_DE', attachmentType = 'pdf' } = body if (!toEmail) return { status: 400, message: 'Recipient email is required' } if (!records || !Array.isArray(records) || records.length === 0) return { status: 400, message: 'No records to send' } const isGerman = language.startsWith('de') const htmlContent = generateHtmlEmail(orgName || '', dateRange || '', summary || {}, language) const dateStr = new Date().toISOString().split('T')[0] const orgSlug = (orgName || '').replace(/[^a-zA-Z0-9]/g, '_') let attachmentBuffer: Buffer let filename: string let contentType: string if (attachmentType === 'csv') { attachmentBuffer = generateCsvReport(records, summary || {}, orgName || '', dateRange || '', language) filename = isGerman ? `Margenanalyse_${orgSlug}_${dateStr}.csv` : `Margin_Analysis_${orgSlug}_${dateStr}.csv` contentType = 'text/csv' } else { attachmentBuffer = await generatePdfReport(records, summary || {}, orgName || '', dateRange || '', language) filename = isGerman ? `Margenanalyse_${orgSlug}_${dateStr}.pdf` : `Margin_Analysis_${orgSlug}_${dateStr}.pdf` contentType = 'application/pdf' } const transporter = nodemailer.createTransport({ host: 'localhost', port: 25, secure: false, tls: { rejectUnauthorized: false } }) const emailOptions: any = { from: 'no-reply@logyou.de', to: toEmail, subject: subject || (isGerman ? `Margenanalyse - ${orgName}` : `Margin Analysis - ${orgName}`), html: htmlContent, attachments: [{ filename, content: attachmentBuffer, contentType }] } if (ccEmail) emailOptions.cc = ccEmail try { await transporter.sendMail(emailOptions) } catch (err: any) { console.error('Email send error:', err) return { status: 500, message: `Failed to send margin report: ${err.message}` } } return { status: 200, message: 'Margin report sent successfully' } } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch (err: any) { try { let authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch (error: any) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) } } return data })