commit 30e1f9b36b162ef2c17762ec70fee5f46fbbe4e8 Author: fpicone Date: Sat Dec 6 18:23:43 2025 +0100 first upload diff --git a/.htaccess.example b/.htaccess.example new file mode 100644 index 0000000..0676b48 --- /dev/null +++ b/.htaccess.example @@ -0,0 +1,56 @@ + + Order allow,deny + Deny from all + + +# Impostazioni PHP +php_value upload_max_filesize 10M +php_value post_max_size 10M +php_value max_execution_time 300 +php_value session.gc_maxlifetime 28800 + +# Protezione directory uploads + + Options -Indexes + php_flag engine off + + +# Compressione GZIP + + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript + + +# Cache statica + + ExpiresActive On + ExpiresByType image/jpg "access plus 1 year" + ExpiresByType image/jpeg "access plus 1 year" + ExpiresByType image/gif "access plus 1 year" + ExpiresByType image/png "access plus 1 year" + ExpiresByType text/css "access plus 1 month" + ExpiresByType application/javascript "access plus 1 month" + + +# Sicurezza Headers + + Header set X-Content-Type-Options "nosniff" + Header set X-Frame-Options "SAMEORIGIN" + Header set X-XSS-Protection "1; mode=block" + diff --git a/README.md b/README.md new file mode 100644 index 0000000..609b076 --- /dev/null +++ b/README.md @@ -0,0 +1,308 @@ +# Territory Manager + +Sistema di gestione territori per cartoline della città. Web application PHP semplice, moderna e facilmente manutenibile. + +## Caratteristiche + +- ✅ Gestione completa territori (CRUD) +- ✅ Sistema di assegnazioni con tracking +- ✅ Link temporanei per condivisione territori +- ✅ Dashboard con liste automatiche: + - Territori da assegnare + - Territori prioritari + - Territori da riconsegnare +- ✅ Statistiche in tempo reale +- ✅ Media percorrenza mensile e annuale +- ✅ Upload immagini piantine territori +- ✅ Export PDF delle liste +- ✅ Sistema di autenticazione +- ✅ Pannello amministrazione +- ✅ Design moderno e responsive + +## Requisiti + +- PHP 7.4 o superiore +- MySQL 5.7 o superiore +- Web Server (Apache/Nginx) +- Estensioni PHP: + - PDO + - pdo_mysql + - GD (per gestione immagini) + +## Installazione + +### 1. Copia i file + +Copia tutti i file nella cartella del tuo web server (es. `htdocs`, `www`, `public_html`). + +### 2. Crea il database + +Accedi a phpMyAdmin o dalla riga di comando MySQL: + +```sql +CREATE DATABASE territory_manager CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` + +Importa lo schema del database: + +```bash +mysql -u root -p territory_manager < database.sql +``` + +Oppure copia il contenuto di `database.sql` ed eseguilo in phpMyAdmin. + +### 3. Configura la connessione + +Modifica il file `config.php` con i tuoi dati: + +```php +define('DB_HOST', 'localhost'); +define('DB_NAME', 'territory_manager'); +define('DB_USER', 'tuo_utente'); +define('DB_PASS', 'tua_password'); +``` + +### 4. Crea la cartella uploads + +Assicurati che la cartella `uploads` esista e abbia i permessi corretti: + +```bash +mkdir uploads +chmod 755 uploads +``` + +### 5. Accedi al sistema + +Apri il browser e vai all'URL della tua installazione: + +``` +http://tuosito.com/territory-manager/ +``` + +**Credenziali di default:** +- Username: `admin` +- Password: `admin123` + +⚠️ **IMPORTANTE**: Cambia immediatamente la password dopo il primo accesso! + +## Struttura File + +``` +territory-manager/ +├── config.php # Configurazione applicazione +├── db.php # Gestione database +├── functions.php # Funzioni helper +├── database.sql # Schema database +├── index.php # Dashboard principale +├── login.php # Pagina login +├── logout.php # Logout +├── header.php # Template header +├── footer.php # Template footer +├── territories.php # Gestione territori +├── assignments.php # Gestione assegnazioni +├── statistics.php # Statistiche +├── settings.php # Impostazioni (admin) +├── export_pdf.php # Export PDF +├── view_territory.php # Visualizzazione pubblica territorio +├── style.css # Stili CSS +├── script.js # JavaScript +├── uploads/ # Cartella immagini (da creare) +└── README.md # Questo file +``` + +## Utilizzo + +### Dashboard + +La dashboard mostra tre liste principali: +- **Territori da Assegnare**: Territori in reparto da più di X giorni +- **Territori Prioritari**: Territori in reparto da più di Y giorni +- **Territori da Riconsegnare**: Territori assegnati da più di Z giorni + +I tempi sono configurabili nelle Impostazioni. + +### Gestione Territori + +1. Vai su "Territori" nel menu +2. Clicca "+ Nuovo Territorio" +3. Compila i campi: + - Numero territorio + - Zona + - Tipologia + - Carica piantina (opzionale) + - Note (opzionale) + +### Assegnazione Territorio + +1. Dalla dashboard, clicca "Assegna" su un territorio disponibile +2. Oppure vai su "Assegnazioni" → "+ Nuova Assegnazione" +3. Compila: + - Seleziona territorio + - Nome assegnatario + - Data assegnazione + - Priorità (opzionale) +4. Viene generato automaticamente un link temporaneo + +### Link Temporanei + +Quando assegni un territorio, viene creato un link unico che permette di visualizzare il territorio senza login. + +Il link scade dopo X giorni (configurabile) e mostra: +- Dettagli territorio +- Piantina +- Informazioni assegnazione + +### Riconsegna + +1. Vai su "Assegnazioni" +2. Clicca "Riconsegna" sul territorio da restituire +3. Conferma la data di restituzione + +### Statistiche + +La pagina Statistiche mostra: +- Totale territori +- Territori assegnati/disponibili +- Attività ultimi 7 giorni +- Media percorrenza mensile +- Media percorrenza annuale +- Statistiche per zona +- Top 10 territori più assegnati +- Top 10 persone +- Territori mai assegnati + +### Export PDF + +Puoi esportare in PDF: +- Lista territori da assegnare +- Lista territori prioritari +- Lista territori da riconsegnare + +Clicca sul pulsante "📄 Export PDF" nella dashboard. + +### Impostazioni (Solo Admin) + +Configura: +- **Giorni validità link**: Durata dei link temporanei +- **Giorni territori da assegnare**: Soglia per "da assegnare" +- **Giorni territori prioritari**: Soglia per "prioritari" +- **Giorni da riconsegnare**: Soglia per "da riconsegnare" + +Gestisci anche: +- Cambio password +- Utenti del sistema + +## Manutenzione + +### Backup Database + +Esegui regolarmente backup del database: + +```bash +mysqldump -u root -p territory_manager > backup_$(date +%Y%m%d).sql +``` + +### Backup File + +Copia periodicamente la cartella `uploads` con le immagini. + +### Pulizia Link Scaduti + +I link scaduti vengono controllati automaticamente. Non è necessaria pulizia manuale. + +### Log Errori + +Gli errori PHP vengono salvati nel log del server. In produzione, disattiva la visualizzazione errori in `config.php`: + +```php +ini_set('display_errors', 0); +error_reporting(0); +``` + +## Personalizzazione + +### Modificare i Colori + +Modifica le variabili CSS in `style.css`: + +```css +:root { + --primary: #3498db; /* Colore principale */ + --success: #27ae60; /* Verde */ + --danger: #e74c3c; /* Rosso */ + --warning: #f39c12; /* Arancione */ +} +``` + +### Aggiungere Campi ai Territori + +1. Modifica la tabella `territories` in MySQL: +```sql +ALTER TABLE territories ADD COLUMN nuovo_campo VARCHAR(100); +``` + +2. Aggiungi il campo nei form in `territories.php` + +3. Aggiorna le query di INSERT/UPDATE + +### Modificare Template Email + +Attualmente non ci sono notifiche email. Per aggiungerle: + +1. Installa PHPMailer o usa `mail()` +2. Crea funzioni in `functions.php` +3. Chiama le funzioni dopo assegnazioni/riconsegne + +## Risoluzione Problemi + +### Errore connessione database +- Verifica credenziali in `config.php` +- Controlla che MySQL sia avviato +- Verifica che il database esista + +### Upload immagini non funziona +- Controlla permessi cartella `uploads` (755) +- Verifica `upload_max_filesize` in `php.ini` +- Controlla `post_max_size` in `php.ini` + +### Sessione scade troppo presto +- Modifica `SESSION_LIFETIME` in `config.php` +- Controlla `session.gc_maxlifetime` in `php.ini` + +### Link temporanei non funzionano +- Verifica che `HTTP_HOST` sia configurato correttamente +- Controlla che `view_territory.php` sia accessibile + +## Sicurezza + +- ✅ Password criptate con bcrypt +- ✅ Protezione SQL injection (prepared statements) +- ✅ Sanitizzazione input +- ✅ Validazione file upload +- ✅ Controllo sessioni +- ✅ Token CSRF non implementato (da aggiungere se necessario) + +**Raccomandazioni:** +1. Cambia password admin dopo installazione +2. Usa HTTPS in produzione +3. Limita accesso alla cartella uploads +4. Aggiorna PHP regolarmente + +## Supporto e Contributi + +Questo è un progetto semplice e facilmente estendibile. Puoi: +- Modificare il codice secondo le tue esigenze +- Aggiungere nuove funzionalità +- Migliorare la grafica + +## Licenza + +Questo progetto è fornito "così com'è" per uso personale e interno. + +## Versione + +**v1.0.0** - 6 dicembre 2025 + +--- + +**Buona gestione dei territori! 🗺️** diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..2c341d0 --- /dev/null +++ b/TODO.md @@ -0,0 +1,170 @@ +# Territory Manager - To Do e Suggerimenti per Miglioramenti Futuri + +## Funzionalità Base Implementate ✅ + +Tutte le funzionalità richieste sono state implementate: +- ✅ Sistema di autenticazione +- ✅ Gestione territori (CRUD completo) +- ✅ Upload immagini piantine +- ✅ Assegnazioni con tracking +- ✅ Link temporanei configurabili +- ✅ Dashboard con 3 liste (da assegnare, prioritari, da riconsegnare) +- ✅ Espansione liste e visualizzazione completa +- ✅ Export PDF delle liste +- ✅ Statistiche tempo reale +- ✅ Media percorrenza mensile e annuale +- ✅ Design moderno e responsive + +## Suggerimenti per Miglioramenti Futuri + +### 1. Notifiche Email +- Inviare email quando un territorio è assegnato +- Notificare quando un territorio è da riconsegnare +- Reminder automatici + +### 2. Miglioramenti Export PDF +- Usare libreria FPDF o TCPDF per veri PDF +- Aggiungere grafici alle statistiche +- Logo personalizzato nei report + +### 3. Ricerca Avanzata +- Filtri multipli combinati +- Ricerca full-text nelle note +- Storico ricerche + +### 4. Calendario +- Vista calendario assegnazioni +- Visualizzazione scadenze +- Pianificazione futura + +### 5. Note e Commenti +- Sistema commenti sulle assegnazioni +- Note private per amministratori +- Storico modifiche + +### 6. Gestione Permessi +- Ruoli utente più granulari +- Permessi per zona +- Visualizzazione sola lettura + +### 7. API REST +- API per integrazioni esterne +- App mobile companion +- Sincronizzazione dati + +### 8. Dashboard Grafici +- Grafici statistiche con Chart.js +- Heatmap assegnazioni +- Timeline territori + +### 9. Backup Automatico +- Backup database automatico +- Export/Import dati +- Restore point + +### 10. Multi-lingua +- Interfaccia in più lingue +- Configurazione lingua per utente + +## Note per Programmatori Neofiti + +### Come Aggiungere un Campo a Territori + +1. **Database**: Aggiungi colonna in MySQL +```sql +ALTER TABLE territories ADD COLUMN descrizione_estesa TEXT; +``` + +2. **Form**: Modifica `territories.php`, sezione form: +```php +
+ + +
+``` + +3. **Salvataggio**: Modifica query INSERT/UPDATE: +```php +$db->query( + "INSERT INTO territories (numero, zona, tipologia, descrizione_estesa) + VALUES (?, ?, ?, ?)", + [$numero, $zona, $tipologia, $descrizione_estesa] +); +``` + +### Come Modificare i Colori + +Apri `style.css` e modifica le variabili: +```css +:root { + --primary: #3498db; /* Cambia questo valore */ +} +``` + +### Come Aggiungere una Pagina + +1. Crea file `mia_pagina.php` +2. Includi header e footer: +```php + + + + + +``` + +3. Aggiungi link nel menu in `header.php` + +## File Importanti da Conoscere + +- `config.php`: Configurazioni generali +- `db.php`: Connessione database e query helper +- `functions.php`: Funzioni utili (auth, sanitize, date) +- `header.php` / `footer.php`: Template layout +- `style.css`: Tutti gli stili +- `database.sql`: Schema database + +## Comandi MySQL Utili + +```sql +-- Vedere tutti i territori +SELECT * FROM territories; + +-- Vedere assegnazioni correnti +SELECT * FROM assignments WHERE returned_date IS NULL; + +-- Vedere statistiche +SELECT COUNT(*) FROM territories; + +-- Reset password admin (dalla riga di comando MySQL) +UPDATE users SET password = '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi' +WHERE username = 'admin'; +-- Password diventa: admin123 +``` + +## Best Practices + +1. **Sempre usa Prepared Statements** per evitare SQL injection +2. **Sanitizza l'input** con `sanitize()` prima di visualizzare +3. **Controlla i permessi** con `requireLogin()` o `requireAdmin()` +4. **Gestisci gli errori** con try-catch +5. **Commenta il codice** per capirlo in futuro +6. **Fai backup** prima di modifiche importanti + +## Contatti e Supporto + +Per domande o problemi, verifica: +1. Log errori PHP +2. Console browser (F12) +3. Permessi file/cartelle +4. Credenziali database + +Buon lavoro! 🚀 diff --git a/assignments.php b/assignments.php new file mode 100644 index 0000000..60b3156 --- /dev/null +++ b/assignments.php @@ -0,0 +1,454 @@ +fetchOne( + "SELECT id FROM assignments WHERE territory_id = ? AND returned_date IS NULL", + [$territory_id] + ); + + if ($existing) { + setFlashMessage('Questo territorio è già assegnato', 'error'); + header('Location: assignments.php'); + exit; + } + + // Genera link temporaneo + $link_token = bin2hex(random_bytes(32)); + $link_expiry_days = (int)$db->getConfig('link_expiry_days', 7); + $link_expires_at = date('Y-m-d H:i:s', strtotime("+$link_expiry_days days")); + + $result = $db->query( + "INSERT INTO assignments (territory_id, assigned_to, assigned_date, is_priority, note, link_token, link_expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?)", + [$territory_id, $assigned_to, $assigned_date, $is_priority, $note, $link_token, $link_expires_at] + ); + + if ($result) { + $assignment_id = $db->getConnection()->lastInsertId(); + setFlashMessage('Territorio assegnato con successo', 'success'); + header("Location: assignments.php?action=view&id=$assignment_id"); + } else { + setFlashMessage('Errore durante l\'assegnazione', 'error'); + header('Location: assignments.php'); + } + exit; + break; + + case 'return': + $territory_id = (int)$_POST['territory_id']; + $returned_date = $_POST['returned_date']; + + $result = $db->query( + "UPDATE assignments SET returned_date = ? WHERE territory_id = ? AND returned_date IS NULL", + [$returned_date, $territory_id] + ); + + if ($result) { + setFlashMessage('Territorio riconsegnato con successo', 'success'); + } else { + setFlashMessage('Errore durante la riconsegna', 'error'); + } + + header('Location: assignments.php'); + exit; + break; + } + } +} + +// Lista assegnazioni +if ($action === 'list') { + $filter = $_GET['filter'] ?? 'current'; + + if ($filter === 'current') { + $assignments = $db->fetchAll(" + SELECT + a.*, + t.numero, + t.zona, + t.tipologia, + DATEDIFF(CURDATE(), a.assigned_date) as days_assigned + FROM assignments a + INNER JOIN territories t ON a.territory_id = t.id + WHERE a.returned_date IS NULL + ORDER BY a.assigned_date ASC + "); + } else { + $assignments = $db->fetchAll(" + SELECT + a.*, + t.numero, + t.zona, + t.tipologia, + DATEDIFF(a.returned_date, a.assigned_date) as days_assigned + FROM assignments a + INNER JOIN territories t ON a.territory_id = t.id + WHERE a.returned_date IS NOT NULL + ORDER BY a.returned_date DESC + LIMIT 100 + "); + } +} + +// Pagina per nuova assegnazione +if ($action === 'assign') { + if ($territory_id) { + $territory = $db->fetchOne("SELECT * FROM territories WHERE id = ?", [$territory_id]); + if (!$territory) { + setFlashMessage('Territorio non trovato', 'error'); + header('Location: assignments.php'); + exit; + } + } else { + // Carica tutti i territori disponibili + $available_territories = $db->fetchAll(" + SELECT t.* + FROM territories t + WHERE t.id NOT IN ( + SELECT territory_id FROM assignments WHERE returned_date IS NULL + ) + ORDER BY t.numero ASC + "); + } + + $is_priority = isset($_GET['priority']) ? 1 : 0; +} + +// Pagina per riconsegna +if ($action === 'return' && $territory_id) { + $assignment = $db->fetchOne(" + SELECT a.*, t.numero, t.zona, t.tipologia + FROM assignments a + INNER JOIN territories t ON a.territory_id = t.id + WHERE a.territory_id = ? AND a.returned_date IS NULL + ", [$territory_id]); + + if (!$assignment) { + setFlashMessage('Assegnazione non trovata', 'error'); + header('Location: assignments.php'); + exit; + } +} + +// Visualizzazione dettaglio assegnazione +if ($action === 'view') { + $assignment_id = $_GET['id'] ?? null; + if ($assignment_id) { + $assignment = $db->fetchOne(" + SELECT a.*, t.numero, t.zona, t.tipologia, t.image_path + FROM assignments a + INNER JOIN territories t ON a.territory_id = t.id + WHERE a.id = ? + ", [$assignment_id]); + + if (!$assignment) { + setFlashMessage('Assegnazione non trovata', 'error'); + header('Location: assignments.php'); + exit; + } + } +} + +include 'header.php'; +?> + + + + +
+ +
+ 0): ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TerritorioZonaTipologiaAssegnato aData AssegnazioneGiorniAzioniData RestituzioneDurata
+ + + Priorità + + + giorni + + Dettagli + Riconsegna + giorni
+ +

