netgescon-master/docs/05-backup-unificazione/DOCS-UNIFIED/02-DOCUMENTAZIONE-TECNICA/08-FRONTEND-UX.md
Pikappa2 480e7eafbd 🎯 NETGESCON - Setup iniziale repository completo
📋 Commit iniziale con:
-  Documentazione unificata in docs/
-  Codice Laravel in netgescon-laravel/
-  Script automazione in scripts/
-  Configurazione sync rsync
-  Struttura organizzata e pulita

🔄 Versione: 2025.07.19-1644
🎯 Sistema pronto per Git distribuito
2025-07-19 16:44:47 +02:00

43 KiB

🎨 CAPITOLO 8 - FRONTEND E UX

Interfaccia Utente, JavaScript e User Experience

Versione: 1.0
Data: 17 Luglio 2025
Ambiente: Laravel 11 + Bootstrap 5 + Alpine.js + Vite


📋 INDICE CAPITOLO

  1. Architettura Frontend
  2. Stack Tecnologico
  3. Componenti UI
  4. JavaScript e Interattività
  5. Responsive Design
  6. Performance e Ottimizzazione
  7. Gestione Stati
  8. Validazione Frontend
  9. Esempi Pratici
  10. Best Practices

1. ARCHITETTURA FRONTEND

1.1 Struttura Generale

resources/
├── js/
│   ├── app.js                  # Entry point principale
│   ├── bootstrap.js            # Configurazione librerie
│   ├── components/             # Componenti riutilizzabili
│   │   ├── forms/             # Componenti form
│   │   ├── tables/            # Componenti tabelle
│   │   ├── modals/            # Componenti modal
│   │   └── charts/            # Componenti grafici
│   ├── modules/               # Moduli specifici
│   │   ├── auth/             # Autenticazione
│   │   ├── dashboard/        # Dashboard
│   │   ├── condominiums/     # Gestione condomini
│   │   └── accounting/       # Contabilità
│   └── utils/                # Utility e helper
├── css/
│   ├── app.css               # Stili principali
│   ├── components/           # Stili componenti
│   └── themes/              # Temi personalizzati
└── views/
    ├── layouts/             # Layout principali
    ├── components/          # Blade components
    └── pages/              # Pagine specifiche

1.2 Principi Architetturali

Modularità

// Esempio struttura modulare
const NetGescon = {
    modules: {
        auth: AuthModule,
        dashboard: DashboardModule,
        condominiums: CondominiumsModule,
        accounting: AccountingModule
    },
    
    init() {
        // Inizializzazione globale
        this.initializeModules();
        this.setupEventListeners();
        this.configureAjax();
    },
    
    initializeModules() {
        Object.keys(this.modules).forEach(key => {
            if (this.modules[key].init) {
                this.modules[key].init();
            }
        });
    }
};

Component-Based

// Componente base riutilizzabile
class BaseComponent {
    constructor(selector, options = {}) {
        this.element = document.querySelector(selector);
        this.options = { ...this.defaults, ...options };
        this.init();
    }
    
    init() {
        this.bindEvents();
        this.render();
    }
    
    bindEvents() {
        // Override in sottoclassi
    }
    
    render() {
        // Override in sottoclassi
    }
    
    destroy() {
        // Cleanup
        this.element.removeEventListener();
    }
}

2. STACK TECNOLOGICO

2.1 Frontend Stack

Framework CSS

// package.json
{
    "devDependencies": {
        "bootstrap": "^5.3.0",
        "sass": "^1.77.8",
        "@fortawesome/fontawesome-free": "^6.5.0"
    }
}

JavaScript Libraries

{
    "dependencies": {
        "alpinejs": "^3.13.0",
        "axios": "^1.6.0",
        "chart.js": "^4.4.0",
        "datatables.net": "^1.13.0",
        "sweetalert2": "^11.10.0"
    }
}

2.2 Build Tools (Vite)

Configurazione Vite

// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
            ],
            refresh: true,
        }),
    ],
    resolve: {
        alias: {
            '@': '/resources/js',
            '~': '/resources/css',
        },
    },
    build: {
        rollupOptions: {
            output: {
                manualChunks: {
                    vendor: ['bootstrap', 'axios', 'alpinejs'],
                    charts: ['chart.js'],
                    tables: ['datatables.net'],
                },
            },
        },
    },
});

2.3 Asset Management

CSS Structure

