Files
roa2web-service-auto/reports-app/frontend/RESPONSIVE_EXPORT_PLAN.md
Marius Mutu 6b13ffa183 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
2025-10-25 14:55:08 +03:00

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