+ +

+ +
+
+ + + + +
+
+
+ + +
+ + + + + + + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ + Annulla +
+
+
+
+ + + + +
+
+
+
+ Territorio: +
+
+ Zona: +
+
+ Assegnato a: +
+
+ Data Assegnazione: +
+
+ +
+ + + +
+ + +
+ +
+ + Annulla +
+
+
+
+ + + time(); + ?> + + + +
+
+

Informazioni Territorio

+
+
+
+
+ Numero: +
+
+ Zona: +
+
+ Tipologia: +
+
+ Assegnato a: +
+
+ Data Assegnazione: +
+
+ Priorità: + + Prioritaria + + Normale + +
+
+ + +
+ Note: +

+
+ +
+
+ +
+
+

Link Condivisione Territorio

+
+
+

+ Questo link permette di visualizzare il territorio senza accedere al sistema. + + Il link è valido fino al + + Il link è scaduto + +

+ + + + +

+ Per generare un nuovo link, è necessario creare una nuova assegnazione. +

+ +
+
+ + +
+ Riconsegna Territorio +
+ + + + + + + diff --git a/config.php b/config.php new file mode 100644 index 0000000..2a01436 --- /dev/null +++ b/config.php @@ -0,0 +1,42 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + $this->connection = new PDO($dsn, DB_USER, DB_PASS, $options); + } catch (PDOException $e) { + die("Errore di connessione al database: " . $e->getMessage()); + } + } + + public static function getInstance() { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + public function getConnection() { + return $this->connection; + } + + // Metodo helper per query semplici + public function query($sql, $params = []) { + try { + $stmt = $this->connection->prepare($sql); + $stmt->execute($params); + return $stmt; + } catch (PDOException $e) { + error_log("Errore query: " . $e->getMessage()); + return false; + } + } + + // Metodo helper per ottenere una singola riga + public function fetchOne($sql, $params = []) { + $stmt = $this->query($sql, $params); + return $stmt ? $stmt->fetch() : null; + } + + // Metodo helper per ottenere tutte le righe + public function fetchAll($sql, $params = []) { + $stmt = $this->query($sql, $params); + return $stmt ? $stmt->fetchAll() : []; + } + + // Metodo helper per ottenere valore configurazione + public function getConfig($key, $default = null) { + $result = $this->fetchOne( + "SELECT config_value FROM config WHERE config_key = ?", + [$key] + ); + return $result ? $result['config_value'] : $default; + } + + // Metodo helper per aggiornare configurazione + public function updateConfig($key, $value) { + return $this->query( + "UPDATE config SET config_value = ? WHERE config_key = ?", + [$value, $key] + ); + } +} + +// Funzione helper globale per ottenere il database +function getDB() { + return Database::getInstance(); +} diff --git a/export_pdf.php b/export_pdf.php new file mode 100644 index 0000000..7d9553a --- /dev/null +++ b/export_pdf.php @@ -0,0 +1,203 @@ +getConfig('warning_days_normal', 90); +$warning_days_priority = (int)$db->getConfig('warning_days_priority', 180); +$warning_days_return = (int)$db->getConfig('warning_days_return', 120); + +// Semplice classe PDF +class PDF { + private $content = ''; + + public function addTitle($title) { + $this->content .= "

$title

"; + } + + public function addText($text) { + $this->content .= "

$text

"; + } + + public function addTable($headers, $rows) { + $this->content .= ""; + $this->content .= ""; + foreach ($headers as $header) { + $this->content .= ""; + } + $this->content .= ""; + + foreach ($rows as $row) { + $this->content .= ""; + foreach ($row as $cell) { + $this->content .= ""; + } + $this->content .= ""; + } + + $this->content .= "
$header
$cell
"; + } + + public function output($filename) { + header('Content-Type: text/html; charset=utf-8'); + header('Content-Disposition: inline; filename="' . $filename . '.html"'); + + echo " + + + + $filename + + +"; + echo $this->content; + echo "
"; + echo "

Generato il " . date('d/m/Y H:i') . " - " . APP_NAME . "

"; + echo "
"; + echo ""; + echo ""; + } +} + +$pdf = new PDF(); + +switch ($type) { + case 'to_assign': + // Territori da assegnare + $territories = $db->fetchAll(" + SELECT + t.id, + t.numero, + t.zona, + t.tipologia, + MAX(a.returned_date) as last_returned_date, + DATEDIFF(CURDATE(), MAX(a.returned_date)) as days_in_depot + FROM territories t + LEFT JOIN assignments a ON t.id = a.territory_id + WHERE t.id NOT IN ( + SELECT territory_id FROM assignments WHERE returned_date IS NULL + ) + GROUP BY t.id, t.numero, t.zona, t.tipologia + HAVING days_in_depot >= ? OR MAX(a.returned_date) IS NULL + ORDER BY last_returned_date ASC NULLS FIRST + ", [$warning_days_normal]); + + $pdf->addTitle('Territori da Assegnare'); + $pdf->addText('Territori in reparto da più di ' . $warning_days_normal . ' giorni'); + $pdf->addText('Totale: ' . count($territories) . ' territori'); + + $headers = ['Numero', 'Zona', 'Tipologia', 'Ultima Restituzione', 'Giorni in Reparto']; + $rows = []; + foreach ($territories as $t) { + $rows[] = [ + htmlspecialchars($t['numero']), + htmlspecialchars($t['zona']), + htmlspecialchars($t['tipologia']), + formatDate($t['last_returned_date']), + ($t['days_in_depot'] ?? 'Mai assegnato') + ]; + } + + $pdf->addTable($headers, $rows); + $pdf->output('Territori_da_Assegnare_' . date('Y-m-d')); + break; + + case 'priority': + // Territori prioritari + $territories = $db->fetchAll(" + SELECT + t.id, + t.numero, + t.zona, + t.tipologia, + MAX(a.returned_date) as last_returned_date, + DATEDIFF(CURDATE(), MAX(a.returned_date)) as days_in_depot + FROM territories t + LEFT JOIN assignments a ON t.id = a.territory_id + WHERE t.id NOT IN ( + SELECT territory_id FROM assignments WHERE returned_date IS NULL + ) + GROUP BY t.id, t.numero, t.zona, t.tipologia + HAVING days_in_depot >= ? + ORDER BY last_returned_date ASC NULLS FIRST + ", [$warning_days_priority]); + + $pdf->addTitle('Territori Prioritari'); + $pdf->addText('Territori in reparto da più di ' . $warning_days_priority . ' giorni'); + $pdf->addText('Totale: ' . count($territories) . ' territori'); + + $headers = ['Numero', 'Zona', 'Tipologia', 'Ultima Restituzione', 'Giorni in Reparto']; + $rows = []; + foreach ($territories as $t) { + $rows[] = [ + htmlspecialchars($t['numero']), + htmlspecialchars($t['zona']), + htmlspecialchars($t['tipologia']), + formatDate($t['last_returned_date']), + ($t['days_in_depot'] ?? 'Mai assegnato') + ]; + } + + $pdf->addTable($headers, $rows); + $pdf->output('Territori_Prioritari_' . date('Y-m-d')); + break; + + case 'to_return': + // Territori da riconsegnare + $territories = $db->fetchAll(" + SELECT + t.id, + t.numero, + t.zona, + t.tipologia, + a.assigned_to, + a.assigned_date, + DATEDIFF(CURDATE(), a.assigned_date) as days_assigned + FROM territories t + INNER JOIN assignments a ON t.id = a.territory_id + WHERE a.returned_date IS NULL + AND DATEDIFF(CURDATE(), a.assigned_date) >= ? + ORDER BY a.assigned_date ASC + ", [$warning_days_return]); + + $pdf->addTitle('Territori da Riconsegnare'); + $pdf->addText('Territori assegnati da più di ' . $warning_days_return . ' giorni'); + $pdf->addText('Totale: ' . count($territories) . ' territori'); + + $headers = ['Numero', 'Zona', 'Tipologia', 'Assegnato a', 'Data Assegnazione', 'Giorni']; + $rows = []; + foreach ($territories as $t) { + $rows[] = [ + htmlspecialchars($t['numero']), + htmlspecialchars($t['zona']), + htmlspecialchars($t['tipologia']), + htmlspecialchars($t['assigned_to']), + formatDate($t['assigned_date']), + $t['days_assigned'] . ' giorni' + ]; + } + + $pdf->addTable($headers, $rows); + $pdf->output('Territori_da_Riconsegnare_' . date('Y-m-d')); + break; + + default: + die('Tipo di export non valido'); +} diff --git a/footer.php b/footer.php new file mode 100644 index 0000000..674ba7a --- /dev/null +++ b/footer.php @@ -0,0 +1,11 @@ + + + + + + + diff --git a/functions.php b/functions.php new file mode 100644 index 0000000..0ae9f2a --- /dev/null +++ b/functions.php @@ -0,0 +1,112 @@ +fetchOne( + "SELECT id, username, email, is_admin FROM users WHERE id = ?", + [$_SESSION['user_id']] + ); +} + +// Login utente +function login($username, $password) { + $db = getDB(); + + $user = $db->fetchOne( + "SELECT id, username, password, email, is_admin FROM users WHERE username = ?", + [$username] + ); + + if ($user && password_verify($password, $user['password'])) { + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['is_admin'] = $user['is_admin']; + $_SESSION['login_time'] = time(); + + return true; + } + + return false; +} + +// Logout utente +function logout() { + session_unset(); + session_destroy(); + session_start(); +} + +// Richiedi autenticazione (redirect a login se non loggato) +function requireLogin() { + if (!isLoggedIn()) { + header('Location: login.php'); + exit; + } +} + +// Richiedi privilegi admin +function requireAdmin() { + requireLogin(); + if (!isAdmin()) { + header('Location: index.php?error=access_denied'); + exit; + } +} + +// Genera un messaggio flash +function setFlashMessage($message, $type = 'info') { + $_SESSION['flash_message'] = $message; + $_SESSION['flash_type'] = $type; +} + +// Ottieni e pulisci il messaggio flash +function getFlashMessage() { + if (isset($_SESSION['flash_message'])) { + $message = [ + 'text' => $_SESSION['flash_message'], + 'type' => $_SESSION['flash_type'] ?? 'info' + ]; + unset($_SESSION['flash_message']); + unset($_SESSION['flash_type']); + return $message; + } + return null; +} + +// Sanitizza input +function sanitize($input) { + return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8'); +} + +// Formatta data +function formatDate($date) { + if (empty($date)) return '-'; + return date('d/m/Y', strtotime($date)); +} + +// Formatta data e ora +function formatDateTime($datetime) { + if (empty($datetime)) return '-'; + return date('d/m/Y H:i', strtotime($datetime)); +} diff --git a/header.php b/header.php new file mode 100644 index 0000000..530e56a --- /dev/null +++ b/header.php @@ -0,0 +1,47 @@ + + + + + + + <?php echo isset($page_title) ? $page_title . ' - ' : ''; ?><?php echo APP_NAME; ?> + + + + + +
+ +
+ +
+ diff --git a/index.php b/index.php new file mode 100644 index 0000000..040d287 --- /dev/null +++ b/index.php @@ -0,0 +1,298 @@ +getConfig('warning_days_normal', 90); +$warning_days_priority = (int)$db->getConfig('warning_days_priority', 180); +$warning_days_return = (int)$db->getConfig('warning_days_return', 120); + +// Ottieni il limite (default 10, se expanded = all) +$limit_normal = isset($_GET['expand_normal']) ? '' : 'LIMIT 10'; +$limit_priority = isset($_GET['expand_priority']) ? '' : 'LIMIT 10'; +$limit_return = isset($_GET['expand_return']) ? '' : 'LIMIT 10'; + +// Territori da assegnare (in reparto da più di X giorni) +$territories_to_assign = $db->fetchAll(" + SELECT + t.id, + t.numero, + t.zona, + t.tipologia, + MAX(a.returned_date) as last_returned_date, + DATEDIFF(CURDATE(), MAX(a.returned_date)) as days_in_depot + FROM territories t + LEFT JOIN assignments a ON t.id = a.territory_id + WHERE t.id NOT IN ( + SELECT territory_id FROM assignments WHERE returned_date IS NULL + ) + GROUP BY t.id, t.numero, t.zona, t.tipologia + HAVING days_in_depot >= ? OR MAX(a.returned_date) IS NULL + ORDER BY last_returned_date ASC NULLS FIRST + $limit_normal +", [$warning_days_normal]); + +// Territori prioritari (in reparto da più di X giorni) +$territories_priority = $db->fetchAll(" + SELECT + t.id, + t.numero, + t.zona, + t.tipologia, + MAX(a.returned_date) as last_returned_date, + DATEDIFF(CURDATE(), MAX(a.returned_date)) as days_in_depot + FROM territories t + LEFT JOIN assignments a ON t.id = a.territory_id + WHERE t.id NOT IN ( + SELECT territory_id FROM assignments WHERE returned_date IS NULL + ) + GROUP BY t.id, t.numero, t.zona, t.tipologia + HAVING days_in_depot >= ? + ORDER BY last_returned_date ASC NULLS FIRST + $limit_priority +", [$warning_days_priority]); + +// Territori da riconsegnare (assegnati da più di X giorni) +$territories_to_return = $db->fetchAll(" + SELECT + t.id, + t.numero, + t.zona, + t.tipologia, + a.assigned_to, + a.assigned_date, + DATEDIFF(CURDATE(), a.assigned_date) as days_assigned + FROM territories t + INNER JOIN assignments a ON t.id = a.territory_id + WHERE a.returned_date IS NULL + AND DATEDIFF(CURDATE(), a.assigned_date) >= ? + ORDER BY a.assigned_date ASC + $limit_return +", [$warning_days_return]); + +// Conteggi totali +$count_to_assign = $db->fetchOne(" + SELECT COUNT(*) as total + FROM ( + SELECT t.id + FROM territories t + LEFT JOIN assignments a ON t.id = a.territory_id + WHERE t.id NOT IN ( + SELECT territory_id FROM assignments WHERE returned_date IS NULL + ) + GROUP BY t.id + HAVING DATEDIFF(CURDATE(), MAX(a.returned_date)) >= ? OR MAX(a.returned_date) IS NULL + ) as subquery +", [$warning_days_normal])['total']; + +$count_priority = $db->fetchOne(" + SELECT COUNT(*) as total + FROM ( + SELECT t.id + FROM territories t + LEFT JOIN assignments a ON t.id = a.territory_id + WHERE t.id NOT IN ( + SELECT territory_id FROM assignments WHERE returned_date IS NULL + ) + GROUP BY t.id + HAVING DATEDIFF(CURDATE(), MAX(a.returned_date)) >= ? + ) as subquery +", [$warning_days_priority])['total']; + +$count_to_return = $db->fetchOne(" + SELECT COUNT(*) as total + FROM assignments + WHERE returned_date IS NULL + AND DATEDIFF(CURDATE(), assigned_date) >= ? +", [$warning_days_return])['total']; + +include 'header.php'; +?> + + + + +
+
+

Territori da Assegnare ()

+
+ 0): ?> + 📄 Export PDF + + Mostra Tutti + + Mostra Meno + + +
+
+
+

