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
617 lines
16 KiB
Markdown
617 lines
16 KiB
Markdown
# ROA2WEB - Responsive Design & Export Functionality Implementation Plan
|
|
|
|
## Overview
|
|
This plan addresses the following requirements:
|
|
1. Make login form and dashboard fully responsive
|
|
2. Fix text size reduction issue in tables on mobile (keep text readable)
|
|
3. Implement consistent Export Excel/PDF buttons across all tables
|
|
4. 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:
|
|
```css
|
|
/* 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:
|
|
1. **Prevent text shrinking in tables**
|
|
2. **Add horizontal scroll on mobile**
|
|
3. **Maintain readable font sizes**
|
|
|
|
```css
|
|
/* 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`**
|
|
|
|
```javascript
|
|
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:
|
|
```javascript
|
|
import {
|
|
exportToExcel,
|
|
exportToPDF,
|
|
exportGeneralTotals as prepareGeneralTotals,
|
|
exportSoldNetBreakdown as prepareSoldNetBreakdown
|
|
} from '@/utils/exportUtils';
|
|
```
|
|
|
|
#### Update export methods:
|
|
```javascript
|
|
// 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:
|
|
|
|
```html
|
|
<!-- 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:
|
|
```css
|
|
/* 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
|
|
```json
|
|
{
|
|
"xlsx": "^0.18.5",
|
|
"jspdf": "^2.5.1",
|
|
"jspdf-autotable": "^3.5.31"
|
|
}
|
|
```
|
|
|
|
## Implementation Order
|
|
1. Create export utility functions
|
|
2. Update button styles in CSS
|
|
3. Add responsive styles to LoginView
|
|
4. Update DashboardView with new export methods
|
|
5. Add consistent button groups to all tables
|
|
6. Update mobile.css with table fixes
|
|
7. 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 |