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

16 KiB

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:

/* 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
/* 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

  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