// Product transaction history report (jsPDF, landscape). Shared by // pages/materials/transactions.vue ("Export PDF" / email) and the "Report" button on the // product edit page's Transactions tab, so both produce the identical document. // Records = rows of GET /api/materials/transactions?productId= (movementQty, movementDate, // externalOrderId, bpartnerName, locator, movementType). export interface TransactionReportProduct { name?: string sku?: string value?: string mpn?: string } export const formatTransactionReportDate = (dateValue: any): string => { if (!dateValue) return '' const d = new Date(dateValue) if (isNaN(d.getTime())) return String(dateValue) return `${String(d.getDate()).padStart(2, '0')}.${String(d.getMonth() + 1).padStart(2, '0')}.${d.getFullYear()}` } export const transactionDirection = (qty: any) => { const isPositive = (Number(qty) || 0) > 0 return { icon: isPositive ? 'ti-arrow-down-left' : 'ti-arrow-up-right', color: isPositive ? '#27ae60' : '#e74c3c', label: isPositive ? 'IN' : 'OUT' } } export const transactionReportFileName = (productName: string | undefined, ext: string) => `product-history-${(productName || 'transactions').replace(/[^a-zA-Z0-9]/g, '_')}-${new Date().toISOString().split('T')[0]}.${ext}` export const buildTransactionReportPdf = async (product: TransactionReportProduct, records: any[]) => { const { jsPDF } = await import('jspdf') const doc = new jsPDF('landscape') const productName = product?.name || 'Product' const now = new Date() const title = `Product History: ${productName}` const date = now.toLocaleDateString('de-DE') const time = now.toLocaleTimeString('de-DE') const qtyOf = (r: any) => Number(r?.movementQty) || 0 const totalIn = records.filter(r => qtyOf(r) > 0).reduce((sum, r) => sum + qtyOf(r), 0) const totalOut = records.filter(r => qtyOf(r) < 0).reduce((sum, r) => sum + Math.abs(qtyOf(r)), 0) const netChange = totalIn - totalOut doc.setFontSize(16) doc.text(title, 14, 20) doc.setFontSize(10) let yPos = 28 if (product?.value) { doc.text(`Article No.: ${product.value}`, 14, yPos); yPos += 6 } if (product?.sku) { doc.text(`SKU: ${product.sku}`, 14, yPos); yPos += 6 } if (product?.mpn) { doc.text(`MPN: ${product.mpn}`, 14, yPos); yPos += 6 } doc.text(`Generated: ${date} ${time}`, 14, yPos) yPos += 10 // Summary cards const cardWidth = 60, cardHeight = 18, cardGap = 8, cardStartX = 14, cardY = yPos const card = (x: number, rgb: [number, number, number], big: string, small: string) => { doc.setFillColor(rgb[0], rgb[1], rgb[2]) doc.roundedRect(x, cardY, cardWidth, cardHeight, 2, 2, 'F') doc.setTextColor(255, 255, 255) doc.setFontSize(12) doc.setFont(undefined as any, 'bold') doc.text(big, x + 5, cardY + 8) doc.setFontSize(7) doc.setFont(undefined as any, 'normal') doc.text(small, x + 5, cardY + 14) } card(cardStartX, [50, 115, 220], String(records.length), 'Total Transactions') card(cardStartX + (cardWidth + cardGap), [39, 174, 96], `+${totalIn}`, 'Total IN') card(cardStartX + 2 * (cardWidth + cardGap), [231, 76, 60], `-${totalOut}`, 'Total OUT') card(cardStartX + 3 * (cardWidth + cardGap), netChange >= 0 ? [32, 156, 238] : [255, 166, 0], `${netChange >= 0 ? '+' : ''}${netChange}`, 'Net Change') yPos = cardY + cardHeight + 10 // Table header doc.setTextColor(0, 0, 0) doc.setFontSize(8) doc.setFont(undefined as any, 'bold') doc.text('#', 14, yPos) doc.text('Type', 28, yPos) doc.text('Date', 50, yPos) doc.text('Quantity', 85, yPos) doc.text('External Order ID', 115, yPos) doc.text('Business Partner', 165, yPos) doc.text('Locator', 235, yPos) doc.text('Movement Type', 265, yPos) doc.setFont(undefined as any, 'normal') yPos += 8 const pageHeight = 190 const totalRows = records.length records.forEach((row, index) => { if (yPos > pageHeight) { doc.addPage() yPos = 20 } const qty = qtyOf(row) const dir = transactionDirection(qty) doc.setTextColor(100, 100, 100) doc.text(String(totalRows - index), 14, yPos) if (qty > 0) doc.setTextColor(39, 174, 96) else doc.setTextColor(231, 76, 60) doc.text(dir.label, 28, yPos) doc.text(qty > 0 ? `+${qty}` : `${qty}`, 85, yPos) doc.setTextColor(0, 0, 0) doc.text(formatTransactionReportDate(row.movementDate), 50, yPos) doc.text(String(row.externalOrderId || ''), 115, yPos) let bpartner = String(row.bpartnerName || '') if (bpartner.length > 35) bpartner = bpartner.substring(0, 32) + '...' doc.text(bpartner, 165, yPos) doc.text(String(row.locator || ''), 235, yPos) doc.text(String(row.movementType || ''), 265, yPos) yPos += 6 }) return doc }