// resources/css/app.scss
@import 'bootstrap';
@import '@fortawesome/fontawesome-free/css/all.css';

// Custom variables
@import 'variables';

// Core styles
@import 'components/base';
@import 'components/forms';
@import 'components/tables';
@import 'components/modals';
@import 'components/dashboard';

// Theme styles
@import 'themes/default';
@import 'themes/dark';

3. COMPONENTI UI

3.1 Componenti Base

Button Component

<!-- resources/views/components/button.blade.php -->
@php
    $classes = [
        'btn',
        'btn-' . ($variant ?? 'primary'),
        $size ? 'btn-' . $size : '',
        $disabled ? 'disabled' : '',
        $loading ? 'btn-loading' : '',
        $attributes->get('class')
    ];
@endphp

<button 
    {{ $attributes->merge(['class' => implode(' ', array_filter($classes))]) }}
    @if($loading) disabled @endif
>
    @if($loading)
        <span class="spinner-border spinner-border-sm me-2"></span>
    @endif
    
    @if($icon)
        <i class="fas fa-{{ $icon }} me-2"></i>
    @endif
    
    {{ $slot }}
</button>

Form Input Component

<!-- resources/views/components/form/input.blade.php -->
<div class="mb-3">
    @if($label)
        <label for="{{ $id ?? $name }}" class="form-label">
            {{ $label }}
            @if($required)
                <span class="text-danger">*</span>
            @endif
        </label>
    @endif
    
    <input 
        type="{{ $type ?? 'text' }}"
        name="{{ $name }}"
        id="{{ $id ?? $name }}"
        class="form-control @error($name) is-invalid @enderror"
        value="{{ old($name, $value ?? '') }}"
        @if($required) required @endif
        {{ $attributes }}
    >
    
    @error($name)
        <div class="invalid-feedback">
            {{ $message }}
        </div>
    @enderror
    
    @if($help)
        <div class="form-text">{{ $help }}</div>
    @endif
</div>

3.2 Componenti Avanzati

DataTable Component

<!-- resources/views/components/datatable.blade.php -->
<div class="table-responsive">
    <table class="table table-striped table-hover" id="{{ $id }}">
        <thead class="table-dark">
            <tr>
                @foreach($columns as $column)
                    <th>{{ $column['title'] }}</th>
                @endforeach
                @if($actions)
                    <th width="150">Azioni</th>
                @endif
            </tr>
        </thead>
        <tbody>
            {{ $slot }}
        </tbody>
    </table>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    $('#{{ $id }}').DataTable({
        processing: true,
        serverSide: true,
        ajax: '{{ $ajax }}',
        columns: @json($columns),
        language: {
            url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/it-IT.json'
        },
        responsive: true,
        dom: 'Bfrtip',
        buttons: ['copy', 'excel', 'pdf', 'print']
    });
});
</script>

Modal Component

<!-- resources/views/components/modal.blade.php -->
<div class="modal fade" id="{{ $id }}" tabindex="-1">
    <div class="modal-dialog {{ $size ? 'modal-' . $size : '' }}">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">{{ $title }}</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                {{ $slot }}
            </div>
            @if($footer)
                <div class="modal-footer">
                    {{ $footer }}
                </div>
            @endif
        </div>
    </div>
</div>

4. JAVASCRIPT E INTERATTIVITÀ

4.1 Gestione AJAX

Configurazione Axios

// resources/js/bootstrap.js
import axios from 'axios';

// Configurazione globale
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

// CSRF Token
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
    window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
}

// Interceptors
axios.interceptors.request.use(request => {
    // Mostra loading
    showLoading();
    return request;
});

axios.interceptors.response.use(
    response => {
        hideLoading();
        return response;
    },
    error => {
        hideLoading();
        handleError(error);
        return Promise.reject(error);
    }
);

Form Handler

// resources/js/components/forms/FormHandler.js
class FormHandler {
    constructor(formSelector, options = {}) {
        this.form = document.querySelector(formSelector);
        this.options = {
            showToast: true,
            resetOnSuccess: true,
            ...options
        };
        this.init();
    }
    
    init() {
        this.form.addEventListener('submit', this.handleSubmit.bind(this));
    }
    