Territori in reparto da più di giorni

+ 0): ?> + + + + + + + + + + + + + + + + + + + + + + + +
NumeroZonaTipologiaUltima RestituzioneGiorni in RepartoAzioni
+ + giorni + + + Assegna +
+ +

Nessun territorio da assegnare

+ +
+
+ + +
+
+

Territori Prioritari ()

+
+ 0): ?> + 📄 Export PDF + + Mostra Tutti + + Mostra Meno + + +
+
+
+

Territori in reparto da più di giorni

+ 0): ?> + + + + + + + + + + + + + + + + + + + + + + + +
NumeroZonaTipologiaUltima RestituzioneGiorni in RepartoAzioni
+ + giorni + + + Assegna +
+ +

Nessun territorio prioritario

+ +
+
+ + +
+
+

Territori da Riconsegnare ()

+
+ 0): ?> + 📄 Export PDF + + Mostra Tutti + + Mostra Meno + + +
+
+
+

Territori assegnati da più di giorni

+ 0): ?> + + + + + + + + + + + + + + + + + + + + + + + + + +
NumeroZonaTipologiaAssegnato aData AssegnazioneGiorni AssegnatoAzioni
+ + giorni + + + Riconsegna +
+ +

Nessun territorio da riconsegnare

+ +
+
+ + diff --git a/login.php b/login.php new file mode 100644 index 0000000..802f4c1 --- /dev/null +++ b/login.php @@ -0,0 +1,73 @@ + + + + + + + Login - <?php echo APP_NAME; ?> + + + + + + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..334360a --- /dev/null +++ b/logout.php @@ -0,0 +1,12 @@ + { + form.addEventListener('submit', function(e) { + if (!confirm('Sei sicuro di voler procedere?')) { + e.preventDefault(); + } + }); +}); + +// Auto-hide flash messages dopo 5 secondi +document.addEventListener('DOMContentLoaded', function() { + const alerts = document.querySelectorAll('.alert'); + alerts.forEach(alert => { + setTimeout(() => { + alert.style.transition = 'opacity 0.5s'; + alert.style.opacity = '0'; + setTimeout(() => alert.remove(), 500); + }, 5000); + }); +}); + +// Copia link negli appunti +function copyLink() { + const linkInput = document.getElementById('share_link'); + if (linkInput) { + linkInput.select(); + linkInput.setSelectionRange(0, 99999); // Per mobile + + try { + document.execCommand('copy'); + alert('Link copiato negli appunti!'); + } catch (err) { + console.error('Errore nella copia:', err); + } + } +} + +// Validazione form +document.querySelectorAll('form').forEach(form => { + form.addEventListener('submit', function(e) { + const requiredFields = form.querySelectorAll('[required]'); + let isValid = true; + + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.style.borderColor = 'var(--danger)'; + } else { + field.style.borderColor = 'var(--border)'; + } + }); + + if (!isValid) { + e.preventDefault(); + alert('Compila tutti i campi obbligatori'); + } + }); +}); diff --git a/settings.php b/settings.php new file mode 100644 index 0000000..52482ac --- /dev/null +++ b/settings.php @@ -0,0 +1,269 @@ +updateConfig('link_expiry_days', $link_expiry_days); + $db->updateConfig('warning_days_normal', $warning_days_normal); + $db->updateConfig('warning_days_priority', $warning_days_priority); + $db->updateConfig('warning_days_return', $warning_days_return); + + setFlashMessage('Configurazioni salvate con successo', 'success'); + header('Location: settings.php'); + exit; + break; + + case 'change_password': + $current_password = $_POST['current_password']; + $new_password = $_POST['new_password']; + $confirm_password = $_POST['confirm_password']; + + $user = getCurrentUser(); + $db_user = $db->fetchOne("SELECT password FROM users WHERE id = ?", [$user['id']]); + + if (!password_verify($current_password, $db_user['password'])) { + setFlashMessage('Password corrente non corretta', 'error'); + } elseif ($new_password !== $confirm_password) { + setFlashMessage('Le nuove password non coincidono', 'error'); + } elseif (strlen($new_password) < 6) { + setFlashMessage('La password deve essere di almeno 6 caratteri', 'error'); + } else { + $hashed = password_hash($new_password, PASSWORD_DEFAULT); + $db->query("UPDATE users SET password = ? WHERE id = ?", [$hashed, $user['id']]); + setFlashMessage('Password modificata con successo', 'success'); + } + + header('Location: settings.php'); + exit; + break; + + case 'add_user': + $username = sanitize($_POST['username']); + $email = sanitize($_POST['email']); + $password = $_POST['password']; + $is_admin = isset($_POST['is_admin']) ? 1 : 0; + + if (strlen($password) < 6) { + setFlashMessage('La password deve essere di almeno 6 caratteri', 'error'); + } else { + $hashed = password_hash($password, PASSWORD_DEFAULT); + $result = $db->query( + "INSERT INTO users (username, email, password, is_admin) VALUES (?, ?, ?, ?)", + [$username, $email, $hashed, $is_admin] + ); + + if ($result) { + setFlashMessage('Utente aggiunto con successo', 'success'); + } else { + setFlashMessage('Errore: username già esistente', 'error'); + } + } + + header('Location: settings.php'); + exit; + break; + + case 'delete_user': + $user_id = (int)$_POST['user_id']; + + // Non permettere di eliminare se stesso + if ($user_id == $_SESSION['user_id']) { + setFlashMessage('Non puoi eliminare il tuo account', 'error'); + } else { + $db->query("DELETE FROM users WHERE id = ?", [$user_id]); + setFlashMessage('Utente eliminato con successo', 'success'); + } + + header('Location: settings.php'); + exit; + break; + } + } +} + +// Carica configurazioni +$config = [ + 'link_expiry_days' => $db->getConfig('link_expiry_days', 7), + 'warning_days_normal' => $db->getConfig('warning_days_normal', 90), + 'warning_days_priority' => $db->getConfig('warning_days_priority', 180), + 'warning_days_return' => $db->getConfig('warning_days_return', 120) +]; + +// Carica utenti +$users = $db->fetchAll("SELECT id, username, email, is_admin, created_at FROM users ORDER BY username"); + +include 'header.php'; +?> + + + + +
+
+

