diff --git a/reports-app/backend/app/routers/trial_balance.py b/reports-app/backend/app/routers/trial_balance.py index 681b365..9f0b792 100644 --- a/reports-app/backend/app/routers/trial_balance.py +++ b/reports-app/backend/app/routers/trial_balance.py @@ -29,7 +29,7 @@ async def get_trial_balance( sort_by: str = Query("CONT", description="Coloană pentru sortare"), sort_order: str = Query("asc", description="Ordinea sortării (asc | desc)"), page: int = Query(1, ge=1, description="Pagina"), - page_size: int = Query(50, ge=1, le=500, description="Mărimea paginii"), + page_size: int = Query(50, ge=1, le=1000000, description="Mărimea paginii"), current_user: CurrentUser = Depends(get_current_user) ): """ diff --git a/reports-app/frontend/src/utils/exportUtils.js b/reports-app/frontend/src/utils/exportUtils.js index e71afae..0df0708 100644 --- a/reports-app/frontend/src/utils/exportUtils.js +++ b/reports-app/frontend/src/utils/exportUtils.js @@ -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 }; diff --git a/reports-app/frontend/src/views/TrialBalanceView.vue b/reports-app/frontend/src/views/TrialBalanceView.vue index 4166993..f2bc808 100644 --- a/reports-app/frontend/src/views/TrialBalanceView.vue +++ b/reports-app/frontend/src/views/TrialBalanceView.vue @@ -8,7 +8,7 @@ Balanță de Verificare

- {{ currentPeriodText }} - {{ companyStore.selectedCompany?.firma || "Selectați companie" }} + {{ currentPeriodText }} - {{ companyStore.selectedCompany?.name || "Selectați companie" }}

@@ -106,6 +106,7 @@ :rows="trialBalanceStore.pagination.pageSize" :total-records="trialBalanceStore.pagination.totalItems" :lazy="true" + :striped-rows="true" paginator-template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown" :rows-per-page-options="[25, 50, 100]" current-page-report-template="Afișare {first} - {last} din {totalRecords} înregistrări" @@ -137,37 +138,37 @@ @@ -376,16 +377,16 @@ const exportExcel = async () => { return; } - // Prepare data for export + // Prepare data for export - Use raw numbers (not formatted) so Excel treats them as numbers const exportData = allData.map((row) => ({ "Cont": row.cont, "Denumire": row.denumire, - "Sold Precedent D": formatCurrency(row.sold_precedent_debit), - "Sold Precedent C": formatCurrency(row.sold_precedent_credit), - "Rulaj Lunar D": formatCurrency(row.rulaj_lunar_debit), - "Rulaj Lunar C": formatCurrency(row.rulaj_lunar_credit), - "Sold Final D": formatCurrency(row.sold_final_debit), - "Sold Final C": formatCurrency(row.sold_final_credit), + "Sold Precedent D": parseFloat(row.sold_precedent_debit) || 0, + "Sold Precedent C": parseFloat(row.sold_precedent_credit) || 0, + "Rulaj Lunar D": parseFloat(row.rulaj_lunar_debit) || 0, + "Rulaj Lunar C": parseFloat(row.rulaj_lunar_credit) || 0, + "Sold Final D": parseFloat(row.sold_final_debit) || 0, + "Sold Final C": parseFloat(row.sold_final_credit) || 0, })); const result = exportToExcel( @@ -454,23 +455,29 @@ const exportPDF = async () => { sold_final_credit: row.sold_final_credit, })); - // Define columns for PDF + // Define columns for PDF with proper configuration + // A4 landscape width: ~297mm total, margins 8mm left+right = 281mm usable + // Use 'auto' width to fill entire page width const columns = [ - { field: "cont", header: "Cont", type: "text" }, - { field: "denumire", header: "Denumire", type: "text" }, - { field: "sold_precedent_debit", header: "Sold Prec. D", type: "currency" }, - { field: "sold_precedent_credit", header: "Sold Prec. C", type: "currency" }, - { field: "rulaj_lunar_debit", header: "Rulaj D", type: "currency" }, - { field: "rulaj_lunar_credit", header: "Rulaj C", type: "currency" }, - { field: "sold_final_debit", header: "Sold Final D", type: "currency" }, - { field: "sold_final_credit", header: "Sold Final C", type: "currency" }, + { field: "cont", header: "Cont", type: "text", width: 'auto' }, + { field: "denumire", header: "Denumire Cont", type: "text", width: 'auto' }, + { field: "sold_precedent_debit", header: "Sold Prec. D", type: "number", width: 'auto' }, + { field: "sold_precedent_credit", header: "Sold Prec. C", type: "number", width: 'auto' }, + { field: "rulaj_lunar_debit", header: "Rulaj D", type: "number", width: 'auto' }, + { field: "rulaj_lunar_credit", header: "Rulaj C", type: "number", width: 'auto' }, + { field: "sold_final_debit", header: "Sold Final D", type: "number", width: 'auto' }, + { field: "sold_final_credit", header: "Sold Final C", type: "number", width: 'auto' }, ]; const result = exportToPDF( exportData, columns, - `balanta_verificare_${currentPeriodText.value.replace(/\s+/g, "_")}`, - `Balanță de Verificare - ${currentPeriodText.value} - ${companyStore.selectedCompany?.firma || ""}` + `balanta-verificare-${currentPeriodText.value.replace(/\s+/g, "-")}`, + { + companyName: companyStore.selectedCompany?.name || "", + title: "Balanta de Verificare", + period: currentPeriodText.value + } ); if (result.success) { @@ -578,6 +585,28 @@ watch( margin-bottom: 0.5rem; } +.text-right { + text-align: right; +} + +/* Enhanced striped rows with better contrast */ +.table-card :deep(.p-datatable .p-datatable-tbody > tr) { + transition: background-color 0.2s ease; +} + +.table-card :deep(.p-datatable .p-datatable-tbody > tr:nth-child(odd)) { + background-color: #ffffff; +} + +.table-card :deep(.p-datatable .p-datatable-tbody > tr:nth-child(even)) { + background-color: #f8f9fa; +} + +.table-card :deep(.p-datatable .p-datatable-tbody > tr:hover) { + background-color: #e3f2fd !important; + cursor: pointer; +} + /* Responsive design */ @media (max-width: 768px) { .trial-balance {