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" interface ProductRecord { name: string sku: string ean: string qtyOnHand: number locatorX?: string locatorY?: string locatorZ?: string locatorPriority?: number locatorTypeName?: string } const formatDate = (date: Date, lang: string) => { const isGerman = lang.startsWith('de') return date.toLocaleDateString(isGerman ? 'de-DE' : 'en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) } const generatePdfReport = async (records: ProductRecord[], organizationName: string, lang: string, showByLocator: boolean = false) => { const isGerman = lang.startsWith('de') const pdfDoc = await PDFDocument.create() const font = await pdfDoc.embedFont(StandardFonts.Helvetica) const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold) // Use landscape for locator view to fit more columns const pageWidth = showByLocator ? 841.89 : 595.28 // A4 landscape or portrait const pageHeight = showByLocator ? 595.28 : 841.89 const margin = 40 const lineHeight = 16 const headerHeight = 22 // Column widths based on view mode const colWidths = showByLocator ? { name: 150, sku: 80, ean: 100, qty: 50, x: 50, y: 50, z: 50, priority: 55, type: 90 } : { name: 200, sku: 100, ean: 120, qty: 75 } const addNewPage = () => { const page = pdfDoc.addPage([pageWidth, pageHeight]) return page } const drawHeader = (page: any, yPos: number) => { const headers = showByLocator ? (isGerman ? ['Produktname', 'SKU', 'EAN', 'Menge', 'X', 'Y', 'Z', 'Prioritaet', 'Lagertyp'] : ['Product Name', 'SKU', 'EAN', 'Qty', 'X', 'Y', 'Z', 'Priority', 'Locator Type']) : (isGerman ? ['Produktname', 'SKU', 'EAN', 'Bestand'] : ['Product Name', 'SKU', 'EAN', 'Qty On Hand']) let xPos = margin // Header background page.drawRectangle({ x: margin - 5, y: yPos - 5, width: pageWidth - (margin * 2) + 10, height: headerHeight, color: rgb(0.9, 0.9, 0.9) }) // Header text const fontSize = showByLocator ? 8 : 10 page.drawText(headers[0], { x: xPos, y: yPos, size: fontSize, font: fontBold }) xPos += colWidths.name page.drawText(headers[1], { x: xPos, y: yPos, size: fontSize, font: fontBold }) xPos += colWidths.sku page.drawText(headers[2], { x: xPos, y: yPos, size: fontSize, font: fontBold }) xPos += colWidths.ean page.drawText(headers[3], { x: xPos, y: yPos, size: fontSize, font: fontBold }) if (showByLocator) { xPos += colWidths.qty page.drawText(headers[4], { x: xPos, y: yPos, size: fontSize, font: fontBold }) xPos += colWidths.x page.drawText(headers[5], { x: xPos, y: yPos, size: fontSize, font: fontBold }) xPos += colWidths.y page.drawText(headers[6], { x: xPos, y: yPos, size: fontSize, font: fontBold }) xPos += colWidths.z page.drawText(headers[7], { x: xPos, y: yPos, size: fontSize, font: fontBold }) xPos += colWidths.priority page.drawText(headers[8], { x: xPos, y: yPos, size: fontSize, font: fontBold }) } return yPos - headerHeight - 5 } const drawRow = (page: any, record: ProductRecord, yPos: number) => { let xPos = margin const fontSize = showByLocator ? 7 : 9 const maxNameLength = showByLocator ? 25 : 35 // Truncate name if too long let displayName = record.name || '' if (displayName.length > maxNameLength) { displayName = displayName.substring(0, maxNameLength - 3) + '...' } page.drawText(displayName, { x: xPos, y: yPos, size: fontSize, font }) xPos += colWidths.name page.drawText(record.sku || '', { x: xPos, y: yPos, size: fontSize, font }) xPos += colWidths.sku page.drawText(record.ean || '', { x: xPos, y: yPos, size: fontSize, font }) xPos += colWidths.ean page.drawText(String(record.qtyOnHand || 0), { x: xPos, y: yPos, size: fontSize, font }) if (showByLocator) { xPos += colWidths.qty page.drawText(record.locatorX || '', { x: xPos, y: yPos, size: fontSize, font }) xPos += colWidths.x page.drawText(record.locatorY || '', { x: xPos, y: yPos, size: fontSize, font }) xPos += colWidths.y page.drawText(record.locatorZ || '', { x: xPos, y: yPos, size: fontSize, font }) xPos += colWidths.z page.drawText(String(record.locatorPriority ?? ''), { x: xPos, y: yPos, size: fontSize, font }) xPos += colWidths.priority let locatorType = record.locatorTypeName || '' if (locatorType.length > 15) { locatorType = locatorType.substring(0, 12) + '...' } page.drawText(locatorType, { x: xPos, y: yPos, size: fontSize, font }) } return yPos - lineHeight } let page = addNewPage() let yPos = pageHeight - margin // Title const title = isGerman ? `Bestandsbericht - ${organizationName}` : `Stock Report - ${organizationName}` page.drawText(title, { x: margin, y: yPos, size: 16, font: fontBold }) yPos -= 25 // Date const dateLabel = isGerman ? 'Erstellt am: ' : 'Generated on: ' page.drawText(dateLabel + formatDate(new Date(), lang), { x: margin, y: yPos, size: 10, font }) yPos -= 15 // Record count const countLabel = isGerman ? 'Anzahl Eintraege: ' : 'Total Records: ' page.drawText(countLabel + records.length, { x: margin, y: yPos, size: 10, font }) // View mode indicator if (showByLocator) { const viewLabel = isGerman ? 'Ansicht: Nach Lagerplatz' : 'View: By Locator' page.drawText(viewLabel, { x: margin + 150, y: yPos, size: 10, font }) } yPos -= 30 // Draw table header yPos = drawHeader(page, yPos) // Draw rows for (const record of records) { if (yPos < margin + 40) { // Add new page page = addNewPage() yPos = pageHeight - margin yPos = drawHeader(page, yPos) } yPos = drawRow(page, record, yPos) } // Summary at the bottom yPos -= 20 if (yPos < margin + 60) { page = addNewPage() yPos = pageHeight - margin } const totalQty = records.reduce((sum, r) => sum + (r.qtyOnHand || 0), 0) const summaryLabel = isGerman ? 'Gesamtbestand: ' : 'Total Stock: ' page.drawText(summaryLabel + totalQty, { x: margin, y: yPos, size: 12, font: fontBold }) const pdfBytes = await pdfDoc.save() return Buffer.from(pdfBytes) } const generateHtmlEmailDE = (organizationName: string, recordCount: number, totalQty: number) => { return ` Bestandsbericht - ${organizationName}