Configurazioni Generali

+
+
+
+ + +
+ + + Numero di giorni per cui i link di condivisione territorio sono validi +
+ +
+ + + Giorni dopo i quali un territorio in reparto è considerato da assegnare +
+ +
+ + + Giorni dopo i quali un territorio in reparto è considerato prioritario +
+ +
+ + + Giorni dopo i quali un territorio assegnato è da riconsegnare +
+ + +
+
+
+ + +
+
+

Cambia Password

+
+
+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ + +
+
+

Gestione Utenti

+
+
+ + + + + + + + + + + + + + + + + + + + + +
UsernameEmailRuoloData CreazioneAzioni
+ + Admin + + Utente + + + +
+ + + +
+ + Tu + +
+ +

Aggiungi Nuovo Utente

+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+ + diff --git a/statistics.php b/statistics.php new file mode 100644 index 0000000..23e372e --- /dev/null +++ b/statistics.php @@ -0,0 +1,414 @@ +fetchOne("SELECT COUNT(*) as total FROM territories")['total']; +$assigned_territories = $db->fetchOne(" + SELECT COUNT(DISTINCT territory_id) as total + FROM assignments + WHERE returned_date IS NULL +")['total']; +$available_territories = $total_territories - $assigned_territories; + +// Media percorrenza mensile (anno corrente) +$monthly_stats = $db->fetchAll(" + SELECT + MONTH(assigned_date) as month, + COUNT(*) as total_assignments, + AVG(DATEDIFF(COALESCE(returned_date, CURDATE()), assigned_date)) as avg_duration + FROM assignments + WHERE YEAR(assigned_date) = ? + GROUP BY MONTH(assigned_date) + ORDER BY month +", [$selected_year]); + +// Media percorrenza annuale +$yearly_stats = $db->fetchAll(" + SELECT + YEAR(assigned_date) as year, + COUNT(*) as total_assignments, + AVG(DATEDIFF(COALESCE(returned_date, CURDATE()), assigned_date)) as avg_duration + FROM assignments + GROUP BY YEAR(assigned_date) + ORDER BY year DESC +"); + +// Territori mai assegnati +$never_assigned = $db->fetchAll(" + SELECT t.* + FROM territories t + LEFT JOIN assignments a ON t.id = a.territory_id + WHERE a.id IS NULL + ORDER BY t.numero +"); + +// Top 10 territori più assegnati +$most_assigned = $db->fetchAll(" + SELECT + t.numero, + t.zona, + t.tipologia, + COUNT(a.id) as assignment_count, + AVG(DATEDIFF(COALESCE(a.returned_date, CURDATE()), a.assigned_date)) as avg_duration + FROM territories t + INNER JOIN assignments a ON t.id = a.territory_id + GROUP BY t.id, t.numero, t.zona, t.tipologia + ORDER BY assignment_count DESC + LIMIT 10 +"); + +// Statistiche per persona (chi ha avuto più territori) +$person_stats = $db->fetchAll(" + SELECT + assigned_to, + COUNT(*) as total_assignments, + COUNT(CASE WHEN returned_date IS NULL THEN 1 END) as current_assignments, + AVG(DATEDIFF(COALESCE(returned_date, CURDATE()), assigned_date)) as avg_duration + FROM assignments + GROUP BY assigned_to + ORDER BY total_assignments DESC + LIMIT 10 +"); + +// Anni disponibili per filtro +$available_years = $db->fetchAll(" + SELECT DISTINCT YEAR(assigned_date) as year + FROM assignments + ORDER BY year DESC +"); + +// Statistiche per zona +$zone_stats = $db->fetchAll(" + SELECT + t.zona, + COUNT(DISTINCT t.id) as total_territories, + COUNT(a.id) as total_assignments, + AVG(DATEDIFF(COALESCE(a.returned_date, CURDATE()), a.assigned_date)) as avg_duration + FROM territories t + LEFT JOIN assignments a ON t.id = a.territory_id + GROUP BY t.zona + ORDER BY total_territories DESC +"); + +// Statistiche tempo reale (ultimi 7 giorni) +$recent_activity = $db->fetchAll(" + SELECT + DATE(assigned_date) as date, + COUNT(*) as assignments + FROM assignments + WHERE assigned_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) + GROUP BY DATE(assigned_date) + ORDER BY date DESC +"); + +$recent_returns = $db->fetchAll(" + SELECT + DATE(returned_date) as date, + COUNT(*) as returns + FROM assignments + WHERE returned_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) + GROUP BY DATE(returned_date) + ORDER BY date DESC +"); + +include 'header.php'; +?> + + + + +
+
+
+
Totale Territori
+
+
+
+
Territori Assegnati
+
+
+
+
Territori Disponibili
+
+
+
+
Mai Assegnati
+
+
+ + +
+
+

