fix log
This commit is contained in:
168
LOG_SYSTEM.md
Normal file
168
LOG_SYSTEM.md
Normal 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;
|
||||
```
|
||||
@@ -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');
|
||||
|
||||
19
database.sql
19
database.sql
@@ -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
284
export_logs_pdf.php
Normal 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);
|
||||
107
functions.php
107
functions.php
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
301
logs.php
Normal 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'; ?>
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user