first upload

This commit is contained in:
fpicone
2025-12-06 18:23:43 +01:00
commit 30e1f9b36b
21 changed files with 3935 additions and 0 deletions

56
.htaccess.example Normal file
View File

@@ -0,0 +1,56 @@
<?php
/**
* File di esempio .htaccess
* Territory Manager
*
* Rinomina questo file in .htaccess per usarlo con Apache
*/
# Abilita RewriteEngine
# RewriteEngine On
# RewriteBase /
# Redirect da HTTP a HTTPS (decommentare in produzione)
# RewriteCond %{HTTPS} off
# RewriteRule ^(.*)$ https://%{HTTP_HOST%{REQUEST_URI} [L,R=301]
# Protezione file sensibili
<FilesMatch "^(config\.php|db\.php|database\.sql)$">
Order allow,deny
Deny from all
</FilesMatch>
# 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
<Directory "uploads">
Options -Indexes
php_flag engine off
</Directory>
# Compressione GZIP
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript
</IfModule>
# Cache statica
<IfModule mod_expires.c>
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"
</IfModule>
# Sicurezza Headers
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
</IfModule>

308
README.md Normal file
View File

@@ -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! 🗺️**

170
TODO.md Normal file
View File

@@ -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
<div class="form-group">
<label for="descrizione_estesa">Descrizione</label>
<textarea id="descrizione_estesa" name="descrizione_estesa"
class="form-control"></textarea>
</div>
```
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
<?php
require_once 'config.php';
require_once 'functions.php';
requireLogin();
$page_title = 'Mia Pagina';
include 'header.php';
?>
<!-- Il tuo contenuto qui -->
<?php include '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! 🚀

454
assignments.php Normal file
View File

@@ -0,0 +1,454 @@
<?php
/**
* Gestione Assegnazioni
* Territory Manager
*/
require_once 'config.php';
require_once 'functions.php';
require_once 'db.php';
requireLogin();
$page_title = 'Gestione Assegnazioni';
$db = getDB();
$action = $_GET['action'] ?? 'list';
$territory_id = $_GET['territory_id'] ?? null;
// Gestione delle azioni POST
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['action'])) {
switch ($_POST['action']) {
case 'assign':
$territory_id = (int)$_POST['territory_id'];
$assigned_to = sanitize($_POST['assigned_to']);
$assigned_date = $_POST['assigned_date'];
$is_priority = isset($_POST['is_priority']) ? 1 : 0;
$note = sanitize($_POST['note'] ?? '');
// Verifica che il territorio non sia già assegnato
$existing = $db->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';
?>
<?php if ($action === 'list'): ?>
<div class="page-header">
<h1>Gestione Assegnazioni</h1>
<a href="?action=assign" class="btn btn-primary">+ Nuova Assegnazione</a>
</div>
<div class="card">
<div class="card-header">
<div class="tabs">
<a href="?filter=current" class="tab <?php echo $filter === 'current' ? 'active' : ''; ?>">
Assegnazioni Correnti
</a>
<a href="?filter=history" class="tab <?php echo $filter === 'history' ? 'active' : ''; ?>">
Storico
</a>
</div>
</div>
<div class="card-body">
<?php if (count($assignments) > 0): ?>
<table class="table">
<thead>
<tr>
<th>Territorio</th>
<th>Zona</th>
<th>Tipologia</th>
<th>Assegnato a</th>
<th>Data Assegnazione</th>
<?php if ($filter === 'current'): ?>
<th>Giorni</th>
<th>Azioni</th>
<?php else: ?>
<th>Data Restituzione</th>
<th>Durata</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($assignments as $a): ?>
<tr>
<td>
<strong><?php echo htmlspecialchars($a['numero']); ?></strong>
<?php if ($a['is_priority']): ?>
<span class="badge badge-danger">Priorità</span>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($a['zona']); ?></td>
<td><?php echo htmlspecialchars($a['tipologia']); ?></td>
<td><?php echo htmlspecialchars($a['assigned_to']); ?></td>
<td><?php echo formatDate($a['assigned_date']); ?></td>
<?php if ($filter === 'current'): ?>
<td>
<span class="badge badge-info"><?php echo $a['days_assigned']; ?> giorni</span>
</td>
<td class="actions">
<a href="?action=view&id=<?php echo $a['id']; ?>" class="btn btn-sm btn-secondary">Dettagli</a>
<a href="?action=return&territory_id=<?php echo $a['territory_id']; ?>" class="btn btn-sm btn-primary">Riconsegna</a>
</td>
<?php else: ?>
<td><?php echo formatDate($a['returned_date']); ?></td>
<td><?php echo $a['days_assigned']; ?> giorni</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">
<?php echo $filter === 'current' ? 'Nessuna assegnazione corrente' : 'Nessuno storico disponibile'; ?>
</p>
<?php endif; ?>
</div>
</div>
<?php elseif ($action === 'assign'): ?>
<div class="page-header">
<h1>Nuova Assegnazione</h1>
</div>
<div class="card">
<div class="card-body">
<form method="POST">
<input type="hidden" name="action" value="assign">
<div class="form-group">
<label for="territory_id">Territorio *</label>
<?php if ($territory_id): ?>
<input type="hidden" name="territory_id" value="<?php echo $territory_id; ?>">
<input type="text" class="form-control" readonly
value="<?php echo htmlspecialchars($territory['numero'] . ' - ' . $territory['zona']); ?>">
<?php else: ?>
<select id="territory_id" name="territory_id" required class="form-control">
<option value="">Seleziona un territorio</option>
<?php foreach ($available_territories as $t): ?>
<option value="<?php echo $t['id']; ?>">
<?php echo htmlspecialchars($t['numero'] . ' - ' . $t['zona'] . ' (' . $t['tipologia'] . ')'); ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</div>
<div class="form-group">
<label for="assigned_to">Assegnato a *</label>
<input type="text" id="assigned_to" name="assigned_to" required class="form-control"
placeholder="Nome della persona">
</div>
<div class="form-group">
<label for="assigned_date">Data Assegnazione *</label>
<input type="date" id="assigned_date" name="assigned_date" required
value="<?php echo date('Y-m-d'); ?>" class="form-control">
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" name="is_priority" value="1" <?php echo $is_priority ? 'checked' : ''; ?>>
Assegnazione Prioritaria
</label>
</div>
<div class="form-group">
<label for="note">Note</label>
<textarea id="note" name="note" rows="3" class="form-control"></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Assegna Territorio</button>
<a href="assignments.php" class="btn btn-secondary">Annulla</a>
</div>
</form>
</div>
</div>
<?php elseif ($action === 'return'): ?>
<div class="page-header">
<h1>Riconsegna Territorio</h1>
</div>
<div class="card">
<div class="card-body">
<div class="detail-grid">
<div class="detail-item">
<strong>Territorio:</strong> <?php echo htmlspecialchars($assignment['numero']); ?>
</div>
<div class="detail-item">
<strong>Zona:</strong> <?php echo htmlspecialchars($assignment['zona']); ?>
</div>
<div class="detail-item">
<strong>Assegnato a:</strong> <?php echo htmlspecialchars($assignment['assigned_to']); ?>
</div>
<div class="detail-item">
<strong>Data Assegnazione:</strong> <?php echo formatDate($assignment['assigned_date']); ?>
</div>
</div>
<form method="POST" style="margin-top: 20px;">
<input type="hidden" name="action" value="return">
<input type="hidden" name="territory_id" value="<?php echo $territory_id; ?>">
<div class="form-group">
<label for="returned_date">Data Restituzione *</label>
<input type="date" id="returned_date" name="returned_date" required
value="<?php echo date('Y-m-d'); ?>" class="form-control">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Conferma Riconsegna</button>
<a href="assignments.php" class="btn btn-secondary">Annulla</a>
</div>
</form>
</div>
</div>
<?php elseif ($action === 'view'): ?>
<?php
$base_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") .
"://" . $_SERVER['HTTP_NAME'];
$share_url = $base_url . "/view_territory.php?token=" . $assignment['link_token'];
$is_link_valid = strtotime($assignment['link_expires_at']) > time();
?>
<div class="page-header">
<h1>Dettagli Assegnazione</h1>
<a href="assignments.php" class="btn btn-secondary">Torna alla Lista</a>
</div>
<div class="card">
<div class="card-header">
<h2>Informazioni Territorio</h2>
</div>
<div class="card-body">
<div class="detail-grid">
<div class="detail-item">
<strong>Numero:</strong> <?php echo htmlspecialchars($assignment['numero']); ?>
</div>
<div class="detail-item">
<strong>Zona:</strong> <?php echo htmlspecialchars($assignment['zona']); ?>
</div>
<div class="detail-item">
<strong>Tipologia:</strong> <?php echo htmlspecialchars($assignment['tipologia']); ?>
</div>
<div class="detail-item">
<strong>Assegnato a:</strong> <?php echo htmlspecialchars($assignment['assigned_to']); ?>
</div>
<div class="detail-item">
<strong>Data Assegnazione:</strong> <?php echo formatDate($assignment['assigned_date']); ?>
</div>
<div class="detail-item">
<strong>Priorità:</strong>
<?php if ($assignment['is_priority']): ?>
<span class="badge badge-danger">Prioritaria</span>
<?php else: ?>
Normale
<?php endif; ?>
</div>
</div>
<?php if ($assignment['note']): ?>
<div class="detail-item" style="margin-top: 15px;">
<strong>Note:</strong>
<p><?php echo nl2br(htmlspecialchars($assignment['note'])); ?></p>
</div>
<?php endif; ?>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Link Condivisione Territorio</h2>
</div>
<div class="card-body">
<p class="help-text">
Questo link permette di visualizzare il territorio senza accedere al sistema.
<?php if ($is_link_valid): ?>
Il link è valido fino al <strong><?php echo formatDateTime($assignment['link_expires_at']); ?></strong>
<?php else: ?>
<span class="badge badge-danger">Il link è scaduto</span>
<?php endif; ?>
</p>
<div class="link-box">
<input type="text" id="share_link" value="<?php echo htmlspecialchars($share_url); ?>"
class="form-control" readonly>
<button onclick="copyLink()" class="btn btn-primary">Copia Link</button>
</div>
<?php if (!$is_link_valid): ?>
<p class="alert alert-warning" style="margin-top: 15px;">
Per generare un nuovo link, è necessario creare una nuova assegnazione.
</p>
<?php endif; ?>
</div>
</div>
<?php if (!$assignment['returned_date']): ?>
<div class="form-actions">
<a href="?action=return&territory_id=<?php echo $assignment['territory_id']; ?>"
class="btn btn-primary">Riconsegna Territorio</a>
</div>
<?php endif; ?>
<?php endif; ?>
<script>
function copyLink() {
const linkInput = document.getElementById('share_link');
linkInput.select();
document.execCommand('copy');
alert('Link copiato negli appunti!');
}
</script>
<?php include 'footer.php'; ?>

42
config.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
/**
* File di Configurazione
* Territory Manager
*/
// Configurazione Database
define('DB_HOST', 'localhost');
define('DB_NAME', 'territoryassigner');
define('DB_USER', 'demo-termanager');
define('DB_PASS', 'Z.p8ibwg4jri');
define('DB_CHARSET', 'utf8mb4');
// Configurazione Applicazione
define('APP_NAME', 'Territory Manager');
define('APP_VERSION', '1.0.0');
define('TIMEZONE', 'Europe/Rome');
// Percorsi
define('BASE_PATH', __DIR__);
define('UPLOAD_PATH', BASE_PATH . '/uploads');
define('UPLOAD_URL', '/uploads');
// Configurazione Upload Immagini
define('MAX_FILE_SIZE', 5 * 1024 * 1024); // 5 MB
define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif', 'pdf']);
// Configurazione Sessione
define('SESSION_LIFETIME', 3600 * 8); // 8 ore
// Impostazioni Timezone
date_default_timezone_set(TIMEZONE);
// Gestione Errori (disattivare in produzione)
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Avvio Sessione
if (session_status() === PHP_SESSION_NONE) {
session_start();
}

102
database.sql Normal file
View File

@@ -0,0 +1,102 @@
-- Database per Gestione Territori
-- Creato il 6 dicembre 2025
CREATE DATABASE IF NOT EXISTS territoryassigner CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE territoryassigner;
-- Tabella utenti
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
is_admin TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabella territori
CREATE TABLE IF NOT EXISTS territories (
id INT AUTO_INCREMENT PRIMARY KEY,
numero VARCHAR(20) NOT NULL,
zona VARCHAR(100) NOT NULL,
tipologia VARCHAR(50) NOT NULL,
image_path VARCHAR(255),
note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_territory (numero, zona)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabella assegnazioni
CREATE TABLE IF NOT EXISTS assignments (
id INT AUTO_INCREMENT PRIMARY KEY,
territory_id INT NOT NULL,
assigned_to VARCHAR(100) NOT NULL,
assigned_date DATE NOT NULL,
returned_date DATE NULL,
link_token VARCHAR(64) UNIQUE,
link_expires_at DATETIME NULL,
is_priority TINYINT(1) DEFAULT 0,
note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (territory_id) REFERENCES territories(id) ON DELETE CASCADE,
INDEX idx_territory (territory_id),
INDEX idx_token (link_token),
INDEX idx_dates (assigned_date, returned_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabella configurazione
CREATE TABLE IF NOT EXISTS config (
id INT AUTO_INCREMENT PRIMARY KEY,
config_key VARCHAR(50) NOT NULL UNIQUE,
config_value VARCHAR(255) NOT NULL,
description TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Inserimento configurazioni di default
INSERT INTO config (config_key, config_value, description) VALUES
('link_expiry_days', '7', 'Giorni di validità dei link temporanei'),
('warning_days_normal', '90', 'Giorni dopo i quali un territorio è da assegnare'),
('warning_days_priority', '180', 'Giorni dopo i quali un territorio è prioritario'),
('warning_days_return', '120', 'Giorni dopo i quali un territorio è da riconsegnare');
-- Utente amministratore di default (password: admin123)
-- IMPORTANTE: Cambiare la password dopo la prima installazione!
INSERT INTO users (username, password, email, is_admin) VALUES
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin@example.com', 1);
-- Vista per territori attualmente assegnati
CREATE OR REPLACE VIEW current_assignments AS
SELECT
t.id as territory_id,
t.numero,
t.zona,
t.tipologia,
a.id as assignment_id,
a.assigned_to,
a.assigned_date,
a.is_priority,
DATEDIFF(CURDATE(), a.assigned_date) as days_assigned
FROM territories t
LEFT JOIN assignments a ON t.id = a.territory_id
WHERE a.returned_date IS NULL
ORDER BY a.assigned_date ASC;
-- Vista per territori disponibili (in reparto)
CREATE OR REPLACE VIEW available_territories AS
SELECT
t.id as territory_id,
t.numero,
t.zona,
t.tipologia,
t.image_path,
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, t.image_path
ORDER BY last_returned_date ASC NULLS FIRST;

84
db.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
/**
* Gestione Connessione Database
* Territory Manager
*/
require_once 'config.php';
class Database {
private static $instance = null;
private $connection;
private function __construct() {
try {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
$options = [
PDO::ATTR_ERRMODE => 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();
}

203
export_pdf.php Normal file
View File

@@ -0,0 +1,203 @@
<?php
/**
* Export PDF
* Territory Manager
* Utilizza FPDF per generare PDF
*/
require_once 'config.php';
require_once 'functions.php';
require_once 'db.php';
requireLogin();
$type = $_GET['type'] ?? '';
$db = getDB();
// Configurazioni
$warning_days_normal = (int)$db->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 .= "<h1 style='color: #2c3e50; margin-bottom: 20px;'>$title</h1>";
}
public function addText($text) {
$this->content .= "<p style='margin: 10px 0;'>$text</p>";
}
public function addTable($headers, $rows) {
$this->content .= "<table style='width: 100%; border-collapse: collapse; margin: 20px 0;'>";
$this->content .= "<thead><tr style='background-color: #34495e; color: white;'>";
foreach ($headers as $header) {
$this->content .= "<th style='padding: 10px; border: 1px solid #ddd; text-align: left;'>$header</th>";
}
$this->content .= "</tr></thead><tbody>";
foreach ($rows as $row) {
$this->content .= "<tr>";
foreach ($row as $cell) {
$this->content .= "<td style='padding: 8px; border: 1px solid #ddd;'>$cell</td>";
}
$this->content .= "</tr>";
}
$this->content .= "</tbody></table>";
}
public function output($filename) {
header('Content-Type: text/html; charset=utf-8');
header('Content-Disposition: inline; filename="' . $filename . '.html"');
echo "<!DOCTYPE html>
<html lang='it'>
<head>
<meta charset='UTF-8'>
<title>$filename</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
@media print {
body { padding: 0; }
}
</style>
</head>
<body>";
echo $this->content;
echo "<div style='margin-top: 40px; text-align: center; color: #7f8c8d;'>";
echo "<p>Generato il " . date('d/m/Y H:i') . " - " . APP_NAME . "</p>";
echo "</div>";
echo "<script>window.print();</script>";
echo "</body></html>";
}
}
$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');
}

11
footer.php Normal file
View File

@@ -0,0 +1,11 @@
</main>
<footer class="footer">
<div class="container">
<p>&copy; <?php echo date('Y'); ?> <?php echo APP_NAME; ?> - Versione <?php echo APP_VERSION; ?></p>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>

112
functions.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
/**
* Funzioni Helper di Autenticazione
* Territory Manager
*/
require_once 'db.php';
// Verifica se l'utente è loggato
function isLoggedIn() {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
// Verifica se l'utente è amministratore
function isAdmin() {
return isLoggedIn() && isset($_SESSION['is_admin']) && $_SESSION['is_admin'] == 1;
}
// Ottieni l'utente corrente
function getCurrentUser() {
if (!isLoggedIn()) {
return null;
}
$db = getDB();
return $db->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));
}

47
header.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
/**
* Header Template
* Territory Manager
*/
$current_page = basename($_SERVER['PHP_SELF'], '.php');
?>
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo isset($page_title) ? $page_title . ' - ' : ''; ?><?php echo APP_NAME; ?></title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<a href="index.php"><?php echo APP_NAME; ?></a>
</div>
<ul class="navbar-menu">
<li><a href="index.php" class="<?php echo $current_page === 'index' ? 'active' : ''; ?>">Dashboard</a></li>
<li><a href="territories.php" class="<?php echo $current_page === 'territories' ? 'active' : ''; ?>">Territori</a></li>
<li><a href="assignments.php" class="<?php echo $current_page === 'assignments' ? 'active' : ''; ?>">Assegnazioni</a></li>
<li><a href="statistics.php" class="<?php echo $current_page === 'statistics' ? 'active' : ''; ?>">Statistiche</a></li>
<?php if (isAdmin()): ?>
<li><a href="settings.php" class="<?php echo $current_page === 'settings' ? 'active' : ''; ?>">Impostazioni</a></li>
<?php endif; ?>
</ul>
<div class="navbar-user">
<span>Ciao, <strong><?php echo htmlspecialchars($_SESSION['username']); ?></strong></span>
<a href="logout.php" class="btn btn-sm btn-secondary">Esci</a>
</div>
</div>
</nav>
<main class="container">
<?php
$flash = getFlashMessage();
if ($flash):
?>
<div class="alert alert-<?php echo $flash['type']; ?>">
<?php echo htmlspecialchars($flash['text']); ?>
</div>
<?php endif; ?>

298
index.php Normal file
View File

@@ -0,0 +1,298 @@
<?php
/**
* Pagina Dashboard (Home)
* Territory Manager
*/
require_once 'config.php';
require_once 'functions.php';
require_once 'db.php';
requireLogin();
$page_title = 'Dashboard';
$db = getDB();
// Carica configurazioni
$warning_days_normal = (int)$db->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';
?>
<div class="dashboard-header">
<h1>Dashboard</h1>
<div class="dashboard-actions">
<a href="territories.php?action=add" class="btn btn-primary">+ Nuovo Territorio</a>
<a href="assignments.php?action=assign" class="btn btn-success">+ Nuova Assegnazione</a>
</div>
</div>
<!-- Territori da Assegnare -->
<div class="card">
<div class="card-header">
<h2>Territori da Assegnare (<?php echo $count_to_assign; ?>)</h2>
<div class="card-actions">
<?php if (count($territories_to_assign) > 0): ?>
<a href="export_pdf.php?type=to_assign" class="btn btn-sm btn-secondary" target="_blank">📄 Export PDF</a>
<?php if (!isset($_GET['expand_normal'])): ?>
<a href="?expand_normal=1" class="btn btn-sm btn-primary">Mostra Tutti</a>
<?php else: ?>
<a href="index.php" class="btn btn-sm btn-secondary">Mostra Meno</a>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<div class="card-body">
<p class="help-text">Territori in reparto da più di <?php echo $warning_days_normal; ?> giorni</p>
<?php if (count($territories_to_assign) > 0): ?>
<table class="table">
<thead>
<tr>
<th>Numero</th>
<th>Zona</th>
<th>Tipologia</th>
<th>Ultima Restituzione</th>
<th>Giorni in Reparto</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($territories_to_assign as $territory): ?>
<tr>
<td><strong><?php echo htmlspecialchars($territory['numero']); ?></strong></td>
<td><?php echo htmlspecialchars($territory['zona']); ?></td>
<td><?php echo htmlspecialchars($territory['tipologia']); ?></td>
<td><?php echo formatDate($territory['last_returned_date']); ?></td>
<td>
<span class="badge badge-warning">
<?php echo $territory['days_in_depot'] ?? 'Mai assegnato'; ?> giorni
</span>
</td>
<td>
<a href="assignments.php?action=assign&territory_id=<?php echo $territory['id']; ?>"
class="btn btn-sm btn-success">Assegna</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessun territorio da assegnare</p>
<?php endif; ?>
</div>
</div>
<!-- Territori Prioritari -->
<div class="card">
<div class="card-header">
<h2>Territori Prioritari (<?php echo $count_priority; ?>)</h2>
<div class="card-actions">
<?php if (count($territories_priority) > 0): ?>
<a href="export_pdf.php?type=priority" class="btn btn-sm btn-secondary" target="_blank">📄 Export PDF</a>
<?php if (!isset($_GET['expand_priority'])): ?>
<a href="?expand_priority=1" class="btn btn-sm btn-primary">Mostra Tutti</a>
<?php else: ?>
<a href="index.php" class="btn btn-sm btn-secondary">Mostra Meno</a>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<div class="card-body">
<p class="help-text">Territori in reparto da più di <?php echo $warning_days_priority; ?> giorni</p>
<?php if (count($territories_priority) > 0): ?>
<table class="table">
<thead>
<tr>
<th>Numero</th>
<th>Zona</th>
<th>Tipologia</th>
<th>Ultima Restituzione</th>
<th>Giorni in Reparto</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($territories_priority as $territory): ?>
<tr class="priority-row">
<td><strong><?php echo htmlspecialchars($territory['numero']); ?></strong></td>
<td><?php echo htmlspecialchars($territory['zona']); ?></td>
<td><?php echo htmlspecialchars($territory['tipologia']); ?></td>
<td><?php echo formatDate($territory['last_returned_date']); ?></td>
<td>
<span class="badge badge-danger">
<?php echo $territory['days_in_depot'] ?? 'Mai assegnato'; ?> giorni
</span>
</td>
<td>
<a href="assignments.php?action=assign&territory_id=<?php echo $territory['id']; ?>&priority=1"
class="btn btn-sm btn-success">Assegna</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessun territorio prioritario</p>
<?php endif; ?>
</div>
</div>
<!-- Territori da Riconsegnare -->
<div class="card">
<div class="card-header">
<h2>Territori da Riconsegnare (<?php echo $count_to_return; ?>)</h2>
<div class="card-actions">
<?php if (count($territories_to_return) > 0): ?>
<a href="export_pdf.php?type=to_return" class="btn btn-sm btn-secondary" target="_blank">📄 Export PDF</a>
<?php if (!isset($_GET['expand_return'])): ?>
<a href="?expand_return=1" class="btn btn-sm btn-primary">Mostra Tutti</a>
<?php else: ?>
<a href="index.php" class="btn btn-sm btn-secondary">Mostra Meno</a>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<div class="card-body">
<p class="help-text">Territori assegnati da più di <?php echo $warning_days_return; ?> giorni</p>
<?php if (count($territories_to_return) > 0): ?>
<table class="table">
<thead>
<tr>
<th>Numero</th>
<th>Zona</th>
<th>Tipologia</th>
<th>Assegnato a</th>
<th>Data Assegnazione</th>
<th>Giorni Assegnato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($territories_to_return as $territory): ?>
<tr>
<td><strong><?php echo htmlspecialchars($territory['numero']); ?></strong></td>
<td><?php echo htmlspecialchars($territory['zona']); ?></td>
<td><?php echo htmlspecialchars($territory['tipologia']); ?></td>
<td><?php echo htmlspecialchars($territory['assigned_to']); ?></td>
<td><?php echo formatDate($territory['assigned_date']); ?></td>
<td>
<span class="badge badge-warning">
<?php echo $territory['days_assigned']; ?> giorni
</span>
</td>
<td>
<a href="assignments.php?action=return&territory_id=<?php echo $territory['id']; ?>"
class="btn btn-sm btn-primary">Riconsegna</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessun territorio da riconsegnare</p>
<?php endif; ?>
</div>
</div>
<?php include 'footer.php'; ?>

73
login.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
/**
* Pagina di Login
* Territory Manager
*/
require_once 'config.php';
require_once 'functions.php';
// Se già loggato, redirect alla home
if (isLoggedIn()) {
header('Location: index.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = sanitize($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$error = 'Inserire username e password';
} else {
if (login($username, $password)) {
header('Location: index.php');
exit;
} else {
$error = 'Credenziali non valide';
}
}
}
?>
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - <?php echo APP_NAME; ?></title>
<link rel="stylesheet" href="style.css">
</head>
<body class="login-page">
<div class="login-container">
<div class="login-box">
<h1><?php echo APP_NAME; ?></h1>
<p class="subtitle">Gestione Territori</p>
<?php if ($error): ?>
<div class="alert alert-error"><?php echo $error; ?></div>
<?php endif; ?>
<form method="POST" action="">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus
value="<?php echo isset($_POST['username']) ? htmlspecialchars($_POST['username']) : ''; ?>">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary btn-block">Accedi</button>
</form>
<div class="login-footer">
<small>Versione <?php echo APP_VERSION; ?></small>
</div>
</div>
</div>
</body>
</html>

12
logout.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
/**
* Logout
* Territory Manager
*/
require_once 'config.php';
require_once 'functions.php';
logout();
header('Location: login.php');
exit;

62
script.js Normal file
View File

@@ -0,0 +1,62 @@
/**
* JavaScript Territory Manager
*/
// Conferma prima di eliminare
document.querySelectorAll('form[onsubmit*="confirm"]').forEach(form => {
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');
}
});
});

269
settings.php Normal file
View File

@@ -0,0 +1,269 @@
<?php
/**
* Impostazioni (Solo Admin)
* Territory Manager
*/
require_once 'config.php';
require_once 'functions.php';
require_once 'db.php';
requireAdmin();
$page_title = 'Impostazioni';
$db = getDB();
// Gestione salvataggio configurazioni
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['action'])) {
switch ($_POST['action']) {
case 'update_config':
$link_expiry_days = (int)$_POST['link_expiry_days'];
$warning_days_normal = (int)$_POST['warning_days_normal'];
$warning_days_priority = (int)$_POST['warning_days_priority'];
$warning_days_return = (int)$_POST['warning_days_return'];
$db->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';
?>
<div class="page-header">
<h1>Impostazioni</h1>
</div>
<!-- Configurazioni Generali -->
<div class="card">
<div class="card-header">
<h2>Configurazioni Generali</h2>
</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="action" value="update_config">
<div class="form-group">
<label for="link_expiry_days">Giorni validità link temporanei</label>
<input type="number" id="link_expiry_days" name="link_expiry_days"
value="<?php echo $config['link_expiry_days']; ?>"
min="1" max="365" required class="form-control">
<small class="form-help">Numero di giorni per cui i link di condivisione territorio sono validi</small>
</div>
<div class="form-group">
<label for="warning_days_normal">Giorni per territori da assegnare</label>
<input type="number" id="warning_days_normal" name="warning_days_normal"
value="<?php echo $config['warning_days_normal']; ?>"
min="1" max="999" required class="form-control">
<small class="form-help">Giorni dopo i quali un territorio in reparto è considerato da assegnare</small>
</div>
<div class="form-group">
<label for="warning_days_priority">Giorni per territori prioritari</label>
<input type="number" id="warning_days_priority" name="warning_days_priority"
value="<?php echo $config['warning_days_priority']; ?>"
min="1" max="999" required class="form-control">
<small class="form-help">Giorni dopo i quali un territorio in reparto è considerato prioritario</small>
</div>
<div class="form-group">
<label for="warning_days_return">Giorni per territori da riconsegnare</label>
<input type="number" id="warning_days_return" name="warning_days_return"
value="<?php echo $config['warning_days_return']; ?>"
min="1" max="999" required class="form-control">
<small class="form-help">Giorni dopo i quali un territorio assegnato è da riconsegnare</small>
</div>
<button type="submit" class="btn btn-primary">Salva Configurazioni</button>
</form>
</div>
</div>
<!-- Cambio Password -->
<div class="card">
<div class="card-header">
<h2>Cambia Password</h2>
</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="action" value="change_password">
<div class="form-group">
<label for="current_password">Password Corrente</label>
<input type="password" id="current_password" name="current_password" required class="form-control">
</div>
<div class="form-group">
<label for="new_password">Nuova Password</label>
<input type="password" id="new_password" name="new_password" required class="form-control">
</div>
<div class="form-group">
<label for="confirm_password">Conferma Nuova Password</label>
<input type="password" id="confirm_password" name="confirm_password" required class="form-control">
</div>
<button type="submit" class="btn btn-primary">Cambia Password</button>
</form>
</div>
</div>
<!-- Gestione Utenti -->
<div class="card">
<div class="card-header">
<h2>Gestione Utenti</h2>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Ruolo</th>
<th>Data Creazione</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><strong><?php echo htmlspecialchars($user['username']); ?></strong></td>
<td><?php echo htmlspecialchars($user['email']); ?></td>
<td>
<?php if ($user['is_admin']): ?>
<span class="badge badge-danger">Admin</span>
<?php else: ?>
<span class="badge badge-info">Utente</span>
<?php endif; ?>
</td>
<td><?php echo formatDate($user['created_at']); ?></td>
<td>
<?php if ($user['id'] != $_SESSION['user_id']): ?>
<form method="POST" style="display:inline;"
onsubmit="return confirm('Sei sicuro di voler eliminare questo utente?');">
<input type="hidden" name="action" value="delete_user">
<input type="hidden" name="user_id" value="<?php echo $user['id']; ?>">
<button type="submit" class="btn btn-sm btn-danger">Elimina</button>
</form>
<?php else: ?>
<span class="badge badge-secondary">Tu</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h3 style="margin-top: 30px;">Aggiungi Nuovo Utente</h3>
<form method="POST" style="margin-top: 20px;">
<input type="hidden" name="action" value="add_user">
<div class="form-row">
<div class="form-group">
<input type="text" name="username" placeholder="Username" required class="form-control">
</div>
<div class="form-group">
<input type="email" name="email" placeholder="Email" class="form-control">
</div>
<div class="form-group">
<input type="password" name="password" placeholder="Password" required class="form-control">
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" name="is_admin" value="1">
Amministratore
</label>
</div>
<button type="submit" class="btn btn-primary">Aggiungi Utente</button>
</div>
</form>
</div>
</div>
<?php include 'footer.php'; ?>

414
statistics.php Normal file
View File

@@ -0,0 +1,414 @@
<?php
/**
* Statistiche
* Territory Manager
*/
require_once 'config.php';
require_once 'functions.php';
require_once 'db.php';
requireLogin();
$page_title = 'Statistiche';
$db = getDB();
// Anno corrente per default
$selected_year = $_GET['year'] ?? date('Y');
$selected_month = $_GET['month'] ?? null;
// Statistiche generali
$total_territories = $db->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';
?>
<div class="page-header">
<h1>Statistiche</h1>
<div class="header-actions">
<form method="GET" style="display: inline-flex; gap: 10px;">
<select name="year" class="form-control" onchange="this.form.submit()">
<?php foreach ($available_years as $y): ?>
<option value="<?php echo $y['year']; ?>" <?php echo $selected_year == $y['year'] ? 'selected' : ''; ?>>
<?php echo $y['year']; ?>
</option>
<?php endforeach; ?>
</select>
</form>
</div>
</div>
<!-- Statistiche Generali -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value"><?php echo $total_territories; ?></div>
<div class="stat-label">Totale Territori</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo $assigned_territories; ?></div>
<div class="stat-label">Territori Assegnati</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo $available_territories; ?></div>
<div class="stat-label">Territori Disponibili</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo count($never_assigned); ?></div>
<div class="stat-label">Mai Assegnati</div>
</div>
</div>
<!-- Attività Recente (7 giorni) -->
<div class="card">
<div class="card-header">
<h2>Attività Ultimi 7 Giorni</h2>
</div>
<div class="card-body">
<div class="activity-grid">
<div>
<h3>Assegnazioni</h3>
<?php if (count($recent_activity) > 0): ?>
<table class="table table-sm">
<tbody>
<?php foreach ($recent_activity as $activity): ?>
<tr>
<td><?php echo formatDate($activity['date']); ?></td>
<td><strong><?php echo $activity['assignments']; ?></strong> assegnazioni</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessuna assegnazione recente</p>
<?php endif; ?>
</div>
<div>
<h3>Riconsegne</h3>
<?php if (count($recent_returns) > 0): ?>
<table class="table table-sm">
<tbody>
<?php foreach ($recent_returns as $returns): ?>
<tr>
<td><?php echo formatDate($returns['date']); ?></td>
<td><strong><?php echo $returns['returns']; ?></strong> riconsegne</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessuna riconsegna recente</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- Media Percorrenza Mensile -->
<div class="card">
<div class="card-header">
<h2>Media Percorrenza Mensile - <?php echo $selected_year; ?></h2>
</div>
<div class="card-body">
<?php if (count($monthly_stats) > 0): ?>
<table class="table">
<thead>
<tr>
<th>Mese</th>
<th>Totale Assegnazioni</th>
<th>Durata Media</th>
</tr>
</thead>
<tbody>
<?php
$month_names = ['', 'Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno',
'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'];
foreach ($monthly_stats as $stat):
?>
<tr>
<td><?php echo $month_names[$stat['month']]; ?></td>
<td><?php echo $stat['total_assignments']; ?></td>
<td><?php echo round($stat['avg_duration']); ?> giorni</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessun dato per l'anno selezionato</p>
<?php endif; ?>
</div>
</div>
<!-- Media Percorrenza Annuale -->
<div class="card">
<div class="card-header">
<h2>Media Percorrenza Annuale</h2>
</div>
<div class="card-body">
<?php if (count($yearly_stats) > 0): ?>
<table class="table">
<thead>
<tr>
<th>Anno</th>
<th>Totale Assegnazioni</th>
<th>Durata Media</th>
</tr>
</thead>
<tbody>
<?php foreach ($yearly_stats as $stat): ?>
<tr>
<td><strong><?php echo $stat['year']; ?></strong></td>
<td><?php echo $stat['total_assignments']; ?></td>
<td><?php echo round($stat['avg_duration']); ?> giorni</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessun dato disponibile</p>
<?php endif; ?>
</div>
</div>
<!-- Statistiche per Zona -->
<div class="card">
<div class="card-header">
<h2>Statistiche per Zona</h2>
</div>
<div class="card-body">
<?php if (count($zone_stats) > 0): ?>
<table class="table">
<thead>
<tr>
<th>Zona</th>
<th>Totale Territori</th>
<th>Totale Assegnazioni</th>
<th>Durata Media</th>
</tr>
</thead>
<tbody>
<?php foreach ($zone_stats as $stat): ?>
<tr>
<td><strong><?php echo htmlspecialchars($stat['zona']); ?></strong></td>
<td><?php echo $stat['total_territories']; ?></td>
<td><?php echo $stat['total_assignments']; ?></td>
<td><?php echo $stat['avg_duration'] ? round($stat['avg_duration']) . ' giorni' : '-'; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessun dato disponibile</p>
<?php endif; ?>
</div>
</div>
<!-- Top 10 Territori Più Assegnati -->
<div class="card">
<div class="card-header">
<h2>Top 10 Territori Più Assegnati</h2>
</div>
<div class="card-body">
<?php if (count($most_assigned) > 0): ?>
<table class="table">
<thead>
<tr>
<th>Territorio</th>
<th>Zona</th>
<th>Tipologia</th>
<th>N° Assegnazioni</th>
<th>Durata Media</th>
</tr>
</thead>
<tbody>
<?php foreach ($most_assigned as $territory): ?>
<tr>
<td><strong><?php echo htmlspecialchars($territory['numero']); ?></strong></td>
<td><?php echo htmlspecialchars($territory['zona']); ?></td>
<td><?php echo htmlspecialchars($territory['tipologia']); ?></td>
<td><?php echo $territory['assignment_count']; ?></td>
<td><?php echo round($territory['avg_duration']); ?> giorni</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessun dato disponibile</p>
<?php endif; ?>
</div>
</div>
<!-- Statistiche per Persona -->
<div class="card">
<div class="card-header">
<h2>Top 10 Persone per Assegnazioni</h2>
</div>
<div class="card-body">
<?php if (count($person_stats) > 0): ?>
<table class="table">
<thead>
<tr>
<th>Nome</th>
<th>Totale Assegnazioni</th>
<th>Assegnazioni Correnti</th>
<th>Durata Media</th>
</tr>
</thead>
<tbody>
<?php foreach ($person_stats as $person): ?>
<tr>
<td><strong><?php echo htmlspecialchars($person['assigned_to']); ?></strong></td>
<td><?php echo $person['total_assignments']; ?></td>
<td><?php echo $person['current_assignments']; ?></td>
<td><?php echo round($person['avg_duration']); ?> giorni</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessun dato disponibile</p>
<?php endif; ?>
</div>
</div>
<!-- Territori Mai Assegnati -->
<?php if (count($never_assigned) > 0): ?>
<div class="card">
<div class="card-header">
<h2>Territori Mai Assegnati (<?php echo count($never_assigned); ?>)</h2>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Numero</th>
<th>Zona</th>
<th>Tipologia</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($never_assigned as $territory): ?>
<tr>
<td><strong><?php echo htmlspecialchars($territory['numero']); ?></strong></td>
<td><?php echo htmlspecialchars($territory['zona']); ?></td>
<td><?php echo htmlspecialchars($territory['tipologia']); ?></td>
<td>
<a href="assignments.php?action=assign&territory_id=<?php echo $territory['id']; ?>"
class="btn btn-sm btn-success">Assegna</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<?php include 'footer.php'; ?>

607
style.css Normal file
View File

@@ -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;
}
}

468
territories.php Normal file
View File

@@ -0,0 +1,468 @@
<?php
/**
* Gestione Territori
* Territory Manager
*/
require_once 'config.php';
require_once 'functions.php';
require_once 'db.php';
requireLogin();
$page_title = 'Gestione Territori';
$db = getDB();
$action = $_GET['action'] ?? 'list';
$territory_id = $_GET['id'] ?? null;
// Gestione delle azioni POST
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['action'])) {
switch ($_POST['action']) {
case 'add':
$numero = sanitize($_POST['numero']);
$zona = sanitize($_POST['zona']);
$tipologia = sanitize($_POST['tipologia']);
$note = sanitize($_POST['note'] ?? '');
// Gestione upload immagine
$image_path = 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) {
$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(
"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';
?>
<?php if ($action === 'list'): ?>
<div class="page-header">
<h1>Gestione Territori</h1>
<a href="?action=add" class="btn btn-primary">+ Nuovo Territorio</a>
</div>
<!-- Filtri di ricerca -->
<div class="card">
<div class="card-body">
<form method="GET" action="" class="filter-form">
<div class="form-row">
<div class="form-group">
<input type="text" name="search" placeholder="Cerca numero, zona o tipologia..."
value="<?php echo htmlspecialchars($search); ?>" class="form-control">
</div>
<div class="form-group">
<select name="zona" class="form-control">
<option value="">Tutte le zone</option>
<?php foreach ($zones as $z): ?>
<option value="<?php echo htmlspecialchars($z['zona']); ?>"
<?php echo $filter_zona === $z['zona'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($z['zona']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<select name="tipologia" class="form-control">
<option value="">Tutte le tipologie</option>
<?php foreach ($types as $t): ?>
<option value="<?php echo htmlspecialchars($t['tipologia']); ?>"
<?php echo $filter_tipologia === $t['tipologia'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($t['tipologia']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">Filtra</button>
<a href="territories.php" class="btn btn-secondary">Reset</a>
</div>
</form>
</div>
</div>
<!-- Tabella territori -->
<div class="card">
<div class="card-body">
<?php if (count($territories) > 0): ?>
<table class="table">
<thead>
<tr>
<th>Numero</th>
<th>Zona</th>
<th>Tipologia</th>
<th>Stato</th>
<th>Assegnato a</th>
<th>Giorni</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($territories as $t): ?>
<tr>
<td><strong><?php echo htmlspecialchars($t['numero']); ?></strong></td>
<td><?php echo htmlspecialchars($t['zona']); ?></td>
<td><?php echo htmlspecialchars($t['tipologia']); ?></td>
<td>
<?php if ($t['current_assignment_id']): ?>
<span class="badge badge-info">Assegnato</span>
<?php else: ?>
<span class="badge badge-success">Disponibile</span>
<?php endif; ?>
</td>
<td><?php echo $t['assigned_to'] ? htmlspecialchars($t['assigned_to']) : '-'; ?></td>
<td><?php echo $t['days_assigned'] ?? '-'; ?></td>
<td class="actions">
<a href="?action=view&id=<?php echo $t['id']; ?>" class="btn btn-sm btn-secondary">Dettagli</a>
<a href="?action=edit&id=<?php echo $t['id']; ?>" class="btn btn-sm btn-primary">Modifica</a>
<?php if (!$t['current_assignment_id']): ?>
<form method="POST" style="display:inline;" onsubmit="return confirm('Sei sicuro di voler eliminare questo territorio?');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?php echo $t['id']; ?>">
<button type="submit" class="btn btn-sm btn-danger">Elimina</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessun territorio trovato</p>
<?php endif; ?>
</div>
</div>
<?php elseif ($action === 'add' || $action === 'edit'): ?>
<div class="page-header">
<h1><?php echo $action === 'add' ? 'Nuovo Territorio' : 'Modifica Territorio'; ?></h1>
</div>
<div class="card">
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="<?php echo $action; ?>">
<?php if ($action === 'edit'): ?>
<input type="hidden" name="id" value="<?php echo $territory['id']; ?>">
<input type="hidden" name="current_image" value="<?php echo htmlspecialchars($territory['image_path'] ?? ''); ?>">
<?php endif; ?>
<div class="form-group">
<label for="numero">Numero Territorio *</label>
<input type="text" id="numero" name="numero" required
value="<?php echo $territory ? htmlspecialchars($territory['numero']) : ''; ?>" class="form-control">
</div>
<div class="form-group">
<label for="zona">Zona *</label>
<input type="text" id="zona" name="zona" required
value="<?php echo $territory ? htmlspecialchars($territory['zona']) : ''; ?>" class="form-control">
</div>
<div class="form-group">
<label for="tipologia">Tipologia *</label>
<input type="text" id="tipologia" name="tipologia" required
value="<?php echo $territory ? htmlspecialchars($territory['tipologia']) : ''; ?>" class="form-control">
<small class="form-help">Es: Residenziale, Commerciale, Rurale, ecc.</small>
</div>
<div class="form-group">
<label for="image">Piantina Territorio</label>
<?php if ($territory && $territory['image_path']): ?>
<div class="current-image">
<img src="<?php echo htmlspecialchars($territory['image_path']); ?>" alt="Piantina" style="max-width: 300px; margin-bottom: 10px;">
</div>
<?php endif; ?>
<input type="file" id="image" name="image" accept="image/*,.pdf" class="form-control">
<small class="form-help">Formati supportati: JPG, PNG, GIF, PDF. Dimensione massima: 5 MB</small>
</div>
<div class="form-group">
<label for="note">Note</label>
<textarea id="note" name="note" rows="4" class="form-control"><?php echo $territory ? htmlspecialchars($territory['note']) : ''; ?></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<?php echo $action === 'add' ? 'Aggiungi Territorio' : 'Salva Modifiche'; ?>
</button>
<a href="territories.php" class="btn btn-secondary">Annulla</a>
</div>
</form>
</div>
</div>
<?php elseif ($action === 'view' && $territory_id): ?>
<?php
$territory = $db->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]);
?>
<div class="page-header">
<h1>Dettagli Territorio: <?php echo htmlspecialchars($territory['numero']); ?></h1>
<div>
<a href="?action=edit&id=<?php echo $territory['id']; ?>" class="btn btn-primary">Modifica</a>
<a href="territories.php" class="btn btn-secondary">Torna alla Lista</a>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="detail-grid">
<div class="detail-item">
<strong>Numero:</strong> <?php echo htmlspecialchars($territory['numero']); ?>
</div>
<div class="detail-item">
<strong>Zona:</strong> <?php echo htmlspecialchars($territory['zona']); ?>
</div>
<div class="detail-item">
<strong>Tipologia:</strong> <?php echo htmlspecialchars($territory['tipologia']); ?>
</div>
</div>
<?php if ($territory['note']): ?>
<div class="detail-item">
<strong>Note:</strong>
<p><?php echo nl2br(htmlspecialchars($territory['note'])); ?></p>
</div>
<?php endif; ?>
<?php if ($territory['image_path']): ?>
<div class="detail-item">
<strong>Piantina:</strong><br>
<a href="<?php echo htmlspecialchars($territory['image_path']); ?>" target="_blank">
<img src="<?php echo htmlspecialchars($territory['image_path']); ?>"
alt="Piantina" style="max-width: 500px; margin-top: 10px; border: 1px solid #ddd;">
</a>
</div>
<?php endif; ?>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Storico Assegnazioni</h2>
</div>
<div class="card-body">
<?php if (count($history) > 0): ?>
<table class="table">
<thead>
<tr>
<th>Assegnato a</th>
<th>Data Assegnazione</th>
<th>Data Restituzione</th>
<th>Durata</th>
<th>Priorità</th>
</tr>
</thead>
<tbody>
<?php foreach ($history as $h): ?>
<tr>
<td><?php echo htmlspecialchars($h['assigned_to']); ?></td>
<td><?php echo formatDate($h['assigned_date']); ?></td>
<td>
<?php
if ($h['returned_date']) {
echo formatDate($h['returned_date']);
} else {
echo '<span class="badge badge-info">In corso</span>';
}
?>
</td>
<td><?php echo $h['days_duration']; ?> giorni</td>
<td>
<?php if ($h['is_priority']): ?>
<span class="badge badge-danger">Priorità</span>
<?php else: ?>
-
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="empty-state">Nessuna assegnazione storica</p>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php include 'footer.php'; ?>

12
uploads/README.md Normal file
View File

@@ -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]`