    async handleSubmit(e) {
        e.preventDefault();
        
        const formData = new FormData(this.form);
        const url = this.form.action;
        const method = this.form.method;
        
        try {
            const response = await axios({
                method,
                url,
                data: formData,
                headers: {
                    'Content-Type': 'multipart/form-data'
                }
            });
            
            this.handleSuccess(response.data);
        } catch (error) {
            this.handleError(error);
        }
    }
    
    handleSuccess(data) {
        if (this.options.showToast) {
            Toast.success(data.message || 'Operazione completata');
        }
        
        if (this.options.resetOnSuccess) {
            this.form.reset();
        }
        
        if (this.options.onSuccess) {
            this.options.onSuccess(data);
        }
    }
    
    handleError(error) {
        if (error.response?.status === 422) {
            this.showValidationErrors(error.response.data.errors);
        } else {
            Toast.error(error.response?.data?.message || 'Errore durante l\'operazione');
        }
    }
    
    showValidationErrors(errors) {
        Object.keys(errors).forEach(field => {
            const input = this.form.querySelector(`[name="${field}"]`);
            if (input) {
                input.classList.add('is-invalid');
                
                let feedback = input.parentNode.querySelector('.invalid-feedback');
                if (!feedback) {
                    feedback = document.createElement('div');
                    feedback.className = 'invalid-feedback';
                    input.parentNode.appendChild(feedback);
                }
                
                feedback.textContent = errors[field][0];
            }
        });
    }
}

4.2 Alpine.js Integration

Dashboard Component

// resources/js/components/dashboard/DashboardStats.js
document.addEventListener('alpine:init', () => {
    Alpine.data('dashboardStats', () => ({
        stats: {},
        loading: true,
        
        async init() {
            await this.loadStats();
            this.setupRealTimeUpdates();
        },
        
        async loadStats() {
            this.loading = true;
            try {
                const response = await axios.get('/api/dashboard/stats');
                this.stats = response.data;
            } catch (error) {
                console.error('Errore caricamento statistiche:', error);
            } finally {
                this.loading = false;
            }
        },
        
        setupRealTimeUpdates() {
            // Aggiorna ogni 30 secondi
            setInterval(() => {
                this.loadStats();
            }, 30000);
        },
        
        formatCurrency(amount) {
            return new Intl.NumberFormat('it-IT', {
                style: 'currency',
                currency: 'EUR'
            }).format(amount);
        }
    }));
});

Template Usage

<!-- resources/views/dashboard.blade.php -->
<div x-data="dashboardStats">
    <div class="row" x-show="!loading">
        <div class="col-md-3">
            <div class="card bg-primary text-white">
                <div class="card-body">
                    <h5 class="card-title">Condomini Attivi</h5>
                    <h2 x-text="stats.active_condominiums"></h2>
                </div>
            </div>
        </div>
        
        <div class="col-md-3">
            <div class="card bg-success text-white">
                <div class="card-body">
                    <h5 class="card-title">Fatturato Mensile</h5>
                    <h2 x-text="formatCurrency(stats.monthly_revenue)"></h2>
                </div>
            </div>
        </div>
    </div>
    
    <div x-show="loading" class="text-center p-4">
        <div class="spinner-border" role="status">
            <span class="visually-hidden">Caricamento...</span>
        </div>
    </div>
</div>

5. RESPONSIVE DESIGN

5.1 Breakpoints Strategy

Custom Breakpoints

// resources/css/variables.scss
$grid-breakpoints: (
    xs: 0,
    sm: 576px,
    md: 768px,
    lg: 992px,
    xl: 1200px,
    xxl: 1400px
);

// Mixins personalizzati
@mixin mobile-only {
    @media (max-width: 767px) {
        @content;
    }
}

@mixin tablet-only {
    @media (min-width: 768px) and (max-width: 991px) {
        @content;
    }
}

@mixin desktop-only {
    @media (min-width: 992px) {
        @content;
    }
}

5.2 Responsive Components

Responsive Table

<!-- resources/views/components/responsive-table.blade.php -->
<div class="table-responsive">
    <table class="table table-striped">
        <thead>
            <tr>
                @foreach($columns as $column)
                    <th class="{{ $column['responsive'] ?? '' }}">
                        {{ $column['title'] }}
                    </th>
                @endforeach
            </tr>
        </thead>
        <tbody>
            {{ $slot }}
        </tbody>
    </table>
</div>

<style>
@media (max-width: 768px) {
    .d-none-mobile {
        display: none !important;
    }
    
    .table-responsive {
        font-size: 0.875rem;
    }
    
    .table td {
        padding: 0.5rem;
    }
}
</style>

