From 30e1f9b36b162ef2c17762ec70fee5f46fbbe4e8 Mon Sep 17 00:00:00 2001 From: fpicone Date: Sat, 6 Dec 2025 18:23:43 +0100 Subject: [PATCH] first upload --- .htaccess.example | 56 +++++ README.md | 308 +++++++++++++++++++++++ TODO.md | 170 +++++++++++++ assignments.php | 454 +++++++++++++++++++++++++++++++++ config.php | 42 ++++ database.sql | 102 ++++++++ db.php | 84 +++++++ export_pdf.php | 203 +++++++++++++++ footer.php | 11 + functions.php | 112 +++++++++ header.php | 47 ++++ index.php | 298 ++++++++++++++++++++++ login.php | 73 ++++++ logout.php | 12 + script.js | 62 +++++ settings.php | 269 ++++++++++++++++++++ statistics.php | 414 +++++++++++++++++++++++++++++++ style.css | 607 +++++++++++++++++++++++++++++++++++++++++++++ territories.php | 468 ++++++++++++++++++++++++++++++++++ uploads/README.md | 12 + view_territory.php | 131 ++++++++++ 21 files changed, 3935 insertions(+) create mode 100644 .htaccess.example create mode 100644 README.md create mode 100644 TODO.md create mode 100644 assignments.php create mode 100644 config.php create mode 100644 database.sql create mode 100644 db.php create mode 100644 export_pdf.php create mode 100644 footer.php create mode 100644 functions.php create mode 100644 header.php create mode 100644 index.php create mode 100644 login.php create mode 100644 logout.php create mode 100644 script.js create mode 100644 settings.php create mode 100644 statistics.php create mode 100644 style.css create mode 100644 territories.php create mode 100644 uploads/README.md create mode 100644 view_territory.php 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 + +

+
+
+ + + +
+ +