Attività Ultimi 7 Giorni

+
+
+
+
+

Assegnazioni

+ 0): ?> + + + + + + + + + +
assegnazioni
+ +

Nessuna assegnazione recente

+ +
+
+

Riconsegne

+ 0): ?> + + + + + + + + + +
riconsegne
+ +

Nessuna riconsegna recente

+ +
+
+
+
+ + +
+
+

Media Percorrenza Mensile -

+
+
+ 0): ?> + + + + + + + + + + + + + + + + + +
MeseTotale AssegnazioniDurata Media
giorni
+ +

Nessun dato per l'anno selezionato

+ +
+
+ + +
+
+

Media Percorrenza Annuale

+
+
+ 0): ?> + + + + + + + + + + + + + + + + + +
AnnoTotale AssegnazioniDurata Media
giorni
+ +

Nessun dato disponibile

+ +
+
+ + +
+
+

Statistiche per Zona

+
+
+ 0): ?> + + + + + + + + + + + + + + + + + + + +
ZonaTotale TerritoriTotale AssegnazioniDurata Media
+ +

Nessun dato disponibile

+ +
+
+ + +
+
+

Top 10 Territori Più Assegnati

+
+
+ 0): ?> + + + + + + + + + + + + + + + + + + + + + +
TerritorioZonaTipologiaN° AssegnazioniDurata Media
giorni
+ +