Mobile Navigation

<!-- resources/views/layouts/partials/mobile-nav.blade.php -->
<div class="mobile-nav d-lg-none">
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container-fluid">
            <a class="navbar-brand" href="{{ route('dashboard') }}">
                NetGescon
            </a>
            
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mobileNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            
            <div class="collapse navbar-collapse" id="mobileNav">
                <ul class="navbar-nav">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('dashboard') }}">
                            <i class="fas fa-home"></i> Dashboard
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('condominiums.index') }}">
                            <i class="fas fa-building"></i> Condomini
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('accounting.index') }}">
                            <i class="fas fa-euro-sign"></i> Contabilità
                        </a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
</div>

6. PERFORMANCE E OTTIMIZZAZIONE

6.1 Lazy Loading

Image Lazy Loading

// resources/js/utils/LazyLoader.js
class LazyLoader {
    constructor() {
        this.images = document.querySelectorAll('img[data-src]');
        this.imageObserver = new IntersectionObserver(this.handleIntersection.bind(this));
        this.init();
    }
    
    init() {
        this.images.forEach(img => {
            this.imageObserver.observe(img);
        });
    }
    
    handleIntersection(entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.classList.remove('lazy');
                this.imageObserver.unobserve(img);
            }
        });
    }
}

// Inizializzazione
document.addEventListener('DOMContentLoaded', () => {
    new LazyLoader();
});

Component Lazy Loading

// resources/js/utils/ComponentLoader.js
class ComponentLoader {
    static async loadComponent(componentName) {
        const module = await import(`../components/${componentName}.js`);
        return module.default;
    }
    
    static async loadOnDemand(selector, componentName) {
        const elements = document.querySelectorAll(selector);
        
        if (elements.length > 0) {
            const Component = await this.loadComponent(componentName);
            
            elements.forEach(el => {
                new Component(el);
            });
        }
    }
}

6.2 Bundle Optimization

Code Splitting

// resources/js/app.js
import './bootstrap';

// Core components caricati immediatamente
import './components/base/Navigation';
import './components/base/Toast';

// Lazy loading per componenti specifici
document.addEventListener('DOMContentLoaded', async () => {
    // Carica componenti solo se necessari
    if (document.querySelector('[data-component="chart"]')) {
        const { default: ChartComponent } = await import('./components/charts/ChartComponent');
        new ChartComponent();
    }
    
    if (document.querySelector('[data-component="datatable"]')) {
        const { default: DataTableComponent } = await import('./components/tables/DataTableComponent');
        new DataTableComponent();
    }
});

6.3 Caching Strategy

Service Worker

// public/sw.js
const CACHE_NAME = 'netgescon-v1';
const urlsToCache = [
    '/',
    '/css/app.css',
    '/js/app.js',
    '/images/logo.png'
];

self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(urlsToCache))
    );
});

self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                if (response) {
                    return response;
                }
                return fetch(event.request);
            })
    );
});

7. GESTIONE STATI

7.1 State Management

Simple State Manager

// resources/js/utils/StateManager.js
class StateManager {
    constructor() {
        this.state = {};
        this.subscribers = {};
    }
    
    setState(key, value) {
        const oldValue = this.state[key];
        this.state[key] = value;
        
        if (this.subscribers[key]) {
            this.subscribers[key].forEach(callback => {
                callback(value, oldValue);
            });
        }
    }
    
    getState(key) {
        return this.state[key];
    }
    
    subscribe(key, callback) {
        if (!this.subscribers[key]) {
            this.subscribers[key] = [];
        }
        this.subscribers[key].push(callback);
    }
    
    unsubscribe(key, callback) {
        if (this.subscribers[key]) {
            this.subscribers[key] = this.subscribers[key].filter(cb => cb !== callback);
        }
    }
}

// Istanza globale
window.StateManager = new StateManager();

Usage Example

// Componente che usa lo state
class UserProfile {
    constructor() {
        this.init();
    }
    
    init() {
        // Sottoscrizione ai cambiamenti
        StateManager.subscribe('user', this.updateProfile.bind(this));
        
        // Caricamento dati utente
        this.loadUserData();
    }
    
    async loadUserData() {
        try {
            const response = await axios.get('/api/user');
            StateManager.setState('user', response.data);
        } catch (error) {
            console.error('Errore caricamento utente:', error);
        }
    }
    