131
view_territory.php Normal file
View File

@@ -0,0 +1,131 @@
<?php
/**
* Visualizzazione Territorio tramite Link Temporaneo
* Territory Manager
*/
require_once 'config.php';
require_once 'db.php';
$token = $_GET['token'] ?? '';
$db = getDB();
if (empty($token)) {
die('Link non valido');
}
// Recupera l'assegnazione
$assignment = $db->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));
}
?>
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Territorio <?php echo htmlspecialchars($assignment['numero']); ?> - <?php echo APP_NAME; ?></title>
<link rel="stylesheet" href="style.css">
</head>
<body class="public-page">
<div class="container">
<?php if ($expired): ?>
<div class="card">
<div class="card-body">
<h1>Link Scaduto</h1>
<p class="alert alert-error">
Questo link è scaduto il <?php echo formatDate($assignment['link_expires_at']); ?>.
Contatta l'amministratore per ottenere un nuovo link.
</p>
</div>
</div>
<?php else: ?>
<div class="page-header">
<h1>Territorio: <?php echo htmlspecialchars($assignment['numero']); ?></h1>
<p class="subtitle">Visualizzazione Territorio</p>
</div>
<div class="card">
<div class="card-header">
<h2>Informazioni Territorio</h2>
</div>
<div class="card-body">
<div class="detail-grid">
<div class="detail-item">
<strong>Numero:</strong> <?php echo htmlspecialchars($assignment['numero']); ?>
</div>
<div class="detail-item">
<strong>Zona:</strong> <?php echo htmlspecialchars($assignment['zona']); ?>
</div>
<div class="detail-item">
<strong>Tipologia:</strong> <?php echo htmlspecialchars($assignment['tipologia']); ?>
</div>
<div class="detail-item">
<strong>Assegnato a:</strong> <?php echo htmlspecialchars($assignment['assigned_to']); ?>
</div>
<div class="detail-item">
<strong>Data Assegnazione:</strong> <?php echo formatDate($assignment['assigned_date']); ?>
</div>
</div>
<?php if ($assignment['territory_note']): ?>
<div class="detail-item" style="margin-top: 20px;">
<strong>Note:</strong>
<p><?php echo nl2br(htmlspecialchars($assignment['territory_note'])); ?></p>
</div>
<?php endif; ?>
<?php if ($assignment['image_path']): ?>
<div class="detail-item" style="margin-top: 20px;">
<strong>Piantina:</strong><br>
<a href="<?php echo htmlspecialchars($assignment['image_path']); ?>" target="_blank">
<img src="<?php echo htmlspecialchars($assignment['image_path']); ?>"
alt="Piantina Territorio"
style="max-width: 100%; margin-top: 10px; border: 1px solid #ddd;">
</a>
</div>
<?php endif; ?>
</div>
</div>
<div class="card">
<div class="card-body">
<p class="help-text">
<strong>Nota:</strong> Questo link è valido fino al
<?php echo date('d/m/Y H:i', strtotime($assignment['link_expires_at'])); ?>
</p>
</div>
</div>
<?php endif; ?>
<div class="footer" style="margin-top: 40px; text-align: center;">
<p>&copy; <?php echo date('Y'); ?> <?php echo APP_NAME; ?></p>
</div>
</div>
</body>
</html>