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
16 KiB
16 KiB
ROA2WEB - Responsive Design & Export Functionality Implementation Plan
Overview
This plan addresses the following requirements:
- Make login form and dashboard fully responsive
- Fix text size reduction issue in tables on mobile (keep text readable)
- Implement consistent Export Excel/PDF buttons across all tables
- Use same button styling throughout the dashboard
Current Issues Identified
- Tables shrink text on mobile making numbers unreadable
- Export buttons are inconsistent (only 2 tables have them)
- Button styles vary across the dashboard
- Login form needs mobile optimization
Implementation Tasks
1. Responsive Login Form Enhancement
File: src/views/LoginView.vue
Changes needed:
/* Add to <style scoped> section */
@media (max-width: 768px) {
.login-container {
padding: 0.5rem;
}
.login-wrapper {
max-width: 100%;
padding: 0 1rem;
}
.login-card {
border-radius: 8px;
}
.login-header {
padding: 1.5rem 1rem;
}
.login-title {
font-size: 1.5rem;
}
.login-form {
padding: 0 1rem 1.5rem 1rem;
}
/* Ensure inputs are touch-friendly */
.p-inputtext,
.p-password input {
min-height: 44px;
font-size: 16px; /* Prevents zoom on iOS */
}
}
@media (max-width: 480px) {
.login-title {
font-size: 1.25rem;
}
.login-subtitle {
font-size: 0.875rem;
}
}
2. Dashboard Table Responsiveness Fix
File: src/views/DashboardView.vue
Key changes:
- Prevent text shrinking in tables
- Add horizontal scroll on mobile
- Maintain readable font sizes
/* Add to <style scoped> section */
/* Mobile Table Styles - Prevent text shrinking */
@media (max-width: 768px) {
/* Horizontal scroll for table containers */
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
margin: 0 -1rem;
padding: 0 1rem;
}
/* Minimum table width to prevent compression */
.summary-table,
.breakdown-table,
.dashboard-table {
min-width: 600px !important;
}
/* Maintain readable text size */
.summary-table td,
.summary-table th,
.breakdown-table td,
.breakdown-table th {
font-size: 14px !important; /* Never go below 14px */
padding: 0.5rem;
white-space: nowrap;
min-width: 80px;
}
/* Amount cells should never shrink */
.amount-cell {
font-size: 14px !important;
font-family: monospace;
white-space: nowrap;
}
/* Stack controls vertically */
.section-controls {
flex-direction: column;
width: 100%;
gap: 0.5rem;
}
.section-controls > * {
width: 100%;
}
/* Button groups on mobile */
.button-group {
display: flex;
gap: 0.5rem;
width: 100%;
}
.button-group .btn {
flex: 1;
}
}
/* Extra small devices */
@media (max-width: 480px) {
.summary-table,
.breakdown-table {
min-width: 500px !important;
font-size: 13px !important;
}
/* Stack button groups vertically on very small screens */
.button-group {
flex-direction: column;
}
.button-group .btn {
width: 100%;
}
}
3. Create Export Utility Functions
New File: src/utils/exportUtils.js
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 {
const doc = new jsPDF('landscape', 'mm', 'a4');
// Add title
doc.setFontSize(16);
doc.text(title, 14, 15);
// Add company info
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 || '-';
})
);
// Add table
doc.autoTable({
head: [tableColumns],
body: tableRows,
startY: 30,
styles: { fontSize: 9, cellPadding: 2 },
headStyles: { fillColor: [102, 126, 234] },
alternateRowStyles: { fillColor: [245, 245, 245] }
});
// Save PDF
doc.save(`${filename}_${new Date().toISOString().split('T')[0]}.pdf`);
return { success: true };
} catch (error) {
console.error('PDF export failed:', error);
return { success: false, error };
}
};
/**
* 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;
};
4. Update Dashboard Export Methods
File: src/views/DashboardView.vue
Add import:
import {
exportToExcel,
exportToPDF,
exportGeneralTotals as prepareGeneralTotals,
exportSoldNetBreakdown as prepareSoldNetBreakdown
} from '@/utils/exportUtils';
Update export methods:
// Export General Totals to Excel
const exportGeneralTotalsExcel = () => {
const data = prepareGeneralTotals(dashboardStore.summary);
const result = exportToExcel(data, 'totaluri_generale', 'Totaluri Generale');
if (result.success) {
toast.add({
severity: 'success',
summary: 'Export Reușit',
detail: 'Fișier Excel generat cu succes',
life: 3000
});
} else {
toast.add({
severity: 'error',
summary: 'Eroare Export',
detail: 'Nu s-a putut genera fișierul Excel',
life: 3000
});
}
};
// Export General Totals to PDF
const exportGeneralTotalsPDF = () => {
const data = prepareGeneralTotals(dashboardStore.summary);
const columns = [
{ field: 'Tip', header: 'Tip', type: 'text' },
{ field: 'Total Facturat', header: 'Total Facturat', type: 'currency' },
{ field: 'Total Încasat', header: 'Total Încasat/Achitat', type: 'currency' },
{ field: 'Sold Net', header: 'Sold Net', type: 'currency' },
{ field: 'Sold În Termen', header: 'Sold În Termen', type: 'currency' },
{ field: 'Sold Restant', header: 'Sold Restant', type: 'currency' }
];
const result = exportToPDF(
data,
columns,
'totaluri_generale',
`Totaluri Generale - ${companyStore.selectedCompany?.name || 'ROA Reports'}`
);
if (result.success) {
toast.add({
severity: 'success',
summary: 'Export Reușit',
detail: 'Fișier PDF generat cu succes',
life: 3000
});
}
};
// Similar methods for Sold Net Breakdown
const exportSoldNetExcel = () => {
const data = prepareSoldNetBreakdown(dashboardStore.summary);
exportToExcel(data, 'detaliere_sold_net', 'Detaliere Sold Net');
};
const exportSoldNetPDF = () => {
const data = prepareSoldNetBreakdown(dashboardStore.summary);
const columns = [
{ field: 'Categorie', header: 'Categorie', type: 'text' },
{ field: 'TOTAL', header: 'TOTAL', type: 'currency' },
{ field: '7 zile', header: '7 zile', type: 'currency' },
{ field: '14 zile', header: '14 zile', type: 'currency' },
{ field: '30 zile', header: '30 zile', type: 'currency' },
{ field: '60 zile', header: '60 zile', type: 'currency' },
{ field: '90 zile', header: '90 zile', type: 'currency' },
{ field: '90+ zile', header: '90+ zile', type: 'currency' }
];
exportToPDF(
data,
columns,
'detaliere_sold_net',
`Detaliere Sold Net - ${companyStore.selectedCompany?.name || 'ROA Reports'}`
);
};
5. Update Table Templates with Consistent Buttons
File: src/views/DashboardView.vue
Update all table headers with consistent button groups:
<!-- General Totals Table -->
<div class="section-header">
<h2 class="section-title">Totaluri Generale</h2>
<div class="section-controls">
<div class="button-group">
<button class="btn btn-sm btn-primary" @click="exportGeneralTotalsExcel">
<i class="pi pi-file-excel"></i>
<span class="btn-text">Excel</span>
</button>
<button class="btn btn-sm btn-primary" @click="exportGeneralTotalsPDF">
<i class="pi pi-file-pdf"></i>
<span class="btn-text">PDF</span>
</button>
<button class="btn btn-sm btn-outline" @click="refreshGeneralTotals">
<i class="pi pi-refresh"></i>
<span class="btn-text">Refresh</span>
</button>
</div>
</div>
</div>
<!-- Sold Net Breakdown Table -->
<div class="section-header">
<h2 class="section-title">DETALIERE SOLD NET</h2>
<div class="section-controls">
<div class="button-group">
<button class="btn btn-sm btn-primary" @click="exportSoldNetExcel">
<i class="pi pi-file-excel"></i>
<span class="btn-text">Excel</span>
</button>
<button class="btn btn-sm btn-primary" @click="exportSoldNetPDF">
<i class="pi pi-file-pdf"></i>
<span class="btn-text">PDF</span>
</button>
<button class="btn btn-sm btn-outline" @click="refreshSoldNetBreakdown">
<i class="pi pi-refresh"></i>
<span class="btn-text">Refresh</span>
</button>
</div>
</div>
</div>
<!-- Trend Section -->
<div class="section-header">
<h2 class="section-title">Analize Trend</h2>
<div class="section-controls">
<div class="control-group">
<label>Perioada:</label>
<select :value="selectedPeriod" class="trend-select" @change="(e) => { selectedPeriod = e.target.value; handlePeriodChange(); }">
<option value="ytd">An curent (YTD)</option>
<option value="12m">Ultimele 12 luni</option>
</select>
</div>
<div class="control-group">
<label>Tip Grafic:</label>
<select :value="selectedChartType" class="trend-select" @change="(e) => { selectedChartType = e.target.value; handleChartTypeChange(); }">
<option value="line">Linie</option>
<option value="bar">Bare</option>
<option value="area">Arie</option>
</select>
</div>
<div class="button-group">
<button class="btn btn-sm btn-primary" @click="exportTrendExcel">
<i class="pi pi-file-excel"></i>
<span class="btn-text">Excel</span>
</button>
<button class="btn btn-sm btn-primary" @click="exportTrendPDF">
<i class="pi pi-file-pdf"></i>
<span class="btn-text">PDF</span>
</button>
<button class="btn btn-sm btn-outline" @click="refreshTrendData">
<i class="pi pi-refresh"></i>
<span class="btn-text">Refresh</span>
</button>
</div>
</div>
</div>
6. Button Styling Updates
File: src/assets/css/components/buttons.css
Add button group styles:
/* Button Groups for Dashboard */
.button-group {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.button-group .btn {
border-radius: var(--radius-md);
}
/* Hide button text on small screens */
@media (max-width: 640px) {
.btn-text {
display: none;
}
.button-group {
width: 100%;
}
.button-group .btn {
flex: 1;
justify-content: center;
}
}
/* Stack buttons vertically on very small screens */
@media (max-width: 480px) {
.button-group {
flex-direction: column;
width: 100%;
}
.button-group .btn {
width: 100%;
}
.btn-text {
display: inline; /* Show text again when stacked */
}
}
/* Primary button style for exports */
.btn-primary {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.btn-primary:hover {
background: var(--color-primary-dark);
border-color: var(--color-primary-dark);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-primary:active {
transform: translateY(0);
}
Testing Checklist
Mobile Testing (320px - 768px)
- Login form is properly sized and centered
- Input fields are touch-friendly (44px min height)
- Tables scroll horizontally
- Table text remains at 14px minimum
- Numbers don't shrink or wrap
- Export buttons are visible and functional
- Buttons stack properly on small screens
Tablet Testing (768px - 1024px)
- Tables display properly with slight compression
- All export buttons visible
- Controls layout is optimal
Desktop Testing (1024px+)
- Full layout displays correctly
- All features accessible
- Export functions work properly
Export Testing
- Excel export generates valid .xlsx files
- PDF export generates readable documents
- All data is included in exports
- Currency formatting is correct
- Date/time stamps are included
Dependencies Required
{
"xlsx": "^0.18.5",
"jspdf": "^2.5.1",
"jspdf-autotable": "^3.5.31"
}
Implementation Order
- Create export utility functions
- Update button styles in CSS
- Add responsive styles to LoginView
- Update DashboardView with new export methods
- Add consistent button groups to all tables
- Update mobile.css with table fixes
- Test on various devices
Notes
- Maintain 14px minimum font size for readability
- Use horizontal scroll instead of text compression
- Keep button styling consistent across all tables
- Stack controls vertically on mobile for better UX
- Test exports with real data to ensure formatting
Success Criteria
- ✅ Login form responsive on all devices
- ✅ Dashboard tables readable on mobile (no text shrinking)
- ✅ All tables have Excel and PDF export buttons
- ✅ Buttons use consistent styling
- ✅ Export functions work correctly
- ✅ Touch-friendly interface on mobile devices