    updateProfile(userData) {
        document.querySelector('#user-name').textContent = userData.name;
        document.querySelector('#user-email').textContent = userData.email;
    }
}

7.2 Form State

Form State Manager

// resources/js/components/forms/FormState.js
class FormState {
    constructor(formElement) {
        this.form = formElement;
        this.initialData = this.getFormData();
        this.currentData = { ...this.initialData };
        this.isDirty = false;
        
        this.init();
    }
    
    init() {
        this.form.addEventListener('input', this.handleInput.bind(this));
        this.form.addEventListener('change', this.handleChange.bind(this));
        
        // Avviso prima di lasciare la pagina se ci sono modifiche
        window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
    }
    
    getFormData() {
        const formData = new FormData(this.form);
        const data = {};
        
        for (let [key, value] of formData.entries()) {
            data[key] = value;
        }
        
        return data;
    }
    
    handleInput(e) {
        this.currentData[e.target.name] = e.target.value;
        this.checkDirty();
    }
    
    handleChange(e) {
        this.currentData[e.target.name] = e.target.value;
        this.checkDirty();
    }
    
    checkDirty() {
        this.isDirty = JSON.stringify(this.currentData) !== JSON.stringify(this.initialData);
        this.updateUI();
    }
    
    updateUI() {
        const saveButton = this.form.querySelector('[type="submit"]');
        if (saveButton) {
            saveButton.disabled = !this.isDirty;
        }
    }
    
    handleBeforeUnload(e) {
        if (this.isDirty) {
            e.preventDefault();
            e.returnValue = '';
        }
    }
    
    reset() {
        this.currentData = { ...this.initialData };
        this.isDirty = false;
        this.updateUI();
    }
}

8. VALIDAZIONE FRONTEND

8.1 Validation Rules

Validator Class

// resources/js/utils/Validator.js
class Validator {
    constructor() {
        this.rules = {
            required: (value) => value !== null && value !== undefined && value !== '',
            email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
            minLength: (value, min) => value.length >= min,
            maxLength: (value, max) => value.length <= max,
            numeric: (value) => /^\d+$/.test(value),
            alpha: (value) => /^[a-zA-Z]+$/.test(value),
            alphanumeric: (value) => /^[a-zA-Z0-9]+$/.test(value),
            phone: (value) => /^[+]?[\d\s\-\(\)]{8,}$/.test(value),
            fiscal_code: (value) => /^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$/i.test(value),
            vat_number: (value) => /^[0-9]{11}$/.test(value)
        };
        
        this.messages = {
            required: 'Questo campo è obbligatorio',
            email: 'Inserire un indirizzo email valido',
            minLength: 'Minimo {min} caratteri',
            maxLength: 'Massimo {max} caratteri',
            numeric: 'Inserire solo numeri',
            alpha: 'Inserire solo lettere',
            alphanumeric: 'Inserire solo lettere e numeri',
            phone: 'Inserire un numero di telefono valido',
            fiscal_code: 'Inserire un codice fiscale valido',
            vat_number: 'Inserire una partita IVA valida'
        };
    }
    
    validate(value, rules) {
        const errors = [];
        
        rules.forEach(rule => {
            const [ruleName, ...params] = rule.split(':');
            const ruleFunction = this.rules[ruleName];
            
            if (ruleFunction && !ruleFunction(value, ...params)) {
                let message = this.messages[ruleName];
                
                // Sostituisci parametri nel messaggio
                params.forEach((param, index) => {
                    message = message.replace(`{${Object.keys(this.rules)[index]}}`, param);
                });
                
                errors.push(message);
            }
        });
        
        return errors;
    }
    
    validateForm(formElement) {
        const errors = {};
        const inputs = formElement.querySelectorAll('[data-validate]');
        
        inputs.forEach(input => {
            const rules = input.dataset.validate.split('|');
            const fieldErrors = this.validate(input.value, rules);
            
            if (fieldErrors.length > 0) {
                errors[input.name] = fieldErrors;
            }
        });
        
        return errors;
    }
}

8.2 Real-time Validation

Live Validator

// resources/js/components/forms/LiveValidator.js
class LiveValidator {
    constructor(formElement) {
        this.form = formElement;
        this.validator = new Validator();
        this.debounceTime = 300;
        
        this.init();
    }
    