Nessun dato disponibile

+ +
+
+ + +
+
+

Top 10 Persone per Assegnazioni

+
+
+ 0): ?> + + + + + + + + + + + + + + + + + + + +
NomeTotale AssegnazioniAssegnazioni CorrentiDurata Media
giorni
+ +

Nessun dato disponibile

+ +
+
+ + + 0): ?> +
+
+

Territori Mai Assegnati ()

+
+
+ + + + + + + + + + + + + + + + + + + +
NumeroZonaTipologiaAzioni
+ Assegna +
+
+
+ + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..7fdb21f --- /dev/null +++ b/style.css @@ -0,0 +1,607 @@ +/** + * Stylesheet Territory Manager + * Design moderno e minimale + */ + +/* Reset e Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary: #3498db; + --primary-dark: #2980b9; + --secondary: #95a5a6; + --success: #27ae60; + --danger: #e74c3c; + --warning: #f39c12; + --info: #3498db; + --dark: #2c3e50; + --light: #ecf0f1; + --white: #ffffff; + --border: #dfe6e9; + --text: #2c3e50; + --text-muted: #7f8c8d; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.6; + color: var(--text); + background-color: #f5f6fa; +} + +/* Navbar */ +.navbar { + background: var(--white); + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + padding: 0; + margin-bottom: 30px; +} + +.navbar .container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + max-width: 1400px; + margin: 0 auto; +} + +.navbar-brand a { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary); + text-decoration: none; +} + +.navbar-menu { + display: flex; + list-style: none; + gap: 5px; +} + +.navbar-menu a { + padding: 10px 20px; + color: var(--text); + text-decoration: none; + border-radius: 6px; + transition: all 0.3s; +} + +.navbar-menu a:hover, +.navbar-menu a.active { + background: var(--primary); + color: var(--white); +} + +.navbar-user { + display: flex; + align-items: center; + gap: 15px; +} + +/* Container */ +.container { + max-width: 1400px; + margin: 0 auto; + padding: 0 20px; +} + +/* Cards */ +.card { + background: var(--white); + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + margin-bottom: 25px; + overflow: hidden; +} + +.card-header { + padding: 20px 25px; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.card-header h2 { + font-size: 1.25rem; + font-weight: 600; + color: var(--dark); +} + +.card-actions { + display: flex; + gap: 10px; +} + +.card-body { + padding: 25px; +} + +/* Buttons */ +.btn { + display: inline-block; + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + text-decoration: none; + transition: all 0.3s; + text-align: center; +} + +.btn-primary { + background: var(--primary); + color: var(--white); +} + +.btn-primary:hover { + background: var(--primary-dark); +} + +.btn-secondary { + background: var(--secondary); + color: var(--white); +} + +.btn-secondary:hover { + background: #7f8c8d; +} + +.btn-success { + background: var(--success); + color: var(--white); +} + +.btn-success:hover { + background: #229954; +} + +.btn-danger { + background: var(--danger); + color: var(--white); +} + +.btn-danger:hover { + background: #c0392b; +} + +.btn-sm { + padding: 6px 12px; + font-size: 0.85rem; +} + +.btn-block { + display: block; + width: 100%; +} + +/* Forms */ +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: var(--dark); +} + +.form-control { + width: 100%; + padding: 10px 15px; + border: 1px solid var(--border); + border-radius: 6px; + font-size: 0.95rem; + transition: border-color 0.3s; +} + +.form-control:focus { + outline: none; + border-color: var(--primary); +} + +.form-help { + display: block; + margin-top: 5px; + font-size: 0.85rem; + color: var(--text-muted); +} + +.form-row { + display: flex; + gap: 15px; + align-items: flex-end; +} + +.form-row .form-group { + flex: 1; +} + +.form-actions { + display: flex; + gap: 10px; + margin-top: 25px; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; +} + +/* Tables */ +.table { + width: 100%; + border-collapse: collapse; +} + +.table thead tr { + background: var(--light); + border-bottom: 2px solid var(--border); +} + +.table th, +.table td { + padding: 12px 15px; + text-align: left; +} + +.table th { + font-weight: 600; + color: var(--dark); +} + +.table tbody tr { + border-bottom: 1px solid var(--border); + transition: background-color 0.2s; +} + +.table tbody tr:hover { + background: #f8f9fa; +} + +.table td.actions { + display: flex; + gap: 5px; + flex-wrap: wrap; +} + +.table-sm td, +.table-sm th { + padding: 8px 12px; +} + +/* Badges */ +.badge { + display: inline-block; + padding: 4px 10px; + border-radius: 12px; + font-size: 0.85rem; + font-weight: 500; +} + +.badge-primary { + background: var(--primary); + color: var(--white); +} + +.badge-success { + background: var(--success); + color: var(--white); +} + +.badge-danger { + background: var(--danger); + color: var(--white); +} + +.badge-warning { + background: var(--warning); + color: var(--white); +} + +.badge-info { + background: var(--info); + color: var(--white); +} + +.badge-secondary { + background: var(--secondary); + color: var(--white); +} + +/* Alerts */ +.alert { + padding: 15px 20px; + border-radius: 8px; + margin-bottom: 20px; +} + +.alert-success { + background: #d4edda; + border: 1px solid #c3e6cb; + color: #155724; +} + +.alert-error { + background: #f8d7da; + border: 1px solid #f5c6cb; + color: #721c24; +} + +.alert-warning { + background: #fff3cd; + border: 1px solid #ffeaa7; + color: #856404; +} + +.alert-info { + background: #d1ecf1; + border: 1px solid #bee5eb; + color: #0c5460; +} + +/* Dashboard */ +.dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +.dashboard-header h1 { + font-size: 2rem; + color: var(--dark); +} + +.dashboard-actions { + display: flex; + gap: 10px; +} + +/* Page Header */ +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +.page-header h1 { + font-size: 2rem; + color: var(--dark); +} + +.subtitle { + color: var(--text-muted); + margin-top: 5px; +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.stat-card { + background: var(--white); + padding: 25px; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + text-align: center; +} + +.stat-value { + font-size: 2.5rem; + font-weight: 700; + color: var(--primary); + margin-bottom: 10px; +} + +.stat-label { + font-size: 0.95rem; + color: var(--text-muted); +} + +/* Activity Grid */ +.activity-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 30px; +} + +/* Details */ +.detail-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 15px; +} + +.detail-item { + margin-bottom: 15px; +} + +.detail-item strong { + display: block; + margin-bottom: 5px; + color: var(--dark); +} + +/* Link Box */ +.link-box { + display: flex; + gap: 10px; + margin-top: 15px; +} + +.link-box input { + flex: 1; +} + +/* Tabs */ +.tabs { + display: flex; + gap: 10px; +} + +.tab { + padding: 10px 20px; + background: transparent; + border: none; + color: var(--text-muted); + text-decoration: none; + border-bottom: 3px solid transparent; + transition: all 0.3s; +} + +.tab.active, +.tab:hover { + color: var(--primary); + border-bottom-color: var(--primary); +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 40px 20px; + color: var(--text-muted); +} + +.help-text { + color: var(--text-muted); + font-size: 0.9rem; + margin-bottom: 15px; +} + +/* Priority Row */ +.priority-row { + background: #fff5f5 !important; +} + +/* Filter Form */ +.filter-form .form-row { + flex-wrap: wrap; +} + +/* Footer */ +.footer { + text-align: center; + padding: 30px 20px; + color: var(--text-muted); + font-size: 0.9rem; +} + +/* Login Page */ +.login-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.login-container { + width: 100%; + max-width: 400px; + padding: 20px; +} + +.login-box { + background: var(--white); + padding: 40px; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0,0,0,0.2); +} + +.login-box h1 { + text-align: center; + color: var(--primary); + margin-bottom: 10px; +} + +.login-box .subtitle { + text-align: center; + margin-bottom: 30px; +} + +.login-footer { + text-align: center; + margin-top: 30px; + color: var(--text-muted); +} + +/* Public Page */ +.public-page { + background: #f5f6fa; + min-height: 100vh; + padding: 40px 20px; +} + +/* Responsive */ +@media (max-width: 768px) { + .navbar .container { + flex-direction: column; + gap: 15px; + } + + .navbar-menu { + flex-direction: column; + width: 100%; + } + + .navbar-menu a { + text-align: center; + } + + .dashboard-header, + .page-header { + flex-direction: column; + gap: 15px; + align-items: flex-start; + } + + .form-row { + flex-direction: column; + } + + .table { + font-size: 0.85rem; + } + + .table td.actions { + flex-direction: column; + } +} + +/* Print Styles */ +@media print { + .navbar, + .btn, + .card-actions, + .form-actions, + .footer { + display: none !important; + } + + .card { + box-shadow: none; + border: 1px solid #ddd; + } +} diff --git a/territories.php b/territories.php new file mode 100644 index 0000000..389318c --- /dev/null +++ b/territories.php @@ -0,0 +1,468 @@ +query( + "INSERT INTO territories (numero, zona, tipologia, image_path, note) VALUES (?, ?, ?, ?, ?)", + [$numero, $zona, $tipologia, $image_path, $note] + ); + + if ($result) { + setFlashMessage('Territorio aggiunto con successo', 'success'); + } else { + setFlashMessage('Errore durante l\'aggiunta del territorio', 'error'); + } + + header('Location: territories.php'); + exit; + break; + + case 'edit': + $id = (int)$_POST['id']; + $numero = sanitize($_POST['numero']); + $zona = sanitize($_POST['zona']); + $tipologia = sanitize($_POST['tipologia']); + $note = sanitize($_POST['note'] ?? ''); + + // Gestione upload nuova immagine + $image_path = $_POST['current_image'] ?? null; + if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) { + $file = $_FILES['image']; + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + + if (in_array($ext, ALLOWED_EXTENSIONS) && $file['size'] <= MAX_FILE_SIZE) { + // Elimina vecchia immagine + if ($image_path && file_exists(BASE_PATH . $image_path)) { + unlink(BASE_PATH . $image_path); + } + + $filename = 'territory_' . time() . '_' . uniqid() . '.' . $ext; + $upload_dir = UPLOAD_PATH; + + if (!is_dir($upload_dir)) { + mkdir($upload_dir, 0755, true); + } + + if (move_uploaded_file($file['tmp_name'], $upload_dir . '/' . $filename)) { + $image_path = UPLOAD_URL . '/' . $filename; + } + } + } + + $result = $db->query( + "UPDATE territories SET numero = ?, zona = ?, tipologia = ?, image_path = ?, note = ? WHERE id = ?", + [$numero, $zona, $tipologia, $image_path, $note, $id] + ); + + if ($result) { + setFlashMessage('Territorio modificato con successo', 'success'); + } else { + setFlashMessage('Errore durante la modifica del territorio', 'error'); + } + + header('Location: territories.php'); + exit; + break; + + case 'delete': + $id = (int)$_POST['id']; + + // Recupera l'immagine per eliminarla + $territory = $db->fetchOne("SELECT image_path FROM territories WHERE id = ?", [$id]); + if ($territory && $territory['image_path']) { + $file_path = BASE_PATH . $territory['image_path']; + if (file_exists($file_path)) { + unlink($file_path); + } + } + + $result = $db->query("DELETE FROM territories WHERE id = ?", [$id]); + + if ($result) { + setFlashMessage('Territorio eliminato con successo', 'success'); + } else { + setFlashMessage('Errore durante l\'eliminazione del territorio', 'error'); + } + + header('Location: territories.php'); + exit; + break; + } + } +} + +// Recupera dati per modifica +$territory = null; +if ($action === 'edit' && $territory_id) { + $territory = $db->fetchOne("SELECT * FROM territories WHERE id = ?", [$territory_id]); + if (!$territory) { + setFlashMessage('Territorio non trovato', 'error'); + header('Location: territories.php'); + exit; + } +} + +// Lista territori con informazioni sullo stato +if ($action === 'list') { + $search = $_GET['search'] ?? ''; + $filter_zona = $_GET['zona'] ?? ''; + $filter_tipologia = $_GET['tipologia'] ?? ''; + + $where = []; + $params = []; + + if ($search) { + $where[] = "(t.numero LIKE ? OR t.zona LIKE ? OR t.tipologia LIKE ?)"; + $search_term = "%$search%"; + $params[] = $search_term; + $params[] = $search_term; + $params[] = $search_term; + } + + if ($filter_zona) { + $where[] = "t.zona = ?"; + $params[] = $filter_zona; + } + + if ($filter_tipologia) { + $where[] = "t.tipologia = ?"; + $params[] = $filter_tipologia; + } + + $where_clause = $where ? "WHERE " . implode(" AND ", $where) : ""; + + $territories = $db->fetchAll(" + SELECT + t.*, + a.id as current_assignment_id, + a.assigned_to, + a.assigned_date, + DATEDIFF(CURDATE(), a.assigned_date) as days_assigned + FROM territories t + LEFT JOIN assignments a ON t.id = a.territory_id AND a.returned_date IS NULL + $where_clause + ORDER BY t.numero ASC + ", $params); + + // Ottieni liste per filtri + $zones = $db->fetchAll("SELECT DISTINCT zona FROM territories ORDER BY zona"); + $types = $db->fetchAll("SELECT DISTINCT tipologia FROM territories ORDER BY tipologia"); +} + +include 'header.php'; +?> + + + + + +
+
+
+
+
+ +
+
+ +
+
+ +
+ + Reset +
+
+
+
+ + +
+
+ 0): ?> + + + + + + + + + + + + + + + + + + + + + + + + + +
NumeroZonaTipologiaStatoAssegnato aGiorniAzioni
+ + Assegnato + + Disponibile + + + Dettagli + Modifica + +
+ + + +
+ +
+ +

Nessun territorio trovato

+ +
+
+ + + + +
+
+
+ + + + + + +
+ + +
+ +
+ + +
+ +
+ + + Es: Residenziale, Commerciale, Rurale, ecc. +
+ +
+ + +
+ Piantina +
+ + + Formati supportati: JPG, PNG, GIF, PDF. Dimensione massima: 5 MB +
+ +
+ + +
+ +
+ + Annulla +
+
+
+
+ + + fetchOne("SELECT * FROM territories WHERE id = ?", [$territory_id]); + if (!$territory) { + setFlashMessage('Territorio non trovato', 'error'); + header('Location: territories.php'); + exit; + } + + // Storico assegnazioni + $history = $db->fetchAll(" + SELECT + assigned_to, + assigned_date, + returned_date, + DATEDIFF(COALESCE(returned_date, CURDATE()), assigned_date) as days_duration, + is_priority, + note + FROM assignments + WHERE territory_id = ? + ORDER BY assigned_date DESC + ", [$territory_id]); + ?> + + + +
+
+
+
+ Numero: +
+
+ Zona: +
+
+ Tipologia: +
+
+ + +
+ Note: +

+
+ + + +
+ Piantina:
+ + Piantina + +
+ +
+
+ +
+
+

Storico Assegnazioni

+
+
+ 0): ?> + + + + + + + + + + + + + + + + + + + + + +
Assegnato aData AssegnazioneData RestituzioneDurataPriorità
+ In corso'; + } + ?> + giorni + + Priorità + + - + +
+ +

