feat: Enhance Trial Balance PDF export with professional formatting
- Add multi-line header: company name, centered title, centered period - Implement pagination with page numbers in footer - Add generation timestamp in footer - Optimize column width distribution (Cont: 7%, Denumire: 33%, Values: 10% each) - Align Cont column to left, all numeric columns to right - Remove debug console.log statements - Fix company name property (.firma → .name) - Use full page width for table (281mm on A4 landscape) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
import jsPDF from 'jspdf';
|
||||
import 'jspdf-autotable';
|
||||
import { jsPDF } from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
|
||||
/**
|
||||
* Format currency values for export
|
||||
@@ -34,14 +34,27 @@ export const exportToExcel = (data, filename, sheetName = 'Sheet1') => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Format number for PDF export
|
||||
*/
|
||||
const formatNumberForPDF = (value) => {
|
||||
if (value == null || value === '' || value === '-') return '-';
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num)) return '-';
|
||||
return new Intl.NumberFormat('ro-RO', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(num);
|
||||
};
|
||||
|
||||
/**
|
||||
* Export data to PDF
|
||||
* @param {Array} data - Array of objects to export
|
||||
* @param {Array} columns - Column definitions [{field: 'key', header: 'Display Name', type: 'currency'}]
|
||||
* @param {Array} columns - Column definitions [{field: 'key', header: 'Display Name', type: 'text|number|currency', width: 30}]
|
||||
* @param {String} filename - Name of the file (without extension)
|
||||
* @param {String} title - Title for the PDF document
|
||||
* @param {Object} header - Header configuration {companyName: '', title: '', period: ''}
|
||||
*/
|
||||
export const exportToPDF = (data, columns, filename, title) => {
|
||||
export const exportToPDF = (data, columns, filename, header) => {
|
||||
try {
|
||||
// Check if data exists
|
||||
if (!data || data.length === 0) {
|
||||
@@ -56,75 +69,191 @@ export const exportToPDF = (data, columns, filename, title) => {
|
||||
}
|
||||
|
||||
const doc = new jsPDF('landscape', 'mm', 'a4');
|
||||
|
||||
// Add title
|
||||
doc.setFontSize(16);
|
||||
doc.text(title, 14, 15);
|
||||
|
||||
// Add generation date
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Generat: ${new Date().toLocaleString('ro-RO')}`, 14, 25);
|
||||
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
const pageHeight = doc.internal.pageSize.getHeight();
|
||||
const marginLeft = 8;
|
||||
const marginRight = 8;
|
||||
const contentWidth = pageWidth - marginLeft - marginRight;
|
||||
|
||||
// Function to add header (called for each page)
|
||||
const addHeader = () => {
|
||||
// Line 1: Company name (left aligned, bold, larger font)
|
||||
doc.setFontSize(13);
|
||||
doc.setFont(undefined, 'bold');
|
||||
const companyName = header.companyName || 'N/A';
|
||||
doc.text(companyName, marginLeft, 15);
|
||||
|
||||
// Line 2: Title "Balanta de Verificare" (centered)
|
||||
doc.setFontSize(14);
|
||||
doc.setFont(undefined, 'bold');
|
||||
const titleWidth = doc.getTextWidth(header.title || '');
|
||||
const titleX = marginLeft + (contentWidth - titleWidth) / 2;
|
||||
doc.text(header.title || '', titleX, 24);
|
||||
|
||||
// Line 3: Period (centered, below title)
|
||||
doc.setFontSize(11);
|
||||
doc.setFont(undefined, 'normal');
|
||||
const periodText = header.period || '';
|
||||
const periodWidth = doc.getTextWidth(periodText);
|
||||
const periodX = marginLeft + (contentWidth - periodWidth) / 2;
|
||||
doc.text(periodText, periodX, 32);
|
||||
};
|
||||
|
||||
// Prepare table data
|
||||
const tableColumns = columns.map(col => col.header);
|
||||
const tableRows = data.map(row =>
|
||||
const tableRows = data.map(row =>
|
||||
columns.map(col => {
|
||||
const value = row[col.field];
|
||||
if (col.type === 'currency') {
|
||||
return formatCurrency(value);
|
||||
} else if (col.type === 'number') {
|
||||
return formatNumberForPDF(value);
|
||||
}
|
||||
return value || '-';
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
// Function to add footer (called for each page)
|
||||
const addFooter = (pageNum, totalPages) => {
|
||||
const footerY = pageHeight - 10; // 10mm from bottom
|
||||
|
||||
// Left side: Generation date
|
||||
doc.setFontSize(8);
|
||||
doc.setFont(undefined, 'normal');
|
||||
doc.text(`Generat: ${new Date().toLocaleString('ro-RO')}`, marginLeft, footerY);
|
||||
|
||||
// Right side: Page numbers
|
||||
const pageText = `Pagina ${pageNum} din ${totalPages}`;
|
||||
const pageTextWidth = doc.getTextWidth(pageText);
|
||||
doc.text(pageText, pageWidth - marginRight - pageTextWidth, footerY);
|
||||
};
|
||||
|
||||
// Check if autoTable is available
|
||||
if (typeof doc.autoTable === 'function') {
|
||||
// Add table using autoTable
|
||||
doc.autoTable({
|
||||
head: [tableColumns],
|
||||
body: tableRows,
|
||||
startY: 30,
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 2,
|
||||
halign: 'center'
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [102, 126, 234],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold'
|
||||
},
|
||||
alternateRowStyles: { fillColor: [245, 245, 245] },
|
||||
columnStyles: {
|
||||
// Right align currency columns
|
||||
...Object.fromEntries(
|
||||
columns.map((col, index) =>
|
||||
col.type === 'currency' ? [index, { halign: 'right' }] : null
|
||||
).filter(Boolean)
|
||||
)
|
||||
if (typeof autoTable === 'function') {
|
||||
// Build column styles - jspdf-autotable uses numeric keys
|
||||
const columnStyles = {};
|
||||
|
||||
// Calculate optimal column widths
|
||||
// Total usable width: pageWidth - marginLeft - marginRight
|
||||
const totalWidth = pageWidth - marginLeft - marginRight; // ~281mm for A4 landscape
|
||||
|
||||
// Define width allocation (proportional)
|
||||
// Cont: 7%, Denumire: 33%, Number columns (6x): 10% each = 100%
|
||||
const widthAllocations = {
|
||||
0: totalWidth * 0.07, // Cont: ~20mm
|
||||
1: totalWidth * 0.33, // Denumire: ~93mm
|
||||
2: totalWidth * 0.10, // Sold Prec D: ~28mm
|
||||
3: totalWidth * 0.10, // Sold Prec C: ~28mm
|
||||
4: totalWidth * 0.10, // Rulaj D: ~28mm
|
||||
5: totalWidth * 0.10, // Rulaj C: ~28mm
|
||||
6: totalWidth * 0.10, // Sold Final D: ~28mm
|
||||
7: totalWidth * 0.10, // Sold Final C: ~28mm
|
||||
};
|
||||
|
||||
columns.forEach((col, index) => {
|
||||
columnStyles[index] = {
|
||||
cellWidth: widthAllocations[index] || 'auto'
|
||||
};
|
||||
|
||||
// Set alignment based on type
|
||||
if (col.type === 'number' || col.type === 'currency') {
|
||||
columnStyles[index].halign = 'right';
|
||||
} else if (col.type === 'text') {
|
||||
// All text columns aligned left (including Cont)
|
||||
columnStyles[index].halign = 'left';
|
||||
}
|
||||
});
|
||||
|
||||
const tableStartY = 36;
|
||||
|
||||
// Add table using autoTable (call as function, not method)
|
||||
autoTable(doc, {
|
||||
head: [tableColumns],
|
||||
body: tableRows,
|
||||
startY: tableStartY,
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 2.5,
|
||||
valign: 'middle',
|
||||
lineColor: [200, 200, 200],
|
||||
lineWidth: 0.1,
|
||||
overflow: 'linebreak'
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [41, 128, 185],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold',
|
||||
halign: 'center',
|
||||
fontSize: 9,
|
||||
cellPadding: 2.5
|
||||
},
|
||||
alternateRowStyles: {
|
||||
fillColor: [248, 248, 248]
|
||||
},
|
||||
columnStyles: columnStyles,
|
||||
margin: { left: marginLeft, right: marginRight, top: tableStartY, bottom: 15 },
|
||||
tableWidth: pageWidth - marginLeft - marginRight, // Use full page width
|
||||
theme: 'grid',
|
||||
didDrawPage: function(data) {
|
||||
// Add header to each page
|
||||
addHeader();
|
||||
},
|
||||
didParseCell: function(data) {
|
||||
// Force alignment based on column type (body cells only)
|
||||
if (data.section === 'body') {
|
||||
const colIndex = data.column.index;
|
||||
const column = columns[colIndex];
|
||||
|
||||
if (column) {
|
||||
if (column.type === 'number' || column.type === 'currency') {
|
||||
data.cell.styles.halign = 'right';
|
||||
} else if (column.type === 'text') {
|
||||
if (colIndex === 0) {
|
||||
data.cell.styles.halign = 'center';
|
||||
} else {
|
||||
data.cell.styles.halign = 'left';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Add footer to all pages AFTER table generation
|
||||
const totalPages = doc.internal.getNumberOfPages();
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
doc.setPage(i);
|
||||
addFooter(i, totalPages);
|
||||
}
|
||||
} else {
|
||||
// Fallback mode (autoTable NOT available)
|
||||
// Add header on first page
|
||||
addHeader();
|
||||
|
||||
// Fallback: manual table creation
|
||||
let yPos = 40;
|
||||
|
||||
let yPos = 45;
|
||||
|
||||
// Draw headers
|
||||
doc.setFontSize(10);
|
||||
doc.setFontSize(8);
|
||||
doc.setFont(undefined, 'bold');
|
||||
tableColumns.forEach((header, index) => {
|
||||
doc.text(header, 14 + (index * 40), yPos);
|
||||
doc.text(header, 14 + (index * 35), yPos);
|
||||
});
|
||||
|
||||
|
||||
// Draw rows
|
||||
doc.setFont(undefined, 'normal');
|
||||
doc.setFontSize(7);
|
||||
tableRows.forEach((row, rowIndex) => {
|
||||
yPos += 10;
|
||||
yPos += 7;
|
||||
row.forEach((cell, cellIndex) => {
|
||||
doc.text(String(cell), 14 + (cellIndex * 40), yPos);
|
||||
doc.text(String(cell), 14 + (cellIndex * 35), yPos);
|
||||
});
|
||||
});
|
||||
|
||||
// Add footer in fallback mode
|
||||
addFooter(1, 1);
|
||||
}
|
||||
|
||||
|
||||
// Save PDF
|
||||
doc.save(`${filename}_${new Date().toISOString().split('T')[0]}.pdf`);
|
||||
return { success: true };
|
||||
|
||||
Reference in New Issue
Block a user