This commit is contained in:
fpicone
2025-12-06 18:42:49 +01:00
parent 30e1f9b36b
commit 61a177475a
11 changed files with 911 additions and 2 deletions

168
LOG_SYSTEM.md Normal file
View File

@@ -0,0 +1,168 @@
# Sistema di Logging - Territory Manager
## Panoramica
Il sistema di logging è stato implementato per tracciare tutte le azioni eseguite dagli utenti all'interno dell'applicazione Territory Manager.
## Funzionalità Implementate
### 1. Database
- **Tabella `activity_logs`**: Memorizza tutti i log delle attività
- `user_id`: ID dell'utente che ha eseguito l'azione
- `username`: Nome utente per riferimento rapido
- `action_type`: Tipo di azione (login, logout, create, update, delete, assign, return, export)
- `action_description`: Descrizione dettagliata dell'azione
- `entity_type`: Tipo di entità coinvolta (territory, assignment, user, config, auth)
- `entity_id`: ID dell'entità coinvolta
- `ip_address`: Indirizzo IP dell'utente
- `user_agent`: User agent del browser
- `created_at`: Timestamp dell'azione
### 2. Funzioni di Logging (functions.php)
#### `logActivity($action_type, $action_description, $entity_type, $entity_id)`
Registra un'attività nel log. Cattura automaticamente:
- Utente corrente dalla sessione
- Indirizzo IP
- User Agent del browser
#### `getActivityLogs($filters, $page, $per_page)`
Recupera i log con filtri e paginazione. Supporta filtri per:
- Utente
- Tipo di azione
- Tipo di entità
- Intervallo di date
- Ricerca testuale
### 3. Pagina Visualizzazione Log (logs.php)
**Accesso**: Solo amministratori
Funzionalità:
- Visualizzazione log con tabella paginata
- Filtri multipli:
- Ricerca testuale
- Utente
- Tipo di azione
- Tipo di entità
- Data da/a
- Paginazione (50 log per pagina)
- Badge colorati per tipo di azione
- Link per esportazione PDF
### 4. Esportazione PDF (export_logs_pdf.php)
**Accesso**: Solo amministratori
Funzionalità:
- Esporta tutti i log filtrati in formato PDF
- Include gli stessi filtri della pagina di visualizzazione
- Genera report con:
- Elenco completo dei log
- Riepilogo per tipo di azione
- Riepilogo per utente
- Timestamp di generazione
- Informazioni sui filtri applicati
### 5. Azioni Tracciate
#### Autenticazione
- `login`: Accesso al sistema
- `logout`: Uscita dal sistema
#### Territori
- `create`: Creazione nuovo territorio
- `update`: Modifica territorio esistente
- `delete`: Eliminazione territorio
#### Assegnazioni
- `assign`: Assegnazione territorio a un proclamatore
- `return`: Riconsegna territorio
#### Utenti
- `create`: Creazione nuovo utente
- `update`: Modifica password
- `delete`: Eliminazione utente
#### Configurazioni
- `update`: Modifica configurazioni di sistema
#### Log
- `export`: Esportazione log in PDF
## Integrazione
Il logging è stato integrato in tutte le operazioni principali:
- `login.php`: Log di accesso
- `logout.php`: Log di uscita
- `territories.php`: Log di creazione, modifica ed eliminazione territori
- `assignments.php`: Log di assegnazione e riconsegna
- `settings.php`: Log di modifiche configurazioni e gestione utenti
- `export_logs_pdf.php`: Log di esportazione
## Menu di Navigazione
Il link "Log" è stato aggiunto nel menu principale, visibile solo agli amministratori.
## Utilizzo
### Visualizzare i Log
1. Accedere come amministratore
2. Cliccare su "Log" nel menu
3. Utilizzare i filtri per trovare log specifici
### Esportare i Log
1. Dalla pagina Log, applicare eventuali filtri
2. Cliccare su "Esporta PDF"
3. Si aprirà una nuova finestra con il report
4. Utilizzare il pulsante "Stampa / Salva come PDF" del browser
## Note Tecniche
- Il sistema cattura automaticamente IP e User Agent per tracciabilità
- I log sono legati agli utenti tramite foreign key (cascade on delete)
- La paginazione limita il carico sul database
- L'export PDF è ottimizzato per la stampa (orientamento landscape)
- I filtri sono persistenti durante la navigazione
## Sicurezza
- Solo gli amministratori possono accedere ai log
- I log non possono essere modificati o eliminati dall'interfaccia
- Tutte le operazioni sensibili vengono registrate
- Gli IP vengono tracciati per audit trail
## Manutenzione
Per pulire vecchi log (da eseguire manualmente nel database):
```sql
-- Elimina log più vecchi di 1 anno
DELETE FROM activity_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);
```
## Database Migration
Per applicare le modifiche al database esistente:
```sql
-- Eseguire lo script SQL aggiornato
SOURCE database.sql;
```
O solo la parte relativa ai log:
```sql
CREATE TABLE IF NOT EXISTS activity_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
username VARCHAR(50) NOT NULL,
action_type VARCHAR(50) NOT NULL,
action_description TEXT NOT NULL,
entity_type VARCHAR(50),
entity_id INT,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user (user_id),
INDEX idx_action_type (action_type),
INDEX idx_created_at (created_at),
INDEX idx_entity (entity_type, entity_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

View File

@@ -52,6 +52,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($result) {
$assignment_id = $db->getConnection()->lastInsertId();
$territory = $db->fetchOne("SELECT numero, zona FROM territories WHERE id = ?", [$territory_id]);
logActivity('assign', "Assegnato territorio {$territory['numero']} - {$territory['zona']} a $assigned_to", 'assignment', $assignment_id);
setFlashMessage('Territorio assegnato con successo', 'success');
header("Location: assignments.php?action=view&id=$assignment_id");
} else {
@@ -65,12 +67,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$territory_id = (int)$_POST['territory_id'];
$returned_date = $_POST['returned_date'];
// Recupera info assegnazione
$assignment = $db->fetchOne(
"SELECT a.id, a.assigned_to, t.numero, t.zona
FROM assignments a
JOIN territories t ON a.territory_id = t.id
WHERE a.territory_id = ? AND a.returned_date IS NULL",
[$territory_id]
);
$result = $db->query(
"UPDATE assignments SET returned_date = ? WHERE territory_id = ? AND returned_date IS NULL",
[$returned_date, $territory_id]
);
if ($result) {
if ($result && $assignment) {
logActivity('return', "Riconsegnato territorio {$assignment['numero']} - {$assignment['zona']} da {$assignment['assigned_to']}", 'assignment', $assignment['id']);
setFlashMessage('Territorio riconsegnato con successo', 'success');
} else {
setFlashMessage('Errore durante la riconsegna', 'error');

View File

@@ -100,3 +100,22 @@ WHERE t.id NOT IN (
)
GROUP BY t.id, t.numero, t.zona, t.tipologia, t.image_path
ORDER BY last_returned_date ASC NULLS FIRST;
-- Tabella log attività
CREATE TABLE IF NOT EXISTS activity_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
username VARCHAR(50) NOT NULL,
action_type VARCHAR(50) NOT NULL,
action_description TEXT NOT NULL,
entity_type VARCHAR(50),
entity_id INT,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user (user_id),
INDEX idx_action_type (action_type),
INDEX idx_created_at (created_at),
INDEX idx_entity (entity_type, entity_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

284
export_logs_pdf.php Normal file
View File

@@ -0,0 +1,284 @@
<?php
/**
* Export Log PDF
* Territory Manager
* Esporta i log delle attività in formato PDF
*/
require_once 'config.php';
require_once 'functions.php';
require_once 'db.php';
requireAdmin(); // Solo admin possono esportare i log
$db = getDB();
// Applica gli stessi filtri della pagina logs.php
$filters = [];
if (!empty($_GET['user_id'])) {
$filters['user_id'] = intval($_GET['user_id']);
}
if (!empty($_GET['action_type'])) {
$filters['action_type'] = $_GET['action_type'];
}
if (!empty($_GET['entity_type'])) {
$filters['entity_type'] = $_GET['entity_type'];
}
if (!empty($_GET['date_from'])) {
$filters['date_from'] = $_GET['date_from'];
}
if (!empty($_GET['date_to'])) {
$filters['date_to'] = $_GET['date_to'];
}
if (!empty($_GET['search'])) {
$filters['search'] = $_GET['search'];
}
// Ottieni tutti i log (senza limite di paginazione per l'export)
$where = [];
$params = [];
if (!empty($filters['user_id'])) {
$where[] = "user_id = ?";
$params[] = $filters['user_id'];
}
if (!empty($filters['action_type'])) {
$where[] = "action_type = ?";
$params[] = $filters['action_type'];
}
if (!empty($filters['entity_type'])) {
$where[] = "entity_type = ?";
$params[] = $filters['entity_type'];
}
if (!empty($filters['date_from'])) {
$where[] = "DATE(created_at) >= ?";
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where[] = "DATE(created_at) <= ?";
$params[] = $filters['date_to'];
}
if (!empty($filters['search'])) {
$where[] = "(action_description LIKE ? OR username LIKE ?)";
$search_term = '%' . $filters['search'] . '%';
$params[] = $search_term;
$params[] = $search_term;
}
$where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
$logs = $db->fetchAll(
"SELECT * FROM activity_logs
$where_clause
ORDER BY created_at DESC",
$params
);
// Classe semplice per generare PDF (HTML per stampa)
class LogPDF {
private $content = '';
public function addTitle($title) {
$this->content .= "<h1 style='color: #2c3e50; margin-bottom: 20px; text-align: center;'>$title</h1>";
}
public function addSubtitle($subtitle) {
$this->content .= "<h2 style='color: #34495e; margin: 20px 0 10px 0; font-size: 16px;'>$subtitle</h2>";
}
public function addText($text) {
$this->content .= "<p style='margin: 5px 0; color: #555;'>$text</p>";
}
public function addTable($headers, $rows) {
$this->content .= "<table style='width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 11px;'>";
$this->content .= "<thead><tr style='background-color: #34495e; color: white;'>";
foreach ($headers as $header) {
$this->content .= "<th style='padding: 8px 6px; border: 1px solid #ddd; text-align: left; font-weight: bold;'>$header</th>";
}
$this->content .= "</tr></thead><tbody>";
$odd = true;
foreach ($rows as $row) {
$bgColor = $odd ? '#ffffff' : '#f8f9fa';
$this->content .= "<tr style='background-color: $bgColor;'>";
foreach ($row as $cell) {
$this->content .= "<td style='padding: 6px; border: 1px solid #ddd;'>$cell</td>";
}
$this->content .= "</tr>";
$odd = !$odd;
}
$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'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>$filename</title>
<style>
* { box-sizing: border-box; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
padding: 20px;
margin: 0;
background: white;
}
@media print {
body { padding: 10px; }
.no-print { display: none; }
table { page-break-inside: auto; }
tr { page-break-inside: avoid; page-break-after: auto; }
thead { display: table-header-group; }
}
@page {
size: A4 landscape;
margin: 1cm;
}
</style>
</head>
<body>";
echo "<div class='no-print' style='margin-bottom: 20px; text-align: center;'>";
echo "<button onclick='window.print()' style='padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px;'>Stampa / Salva come PDF</button>";
echo "<button onclick='window.close()' style='padding: 10px 20px; background: #6c757d; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; margin-left: 10px;'>Chiudi</button>";
echo "</div>";
echo $this->content;
echo "<div style='margin-top: 40px; text-align: center; color: #7f8c8d; font-size: 11px;'>";
echo "<p>Generato il " . date('d/m/Y H:i') . " da " . htmlspecialchars($_SESSION['username']) . " - " . APP_NAME . "</p>";
echo "</div>";
echo "</body></html>";
}
}
$pdf = new LogPDF();
// Titolo
$pdf->addTitle('Log Attività Sistema');
// Informazioni sul report
$info_text = "Report generato il " . date('d/m/Y H:i');
if (!empty($filters)) {
$info_text .= " - Filtri applicati: ";
$filter_parts = [];
if (!empty($filters['user_id'])) {
$user = $db->fetchOne("SELECT username FROM users WHERE id = ?", [$filters['user_id']]);
$filter_parts[] = "Utente: " . htmlspecialchars($user['username']);
}
if (!empty($filters['action_type'])) {
$filter_parts[] = "Azione: " . htmlspecialchars($filters['action_type']);
}
if (!empty($filters['entity_type'])) {
$filter_parts[] = "Entità: " . htmlspecialchars($filters['entity_type']);
}
if (!empty($filters['date_from'])) {
$filter_parts[] = "Dal: " . formatDate($filters['date_from']);
}
if (!empty($filters['date_to'])) {
$filter_parts[] = "Al: " . formatDate($filters['date_to']);
}
if (!empty($filters['search'])) {
$filter_parts[] = "Ricerca: " . htmlspecialchars($filters['search']);
}
$info_text .= implode(', ', $filter_parts);
}
$pdf->addText($info_text);
$pdf->addText("Totale log: " . count($logs));
// Prepara i dati per la tabella
$headers = ['Data/Ora', 'Utente', 'Azione', 'Descrizione', 'Entità', 'IP'];
$rows = [];
foreach ($logs as $log) {
$action_badge = strtoupper(htmlspecialchars($log['action_type']));
$entity_info = '-';
if ($log['entity_type']) {
$entity_info = htmlspecialchars($log['entity_type']);
if ($log['entity_id']) {
$entity_info .= ' #' . $log['entity_id'];
}
}
$rows[] = [
formatDateTime($log['created_at']),
htmlspecialchars($log['username']),
$action_badge,
htmlspecialchars($log['action_description']),
$entity_info,
htmlspecialchars($log['ip_address'])
];
}
$pdf->addTable($headers, $rows);
// Riepilogo per tipo di azione
$action_summary = $db->fetchAll(
"SELECT action_type, COUNT(*) as count
FROM activity_logs
$where_clause
GROUP BY action_type
ORDER BY count DESC",
$params
);
if (!empty($action_summary)) {
$pdf->addSubtitle('Riepilogo per Tipo di Azione');
$summary_headers = ['Tipo Azione', 'Numero Occorrenze'];
$summary_rows = [];
foreach ($action_summary as $summary) {
$summary_rows[] = [
strtoupper(htmlspecialchars($summary['action_type'])),
number_format($summary['count'])
];
}
$pdf->addTable($summary_headers, $summary_rows);
}
// Riepilogo per utente
$user_summary = $db->fetchAll(
"SELECT username, COUNT(*) as count
FROM activity_logs
$where_clause
GROUP BY username
ORDER BY count DESC",
$params
);
if (!empty($user_summary)) {
$pdf->addSubtitle('Riepilogo per Utente');
$user_headers = ['Utente', 'Numero Azioni'];
$user_rows = [];
foreach ($user_summary as $summary) {
$user_rows[] = [
htmlspecialchars($summary['username']),
number_format($summary['count'])
];
}
$pdf->addTable($user_headers, $user_rows);
}
// Output del PDF
$filename = 'log_attivita_' . date('Y-m-d_H-i-s');
$pdf->output($filename);
// Log dell'esportazione
logActivity('export', 'Esportazione log attività in PDF', 'logs', null);

View File

@@ -110,3 +110,110 @@ function formatDateTime($datetime) {
if (empty($datetime)) return '-';
return date('d/m/Y H:i', strtotime($datetime));
}
/**
* Registra un'attività nel log
*
* @param string $action_type Tipo di azione (es: 'login', 'create', 'update', 'delete', 'assign', 'return')
* @param string $action_description Descrizione dell'azione
* @param string $entity_type Tipo di entità (es: 'territory', 'assignment', 'user', 'config')
* @param int $entity_id ID dell'entità coinvolta
*/
function logActivity($action_type, $action_description, $entity_type = null, $entity_id = null) {
if (!isLoggedIn()) {
return false;
}
$db = getDB();
// Ottieni informazioni sull'utente e sulla richiesta
$user_id = $_SESSION['user_id'];
$username = $_SESSION['username'];
$ip_address = $_SERVER['REMOTE_ADDR'] ?? null;
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? null;
try {
$db->execute(
"INSERT INTO activity_logs
(user_id, username, action_type, action_description, entity_type, entity_id, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[$user_id, $username, $action_type, $action_description, $entity_type, $entity_id, $ip_address, $user_agent]
);
return true;
} catch (Exception $e) {
error_log("Errore nel log dell'attività: " . $e->getMessage());
return false;
}
}
/**
* Ottieni i log delle attività con filtri e paginazione
*
* @param array $filters Array di filtri (user_id, action_type, entity_type, date_from, date_to)
* @param int $page Numero pagina
* @param int $per_page Elementi per pagina
* @return array Array con 'logs' e 'total'
*/
function getActivityLogs($filters = [], $page = 1, $per_page = 50) {
$db = getDB();
$where = [];
$params = [];
if (!empty($filters['user_id'])) {
$where[] = "user_id = ?";
$params[] = $filters['user_id'];
}
if (!empty($filters['action_type'])) {
$where[] = "action_type = ?";
$params[] = $filters['action_type'];
}
if (!empty($filters['entity_type'])) {
$where[] = "entity_type = ?";
$params[] = $filters['entity_type'];
}
if (!empty($filters['date_from'])) {
$where[] = "DATE(created_at) >= ?";
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where[] = "DATE(created_at) <= ?";
$params[] = $filters['date_to'];
}
if (!empty($filters['search'])) {
$where[] = "(action_description LIKE ? OR username LIKE ?)";
$search_term = '%' . $filters['search'] . '%';
$params[] = $search_term;
$params[] = $search_term;
}
$where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
// Conta totale
$total = $db->fetchOne(
"SELECT COUNT(*) as count FROM activity_logs $where_clause",
$params
)['count'];
// Ottieni log con paginazione
$offset = ($page - 1) * $per_page;
$logs = $db->fetchAll(
"SELECT * FROM activity_logs
$where_clause
ORDER BY created_at DESC
LIMIT ? OFFSET ?",
array_merge($params, [$per_page, $offset])
);
return [
'logs' => $logs,
'total' => $total,
'pages' => ceil($total / $per_page),
'current_page' => $page
];
}

View File

@@ -26,6 +26,7 @@ $current_page = basename($_SERVER['PHP_SELF'], '.php');
<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="logs.php" class="<?php echo $current_page === 'logs' ? 'active' : ''; ?>">Log</a></li>
<li><a href="settings.php" class="<?php echo $current_page === 'settings' ? 'active' : ''; ?>">Impostazioni</a></li>
<?php endif; ?>
</ul>

View File

@@ -23,6 +23,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = 'Inserire username e password';
} else {
if (login($username, $password)) {
logActivity('login', 'Accesso effettuato al sistema', 'auth', null);
header('Location: index.php');
exit;
} else {

View File

@@ -7,6 +7,7 @@
require_once 'config.php';
require_once 'functions.php';
logActivity('logout', 'Uscita dal sistema', 'auth', null);
logout();
header('Location: login.php');
exit;

301
logs.php Normal file
View File

@@ -0,0 +1,301 @@
<?php
/**
* Pagina Visualizzazione Log Attività
* Territory Manager
*/
require_once 'config.php';
require_once 'functions.php';
require_once 'db.php';
requireAdmin(); // Solo gli admin possono visualizzare i log
$page_title = 'Log Attività';
$db = getDB();
// Parametri per filtri e paginazione
$filters = [];
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$per_page = 50;
// Applica filtri
if (!empty($_GET['user_id'])) {
$filters['user_id'] = intval($_GET['user_id']);
}
if (!empty($_GET['action_type'])) {
$filters['action_type'] = $_GET['action_type'];
}
if (!empty($_GET['entity_type'])) {
$filters['entity_type'] = $_GET['entity_type'];
}
if (!empty($_GET['date_from'])) {
$filters['date_from'] = $_GET['date_from'];
}
if (!empty($_GET['date_to'])) {
$filters['date_to'] = $_GET['date_to'];
}
if (!empty($_GET['search'])) {
$filters['search'] = $_GET['search'];
}
// Ottieni log
$result = getActivityLogs($filters, $page, $per_page);
$logs = $result['logs'];
$total = $result['total'];
$total_pages = $result['pages'];
// Ottieni lista utenti per filtro
$users = $db->fetchAll("SELECT id, username FROM users ORDER BY username");
// Ottieni tipi di azione unici
$action_types = $db->fetchAll("SELECT DISTINCT action_type FROM activity_logs ORDER BY action_type");
// Ottieni tipi di entità unici
$entity_types = $db->fetchAll("SELECT DISTINCT entity_type FROM activity_logs WHERE entity_type IS NOT NULL ORDER BY entity_type");
include 'header.php';
?>
<div class="page-header">
<h1>
<svg width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 2.5a2.5 2.5 0 0 1 5 0 2.5 2.5 0 0 1 5 0v.006c0 .07 0 .27-.038.494H15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 14.5V7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2.038A2.968 2.968 0 0 1 3 2.506V2.5zm1.068.5H7v-.5a1.5 1.5 0 1 0-3 0c0 .085.002.274.045.43a.522.522 0 0 0 .023.07zM9 3h2.932a.56.56 0 0 0 .023-.07c.043-.156.045-.345.045-.43a1.5 1.5 0 0 0-3 0V3zM1 4v2h6V4H1zm8 0v2h6V4H9zm5 3H9v8h4.5a.5.5 0 0 0 .5-.5V7zm-7 8V7H2v7.5a.5.5 0 0 0 .5.5H7z"/>
</svg>
<?php echo $page_title; ?>
</h1>
<div class="page-actions">
<a href="export_logs_pdf.php?<?php echo http_build_query($filters); ?>" class="btn btn-primary" target="_blank">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
Esporta PDF
</a>
</div>
</div>
<!-- Filtri -->
<div class="card">
<div class="card-header">
<h3>Filtri</h3>
</div>
<div class="card-body">
<form method="GET" action="logs.php" class="filter-form">
<div class="form-row">
<div class="form-group">
<label for="search">Cerca</label>
<input type="text" id="search" name="search" class="form-control"
placeholder="Cerca nella descrizione o utente..."
value="<?php echo htmlspecialchars($_GET['search'] ?? ''); ?>">
</div>
<div class="form-group">
<label for="user_id">Utente</label>
<select id="user_id" name="user_id" class="form-control">
<option value="">Tutti gli utenti</option>
<?php foreach ($users as $user): ?>
<option value="<?php echo $user['id']; ?>"
<?php echo (isset($_GET['user_id']) && $_GET['user_id'] == $user['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($user['username']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="action_type">Tipo Azione</label>
<select id="action_type" name="action_type" class="form-control">
<option value="">Tutte le azioni</option>
<?php foreach ($action_types as $type): ?>
<option value="<?php echo htmlspecialchars($type['action_type']); ?>"
<?php echo (isset($_GET['action_type']) && $_GET['action_type'] == $type['action_type']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($type['action_type']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="entity_type">Tipo Entità</label>
<select id="entity_type" name="entity_type" class="form-control">
<option value="">Tutte le entità</option>
<?php foreach ($entity_types as $type): ?>
<option value="<?php echo htmlspecialchars($type['entity_type']); ?>"
<?php echo (isset($_GET['entity_type']) && $_GET['entity_type'] == $type['entity_type']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($type['entity_type']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="date_from">Data Da</label>
<input type="date" id="date_from" name="date_from" class="form-control"
value="<?php echo htmlspecialchars($_GET['date_from'] ?? ''); ?>">
</div>
<div class="form-group">
<label for="date_to">Data A</label>
<input type="date" id="date_to" name="date_to" class="form-control"
value="<?php echo htmlspecialchars($_GET['date_to'] ?? ''); ?>">
</div>
<div class="form-group" style="align-self: flex-end;">
<button type="submit" class="btn btn-primary">Filtra</button>
<a href="logs.php" class="btn btn-secondary">Reset</a>
</div>
</div>
</form>
</div>
</div>
<!-- Risultati -->
<div class="card">
<div class="card-header">
<h3>Risultati (<?php echo number_format($total); ?> log)</h3>
</div>
<div class="card-body">
<?php if (empty($logs)): ?>
<p class="text-muted">Nessun log trovato con i filtri selezionati.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Data/Ora</th>
<th>Utente</th>
<th>Azione</th>
<th>Descrizione</th>
<th>Entità</th>
<th>IP</th>
</tr>
</thead>
<tbody>
<?php foreach ($logs as $log): ?>
<tr>
<td><?php echo formatDateTime($log['created_at']); ?></td>
<td>
<strong><?php echo htmlspecialchars($log['username']); ?></strong>
</td>
<td>
<span class="badge badge-<?php
echo match($log['action_type']) {
'login', 'create' => 'success',
'update', 'assign' => 'info',
'delete', 'logout' => 'danger',
'return' => 'warning',
default => 'secondary'
};
?>">
<?php echo htmlspecialchars($log['action_type']); ?>
</span>
</td>
<td><?php echo htmlspecialchars($log['action_description']); ?></td>
<td>
<?php if ($log['entity_type']): ?>
<small class="text-muted">
<?php echo htmlspecialchars($log['entity_type']); ?>
<?php if ($log['entity_id']): ?>
#<?php echo $log['entity_id']; ?>
<?php endif; ?>
</small>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<small class="text-muted"><?php echo htmlspecialchars($log['ip_address']); ?></small>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Paginazione -->
<?php if ($total_pages > 1): ?>
<div class="pagination">
<?php
$query_params = $_GET;
unset($query_params['page']);
$base_query = !empty($query_params) ? '&' . http_build_query($query_params) : '';
?>
<?php if ($page > 1): ?>
<a href="?page=1<?php echo $base_query; ?>" class="btn btn-sm btn-secondary">Prima</a>
<a href="?page=<?php echo $page - 1; ?><?php echo $base_query; ?>" class="btn btn-sm btn-secondary">Precedente</a>
<?php endif; ?>
<span class="pagination-info">
Pagina <?php echo $page; ?> di <?php echo $total_pages; ?>
</span>
<?php if ($page < $total_pages): ?>
<a href="?page=<?php echo $page + 1; ?><?php echo $base_query; ?>" class="btn btn-sm btn-secondary">Successiva</a>
<a href="?page=<?php echo $total_pages; ?><?php echo $base_query; ?>" class="btn btn-sm btn-secondary">Ultima</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<style>
.filter-form .form-row {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.filter-form .form-group {
flex: 1;
min-width: 200px;
}
.badge {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-success { background-color: #28a745; color: white; }
.badge-info { background-color: #17a2b8; color: white; }
.badge-warning { background-color: #ffc107; color: black; }
.badge-danger { background-color: #dc3545; color: white; }
.badge-secondary { background-color: #6c757d; color: white; }
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #dee2e6;
}
.pagination-info {
margin: 0 1rem;
font-weight: 500;
}
.table-responsive {
overflow-x: auto;
}
.table tbody tr:hover {
background-color: #f8f9fa;
}
</style>
<?php include 'footer.php'; ?>

View File

@@ -28,6 +28,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db->updateConfig('warning_days_priority', $warning_days_priority);
$db->updateConfig('warning_days_return', $warning_days_return);
logActivity('update', 'Aggiornate configurazioni di sistema', 'config', null);
setFlashMessage('Configurazioni salvate con successo', 'success');
header('Location: settings.php');
exit;
@@ -50,6 +51,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} else {
$hashed = password_hash($new_password, PASSWORD_DEFAULT);
$db->query("UPDATE users SET password = ? WHERE id = ?", [$hashed, $user['id']]);
logActivity('update', 'Modificata la propria password', 'user', $user['id']);
setFlashMessage('Password modificata con successo', 'success');
}
@@ -73,6 +75,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
);
if ($result) {
$new_user_id = $db->getConnection()->lastInsertId();
$role = $is_admin ? 'amministratore' : 'utente';
logActivity('create', "Creato nuovo utente '$username' con ruolo $role", 'user', $new_user_id);
setFlashMessage('Utente aggiunto con successo', 'success');
} else {
setFlashMessage('Errore: username già esistente', 'error');
@@ -90,7 +95,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($user_id == $_SESSION['user_id']) {
setFlashMessage('Non puoi eliminare il tuo account', 'error');
} else {
$user_to_delete = $db->fetchOne("SELECT username FROM users WHERE id = ?", [$user_id]);
$db->query("DELETE FROM users WHERE id = ?", [$user_id]);
if ($user_to_delete) {
logActivity('delete', "Eliminato utente '{$user_to_delete['username']}'", 'user', $user_id);
}
setFlashMessage('Utente eliminato con successo', 'success');
}

View File

@@ -52,6 +52,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
);
if ($result) {
$territory_id = $db->getConnection()->lastInsertId();
logActivity('create', "Creato territorio $numero - $zona", 'territory', $territory_id);
setFlashMessage('Territorio aggiunto con successo', 'success');
} else {
setFlashMessage('Errore durante l\'aggiunta del territorio', 'error');
@@ -99,6 +101,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
);
if ($result) {
logActivity('update', "Modificato territorio $numero - $zona", 'territory', $id);
setFlashMessage('Territorio modificato con successo', 'success');
} else {
setFlashMessage('Errore durante la modifica del territorio', 'error');
@@ -112,7 +115,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)$_POST['id'];
// Recupera l'immagine per eliminarla
$territory = $db->fetchOne("SELECT image_path FROM territories WHERE id = ?", [$id]);
$territory = $db->fetchOne("SELECT numero, zona, image_path FROM territories WHERE id = ?", [$id]);
if ($territory && $territory['image_path']) {
$file_path = BASE_PATH . $territory['image_path'];
if (file_exists($file_path)) {
@@ -123,6 +126,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = $db->query("DELETE FROM territories WHERE id = ?", [$id]);
if ($result) {
if ($territory) {
logActivity('delete', "Eliminato territorio {$territory['numero']} - {$territory['zona']}", 'territory', $id);
}
setFlashMessage('Territorio eliminato con successo', 'success');
} else {
setFlashMessage('Errore durante l\'eliminazione del territorio', 'error');