    init() {
        const inputs = this.form.querySelectorAll('[data-validate]');
        
        inputs.forEach(input => {
            input.addEventListener('input', this.debounce(
                this.validateField.bind(this, input),
                this.debounceTime
            ));
            
            input.addEventListener('blur', this.validateField.bind(this, input));
        });
        
        this.form.addEventListener('submit', this.validateForm.bind(this));
    }
    
    validateField(input) {
        const rules = input.dataset.validate.split('|');
        const errors = this.validator.validate(input.value, rules);
        
        this.showFieldErrors(input, errors);
    }
    
    validateForm(e) {
        const errors = this.validator.validateForm(this.form);
        
        if (Object.keys(errors).length > 0) {
            e.preventDefault();
            this.showFormErrors(errors);
        }
    }
    
    showFieldErrors(input, errors) {
        const errorContainer = input.parentNode.querySelector('.validation-errors');
        
        if (errors.length > 0) {
            input.classList.add('is-invalid');
            
            if (errorContainer) {
                errorContainer.innerHTML = errors.map(error => 
                    `<div class="invalid-feedback d-block">${error}</div>`
                ).join('');
            }
        } else {
            input.classList.remove('is-invalid');
            input.classList.add('is-valid');
            
            if (errorContainer) {
                errorContainer.innerHTML = '';
            }
        }
    }
    
    showFormErrors(errors) {
        Object.keys(errors).forEach(fieldName => {
            const input = this.form.querySelector(`[name="${fieldName}"]`);
            if (input) {
                this.showFieldErrors(input, errors[fieldName]);
            }
        });
    }
    
    debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }
}

9. ESEMPI PRATICI

9.1 Condominium Form

Complete Form Implementation

<!-- resources/views/condominiums/create.blade.php -->
<form id="condominiumForm" action="{{ route('condominiums.store') }}" method="POST" 
      x-data="condominiumForm" @submit.prevent="submitForm">
    @csrf
    
    <div class="row">
        <div class="col-md-6">
            <x-form.input 
                name="name" 
                label="Nome Condominio" 
                data-validate="required|minLength:3"
                required 
            />
        </div>
        
        <div class="col-md-6">
            <x-form.input 
                name="fiscal_code" 
                label="Codice Fiscale" 
                data-validate="required|fiscal_code"
                required 
            />
        </div>
    </div>
    
    <div class="row">
        <div class="col-md-8">
            <x-form.input 
                name="address" 
                label="Indirizzo" 
                data-validate="required|minLength:5"
                required 
            />
        </div>
        
        <div class="col-md-4">
            <x-form.input 
                name="postal_code" 
                label="CAP" 
                data-validate="required|numeric|minLength:5|maxLength:5"
                required 
            />
        </div>
    </div>
    
    <div class="row">
        <div class="col-md-6">
            <select name="city" class="form-select" data-validate="required" required>
                <option value="">Seleziona città</option>
                <option value="milano">Milano</option>
                <option value="roma">Roma</option>
                <option value="napoli">Napoli</option>
            </select>
        </div>
        
        <div class="col-md-6">
            <x-form.input 
                name="phone" 
                label="Telefono" 
                data-validate="phone"
                type="tel" 
            />
        </div>
    </div>
    
    <div class="d-flex justify-content-end">
        <button type="button" class="btn btn-secondary me-2" @click="resetForm">
            Annulla
        </button>
        <x-button type="submit" :loading="submitting">
            Salva Condominio
        </x-button>
    </div>
</form>

<script>
document.addEventListener('alpine:init', () => {
    Alpine.data('condominiumForm', () => ({
        submitting: false,
        
        async submitForm() {
            this.submitting = true;
            
            try {
                const formData = new FormData(this.$el);
                const response = await axios.post(this.$el.action, formData);
                
                Toast.success('Condominio salvato con successo');
                window.location.href = '/condominiums';
            } catch (error) {
                if (error.response.status === 422) {
                    this.showValidationErrors(error.response.data.errors);
                } else {
                    Toast.error('Errore durante il salvataggio');
                }
            } finally {
                this.submitting = false;
            }
        },
        
        resetForm() {
            this.$el.reset();
            this.$el.querySelectorAll('.is-invalid').forEach(el => {
                el.classList.remove('is-invalid');
            });
        },
        
        showValidationErrors(errors) {
            Object.keys(errors).forEach(field => {
                const input = this.$el.querySelector(`[name="${field}"]`);
                if (input) {
                    input.classList.add('is-invalid');
                    
                    let feedback = input.parentNode.querySelector('.invalid-feedback');
                    if (!feedback) {
                        feedback = document.createElement('div');
                        feedback.className = 'invalid-feedback';
                        input.parentNode.appendChild(feedback);
                    }
                    
                    feedback.textContent = errors[field][0];
                }
            });
        }
    }));
});

