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 TransactionRecord { id: string | number movementDate: string movementQty: number movementType: string movementTypeId: string locator: string bpartnerName: string externalOrderId: 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 formatMovementDate = (dateStr: string, lang: string) => { if (!dateStr) return '' const isGerman = lang.startsWith('de') const date = new Date(dateStr) return date.toLocaleDateString(isGerman ? 'de-DE' : 'en-US') } const getMovementTypeInfo = (qty: number) => { const isPositive = (qty || 0) > 0 return { label: isPositive ? 'IN' : 'OUT', color: isPositive ? rgb(0.15, 0.68, 0.38) : rgb(0.91, 0.30, 0.24) } } const generateCsvReport = (records: TransactionRecord[], productName: string, lang: string) => { const isGerman = lang.startsWith('de') const headers = isGerman ? ['Nr.', 'Typ', 'Datum', 'Menge', 'Externe Bestellnummer', 'Geschaeftspartner', 'Lagerort', 'Bewegungsart'] : ['No.', 'Type', 'Date', 'Quantity', 'External Order ID', 'Business Partner', 'Locator', 'Movement Type'] const totalRows = records.length const rows = records.map((r, index) => { const typeInfo = getMovementTypeInfo(r.movementQty) const qty = r.movementQty || 0 const qtyStr = qty > 0 ? `+${qty}` : `${qty}` return [ totalRows - index, typeInfo.label, formatMovementDate(r.movementDate, lang), qtyStr, r.externalOrderId || '', r.bpartnerName || '', r.locator || '', r.movementType || '' ] }) 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 generatePdfReport = async (records: TransactionRecord[], productName: string, productSku: string, productValue: string, productMpn: 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) // Use landscape for better fit (matching jsPDF dimensions) const pageWidth = 841.89 // A4 landscape const pageHeight = 595.28 const margin = 14 // Match client-side margin const lineHeight = 6 const headerHeight = 8 // Column positions matching client-side (x positions) const colX = { no: 14, type: 28, date: 50, qty: 85, extOrder: 115, bpartner: 165, locator: 235, movType: 265 } const addNewPage = () => { return pdfDoc.addPage([pageWidth, pageHeight]) } const drawTableHeader = (page: any, yPos: number) => { const fontSize = 8 page.drawText('#', { x: colX.no, y: yPos, size: fontSize, font: fontBold }) page.drawText('Type', { x: colX.type, y: yPos, size: fontSize, font: fontBold }) page.drawText('Date', { x: colX.date, y: yPos, size: fontSize, font: fontBold }) page.drawText(isGerman ? 'Menge' : 'Quantity', { x: colX.qty, y: yPos, size: fontSize, font: fontBold }) page.drawText(isGerman ? 'Ext. Bestellnr.' : 'External Order ID', { x: colX.extOrder, y: yPos, size: fontSize, font: fontBold }) page.drawText(isGerman ? 'Geschaeftspartner' : 'Business Partner', { x: colX.bpartner, y: yPos, size: fontSize, font: fontBold }) page.drawText(isGerman ? 'Lagerort' : 'Locator', { x: colX.locator, y: yPos, size: fontSize, font: fontBold }) page.drawText(isGerman ? 'Bewegungsart' : 'Movement Type', { x: colX.movType, y: yPos, size: fontSize, font: fontBold }) return yPos - headerHeight - 5 } const drawRow = (page: any, record: TransactionRecord, yPos: number, rowNumber: number) => { const fontSize = 8 const qty = record.movementQty || 0 const typeInfo = getMovementTypeInfo(qty) const qtyStr = qty > 0 ? `+${qty}` : `${qty}` // Row number (grey) page.drawText(String(rowNumber), { x: colX.no, y: yPos, size: fontSize, font, color: rgb(0.4, 0.4, 0.4) }) // Type with color (green for IN, red for OUT) page.drawText(typeInfo.label, { x: colX.type, y: yPos, size: fontSize, font: fontBold, color: typeInfo.color }) // Date page.drawText(formatMovementDate(record.movementDate, lang), { x: colX.date, y: yPos, size: fontSize, font }) // Quantity with color page.drawText(qtyStr, { x: colX.qty, y: yPos, size: fontSize, font: fontBold, color: typeInfo.color }) // External Order ID page.drawText(record.externalOrderId || '', { x: colX.extOrder, y: yPos, size: fontSize, font }) // Business Partner (truncate if needed) let bpartner = record.bpartnerName || '' if (bpartner.length > 35) bpartner = bpartner.substring(0, 32) + '...' page.drawText(bpartner, { x: colX.bpartner, y: yPos, size: fontSize, font }) // Locator page.drawText(record.locator || '', { x: colX.locator, y: yPos, size: fontSize, font }) // Movement Type page.drawText(record.movementType || '', { x: colX.movType, y: yPos, size: fontSize, font }) return yPos - lineHeight } let page = addNewPage() let yPos = pageHeight - 20 // Start from top with small margin // Calculate summary stats const totalIn = records.filter(r => (r.movementQty || 0) > 0).reduce((sum, r) => sum + (r.movementQty || 0), 0) const totalOut = records.filter(r => (r.movementQty || 0) < 0).reduce((sum, r) => sum + Math.abs(r.movementQty || 0), 0) const netChange = totalIn - totalOut // Title const titleLabel = isGerman ? 'Produkthistorie: ' : 'Product History: ' page.drawText(titleLabel + productName, { x: margin, y: yPos, size: 16, font: fontBold }) yPos -= 16 // Product details (Value, SKU, MPN) if (productValue) { page.drawText(`${isGerman ? 'Artikelnr.' : 'Article No.'}: ${productValue}`, { x: margin, y: yPos, size: 10, font }) yPos -= 12 } if (productSku) { page.drawText(`SKU: ${productSku}`, { x: margin, y: yPos, size: 10, font }) yPos -= 12 } if (productMpn) { page.drawText(`MPN: ${productMpn}`, { x: margin, y: yPos, size: 10, font }) yPos -= 12 } // Generated date/time const dateLabel = isGerman ? 'Erstellt: ' : 'Generated: ' page.drawText(dateLabel + formatDate(new Date(), lang), { x: margin, y: yPos, size: 10, font }) yPos -= 14 // Summary cards (matching client-side dimensions: 60x18 with gap 8) const cardWidth = 60 const cardHeight = 18 const cardGap = 8 const cardStartX = margin const cardY = yPos - cardHeight // Helper to draw a card (matching client-side style) const drawCard = (x: number, value: string, label: string, bgColor: { r: number, g: number, b: number }) => { // Draw filled rectangle page.drawRectangle({ x: x, y: cardY, width: cardWidth, height: cardHeight, color: rgb(bgColor.r, bgColor.g, bgColor.b) }) // Value text (top portion of card) - positioned like jsPDF: cardY + 8 from top page.drawText(value, { x: x + 5, y: cardY + cardHeight - 8, size: 12, font: fontBold, color: rgb(1, 1, 1) }) // Label text (bottom portion of card) - positioned like jsPDF: cardY + 14 from top page.drawText(label, { x: x + 5, y: cardY + 3, size: 7, font, color: rgb(1, 1, 1) }) } // Card 1: Total Transactions (blue/primary - rgb(50, 115, 220) = 0.196, 0.451, 0.863) drawCard(cardStartX, String(records.length), isGerman ? 'Transaktionen' : 'Total Transactions', { r: 0.196, g: 0.451, b: 0.863 }) // Card 2: Total IN (green - rgb(39, 174, 96) = 0.153, 0.682, 0.376) drawCard(cardStartX + cardWidth + cardGap, `+${totalIn}`, isGerman ? 'Eingang' : 'Total IN', { r: 0.153, g: 0.682, b: 0.376 }) // Card 3: Total OUT (red - rgb(231, 76, 60) = 0.906, 0.298, 0.235) drawCard(cardStartX + 2 * (cardWidth + cardGap), `-${totalOut}`, isGerman ? 'Ausgang' : 'Total OUT', { r: 0.906, g: 0.298, b: 0.235 }) // Card 4: Net Change (info blue for positive rgb(32, 156, 238) = 0.125, 0.612, 0.933, warning orange for negative rgb(255, 166, 0) = 1, 0.651, 0) const netColor = netChange >= 0 ? { r: 0.125, g: 0.612, b: 0.933 } : { r: 1, g: 0.651, b: 0 } drawCard(cardStartX + 3 * (cardWidth + cardGap), `${netChange >= 0 ? '+' : ''}${netChange}`, isGerman ? 'Nettoveraend.' : 'Net Change', netColor) yPos = cardY - 12 // Draw table header yPos = drawTableHeader(page, yPos) // Draw rows with reversed numbering (1 for oldest) const totalRows = records.length const bottomMargin = 30 records.forEach((record, index) => { if (yPos < bottomMargin) { page = addNewPage() yPos = pageHeight - 20 yPos = drawTableHeader(page, yPos) } const rowNumber = totalRows - index yPos = drawRow(page, record, yPos, rowNumber) }) const pdfBytes = await pdfDoc.save() return Buffer.from(pdfBytes) } const generateHtmlEmailDE = (productName: string, productSku: string, recordCount: number, totalIn: number, totalOut: number) => { return `