1803 lines
49 KiB
Markdown
Executable File
1803 lines
49 KiB
Markdown
Executable File
# ⚡ JAVASCRIPT - Documentazione Completa NetGescon
|
|
|
|
> **Sistema JavaScript dell'interfaccia universale**
|
|
> **📅 Creato:** 21/07/2025
|
|
> **🔄 Ultimo aggiornamento:** 21/07/2025
|
|
> **🎯 Priorità:** ⚡ ALTO
|
|
|
|
---
|
|
|
|
## 📋 **OVERVIEW JAVASCRIPT SYSTEM**
|
|
|
|
Il Sistema JavaScript NetGescon fornisce **INTERATTIVITÀ AVANZATA** per:
|
|
- 🎭 **Core Animations & Transitions**
|
|
- 🔄 **State Management**
|
|
- 📡 **AJAX & API Calls**
|
|
- 📱 **Responsive Interactions**
|
|
- 🎯 **Form Enhancements**
|
|
- 📊 **Dashboard Dynamics**
|
|
- 🔧 **Utility Functions**
|
|
|
|
### 🎯 **JavaScript Philosophy**
|
|
- **Performance First** - Codice ottimizzato e lazy loading
|
|
- **Progressive Enhancement** - Funziona senza JS, migliora con JS
|
|
- **Modular Architecture** - Componenti riutilizzabili e indipendenti
|
|
- **Modern Standards** - ES6+, Vanilla JS, No jQuery dependency
|
|
|
|
---
|
|
|
|
## 🏗️ **ARCHITECTURE OVERVIEW**
|
|
|
|
### 📁 **File Structure**
|
|
```
|
|
/public/js/
|
|
├── core/
|
|
│ ├── app.js # Core application logic
|
|
│ ├── config.js # Configuration constants
|
|
│ ├── utils.js # Utility functions
|
|
│ └── events.js # Event management system
|
|
├── components/
|
|
│ ├── sidebar.js # Sidebar functionality
|
|
│ ├── header.js # Header interactions
|
|
│ ├── cards.js # Cards enhancements
|
|
│ ├── forms.js # Form improvements
|
|
│ ├── tables.js # Table features
|
|
│ └── modals.js # Modal management
|
|
├── modules/
|
|
│ ├── dashboard.js # Dashboard specific
|
|
│ ├── stabili.js # Stabili management
|
|
│ ├── condomini.js # Condomini features
|
|
│ └── reports.js # Reports functionality
|
|
├── libs/
|
|
│ ├── chart.min.js # Chart.js library
|
|
│ ├── datatables.min.js # DataTables library
|
|
│ └── alpine.min.js # Alpine.js (if used)
|
|
└── netgescon-admin.js # Main compiled file
|
|
```
|
|
|
|
---
|
|
|
|
## 🎭 **CORE APPLICATION**
|
|
|
|
### 📁 **File:** `/public/js/core/app.js`
|
|
|
|
```javascript
|
|
/**
|
|
* NetGescon Admin - Core Application
|
|
* @version 2.1.0
|
|
* @description Main application initialization and coordination
|
|
*/
|
|
|
|
class NetGesconApp {
|
|
constructor() {
|
|
this.version = '2.1.0';
|
|
this.debug = window.location.hostname === 'localhost';
|
|
this.components = new Map();
|
|
this.modules = new Map();
|
|
this.initialized = false;
|
|
|
|
// Bind context
|
|
this.init = this.init.bind(this);
|
|
this.log = this.log.bind(this);
|
|
this.error = this.error.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Initialize the application
|
|
*/
|
|
async init() {
|
|
if (this.initialized) {
|
|
this.log('App already initialized');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.log('Initializing NetGescon Admin...', this.version);
|
|
|
|
// 1. Initialize core systems
|
|
await this.initializeCore();
|
|
|
|
// 2. Initialize components
|
|
await this.initializeComponents();
|
|
|
|
// 3. Initialize modules based on page
|
|
await this.initializeModules();
|
|
|
|
// 4. Setup global event handlers
|
|
this.setupGlobalHandlers();
|
|
|
|
// 5. Perform system checks
|
|
this.performSystemChecks();
|
|
|
|
this.initialized = true;
|
|
this.log('NetGescon Admin initialized successfully');
|
|
|
|
// Dispatch custom event
|
|
this.dispatch('netgescon:ready', { version: this.version });
|
|
|
|
} catch (error) {
|
|
this.error('Failed to initialize NetGescon Admin:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize core systems
|
|
*/
|
|
async initializeCore() {
|
|
// Initialize configuration
|
|
await NetGesconConfig.init();
|
|
|
|
// Initialize utilities
|
|
NetGesconUtils.init();
|
|
|
|
// Initialize event system
|
|
NetGesconEvents.init();
|
|
|
|
// Setup CSRF token
|
|
this.setupCSRF();
|
|
|
|
// Setup global error handling
|
|
this.setupErrorHandling();
|
|
}
|
|
|
|
/**
|
|
* Initialize UI components
|
|
*/
|
|
async initializeComponents() {
|
|
const componentLoaders = [
|
|
{ name: 'sidebar', module: () => import('./components/sidebar.js') },
|
|
{ name: 'header', module: () => import('./components/header.js') },
|
|
{ name: 'cards', module: () => import('./components/cards.js') },
|
|
{ name: 'forms', module: () => import('./components/forms.js') },
|
|
{ name: 'tables', module: () => import('./components/tables.js') },
|
|
{ name: 'modals', module: () => import('./components/modals.js') }
|
|
];
|
|
|
|
for (const loader of componentLoaders) {
|
|
try {
|
|
const module = await loader.module();
|
|
const Component = module.default || module[loader.name];
|
|
|
|
if (Component && typeof Component.init === 'function') {
|
|
await Component.init();
|
|
this.components.set(loader.name, Component);
|
|
this.log(`Component '${loader.name}' initialized`);
|
|
}
|
|
} catch (error) {
|
|
this.error(`Failed to load component '${loader.name}':`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize page-specific modules
|
|
*/
|
|
async initializeModules() {
|
|
const page = this.getCurrentPage();
|
|
const moduleMap = {
|
|
'dashboard': () => import('./modules/dashboard.js'),
|
|
'stabili': () => import('./modules/stabili.js'),
|
|
'condomini': () => import('./modules/condomini.js'),
|
|
'reports': () => import('./modules/reports.js')
|
|
};
|
|
|
|
if (moduleMap[page]) {
|
|
try {
|
|
const module = await moduleMap[page]();
|
|
const Module = module.default || module[page];
|
|
|
|
if (Module && typeof Module.init === 'function') {
|
|
await Module.init();
|
|
this.modules.set(page, Module);
|
|
this.log(`Module '${page}' initialized`);
|
|
}
|
|
} catch (error) {
|
|
this.error(`Failed to load module '${page}':`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup global event handlers
|
|
*/
|
|
setupGlobalHandlers() {
|
|
// Window resize handler
|
|
window.addEventListener('resize', NetGesconUtils.throttle(() => {
|
|
this.dispatch('netgescon:resize', {
|
|
width: window.innerWidth,
|
|
height: window.innerHeight
|
|
});
|
|
}, 250));
|
|
|
|
// Visibility change handler
|
|
document.addEventListener('visibilitychange', () => {
|
|
this.dispatch('netgescon:visibility', {
|
|
hidden: document.hidden
|
|
});
|
|
});
|
|
|
|
// Before unload handler
|
|
window.addEventListener('beforeunload', (e) => {
|
|
const hasUnsavedChanges = this.checkUnsavedChanges();
|
|
if (hasUnsavedChanges) {
|
|
e.preventDefault();
|
|
e.returnValue = 'Ci sono modifiche non salvate. Sei sicuro di voler uscire?';
|
|
return e.returnValue;
|
|
}
|
|
});
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
this.handleKeyboardShortcuts(e);
|
|
});
|
|
|
|
// Handle loading states
|
|
this.setupLoadingStates();
|
|
}
|
|
|
|
/**
|
|
* Setup CSRF token for AJAX requests
|
|
*/
|
|
setupCSRF() {
|
|
const token = document.querySelector('meta[name="csrf-token"]');
|
|
if (token) {
|
|
// Set default header for fetch requests
|
|
window.fetch = new Proxy(window.fetch, {
|
|
apply: function(target, thisArg, argumentsList) {
|
|
const [url, options = {}] = argumentsList;
|
|
|
|
if (options.method && options.method.toUpperCase() !== 'GET') {
|
|
options.headers = {
|
|
'X-CSRF-TOKEN': token.getAttribute('content'),
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
...options.headers
|
|
};
|
|
}
|
|
|
|
return target.apply(thisArg, [url, options]);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup global error handling
|
|
*/
|
|
setupErrorHandling() {
|
|
window.addEventListener('error', (e) => {
|
|
this.error('JavaScript Error:', e.error);
|
|
});
|
|
|
|
window.addEventListener('unhandledrejection', (e) => {
|
|
this.error('Unhandled Promise Rejection:', e.reason);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup loading states management
|
|
*/
|
|
setupLoadingStates() {
|
|
// Intercept form submissions
|
|
document.addEventListener('submit', (e) => {
|
|
const form = e.target;
|
|
if (form.classList.contains('netgescon-form')) {
|
|
this.showFormLoading(form);
|
|
}
|
|
});
|
|
|
|
// Intercept AJAX requests
|
|
const originalFetch = window.fetch;
|
|
window.fetch = function(...args) {
|
|
const request = originalFetch.apply(this, args);
|
|
|
|
// Show global loading indicator for slow requests
|
|
const timeout = setTimeout(() => {
|
|
NetGesconApp.instance.showGlobalLoading();
|
|
}, 500);
|
|
|
|
request.finally(() => {
|
|
clearTimeout(timeout);
|
|
NetGesconApp.instance.hideGlobalLoading();
|
|
});
|
|
|
|
return request;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Handle keyboard shortcuts
|
|
*/
|
|
handleKeyboardShortcuts(e) {
|
|
// Ctrl/Cmd + K = Search
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
this.openSearch();
|
|
}
|
|
|
|
// Esc = Close modals/dropdowns
|
|
if (e.key === 'Escape') {
|
|
this.closeAllOverlays();
|
|
}
|
|
|
|
// Ctrl/Cmd + S = Save
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
const activeForm = document.querySelector('form:focus-within');
|
|
if (activeForm) {
|
|
e.preventDefault();
|
|
activeForm.dispatchEvent(new Event('submit'));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current page identifier
|
|
*/
|
|
getCurrentPage() {
|
|
const path = window.location.pathname;
|
|
const segments = path.split('/').filter(Boolean);
|
|
|
|
if (segments.includes('admin')) {
|
|
const adminIndex = segments.indexOf('admin');
|
|
return segments[adminIndex + 1] || 'dashboard';
|
|
}
|
|
|
|
return 'dashboard';
|
|
}
|
|
|
|
/**
|
|
* Check for unsaved changes
|
|
*/
|
|
checkUnsavedChanges() {
|
|
const forms = document.querySelectorAll('.netgescon-form[data-track-changes]');
|
|
return Array.from(forms).some(form => form.dataset.hasChanges === 'true');
|
|
}
|
|
|
|
/**
|
|
* Perform system checks
|
|
*/
|
|
performSystemChecks() {
|
|
// Check browser compatibility
|
|
this.checkBrowserSupport();
|
|
|
|
// Check required dependencies
|
|
this.checkDependencies();
|
|
|
|
// Check API connectivity
|
|
this.checkAPIConnection();
|
|
}
|
|
|
|
/**
|
|
* Check browser support
|
|
*/
|
|
checkBrowserSupport() {
|
|
const requiredFeatures = [
|
|
'fetch',
|
|
'Promise',
|
|
'Map',
|
|
'Set',
|
|
'IntersectionObserver'
|
|
];
|
|
|
|
const unsupported = requiredFeatures.filter(feature => !window[feature]);
|
|
|
|
if (unsupported.length > 0) {
|
|
this.error('Browser missing required features:', unsupported);
|
|
this.showBrowserWarning();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check required dependencies
|
|
*/
|
|
checkDependencies() {
|
|
const dependencies = ['Chart', 'bootstrap'];
|
|
const missing = dependencies.filter(dep => !window[dep]);
|
|
|
|
if (missing.length > 0) {
|
|
this.error('Missing dependencies:', missing);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check API connectivity
|
|
*/
|
|
async checkAPIConnection() {
|
|
try {
|
|
const response = await fetch('/api/health');
|
|
if (!response.ok) {
|
|
throw new Error(`API returned ${response.status}`);
|
|
}
|
|
this.log('API connection successful');
|
|
} catch (error) {
|
|
this.error('API connection failed:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show form loading state
|
|
*/
|
|
showFormLoading(form) {
|
|
const submitBtn = form.querySelector('[type="submit"]');
|
|
if (submitBtn) {
|
|
submitBtn.disabled = true;
|
|
submitBtn.classList.add('loading');
|
|
|
|
const originalText = submitBtn.textContent;
|
|
submitBtn.textContent = 'Caricamento...';
|
|
submitBtn.dataset.originalText = originalText;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show global loading indicator
|
|
*/
|
|
showGlobalLoading() {
|
|
const loader = document.getElementById('global-loader');
|
|
if (loader) {
|
|
loader.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide global loading indicator
|
|
*/
|
|
hideGlobalLoading() {
|
|
const loader = document.getElementById('global-loader');
|
|
if (loader) {
|
|
loader.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open search functionality
|
|
*/
|
|
openSearch() {
|
|
const searchModal = document.getElementById('search-modal');
|
|
if (searchModal) {
|
|
// Open search modal
|
|
const modal = new bootstrap.Modal(searchModal);
|
|
modal.show();
|
|
|
|
// Focus search input
|
|
const input = searchModal.querySelector('input[type="search"]');
|
|
if (input) {
|
|
setTimeout(() => input.focus(), 100);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close all overlays
|
|
*/
|
|
closeAllOverlays() {
|
|
// Close modals
|
|
document.querySelectorAll('.modal.show').forEach(modal => {
|
|
const modalInstance = bootstrap.Modal.getInstance(modal);
|
|
if (modalInstance) {
|
|
modalInstance.hide();
|
|
}
|
|
});
|
|
|
|
// Close dropdowns
|
|
document.querySelectorAll('.dropdown-menu.show').forEach(dropdown => {
|
|
dropdown.classList.remove('show');
|
|
});
|
|
|
|
// Close sidebar on mobile
|
|
if (window.innerWidth < 768) {
|
|
const sidebar = document.getElementById('sidebar');
|
|
if (sidebar && sidebar.classList.contains('show')) {
|
|
sidebar.classList.remove('show');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show browser warning
|
|
*/
|
|
showBrowserWarning() {
|
|
const warning = document.createElement('div');
|
|
warning.className = 'alert alert-warning alert-dismissible fixed-top';
|
|
warning.style.zIndex = '9999';
|
|
warning.innerHTML = `
|
|
<strong>Browser non supportato</strong>
|
|
Alcune funzionalità potrebbero non funzionare correttamente.
|
|
Ti consigliamo di aggiornare il browser.
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
document.body.insertBefore(warning, document.body.firstChild);
|
|
}
|
|
|
|
/**
|
|
* Dispatch custom event
|
|
*/
|
|
dispatch(eventName, detail = {}) {
|
|
const event = new CustomEvent(eventName, { detail });
|
|
document.dispatchEvent(event);
|
|
this.log(`Event dispatched: ${eventName}`, detail);
|
|
}
|
|
|
|
/**
|
|
* Get component instance
|
|
*/
|
|
getComponent(name) {
|
|
return this.components.get(name);
|
|
}
|
|
|
|
/**
|
|
* Get module instance
|
|
*/
|
|
getModule(name) {
|
|
return this.modules.get(name);
|
|
}
|
|
|
|
/**
|
|
* Debug logging
|
|
*/
|
|
log(...args) {
|
|
if (this.debug) {
|
|
console.log('[NetGescon]', ...args);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error logging
|
|
*/
|
|
error(...args) {
|
|
console.error('[NetGescon]', ...args);
|
|
|
|
// Send error to monitoring service if available
|
|
if (window.Sentry) {
|
|
window.Sentry.captureException(new Error(args.join(' ')));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create singleton instance
|
|
NetGesconApp.instance = new NetGesconApp();
|
|
|
|
// Initialize when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', NetGesconApp.instance.init);
|
|
} else {
|
|
NetGesconApp.instance.init();
|
|
}
|
|
|
|
// Export for use in other modules
|
|
window.NetGesconApp = NetGesconApp;
|
|
```
|
|
|
|
---
|
|
|
|
## ⚙️ **CONFIGURATION SYSTEM**
|
|
|
|
### 📁 **File:** `/public/js/core/config.js`
|
|
|
|
```javascript
|
|
/**
|
|
* NetGescon Configuration System
|
|
* @description Centralized configuration management
|
|
*/
|
|
|
|
class NetGesconConfig {
|
|
constructor() {
|
|
this.data = new Map();
|
|
this.initialized = false;
|
|
}
|
|
|
|
/**
|
|
* Initialize configuration
|
|
*/
|
|
async init() {
|
|
if (this.initialized) return;
|
|
|
|
// Load from meta tags
|
|
this.loadFromMeta();
|
|
|
|
// Load from API if needed
|
|
await this.loadFromAPI();
|
|
|
|
// Set defaults
|
|
this.setDefaults();
|
|
|
|
this.initialized = true;
|
|
}
|
|
|
|
/**
|
|
* Load configuration from meta tags
|
|
*/
|
|
loadFromMeta() {
|
|
const metaTags = document.querySelectorAll('meta[name^="netgescon-"]');
|
|
|
|
metaTags.forEach(meta => {
|
|
const key = meta.name.replace('netgescon-', '');
|
|
const value = this.parseValue(meta.content);
|
|
this.set(key, value);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load configuration from API
|
|
*/
|
|
async loadFromAPI() {
|
|
try {
|
|
const response = await fetch('/api/config');
|
|
if (response.ok) {
|
|
const config = await response.json();
|
|
Object.entries(config).forEach(([key, value]) => {
|
|
this.set(key, value);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to load config from API:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set default configuration values
|
|
*/
|
|
setDefaults() {
|
|
const defaults = {
|
|
// App settings
|
|
'app.name': 'NetGescon',
|
|
'app.version': '2.1.0',
|
|
'app.debug': false,
|
|
'app.environment': 'production',
|
|
|
|
// UI settings
|
|
'ui.theme': 'default',
|
|
'ui.sidebar.collapsed': false,
|
|
'ui.animations.enabled': true,
|
|
'ui.animations.duration': 300,
|
|
|
|
// Performance settings
|
|
'performance.lazy_load': true,
|
|
'performance.cache_ttl': 300000, // 5 minutes
|
|
'performance.throttle_delay': 250,
|
|
|
|
// API settings
|
|
'api.base_url': '/api',
|
|
'api.timeout': 30000,
|
|
'api.retry_attempts': 3,
|
|
|
|
// Features
|
|
'features.real_time_updates': true,
|
|
'features.offline_support': false,
|
|
'features.analytics': true,
|
|
|
|
// Localization
|
|
'locale.language': 'it',
|
|
'locale.timezone': 'Europe/Rome',
|
|
'locale.currency': 'EUR',
|
|
'locale.date_format': 'DD/MM/YYYY',
|
|
'locale.time_format': 'HH:mm',
|
|
|
|
// Tables
|
|
'tables.default_page_size': 25,
|
|
'tables.page_size_options': [10, 25, 50, 100],
|
|
'tables.search_delay': 500,
|
|
|
|
// Forms
|
|
'forms.auto_save': true,
|
|
'forms.auto_save_delay': 2000,
|
|
'forms.validation_delay': 300,
|
|
|
|
// Notifications
|
|
'notifications.position': 'top-right',
|
|
'notifications.auto_dismiss': true,
|
|
'notifications.dismiss_delay': 5000
|
|
};
|
|
|
|
Object.entries(defaults).forEach(([key, value]) => {
|
|
if (!this.has(key)) {
|
|
this.set(key, value);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse string value to appropriate type
|
|
*/
|
|
parseValue(value) {
|
|
if (value === 'true') return true;
|
|
if (value === 'false') return false;
|
|
if (value === 'null') return null;
|
|
if (value === 'undefined') return undefined;
|
|
|
|
// Try to parse as number
|
|
const numValue = Number(value);
|
|
if (!isNaN(numValue) && isFinite(numValue)) {
|
|
return numValue;
|
|
}
|
|
|
|
// Try to parse as JSON
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get configuration value
|
|
*/
|
|
get(key, defaultValue = null) {
|
|
return this.data.get(key) ?? defaultValue;
|
|
}
|
|
|
|
/**
|
|
* Set configuration value
|
|
*/
|
|
set(key, value) {
|
|
this.data.set(key, value);
|
|
|
|
// Dispatch change event
|
|
document.dispatchEvent(new CustomEvent('netgescon:config:change', {
|
|
detail: { key, value, config: this }
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Check if configuration key exists
|
|
*/
|
|
has(key) {
|
|
return this.data.has(key);
|
|
}
|
|
|
|
/**
|
|
* Remove configuration key
|
|
*/
|
|
remove(key) {
|
|
const existed = this.data.delete(key);
|
|
if (existed) {
|
|
document.dispatchEvent(new CustomEvent('netgescon:config:remove', {
|
|
detail: { key, config: this }
|
|
}));
|
|
}
|
|
return existed;
|
|
}
|
|
|
|
/**
|
|
* Get all configuration data
|
|
*/
|
|
all() {
|
|
return Object.fromEntries(this.data);
|
|
}
|
|
|
|
/**
|
|
* Get configuration subset by prefix
|
|
*/
|
|
getByPrefix(prefix) {
|
|
const result = {};
|
|
for (const [key, value] of this.data) {
|
|
if (key.startsWith(prefix)) {
|
|
const shortKey = key.substring(prefix.length);
|
|
result[shortKey] = value;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Merge configuration object
|
|
*/
|
|
merge(config) {
|
|
Object.entries(config).forEach(([key, value]) => {
|
|
this.set(key, value);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Save configuration to localStorage
|
|
*/
|
|
save() {
|
|
try {
|
|
const config = this.all();
|
|
localStorage.setItem('netgescon_config', JSON.stringify(config));
|
|
} catch (error) {
|
|
console.warn('Failed to save config to localStorage:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load configuration from localStorage
|
|
*/
|
|
load() {
|
|
try {
|
|
const saved = localStorage.getItem('netgescon_config');
|
|
if (saved) {
|
|
const config = JSON.parse(saved);
|
|
this.merge(config);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to load config from localStorage:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create singleton instance
|
|
const NetGesconConfig = new NetGesconConfig();
|
|
|
|
// Export for use in other modules
|
|
window.NetGesconConfig = NetGesconConfig;
|
|
```
|
|
|
|
---
|
|
|
|
## 🛠️ **UTILITY FUNCTIONS**
|
|
|
|
### 📁 **File:** `/public/js/core/utils.js`
|
|
|
|
```javascript
|
|
/**
|
|
* NetGescon Utility Functions
|
|
* @description Common utility functions used throughout the application
|
|
*/
|
|
|
|
class NetGesconUtils {
|
|
constructor() {
|
|
this.cache = new Map();
|
|
this.timers = new Map();
|
|
}
|
|
|
|
/**
|
|
* Initialize utilities
|
|
*/
|
|
init() {
|
|
// Setup global utility functions
|
|
this.setupGlobalHelpers();
|
|
|
|
// Setup performance monitoring
|
|
this.setupPerformanceMonitoring();
|
|
}
|
|
|
|
/**
|
|
* Setup global helper functions
|
|
*/
|
|
setupGlobalHelpers() {
|
|
// Add utility functions to window for global access
|
|
window.$ = this.querySelector.bind(this);
|
|
window.$$ = this.querySelectorAll.bind(this);
|
|
window.debounce = this.debounce.bind(this);
|
|
window.throttle = this.throttle.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Setup performance monitoring
|
|
*/
|
|
setupPerformanceMonitoring() {
|
|
// Monitor DOM mutations
|
|
if (window.MutationObserver) {
|
|
const observer = new MutationObserver((mutations) => {
|
|
const addedNodes = mutations.reduce((acc, mutation) => {
|
|
return acc + mutation.addedNodes.length;
|
|
}, 0);
|
|
|
|
if (addedNodes > 50) {
|
|
console.warn('Large DOM mutation detected:', addedNodes, 'nodes');
|
|
}
|
|
});
|
|
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
}
|
|
}
|
|
|
|
// =========================
|
|
// DOM UTILITIES
|
|
// =========================
|
|
|
|
/**
|
|
* Enhanced querySelector
|
|
*/
|
|
querySelector(selector, context = document) {
|
|
return context.querySelector(selector);
|
|
}
|
|
|
|
/**
|
|
* Enhanced querySelectorAll
|
|
*/
|
|
querySelectorAll(selector, context = document) {
|
|
return Array.from(context.querySelectorAll(selector));
|
|
}
|
|
|
|
/**
|
|
* Create element with attributes and content
|
|
*/
|
|
createElement(tag, attributes = {}, content = '') {
|
|
const element = document.createElement(tag);
|
|
|
|
Object.entries(attributes).forEach(([key, value]) => {
|
|
if (key === 'className') {
|
|
element.className = value;
|
|
} else if (key === 'innerHTML') {
|
|
element.innerHTML = value;
|
|
} else if (key === 'textContent') {
|
|
element.textContent = value;
|
|
} else {
|
|
element.setAttribute(key, value);
|
|
}
|
|
});
|
|
|
|
if (content) {
|
|
element.innerHTML = content;
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* Check if element is visible in viewport
|
|
*/
|
|
isInViewport(element, threshold = 0) {
|
|
const rect = element.getBoundingClientRect();
|
|
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
|
|
return (
|
|
rect.top >= -threshold &&
|
|
rect.left >= -threshold &&
|
|
rect.bottom <= windowHeight + threshold &&
|
|
rect.right <= windowWidth + threshold
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Smooth scroll to element
|
|
*/
|
|
scrollToElement(element, options = {}) {
|
|
const defaults = {
|
|
behavior: 'smooth',
|
|
block: 'start',
|
|
inline: 'nearest'
|
|
};
|
|
|
|
const config = { ...defaults, ...options };
|
|
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
|
|
if (element) {
|
|
element.scrollIntoView(config);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get element dimensions and position
|
|
*/
|
|
getElementInfo(element) {
|
|
const rect = element.getBoundingClientRect();
|
|
const style = window.getComputedStyle(element);
|
|
|
|
return {
|
|
width: rect.width,
|
|
height: rect.height,
|
|
top: rect.top + window.scrollY,
|
|
left: rect.left + window.scrollX,
|
|
bottom: rect.bottom + window.scrollY,
|
|
right: rect.right + window.scrollX,
|
|
visible: this.isInViewport(element),
|
|
zIndex: parseInt(style.zIndex) || 0,
|
|
opacity: parseFloat(style.opacity)
|
|
};
|
|
}
|
|
|
|
// =========================
|
|
// PERFORMANCE UTILITIES
|
|
// =========================
|
|
|
|
/**
|
|
* Debounce function execution
|
|
*/
|
|
debounce(func, wait, immediate = false) {
|
|
let timeout;
|
|
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
timeout = null;
|
|
if (!immediate) func(...args);
|
|
};
|
|
|
|
const callNow = immediate && !timeout;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
|
|
if (callNow) func(...args);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Throttle function execution
|
|
*/
|
|
throttle(func, limit) {
|
|
let inThrottle;
|
|
|
|
return function(...args) {
|
|
if (!inThrottle) {
|
|
func.apply(this, args);
|
|
inThrottle = true;
|
|
setTimeout(() => inThrottle = false, limit);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Request animation frame with fallback
|
|
*/
|
|
requestAnimationFrame(callback) {
|
|
return window.requestAnimationFrame ||
|
|
window.webkitRequestAnimationFrame ||
|
|
window.mozRequestAnimationFrame ||
|
|
((cb) => setTimeout(cb, 16));
|
|
}
|
|
|
|
/**
|
|
* Lazy load with Intersection Observer
|
|
*/
|
|
lazyLoad(elements, callback, options = {}) {
|
|
const defaults = {
|
|
root: null,
|
|
rootMargin: '50px',
|
|
threshold: 0.1
|
|
};
|
|
|
|
const config = { ...defaults, ...options };
|
|
|
|
if (!window.IntersectionObserver) {
|
|
// Fallback for browsers without Intersection Observer
|
|
elements.forEach(callback);
|
|
return;
|
|
}
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
callback(entry.target);
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, config);
|
|
|
|
elements.forEach(element => observer.observe(element));
|
|
|
|
return observer;
|
|
}
|
|
|
|
// =========================
|
|
// DATA UTILITIES
|
|
// =========================
|
|
|
|
/**
|
|
* Deep clone object
|
|
*/
|
|
deepClone(obj) {
|
|
if (obj === null || typeof obj !== 'object') {
|
|
return obj;
|
|
}
|
|
|
|
if (obj instanceof Date) {
|
|
return new Date(obj.getTime());
|
|
}
|
|
|
|
if (obj instanceof Array) {
|
|
return obj.map(item => this.deepClone(item));
|
|
}
|
|
|
|
if (typeof obj === 'object') {
|
|
const cloned = {};
|
|
Object.keys(obj).forEach(key => {
|
|
cloned[key] = this.deepClone(obj[key]);
|
|
});
|
|
return cloned;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merge objects deeply
|
|
*/
|
|
deepMerge(target, source) {
|
|
const result = this.deepClone(target);
|
|
|
|
Object.keys(source).forEach(key => {
|
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
result[key] = this.deepMerge(result[key] || {}, source[key]);
|
|
} else {
|
|
result[key] = source[key];
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get nested object property safely
|
|
*/
|
|
get(obj, path, defaultValue = undefined) {
|
|
const keys = path.split('.');
|
|
let result = obj;
|
|
|
|
for (const key of keys) {
|
|
if (result == null || typeof result !== 'object') {
|
|
return defaultValue;
|
|
}
|
|
result = result[key];
|
|
}
|
|
|
|
return result !== undefined ? result : defaultValue;
|
|
}
|
|
|
|
/**
|
|
* Set nested object property
|
|
*/
|
|
set(obj, path, value) {
|
|
const keys = path.split('.');
|
|
const lastKey = keys.pop();
|
|
let current = obj;
|
|
|
|
for (const key of keys) {
|
|
if (!(key in current) || typeof current[key] !== 'object') {
|
|
current[key] = {};
|
|
}
|
|
current = current[key];
|
|
}
|
|
|
|
current[lastKey] = value;
|
|
return obj;
|
|
}
|
|
|
|
// =========================
|
|
// VALIDATION UTILITIES
|
|
// =========================
|
|
|
|
/**
|
|
* Validate email address
|
|
*/
|
|
isValidEmail(email) {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
}
|
|
|
|
/**
|
|
* Validate Italian phone number
|
|
*/
|
|
isValidPhone(phone) {
|
|
const phoneRegex = /^(\+39\s?)?((3[0-9]{2}\s?\d{6,7})|(0[0-9]{1,3}\s?\d{6,8}))$/;
|
|
return phoneRegex.test(phone.replace(/\s/g, ''));
|
|
}
|
|
|
|
/**
|
|
* Validate Italian fiscal code
|
|
*/
|
|
isValidFiscalCode(code) {
|
|
const fiscalCodeRegex = /^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$/;
|
|
return fiscalCodeRegex.test(code.toUpperCase());
|
|
}
|
|
|
|
/**
|
|
* Validate Italian VAT number
|
|
*/
|
|
isValidVAT(vat) {
|
|
const vatRegex = /^[0-9]{11}$/;
|
|
return vatRegex.test(vat);
|
|
}
|
|
|
|
// =========================
|
|
// FORMAT UTILITIES
|
|
// =========================
|
|
|
|
/**
|
|
* Format currency
|
|
*/
|
|
formatCurrency(amount, currency = 'EUR', locale = 'it-IT') {
|
|
return new Intl.NumberFormat(locale, {
|
|
style: 'currency',
|
|
currency: currency
|
|
}).format(amount);
|
|
}
|
|
|
|
/**
|
|
* Format date
|
|
*/
|
|
formatDate(date, format = 'DD/MM/YYYY', locale = 'it-IT') {
|
|
if (!date) return '';
|
|
|
|
const d = new Date(date);
|
|
if (isNaN(d.getTime())) return '';
|
|
|
|
const options = {};
|
|
|
|
if (format.includes('DD')) options.day = '2-digit';
|
|
if (format.includes('MM')) options.month = '2-digit';
|
|
if (format.includes('YYYY')) options.year = 'numeric';
|
|
if (format.includes('HH')) options.hour = '2-digit';
|
|
if (format.includes('mm')) options.minute = '2-digit';
|
|
|
|
return d.toLocaleDateString(locale, options);
|
|
}
|
|
|
|
/**
|
|
* Format file size
|
|
*/
|
|
formatFileSize(bytes, decimals = 2) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
}
|
|
|
|
/**
|
|
* Slugify string
|
|
*/
|
|
slugify(text) {
|
|
return text
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.replace(/[^a-z0-9\s-]/g, '')
|
|
.trim()
|
|
.replace(/[\s-]+/g, '-');
|
|
}
|
|
|
|
// =========================
|
|
// CACHE UTILITIES
|
|
// =========================
|
|
|
|
/**
|
|
* Cache with TTL
|
|
*/
|
|
cache(key, value, ttl = 300000) { // 5 minutes default
|
|
if (value === undefined) {
|
|
// Get from cache
|
|
const cached = this.cache.get(key);
|
|
if (cached && Date.now() < cached.expires) {
|
|
return cached.value;
|
|
}
|
|
this.cache.delete(key);
|
|
return null;
|
|
} else {
|
|
// Set cache
|
|
this.cache.set(key, {
|
|
value: value,
|
|
expires: Date.now() + ttl
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear expired cache entries
|
|
*/
|
|
cleanCache() {
|
|
const now = Date.now();
|
|
for (const [key, cached] of this.cache) {
|
|
if (now >= cached.expires) {
|
|
this.cache.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =========================
|
|
// ASYNC UTILITIES
|
|
// =========================
|
|
|
|
/**
|
|
* Sleep function
|
|
*/
|
|
sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* Retry function with backoff
|
|
*/
|
|
async retry(fn, maxAttempts = 3, delay = 1000) {
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
if (attempt === maxAttempts) {
|
|
throw error;
|
|
}
|
|
await this.sleep(delay * attempt);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Timeout promise
|
|
*/
|
|
timeout(promise, ms) {
|
|
return Promise.race([
|
|
promise,
|
|
new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error('Timeout')), ms)
|
|
)
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Create singleton instance
|
|
const NetGesconUtils = new NetGesconUtils();
|
|
|
|
// Export for use in other modules
|
|
window.NetGesconUtils = NetGesconUtils;
|
|
```
|
|
|
|
---
|
|
|
|
## 📡 **API MANAGEMENT**
|
|
|
|
### 📁 **File:** `/public/js/core/api.js`
|
|
|
|
```javascript
|
|
/**
|
|
* NetGescon API Management
|
|
* @description Centralized API communication layer
|
|
*/
|
|
|
|
class NetGesconAPI {
|
|
constructor() {
|
|
this.baseURL = NetGesconConfig.get('api.base_url', '/api');
|
|
this.timeout = NetGesconConfig.get('api.timeout', 30000);
|
|
this.retryAttempts = NetGesconConfig.get('api.retry_attempts', 3);
|
|
this.cache = new Map();
|
|
this.pendingRequests = new Map();
|
|
}
|
|
|
|
/**
|
|
* Make HTTP request
|
|
*/
|
|
async request(url, options = {}) {
|
|
const fullURL = url.startsWith('http') ? url : `${this.baseURL}${url}`;
|
|
const requestKey = `${options.method || 'GET'}:${fullURL}`;
|
|
|
|
// Deduplicate identical requests
|
|
if (this.pendingRequests.has(requestKey)) {
|
|
return this.pendingRequests.get(requestKey);
|
|
}
|
|
|
|
const defaults = {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
credentials: 'same-origin'
|
|
};
|
|
|
|
const config = this.mergeConfig(defaults, options);
|
|
|
|
// Add CSRF token for mutations
|
|
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(config.method)) {
|
|
const token = document.querySelector('meta[name="csrf-token"]');
|
|
if (token) {
|
|
config.headers['X-CSRF-TOKEN'] = token.getAttribute('content');
|
|
}
|
|
}
|
|
|
|
const requestPromise = this.executeRequest(fullURL, config);
|
|
this.pendingRequests.set(requestKey, requestPromise);
|
|
|
|
try {
|
|
const result = await requestPromise;
|
|
return result;
|
|
} finally {
|
|
this.pendingRequests.delete(requestKey);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute request with timeout and retry
|
|
*/
|
|
async executeRequest(url, config) {
|
|
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
|
|
const response = await fetch(url, {
|
|
...config,
|
|
signal: controller.signal
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!response.ok) {
|
|
throw new HTTPError(response.status, response.statusText, response);
|
|
}
|
|
|
|
const data = await this.parseResponse(response);
|
|
return { data, response };
|
|
|
|
} catch (error) {
|
|
if (attempt === this.retryAttempts || !this.shouldRetry(error)) {
|
|
throw error;
|
|
}
|
|
|
|
await NetGesconUtils.sleep(1000 * attempt);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse response based on content type
|
|
*/
|
|
async parseResponse(response) {
|
|
const contentType = response.headers.get('content-type');
|
|
|
|
if (contentType && contentType.includes('application/json')) {
|
|
return await response.json();
|
|
}
|
|
|
|
if (contentType && contentType.includes('text/')) {
|
|
return await response.text();
|
|
}
|
|
|
|
return await response.blob();
|
|
}
|
|
|
|
/**
|
|
* Check if error should trigger retry
|
|
*/
|
|
shouldRetry(error) {
|
|
if (error.name === 'AbortError') return false;
|
|
if (error instanceof HTTPError) {
|
|
return error.status >= 500 || error.status === 429;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Merge request configurations
|
|
*/
|
|
mergeConfig(defaults, options) {
|
|
return {
|
|
...defaults,
|
|
...options,
|
|
headers: {
|
|
...defaults.headers,
|
|
...options.headers
|
|
}
|
|
};
|
|
}
|
|
|
|
// =========================
|
|
// HTTP METHODS
|
|
// =========================
|
|
|
|
/**
|
|
* GET request
|
|
*/
|
|
async get(url, params = {}, options = {}) {
|
|
const searchParams = new URLSearchParams(params);
|
|
const fullURL = searchParams.toString() ? `${url}?${searchParams}` : url;
|
|
|
|
return this.request(fullURL, {
|
|
method: 'GET',
|
|
...options
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST request
|
|
*/
|
|
async post(url, data = {}, options = {}) {
|
|
return this.request(url, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
...options
|
|
});
|
|
}
|
|
|
|
/**
|
|
* PUT request
|
|
*/
|
|
async put(url, data = {}, options = {}) {
|
|
return this.request(url, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
...options
|
|
});
|
|
}
|
|
|
|
/**
|
|
* PATCH request
|
|
*/
|
|
async patch(url, data = {}, options = {}) {
|
|
return this.request(url, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
...options
|
|
});
|
|
}
|
|
|
|
/**
|
|
* DELETE request
|
|
*/
|
|
async delete(url, options = {}) {
|
|
return this.request(url, {
|
|
method: 'DELETE',
|
|
...options
|
|
});
|
|
}
|
|
|
|
// =========================
|
|
// SPECIALIZED METHODS
|
|
// =========================
|
|
|
|
/**
|
|
* Upload file
|
|
*/
|
|
async upload(url, file, onProgress = null) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
xhr.upload.addEventListener('progress', (e) => {
|
|
if (e.lengthComputable && onProgress) {
|
|
const percentComplete = (e.loaded / e.total) * 100;
|
|
onProgress(percentComplete);
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('load', () => {
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
try {
|
|
const response = JSON.parse(xhr.responseText);
|
|
resolve(response);
|
|
} catch (error) {
|
|
resolve(xhr.responseText);
|
|
}
|
|
} else {
|
|
reject(new HTTPError(xhr.status, xhr.statusText));
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('error', () => {
|
|
reject(new Error('Upload failed'));
|
|
});
|
|
|
|
xhr.open('POST', url.startsWith('http') ? url : `${this.baseURL}${url}`);
|
|
|
|
// Add CSRF token
|
|
const token = document.querySelector('meta[name="csrf-token"]');
|
|
if (token) {
|
|
xhr.setRequestHeader('X-CSRF-TOKEN', token.getAttribute('content'));
|
|
}
|
|
|
|
xhr.send(formData);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Download file
|
|
*/
|
|
async download(url, filename = null) {
|
|
try {
|
|
const response = await fetch(url.startsWith('http') ? url : `${this.baseURL}${url}`);
|
|
|
|
if (!response.ok) {
|
|
throw new HTTPError(response.status, response.statusText);
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const downloadURL = window.URL.createObjectURL(blob);
|
|
|
|
const link = document.createElement('a');
|
|
link.href = downloadURL;
|
|
link.download = filename || this.getFilenameFromResponse(response);
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
window.URL.revokeObjectURL(downloadURL);
|
|
|
|
} catch (error) {
|
|
console.error('Download failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract filename from response headers
|
|
*/
|
|
getFilenameFromResponse(response) {
|
|
const disposition = response.headers.get('content-disposition');
|
|
if (disposition) {
|
|
const matches = disposition.match(/filename="([^"]+)"/);
|
|
if (matches) return matches[1];
|
|
}
|
|
return 'download';
|
|
}
|
|
|
|
// =========================
|
|
// CACHING
|
|
// =========================
|
|
|
|
/**
|
|
* Get with cache
|
|
*/
|
|
async getCached(url, params = {}, ttl = 300000) {
|
|
const cacheKey = `${url}:${JSON.stringify(params)}`;
|
|
const cached = this.cache.get(cacheKey);
|
|
|
|
if (cached && Date.now() < cached.expires) {
|
|
return cached.data;
|
|
}
|
|
|
|
const result = await this.get(url, params);
|
|
|
|
this.cache.set(cacheKey, {
|
|
data: result,
|
|
expires: Date.now() + ttl
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Clear cache
|
|
*/
|
|
clearCache(pattern = null) {
|
|
if (pattern) {
|
|
for (const key of this.cache.keys()) {
|
|
if (key.includes(pattern)) {
|
|
this.cache.delete(key);
|
|
}
|
|
}
|
|
} else {
|
|
this.cache.clear();
|
|
}
|
|
}
|
|
|
|
// =========================
|
|
// ERROR HANDLING
|
|
// =========================
|
|
|
|
/**
|
|
* Handle API errors globally
|
|
*/
|
|
handleError(error) {
|
|
if (error instanceof HTTPError) {
|
|
switch (error.status) {
|
|
case 401:
|
|
this.handleUnauthorized();
|
|
break;
|
|
case 403:
|
|
this.handleForbidden();
|
|
break;
|
|
case 422:
|
|
this.handleValidationError(error);
|
|
break;
|
|
case 500:
|
|
this.handleServerError(error);
|
|
break;
|
|
default:
|
|
this.handleGenericError(error);
|
|
}
|
|
} else {
|
|
this.handleGenericError(error);
|
|
}
|
|
}
|
|
|
|
handleUnauthorized() {
|
|
// Redirect to login
|
|
window.location.href = '/login';
|
|
}
|
|
|
|
handleForbidden() {
|
|
NetGesconNotifications.error('Non hai i permessi per eseguire questa azione');
|
|
}
|
|
|
|
handleValidationError(error) {
|
|
// Handle form validation errors
|
|
NetGesconNotifications.error('Controlla i dati inseriti');
|
|
}
|
|
|
|
handleServerError(error) {
|
|
NetGesconNotifications.error('Errore del server. Riprova più tardi.');
|
|
}
|
|
|
|
handleGenericError(error) {
|
|
NetGesconNotifications.error('Si è verificato un errore imprevisto');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* HTTP Error class
|
|
*/
|
|
class HTTPError extends Error {
|
|
constructor(status, statusText, response = null) {
|
|
super(`HTTP ${status}: ${statusText}`);
|
|
this.name = 'HTTPError';
|
|
this.status = status;
|
|
this.statusText = statusText;
|
|
this.response = response;
|
|
}
|
|
}
|
|
|
|
// Create singleton instance
|
|
const NetGesconAPI = new NetGesconAPI();
|
|
|
|
// Export for use in other modules
|
|
window.NetGesconAPI = NetGesconAPI;
|
|
window.HTTPError = HTTPError;
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 **FILES CORRELATI**
|
|
|
|
### ⚡ **Core JavaScript Files**
|
|
- `/public/js/core/app.js` - Main application logic
|
|
- `/public/js/core/config.js` - Configuration management
|
|
- `/public/js/core/utils.js` - Utility functions
|
|
- `/public/js/core/api.js` - API communication layer
|
|
- `/public/js/core/events.js` - Event management system
|
|
|
|
### 🧩 **Component Files**
|
|
- `/public/js/components/sidebar.js` - Sidebar functionality
|
|
- `/public/js/components/header.js` - Header interactions
|
|
- `/public/js/components/cards.js` - Cards enhancements
|
|
- `/public/js/components/forms.js` - Form improvements
|
|
- `/public/js/components/tables.js` - Table features
|
|
- `/public/js/components/modals.js` - Modal management
|
|
|
|
### 📦 **Module Files**
|
|
- `/public/js/modules/dashboard.js` - Dashboard specific
|
|
- `/public/js/modules/stabili.js` - Stabili management
|
|
- `/public/js/modules/condomini.js` - Condomini features
|
|
- `/public/js/modules/reports.js` - Reports functionality
|
|
|
|
---
|
|
|
|
## 🧪 **TESTING CHECKLIST**
|
|
|
|
### ✅ **Core Functionality**
|
|
- [ ] App initialization successful
|
|
- [ ] Configuration loading working
|
|
- [ ] Utility functions available
|
|
- [ ] API communication functional
|
|
- [ ] Error handling proper
|
|
|
|
### ✅ **Performance Tests**
|
|
- [ ] Initial load time < 2s
|
|
- [ ] Memory usage stable
|
|
- [ ] No memory leaks
|
|
- [ ] Smooth animations
|
|
- [ ] Efficient DOM updates
|
|
|
|
### ✅ **Browser Compatibility**
|
|
- [ ] Chrome 90+
|
|
- [ ] Firefox 88+
|
|
- [ ] Safari 14+
|
|
- [ ] Edge 90+
|
|
- [ ] Mobile browsers
|
|
|
|
---
|
|
|
|
## 🎯 **NEXT STEPS**
|
|
|
|
### 🔮 **JavaScript Evolution**
|
|
- [ ] **Service Worker** - Offline support e caching
|
|
- [ ] **Web Workers** - Heavy computations in background
|
|
- [ ] **WebAssembly** - Performance-critical operations
|
|
- [ ] **Progressive Web App** - App-like experience
|
|
- [ ] **Real-time Updates** - WebSocket integration
|
|
|
|
---
|
|
|
|
*📝 Documento mantenuto da: GitHub Copilot AI Assistant*
|
|
*🔗 Componente: JavaScript System NetGescon Interface v2.1.0*
|