Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot

Modern ERP Reports Application with microservices architecture

Tech Stack:
- Backend: FastAPI + python-oracledb (Oracle DB integration)
- Frontend: Vue.js 3 + PrimeVue + Vite
- Telegram Bot: python-telegram-bot + SQLite
- Infrastructure: Shared database pool, JWT authentication, SSH tunnel

Features:
- FastAPI backend with async Oracle connection pool
- Vue.js 3 responsive frontend with PrimeVue components
- Telegram bot alternative interface
- Microservices architecture with shared components
- Complete deployment support (Linux Docker + Windows IIS)
- Comprehensive testing (Playwright E2E + pytest)

Repository Structure:
- reports-app/ - Main application (backend, frontend, telegram-bot)
- shared/ - Shared components (database pool, auth, utils)
- deployment/ - Deployment scripts (Linux & Windows)
- docs/ - Project documentation
- security/ - Security scanning and git hooks
This commit is contained in:
2025-10-25 14:55:08 +03:00
commit 6b13ffa183
237 changed files with 70035 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
import * as XLSX from 'xlsx';
import jsPDF from 'jspdf';
import 'jspdf-autotable';
/**
* Format currency values for export
*/
const formatCurrency = (value) => {
if (value == null || value === '-') return '-';
return new Intl.NumberFormat('ro-RO', {
style: 'currency',
currency: 'RON',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(value);
};
/**
* Export data to Excel
* @param {Array} data - Array of objects to export
* @param {String} filename - Name of the file (without extension)
* @param {String} sheetName - Name of the Excel sheet
*/
export const exportToExcel = (data, filename, sheetName = 'Sheet1') => {
try {
const ws = XLSX.utils.json_to_sheet(data);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, sheetName);
XLSX.writeFile(wb, `${filename}_${new Date().toISOString().split('T')[0]}.xlsx`);
return { success: true };
} catch (error) {
console.error('Excel export failed:', error);
return { success: false, error };
}
};
/**
* 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 {String} filename - Name of the file (without extension)
* @param {String} title - Title for the PDF document
*/
export const exportToPDF = (data, columns, filename, title) => {
try {
// Check if data exists
if (!data || data.length === 0) {
console.error('No data to export');
return { success: false, error: 'No data available' };
}
// Check if jsPDF is properly imported
if (typeof jsPDF === 'undefined') {
console.error('jsPDF not properly imported');
return { success: false, error: 'PDF library not available' };
}
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);
// Prepare table data
const tableColumns = columns.map(col => col.header);
const tableRows = data.map(row =>
columns.map(col => {
const value = row[col.field];
if (col.type === 'currency') {
return formatCurrency(value);
}
return value || '-';
})
);
// 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)
)
}
});
} else {
// Fallback: manual table creation
let yPos = 40;
// Draw headers
doc.setFontSize(10);
doc.setFont(undefined, 'bold');
tableColumns.forEach((header, index) => {
doc.text(header, 14 + (index * 40), yPos);
});
// Draw rows
doc.setFont(undefined, 'normal');
tableRows.forEach((row, rowIndex) => {
yPos += 10;
row.forEach((cell, cellIndex) => {
doc.text(String(cell), 14 + (cellIndex * 40), yPos);
});
});
}
// Save PDF
doc.save(`${filename}_${new Date().toISOString().split('T')[0]}.pdf`);
return { success: true };
} catch (error) {
console.error('PDF export error details:', error);
return { success: false, error: error.message || 'PDF generation failed' };
}
};
/**
* Export General Totals table
*/
export const exportGeneralTotals = (summaryData) => {
const data = [
{
Tip: 'Clienți',
'Total Facturat': summaryData?.clienti_total_facturat || 0,
'Total Încasat': summaryData?.clienti_total_incasat || 0,
'Sold Net': summaryData?.clienti_sold_total || 0,
'Sold În Termen': summaryData?.clienti_sold_in_termen || 0,
'Sold Restant': summaryData?.clienti_sold_restant || 0
},
{
Tip: 'Furnizori',
'Total Facturat': summaryData?.furnizori_total_facturat || 0,
'Total Achitat': summaryData?.furnizori_total_achitat || 0,
'Sold Net': summaryData?.furnizori_sold_total || 0,
'Sold În Termen': summaryData?.furnizori_sold_in_termen || 0,
'Sold Restant': summaryData?.furnizori_sold_restant || 0
},
{
Tip: 'Trezorerie',
'Total Facturat': '-',
'Total Încasat/Achitat': '-',
'Sold Net': summaryData?.trezorerie_sold || 0,
'Sold În Termen': '-',
'Sold Restant': '-'
}
];
return data;
};
/**
* Export Sold Net Breakdown table
*/
export const exportSoldNetBreakdown = (summaryData) => {
const data = [
{
Categorie: 'Clienți - Restant',
'TOTAL': summaryData?.clienti_sold_restant || 0,
'7 zile': summaryData?.clienti_restant_7 || 0,
'14 zile': summaryData?.clienti_restant_14 || 0,
'30 zile': summaryData?.clienti_restant_30 || 0,
'60 zile': summaryData?.clienti_restant_60 || 0,
'90 zile': summaryData?.clienti_restant_90 || 0,
'90+ zile': summaryData?.clienti_restant_over_90 || 0
},
{
Categorie: 'Furnizori - Restant',
'TOTAL': summaryData?.furnizori_sold_restant || 0,
'7 zile': summaryData?.furnizori_restant_7 || 0,
'14 zile': summaryData?.furnizori_restant_14 || 0,
'30 zile': summaryData?.furnizori_restant_30 || 0,
'60 zile': summaryData?.furnizori_restant_60 || 0,
'90 zile': summaryData?.furnizori_restant_90 || 0,
'90+ zile': summaryData?.furnizori_restant_over_90 || 0
}
];
return data;
};
/**
* Export Trend Data
*/
export const exportTrendData = (trendsData, period, chartType) => {
if (!trendsData || !trendsData.labels || !trendsData.datasets) {
return [];
}
const data = trendsData.labels.map((label, index) => {
const row = { Perioada: label };
trendsData.datasets.forEach(dataset => {
const value = dataset.data[index];
row[dataset.label] = value || 0;
});
return row;
});
return data;
};

View File