// Inizializza validazione live
document.addEventListener('DOMContentLoaded', () => {
    new LiveValidator(document.getElementById('condominiumForm'));
});
</script>

9.2 Dashboard Charts

Chart Component

<!-- resources/views/dashboard/charts.blade.php -->
<div class="row">
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">
                <h5>Fatturato Mensile</h5>
            </div>
            <div class="card-body">
                <canvas id="revenueChart" width="400" height="200"></canvas>
            </div>
        </div>
    </div>
    
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">
                <h5>Distribuzione Condomini</h5>
            </div>
            <div class="card-body">
                <canvas id="condominiumsChart" width="400" height="200"></canvas>
            </div>
        </div>
    </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', async () => {
    // Carica dati charts
    const response = await axios.get('/api/dashboard/charts');
    const data = response.data;
    
    // Revenue Chart
    const revenueCtx = document.getElementById('revenueChart').getContext('2d');
    new Chart(revenueCtx, {
        type: 'line',
        data: {
            labels: data.revenue.labels,
            datasets: [{
                label: 'Fatturato €',
                data: data.revenue.values,
                borderColor: 'rgb(75, 192, 192)',
                backgroundColor: 'rgba(75, 192, 192, 0.2)',
                tension: 0.1
            }]
        },
        options: {
            responsive: true,
            plugins: {
                title: {
                    display: true,
                    text: 'Fatturato Mensile'
                }
            },
            scales: {
                y: {
                    beginAtZero: true,
                    ticks: {
                        callback: function(value) {
                            return '€' + value.toLocaleString();
                        }
                    }
                }
            }
        }
    });
    
    // Condominiums Chart
    const condominiumsCtx = document.getElementById('condominiumsChart').getContext('2d');
    new Chart(condominiumsCtx, {
        type: 'doughnut',
        data: {
            labels: data.condominiums.labels,
            datasets: [{
                data: data.condominiums.values,
                backgroundColor: [
                    '#FF6384',
                    '#36A2EB',
                    '#FFCE56',
                    '#4BC0C0',
                    '#9966FF'
                ]
            }]
        },
        options: {
            responsive: true,
            plugins: {
                legend: {
                    position: 'bottom'
                }
            }
        }
    });
});
</script>

10. BEST PRACTICES

10.1 Code Organization

Module Pattern

// resources/js/modules/condominium/CondominiumModule.js
const CondominiumModule = (() => {
    // Private variables
    let initialized = false;
    let currentCondominium = null;
    
    // Private methods
    const loadCondominium = async (id) => {
        try {
            const response = await axios.get(`/api/condominiums/${id}`);
            currentCondominium = response.data;
            return currentCondominium;
        } catch (error) {
            console.error('Error loading condominium:', error);
            throw error;
        }
    };
    
    const updateUI = (condominium) => {
        document.querySelector('#condominium-name').textContent = condominium.name;
        document.querySelector('#condominium-address').textContent = condominium.address;
    };
    
    // Public API
    return {
        init() {
            if (initialized) return;
            
            this.bindEvents();
            initialized = true;
        },
        
        bindEvents() {
            document.addEventListener('click', (e) => {
                if (e.target.matches('[data-action="load-condominium"]')) {
                    const id = e.target.dataset.condominiumId;
                    this.loadAndDisplay(id);
                }
            });
        },
        
        async loadAndDisplay(id) {
            try {
                const condominium = await loadCondominium(id);
                updateUI(condominium);
            } catch (error) {
                Toast.error('Errore caricamento condominio');
            }
        },
        
        getCurrentCondominium() {
            return currentCondominium;
        }
    };
})();

10.2 Error Handling

Global Error Handler

// resources/js/utils/ErrorHandler.js
class ErrorHandler {
    static handle(error, context = '') {
        console.error(`[${context}] Error:`, error);
        
        if (error.response) {
            // Server response error
            this.handleServerError(error.response);
        } else if (error.request) {
            // Network error
            this.handleNetworkError();
        } else {
            // Generic error
            this.handleGenericError(error.message);
        }
    }
    