Nessuna assegnazione storica

+ +
+
+ + + diff --git a/uploads/README.md b/uploads/README.md new file mode 100644 index 0000000..577cdf6 --- /dev/null +++ b/uploads/README.md @@ -0,0 +1,12 @@ +# uploads/ + +Questa cartella contiene le immagini delle piantine dei territori caricate dall'amministratore. + +**Nota**: Assicurati che questa cartella abbia i permessi corretti (755). + +```bash +chmod 755 uploads +``` + +Le immagini vengono nominate automaticamente con il formato: +`territory_[timestamp]_[id-unico].[estensione]` diff --git a/view_territory.php b/view_territory.php new file mode 100644 index 0000000..46595bc --- /dev/null +++ b/view_territory.php @@ -0,0 +1,131 @@ +fetchOne(" + SELECT + a.*, + t.numero, + t.zona, + t.tipologia, + t.image_path, + t.note as territory_note + FROM assignments a + INNER JOIN territories t ON a.territory_id = t.id + WHERE a.link_token = ? +", [$token]); + +if (!$assignment) { + die('Link non valido o scaduto'); +} + +// Verifica scadenza +if (strtotime($assignment['link_expires_at']) < time()) { + $expired = true; +} else { + $expired = false; +} + +function formatDate($date) { + if (empty($date)) return '-'; + return date('d/m/Y', strtotime($date)); +} +?> + + + + + + Territorio <?php echo htmlspecialchars($assignment['numero']); ?> - <?php echo APP_NAME; ?> + + + +
+ +
+
+

Link Scaduto

+

+ Questo link è scaduto il . + Contatta l'amministratore per ottenere un nuovo link. +

+
+
+ + + +
+
+

Informazioni Territorio

+
+
+
+
+ Numero: +
+
+ Zona: +
+
+ Tipologia: +
+
+ Assegnato a: +
+
+ Data Assegnazione: +
+
+ + +
+ Note: +

+
+ + + +
+ Piantina:
+ + Piantina Territorio + +
+ +
+
+ +
+
+

+ Nota: Questo link è valido fino al + +

+
+
+ + + +
+ +