Bestandsbericht

${organizationName}

Sehr geehrte Damen und Herren,

anbei finden Sie den aktuellen Bestandsbericht als PDF-Anhang.

Anzahl Produkte ${recordCount}
Gesamtbestand ${totalQty}

Mit freundlichen Gruessen

Diese E-Mail wurde automatisch versendet.

` } const generateHtmlEmailEN = (organizationName: string, recordCount: number, totalQty: number) => { return ` Stock Report - ${organizationName}

Stock Report

${organizationName}

Dear Sir or Madam,

Please find attached the current stock report as a PDF.

Total Products ${recordCount}
Total Stock ${totalQty}

Best regards

This email was sent automatically.

` } const generateCsvReport = (records: ProductRecord[], organizationName: string, lang: string, showByLocator: boolean = false) => { const isGerman = lang.startsWith('de') const headers = showByLocator ? (isGerman ? ['Nr.', 'Produktname', 'SKU', 'EAN', 'Menge', 'X', 'Y', 'Z', 'Prioritaet', 'Lagertyp'] : ['No.', 'Product Name', 'SKU', 'EAN', 'Qty', 'X', 'Y', 'Z', 'Priority', 'Locator Type']) : (isGerman ? ['Nr.', 'Produktname', 'SKU', 'EAN', 'Bestand'] : ['No.', 'Product Name', 'SKU', 'EAN', 'Qty On Hand']) const rows = records.map((r, index) => { if (showByLocator) { return [ index + 1, r.name || '', r.sku || '', r.ean || '', r.qtyOnHand || 0, r.locatorX || '', r.locatorY || '', r.locatorZ || '', r.locatorPriority ?? '', r.locatorTypeName || '' ] } return [ index + 1, r.name || '', r.sku || '', r.ean || '', r.qtyOnHand || 0 ] }) const csvContent = [ headers.join(';'), ...rows.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(';')) ].join('\n') return Buffer.from('\uFEFF' + csvContent, 'utf-8') } const handleFunc = async (event: any, authToken: any = null) => { let data: any = { status: 200, message: 'Stock report sent successfully' } const token = authToken ?? await getTokenHelper(event) const body = await readBody(event) const { toEmail, ccEmail, subject, records, organizationName, language = 'de_DE', showByLocator = false, format = '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' } } if (!organizationName) { return { status: 400, message: 'Organization name is required' } } const isGerman = language.startsWith('de') const totalQty = records.reduce((sum: number, r: ProductRecord) => sum + (r.qtyOnHand || 0), 0) // Generate attachment based on format let attachmentBuffer: Buffer let attachmentFilename: string let attachmentContentType: string if (format === 'csv') { attachmentBuffer = generateCsvReport(records, organizationName, language, showByLocator) attachmentFilename = isGerman ? `Bestandsbericht_${organizationName.replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().split('T')[0]}.csv` : `Stock_Report_${organizationName.replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().split('T')[0]}.csv` attachmentContentType = 'text/csv; charset=utf-8' } else { attachmentBuffer = await generatePdfReport(records, organizationName, language, showByLocator) attachmentFilename = isGerman ? `Bestandsbericht_${organizationName.replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().split('T')[0]}.pdf` : `Stock_Report_${organizationName.replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().split('T')[0]}.pdf` attachmentContentType = 'application/pdf' } // Generate HTML email const htmlContent = isGerman ? generateHtmlEmailDE(organizationName, records.length, totalQty) : generateHtmlEmailEN(organizationName, records.length, totalQty) // Create transporter const transporter = nodemailer.createTransport({ host: 'localhost', port: 25, secure: false, tls: { rejectUnauthorized: false } }) // Build email options const emailOptions: any = { from: 'no-reply@logyou.de', to: toEmail, subject: subject || (isGerman ? `Bestandsbericht - ${organizationName}` : `Stock Report - ${organizationName}`), html: htmlContent, attachments: [ { filename: attachmentFilename, content: attachmentBuffer, contentType: attachmentContentType } ] } if (ccEmail) { emailOptions.cc = ccEmail } // Send email try { await transporter.sendMail(emailOptions) } catch (err: any) { console.error('Email send error:', err) return { status: 500, message: `Failed to send stock report: ${err.message}` } } return data } 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 })