    static handleServerError(response) {
        switch (response.status) {
            case 401:
                Toast.error('Sessione scaduta. Effettuare nuovamente il login.');
                setTimeout(() => {
                    window.location.href = '/login';
                }, 2000);
                break;
                
            case 403:
                Toast.error('Non hai i permessi per eseguire questa operazione.');
                break;
                
            case 404:
                Toast.error('Risorsa non trovata.');
                break;
                
            case 422:
                // Validation errors - handled by form components
                break;
                
            case 500:
                Toast.error('Errore interno del server. Riprova più tardi.');
                break;
                
            default:
                Toast.error(response.data.message || 'Errore sconosciuto');
        }
    }
    
    static handleNetworkError() {
        Toast.error('Errore di connessione. Controlla la tua connessione internet.');
    }
    
    static handleGenericError(message) {
        Toast.error(message || 'Si è verificato un errore imprevisto.');
    }
}

// Setup global error handling
window.addEventListener('error', (e) => {
    ErrorHandler.handle(e.error, 'Global');
});

window.addEventListener('unhandledrejection', (e) => {
    ErrorHandler.handle(e.reason, 'Promise');
});

10.3 Performance Tips

Optimization Checklist

// resources/js/utils/Performance.js
class PerformanceOptimizer {
    static init() {
        this.setupImageOptimization();
        this.setupScrollOptimization();
        this.setupResizeOptimization();
        this.measurePerformance();
    }
    
    static setupImageOptimization() {
        // Lazy loading images
        const images = document.querySelectorAll('img[data-src]');
        const imageObserver = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const img = entry.target;
                    img.src = img.dataset.src;
                    img.classList.remove('lazy');
                    imageObserver.unobserve(img);
                }
            });
        });
        
        images.forEach(img => imageObserver.observe(img));
    }
    
    static setupScrollOptimization() {
        let ticking = false;
        
        const handleScroll = () => {
            if (!ticking) {
                requestAnimationFrame(() => {
                    // Scroll handling logic
                    ticking = false;
                });
                ticking = true;
            }
        };
        
        window.addEventListener('scroll', handleScroll, { passive: true });
    }
    
    static setupResizeOptimization() {
        let resizeTimeout;
        
        const handleResize = () => {
            clearTimeout(resizeTimeout);
            resizeTimeout = setTimeout(() => {
                // Resize handling logic
                this.recalculateLayout();
            }, 250);
        };
        
        window.addEventListener('resize', handleResize);
    }
    
    static measurePerformance() {
        // Performance monitoring
        new PerformanceObserver((list) => {
            list.getEntries().forEach(entry => {
                if (entry.entryType === 'navigation') {
                    console.log('Page load time:', entry.loadEventEnd - entry.loadEventStart);
                }
            });
        }).observe({ entryTypes: ['navigation'] });
    }
    
    static recalculateLayout() {
        // Layout recalculation logic
        const tables = document.querySelectorAll('.table-responsive');
        tables.forEach(table => {
            // Recalculate table dimensions
        });
    }
}

// Initialize performance optimization
document.addEventListener('DOMContentLoaded', () => {
    PerformanceOptimizer.init();
});

🎯 RIEPILOGO CAPITOLO 8

Completato

  • Architettura Frontend: Struttura modulare, componenti base
  • Stack Tecnologico: Bootstrap 5, Alpine.js, Vite, Axios
  • Componenti UI: Button, Form, DataTable, Modal components
  • JavaScript: AJAX, Alpine.js, Event handling
  • Responsive Design: Breakpoints, mobile navigation
  • Performance: Lazy loading, code splitting, caching
  • State Management: Semplice gestore stato
  • Validazione: Real-time validation, regole custom
  • Esempi Pratici: Form completo, dashboard charts
  • Best Practices: Pattern modulari, error handling, ottimizzazione

🎯 Focus Principali

  • Interfaccia moderna e responsive
  • Componenti riutilizzabili
  • Performance ottimizzata
  • Validazione robusta
  • Gestione errori completa

🔧 Pronto per Implementazione

  • Tutti i componenti base pronti
  • Esempi pratici testabili
  • Best practices definite
  • Struttura scalabile e mantenibile

Capitolo 8 completato con successo! 🎨