deadline feature
This commit is contained in:
parent
174fa73c2c
commit
d7b6a58407
@ -7,7 +7,7 @@ ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
// This should be equal to: PATH_TO_VANGUARD_FOLDER/extra/auth.php
|
||||
include('../../extra/auth.php');
|
||||
include(__DIR__ . '/../../../extra/auth.php');
|
||||
//require_once __DIR__ . '/extra/auth.php';
|
||||
|
||||
// Here we just check if user is not
|
||||
|
||||
@ -59,6 +59,22 @@
|
||||
|
||||
|
||||
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon"><i class="bx bx-calendar-check"></i>
|
||||
</div>
|
||||
<div class="menu-title">Scadenzario</div>
|
||||
</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="scadenzario/index.php"><i class='bx bx-radio-circle'></i>Lista Scadenze</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="scadenzario/calendar.php"><i class='bx bx-radio-circle'></i>Calendario</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="menu-label">Others</li>
|
||||
|
||||
|
||||
|
||||
@ -381,7 +381,7 @@
|
||||
<div>Magazzino</div>
|
||||
|
||||
</button>
|
||||
<button class="dash-btn btn-scadenziario" onclick="location.href='activities_deadlines.php'">
|
||||
<button class="dash-btn btn-scadenziario" onclick="location.href='scadenzario/index.php'">
|
||||
<div class="dash-icon">⏰</div>
|
||||
<div>Scadenziario</div>
|
||||
</button>
|
||||
|
||||
75
public/userarea/scadenzario/INSTALLATION.md
Normal file
75
public/userarea/scadenzario/INSTALLATION.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Installation — Scadenzario
|
||||
|
||||
## 1. Database
|
||||
|
||||
Run the schema script:
|
||||
|
||||
```bash
|
||||
mysql -u <user> -p <database> < public/userarea/scadenzario/sql/create_tables.sql
|
||||
```
|
||||
|
||||
This creates 5 tables:
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `scad_deadlines` | Main deadline records |
|
||||
| `scad_deadline_employee` | M2M assignment of individual employees |
|
||||
| `scad_deadline_attachments` | File attachments |
|
||||
| `scad_deadline_histories` | Audit log (created/updated/completed/...) |
|
||||
| `scad_deadline_notifications` | Sent-notification log (deduplication) |
|
||||
|
||||
Departments are stored as a comma-separated string in `scad_deadlines.departments` (matching `employees.department` values). No separate `departments` table.
|
||||
|
||||
## 2. Filesystem permissions
|
||||
|
||||
The `attachments/` folder must be writable by the web server:
|
||||
|
||||
```bash
|
||||
chmod 755 public/userarea/scadenzario/attachments
|
||||
chown www-data:www-data public/userarea/scadenzario/attachments
|
||||
```
|
||||
|
||||
The included `.htaccess` denies direct web access. Files are served only through the auth-protected `ajax/download_attachment.php` endpoint.
|
||||
|
||||
## 3. SMTP configuration
|
||||
|
||||
Email notifications use the project-wide SMTP settings in `.env`:
|
||||
|
||||
```env
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=smtp.example.com
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=your_user
|
||||
MAIL_PASSWORD=your_password
|
||||
MAIL_ENCRYPTION=ssl
|
||||
MAIL_FROM_ADDRESS=scadenzario@your-domain.com
|
||||
MAIL_FROM_NAME="Scadenzario"
|
||||
APP_URL=https://your-domain.com
|
||||
```
|
||||
|
||||
## 4. Cron schedule
|
||||
|
||||
Add to the system crontab (run as the web user):
|
||||
|
||||
```cron
|
||||
0 7 * * * php /var/www/html/public/userarea/scadenzario/cron/send_notifications.php >> /var/log/scadenzario.log 2>&1
|
||||
```
|
||||
|
||||
This sends notifications daily at 07:00 for:
|
||||
|
||||
- **Approaching** — `due_date <= today + notification_days` (per-deadline lead time)
|
||||
- **Overdue** — `due_date < today`
|
||||
|
||||
Completed deadlines are skipped. Recipients without an `auth_user_id` are silently skipped.
|
||||
|
||||
## 5. Linking employees to auth users
|
||||
|
||||
For an employee to receive email notifications:
|
||||
|
||||
1. The corresponding `auth_users` row must exist with a valid `email`.
|
||||
2. Set `employees.auth_user_id` to that user ID:
|
||||
```sql
|
||||
UPDATE employees SET auth_user_id = <user_id> WHERE id = <employee_id>;
|
||||
```
|
||||
|
||||
Employees without `auth_user_id` are silently skipped by the cron.
|
||||
18
public/userarea/scadenzario/ajax/auth_check.php
Normal file
18
public/userarea/scadenzario/ajax/auth_check.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Auth check for AJAX endpoints.
|
||||
* Include this at the top of every ajax handler.
|
||||
* Sets $currentUserId from session or returns 401 JSON.
|
||||
*/
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (empty($_SESSION['iduserlogin'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'message' => 'Non autorizzato. Effettua il login.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$currentUserId = (int)$_SESSION['iduserlogin'];
|
||||
107
public/userarea/scadenzario/ajax/complete_deadline.php
Normal file
107
public/userarea/scadenzario/ajax/complete_deadline.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadlines WHERE id = ? AND status = 'active'");
|
||||
$stmt->execute([$id]);
|
||||
$deadline = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$deadline) {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata o già completata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Mark as completed
|
||||
$pdo->prepare("UPDATE scad_deadlines SET status = 'completed', completed_at = NOW(), completed_by = ? WHERE id = ?")
|
||||
->execute([$currentUserId, $id]);
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action) VALUES (?, ?, 'completed')")
|
||||
->execute([$id, $currentUserId]);
|
||||
|
||||
$newId = null;
|
||||
|
||||
// If recurring, create next deadline
|
||||
if ($deadline['recurrence_type'] !== 'once') {
|
||||
$dueDate = new DateTime($deadline['due_date']);
|
||||
$checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null;
|
||||
|
||||
switch ($deadline['recurrence_type']) {
|
||||
case 'monthly': $interval = new DateInterval('P1M'); break;
|
||||
case 'quarterly': $interval = new DateInterval('P3M'); break;
|
||||
case 'semiannual': $interval = new DateInterval('P6M'); break;
|
||||
case 'annual': $interval = new DateInterval('P1Y'); break;
|
||||
case 'biennial': $interval = new DateInterval('P2Y'); break;
|
||||
case 'triennial': $interval = new DateInterval('P3Y'); break;
|
||||
case 'quinquennial': $interval = new DateInterval('P5Y'); break;
|
||||
default: $interval = null;
|
||||
}
|
||||
|
||||
if ($interval) {
|
||||
$dueDate->add($interval);
|
||||
if ($checkDate) $checkDate->add($interval);
|
||||
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO scad_deadlines
|
||||
(category, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$ins->execute([
|
||||
$deadline['category'], $deadline['topic'], $deadline['law_regulation'],
|
||||
$deadline['recurrence_type'], $dueDate->format('Y-m-d'),
|
||||
$checkDate ? $checkDate->format('Y-m-d') : null,
|
||||
$deadline['document_date'],
|
||||
$deadline['notification_days'], $deadline['storage_location'],
|
||||
$deadline['notes'], $deadline['created_by'], $deadline['departments']
|
||||
]);
|
||||
|
||||
$newId = $pdo->lastInsertId();
|
||||
|
||||
// Copy employee assignments
|
||||
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?");
|
||||
$empStmt->execute([$id]);
|
||||
$empIds = $empStmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (!empty($empIds)) {
|
||||
$insertEmp = $pdo->prepare("INSERT INTO scad_deadline_employee (deadline_id, employee_id) VALUES (?, ?)");
|
||||
foreach ($empIds as $empId) {
|
||||
$insertEmp->execute([$newId, $empId]);
|
||||
}
|
||||
}
|
||||
|
||||
// History for new
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)")
|
||||
->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
$msg = 'Scadenza completata con successo.';
|
||||
if ($newId) {
|
||||
$msg .= ' Nuova scadenza creata con data ' . $dueDate->format('d/m/Y') . '.';
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
43
public/userarea/scadenzario/ajax/delete_attachment.php
Normal file
43
public/userarea/scadenzario/ajax/delete_attachment.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadline_attachments WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$att = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$att) {
|
||||
echo json_encode(['success' => false, 'message' => 'Allegato non trovato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Delete file
|
||||
$filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
// Delete DB record
|
||||
$pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]);
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_removed', ?)")
|
||||
->execute([$att['deadline_id'], $currentUserId, $att['original_name']]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Allegato eliminato.']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
26
public/userarea/scadenzario/ajax/delete_deadline.php
Normal file
26
public/userarea/scadenzario/ajax/delete_deadline.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM scad_deadlines WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
echo json_encode(['success' => true, 'message' => 'Scadenza eliminata con successo.']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
36
public/userarea/scadenzario/ajax/download_attachment.php
Normal file
36
public/userarea/scadenzario/ajax/download_attachment.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
http_response_code(400);
|
||||
echo 'ID non valido.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadline_attachments WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$att = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$att) {
|
||||
http_response_code(404);
|
||||
echo 'Allegato non trovato.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
|
||||
if (!file_exists($filePath)) {
|
||||
http_response_code(404);
|
||||
echo 'File non trovato sul server.';
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: ' . ($att['mime_type'] ?: 'application/octet-stream'));
|
||||
header('Content-Disposition: attachment; filename="' . addslashes($att['original_name']) . '"');
|
||||
header('Content-Length: ' . filesize($filePath));
|
||||
readfile($filePath);
|
||||
exit;
|
||||
92
public/userarea/scadenzario/ajax/get_calendar_events.php
Normal file
92
public/userarea/scadenzario/ajax/get_calendar_events.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$start = $_GET['start'] ?? null;
|
||||
$end = $_GET['end'] ?? null;
|
||||
$filterStatus = $_GET['status'] ?? '';
|
||||
$filterDept = $_GET['department'] ?? '';
|
||||
$filterEmployee = $_GET['employee'] ?? '';
|
||||
|
||||
$sql = "SELECT DISTINCT d.id, d.topic, d.category, d.due_date, d.status, d.notification_days, d.departments
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
|
||||
LEFT JOIN employees e ON e.id = de.employee_id";
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($start && $end) {
|
||||
$where[] = "d.due_date >= ? AND d.due_date <= ?";
|
||||
$params[] = $start;
|
||||
$params[] = $end;
|
||||
}
|
||||
|
||||
if ($filterStatus === 'non-completata') {
|
||||
$where[] = "d.status != 'completed'";
|
||||
} elseif ($filterStatus === 'completata') {
|
||||
$where[] = "d.status = 'completed'";
|
||||
} elseif ($filterStatus === 'scaduta') {
|
||||
$where[] = "d.status = 'active' AND d.due_date < CURDATE()";
|
||||
} elseif ($filterStatus === 'in-scadenza') {
|
||||
$where[] = "d.status = 'active' AND d.due_date >= CURDATE() AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
} elseif ($filterStatus === 'attiva') {
|
||||
$where[] = "d.status = 'active' AND d.due_date > DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
}
|
||||
|
||||
if ($filterDept) {
|
||||
$where[] = "(e.department = ? OR FIND_IN_SET(?, d.departments))";
|
||||
$params[] = $filterDept;
|
||||
$params[] = $filterDept;
|
||||
}
|
||||
|
||||
if ($filterEmployee) {
|
||||
$where[] = "CONCAT(e.first_name, ' ', e.last_name) = ?";
|
||||
$params[] = $filterEmployee;
|
||||
}
|
||||
|
||||
if (!empty($where)) {
|
||||
$sql .= " WHERE " . implode(' AND ', $where);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$events = [];
|
||||
|
||||
foreach ($deadlines as $d) {
|
||||
$isCompleted = $d['status'] === 'completed';
|
||||
$isOverdue = !$isCompleted && $d['due_date'] < $today;
|
||||
$approachDate = date('Y-m-d', strtotime($today . ' + ' . (int)$d['notification_days'] . ' days'));
|
||||
$isApproaching = !$isCompleted && !$isOverdue && $d['due_date'] <= $approachDate;
|
||||
|
||||
if ($isCompleted) { $color = '#198754'; }
|
||||
elseif ($isOverdue) { $color = '#dc3545'; }
|
||||
elseif ($isApproaching) { $color = '#e8930c'; }
|
||||
else { $color = '#5a8fd8'; }
|
||||
|
||||
$title = $d['topic'];
|
||||
if ($d['category']) $title = $d['category'] . ': ' . $title;
|
||||
|
||||
$events[] = [
|
||||
'id' => $d['id'],
|
||||
'title' => $title,
|
||||
'start' => $d['due_date'],
|
||||
'backgroundColor' => $color,
|
||||
'borderColor' => $color,
|
||||
'url' => 'scadenzario/detail.php?id=' . $d['id'],
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode($events);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([]);
|
||||
}
|
||||
45
public/userarea/scadenzario/ajax/get_deadline.php
Normal file
45
public/userarea/scadenzario/ajax/get_deadline.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadlines WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$deadline = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$deadline) {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get assigned employee IDs
|
||||
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?");
|
||||
$empStmt->execute([$id]);
|
||||
$deadline['employee_ids'] = $empStmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Parse departments into array
|
||||
$deadline['department_names'] = [];
|
||||
if (!empty($deadline['departments'])) {
|
||||
$deadline['department_names'] = array_map('trim', explode(',', $deadline['departments']));
|
||||
}
|
||||
|
||||
// Get attachments
|
||||
$attStmt = $pdo->prepare("SELECT id, original_name, mime_type, size, created_at FROM scad_deadline_attachments WHERE deadline_id = ? ORDER BY created_at DESC");
|
||||
$attStmt->execute([$id]);
|
||||
$deadline['attachments'] = $attStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $deadline]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
49
public/userarea/scadenzario/ajax/get_history.php
Normal file
49
public/userarea/scadenzario/ajax/get_history.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT h.*,
|
||||
au.first_name as user_first_name,
|
||||
au.last_name as user_last_name
|
||||
FROM scad_deadline_histories h
|
||||
LEFT JOIN auth_users au ON au.id = h.user_id
|
||||
WHERE h.deadline_id = ?
|
||||
ORDER BY h.created_at DESC
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$history = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Format for display
|
||||
$actionLabels = [
|
||||
'created' => 'Creata',
|
||||
'updated' => 'Modificata',
|
||||
'completed' => 'Completata',
|
||||
'attachment_added' => 'Allegato aggiunto',
|
||||
'attachment_removed' => 'Allegato rimosso',
|
||||
'notification_sent' => 'Notifica inviata'
|
||||
];
|
||||
|
||||
foreach ($history as &$h) {
|
||||
$h['action_label'] = $actionLabels[$h['action']] ?? $h['action'];
|
||||
$h['user_name'] = trim(($h['user_first_name'] ?? '') . ' ' . ($h['user_last_name'] ?? '')) ?: 'Sistema';
|
||||
$h['changes'] = $h['changes'] ? json_decode($h['changes'], true) : null;
|
||||
unset($h['user_first_name'], $h['user_last_name']);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $history]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
116
public/userarea/scadenzario/ajax/save_deadline.php
Normal file
116
public/userarea/scadenzario/ajax/save_deadline.php
Normal file
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$category = trim($_POST['category'] ?? '') ?: null;
|
||||
$topic = trim($_POST['topic'] ?? '');
|
||||
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
|
||||
$recurrence_type = $_POST['recurrence_type'] ?? 'once';
|
||||
$due_date = $_POST['due_date'] ?? '';
|
||||
$check_date = trim($_POST['check_date'] ?? '') ?: null;
|
||||
$document_date = trim($_POST['document_date'] ?? '') ?: null;
|
||||
$notification_days = isset($_POST['notification_days']) && is_numeric($_POST['notification_days']) ? (int)$_POST['notification_days'] : 7;
|
||||
$storage_location = trim($_POST['storage_location'] ?? '') ?: null;
|
||||
$notes = trim($_POST['notes'] ?? '') ?: null;
|
||||
$employee_ids = $_POST['employee_ids'] ?? [];
|
||||
$department_names = $_POST['department_names'] ?? [];
|
||||
|
||||
// Validation
|
||||
if ($topic === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il campo Tema è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
if ($due_date === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $due_date)) {
|
||||
echo json_encode(['success' => false, 'message' => 'La data di scadenza è obbligatoria.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$validRecurrences = ['once', 'monthly', 'quarterly', 'semiannual', 'annual', 'biennial', 'triennial', 'quinquennial'];
|
||||
if (!in_array($recurrence_type, $validRecurrences)) {
|
||||
$recurrence_type = 'once';
|
||||
}
|
||||
|
||||
if (!is_array($employee_ids)) {
|
||||
$employee_ids = [];
|
||||
}
|
||||
$employee_ids = array_filter(array_map('intval', $employee_ids));
|
||||
|
||||
if (!is_array($department_names)) {
|
||||
$department_names = [];
|
||||
}
|
||||
$department_names = array_filter(array_map('trim', $department_names));
|
||||
$departmentsStr = !empty($department_names) ? implode(', ', $department_names) : null;
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE scad_deadlines SET
|
||||
category = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
|
||||
due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
|
||||
storage_location = ?, notes = ?, departments = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$category, $topic, $law_regulation, $recurrence_type,
|
||||
$due_date, $check_date, $document_date, $notification_days,
|
||||
$storage_location, $notes, $departmentsStr, $id
|
||||
]);
|
||||
|
||||
// Re-link employees
|
||||
$pdo->prepare("DELETE FROM scad_deadline_employee WHERE deadline_id = ?")->execute([$id]);
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action) VALUES (?, ?, 'updated')")
|
||||
->execute([$id, $currentUserId ?: null]);
|
||||
|
||||
$deadlineId = $id;
|
||||
} else {
|
||||
// INSERT
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO scad_deadlines
|
||||
(category, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$category, $topic, $law_regulation, $recurrence_type,
|
||||
$due_date, $check_date, $document_date, $notification_days,
|
||||
$storage_location, $notes, $currentUserId, $departmentsStr
|
||||
]);
|
||||
|
||||
$deadlineId = $pdo->lastInsertId();
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action) VALUES (?, ?, 'created')")
|
||||
->execute([$deadlineId, $currentUserId ?: null]);
|
||||
}
|
||||
|
||||
// Link employees
|
||||
if (!empty($employee_ids)) {
|
||||
$insertEmployee = $pdo->prepare("INSERT INTO scad_deadline_employee (deadline_id, employee_id) VALUES (?, ?)");
|
||||
foreach ($employee_ids as $empId) {
|
||||
$insertEmployee->execute([$deadlineId, $empId]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.',
|
||||
'id' => $deadlineId
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
72
public/userarea/scadenzario/ajax/upload_attachment.php
Normal file
72
public/userarea/scadenzario/ajax/upload_attachment.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_POST['deadline_id']) || !is_numeric($_POST['deadline_id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID scadenza non valido.']);
|
||||
exit;
|
||||
}
|
||||
if (empty($_FILES['files']['name'][0])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Nessun file selezionato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$deadlineId = (int)$_POST['deadline_id'];
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Verify deadline exists
|
||||
$check = $pdo->prepare("SELECT id FROM scad_deadlines WHERE id = ?");
|
||||
$check->execute([$deadlineId]);
|
||||
if (!$check->fetch()) {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$uploadDir = __DIR__ . '/../attachments/';
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$inserted = [];
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO scad_deadline_attachments (deadline_id, original_name, stored_name, mime_type, size, uploaded_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$histStmt = $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_added', ?)");
|
||||
|
||||
$fileCount = count($_FILES['files']['name']);
|
||||
for ($i = 0; $i < $fileCount; $i++) {
|
||||
if ($_FILES['files']['error'][$i] !== UPLOAD_ERR_OK) continue;
|
||||
|
||||
$originalName = $_FILES['files']['name'][$i];
|
||||
$mimeType = $_FILES['files']['type'][$i];
|
||||
$size = $_FILES['files']['size'][$i];
|
||||
$storedName = uniqid('att_') . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
|
||||
|
||||
if (!move_uploaded_file($_FILES['files']['tmp_name'][$i], $uploadDir . $storedName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt->execute([$deadlineId, $originalName, $storedName, $mimeType, $size, $currentUserId]);
|
||||
$histStmt->execute([$deadlineId, $currentUserId, $originalName]);
|
||||
$inserted[] = ['id' => $pdo->lastInsertId(), 'original_name' => $originalName, 'stored_name' => $storedName];
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => count($inserted) . ' file caricato/i con successo.',
|
||||
'files' => $inserted
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
3
public/userarea/scadenzario/attachments/.gitignore
vendored
Normal file
3
public/userarea/scadenzario/attachments/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!.htaccess
|
||||
1
public/userarea/scadenzario/attachments/.htaccess
Normal file
1
public/userarea/scadenzario/attachments/.htaccess
Normal file
@ -0,0 +1 @@
|
||||
Deny from all
|
||||
222
public/userarea/scadenzario/calendar.php
Normal file
222
public/userarea/scadenzario/calendar.php
Normal file
@ -0,0 +1,222 @@
|
||||
<?php include('../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$employees = $pdo->query("SELECT id, first_name, last_name, department FROM employees WHERE status = 'active' ORDER BY first_name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$departments = $pdo->query("SELECT DISTINCT department FROM employees WHERE department IS NOT NULL AND department != '' ORDER BY department")->fetchAll(PDO::FETCH_COLUMN);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<base href="/userarea/">
|
||||
<?php include('../cssinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.9/index.global.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.9/locales/it.global.min.js"></script>
|
||||
<title>Calendario - Scadenzario</title>
|
||||
<script>if(window.innerWidth>1024)document.addEventListener('DOMContentLoaded',function(){document.getElementById('appWrapper').classList.add('toggled')})</script>
|
||||
<style>
|
||||
:root {
|
||||
--scad-primary: #5a8fd8;
|
||||
--scad-primary-hover: #4578c0;
|
||||
--scad-heading: #2c3e6b;
|
||||
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
||||
--scad-card-border: #dde4f0;
|
||||
}
|
||||
.scad-card {
|
||||
border: none; border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden;
|
||||
}
|
||||
.scad-card .card-header {
|
||||
background: var(--scad-card-bg);
|
||||
border-bottom: 1px solid var(--scad-card-border);
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
.scad-card .card-header h5 {
|
||||
font-weight: 700; color: var(--scad-heading);
|
||||
margin: 0; font-size: 1.05rem;
|
||||
}
|
||||
.scad-card .card-body { padding: 1.25rem; }
|
||||
.btn-scad-outline {
|
||||
background: transparent; border: 1.5px solid var(--scad-primary); color: var(--scad-primary);
|
||||
font-weight: 600; font-size: 0.85rem; padding: 0.45rem 1rem; border-radius: 0.5rem; transition: all 0.2s;
|
||||
}
|
||||
.btn-scad-outline:hover { background: var(--scad-primary); color: #fff; }
|
||||
.scad-breadcrumb { background: transparent; padding: 0; margin-bottom: 1rem; }
|
||||
.scad-breadcrumb .breadcrumb-item a { color: var(--scad-primary); text-decoration: none; font-weight: 500; }
|
||||
.scad-breadcrumb .breadcrumb-item a:hover { color: var(--scad-primary-hover); }
|
||||
.scad-breadcrumb .breadcrumb-item.active { color: #6c757d; font-weight: 600; }
|
||||
|
||||
/* FullCalendar overrides */
|
||||
.fc { font-size: 0.9rem; }
|
||||
.fc .fc-toolbar-title { font-size: 1.15rem; font-weight: 700; color: var(--scad-heading); }
|
||||
.fc .fc-button-primary {
|
||||
background: var(--scad-primary); border-color: var(--scad-primary);
|
||||
font-weight: 600; font-size: 0.82rem; border-radius: 0.4rem;
|
||||
}
|
||||
.fc .fc-button-primary:hover { background: var(--scad-primary-hover); border-color: var(--scad-primary-hover); }
|
||||
.fc .fc-button-primary:disabled { background: #9bbce6; border-color: #9bbce6; }
|
||||
.fc .fc-button-primary:not(:disabled).fc-button-active { background: var(--scad-heading); border-color: var(--scad-heading); }
|
||||
.fc .fc-daygrid-day-number { color: var(--scad-heading); font-weight: 500; }
|
||||
.fc .fc-daygrid-day.fc-day-today { background: #f0f4ff; }
|
||||
.fc .fc-event { border-radius: 0.3rem; padding: 2px 4px; font-weight: 600; cursor: pointer; }
|
||||
.fc .fc-event:hover { filter: brightness(0.9); }
|
||||
.fc .fc-list-event:hover td { background: #f0f4ff; }
|
||||
|
||||
/* Legend */
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; }
|
||||
.legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.82rem; color: #6c757d; }
|
||||
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.fc .fc-toolbar { flex-direction: column; gap: 0.5rem; }
|
||||
.fc .fc-toolbar-title { font-size: 1rem; }
|
||||
/* Stack list events vertically on mobile */
|
||||
.fc .fc-list-table { display: block; }
|
||||
.fc .fc-list-table tbody { display: block; }
|
||||
.fc .fc-list-day { display: block; }
|
||||
.fc .fc-list-day th { display: block; padding: 8px 12px; }
|
||||
.fc .fc-list-event { display: flex; flex-direction: column; padding: 8px 12px; border-bottom: 1px solid #e8eeff; }
|
||||
.fc .fc-list-event td { display: block; border: none; padding: 0; }
|
||||
.fc .fc-list-event-time { font-size: 0.75rem; color: #8e99b0; order: 2; }
|
||||
.fc .fc-list-event-graphic { display: none; }
|
||||
.fc .fc-list-event-title { font-size: 0.9rem; word-break: break-word; white-space: normal; order: 1; margin-bottom: 2px; }
|
||||
.fc .fc-list-event-dot { display: inline-block; margin-right: 6px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('../include/navbar.php'); ?>
|
||||
<?php include('../include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<nav class="scad-breadcrumb" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="scadenzario/index.php">Scadenzario</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Calendario</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<a href="scadenzario/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-list"></i><span>Lista Scadenze</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar mb-3">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-12 col-sm-6 col-md-auto">
|
||||
<select id="filterStatus" class="form-select" style="font-size:0.85rem;border-color:#d0d9e8;min-width:160px">
|
||||
<option value="non-completata" selected>Stato: Non completate</option>
|
||||
<option value="">Stato: Tutti</option>
|
||||
<option value="attiva">Attive</option>
|
||||
<option value="in-scadenza">In scadenza</option>
|
||||
<option value="scaduta">Scadute</option>
|
||||
<option value="completata">Completate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-auto">
|
||||
<select id="filterDepartment" class="form-select" style="font-size:0.85rem;border-color:#d0d9e8;min-width:160px">
|
||||
<option value="">Reparto: Tutti</option>
|
||||
<?php foreach ($departments as $dept): ?>
|
||||
<option value="<?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-auto">
|
||||
<select id="filterEmployee" class="form-select" style="font-size:0.85rem;border-color:#d0d9e8;min-width:160px">
|
||||
<option value="">Responsabile: Tutti</option>
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<option value="<?= htmlspecialchars(trim($emp['first_name'] . ' ' . $emp['last_name']), ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars(trim($emp['first_name'] . ' ' . $emp['last_name']), ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto">
|
||||
<button id="btnResetFilters" class="btn btn-sm btn-light border w-100 w-sm-auto" title="Reset filtri">
|
||||
<i class="fa-solid fa-rotate-left me-1"></i> Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card scad-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<h5><i class="fa-solid fa-calendar-days me-2"></i>Calendario Scadenze</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="legend">
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#5a8fd8"></span> Attiva</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#e8930c"></span> In scadenza</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#dc3545"></span> Scaduta</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#198754"></span> Completata</div>
|
||||
</div>
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include('../include/footer.php'); ?>
|
||||
</div>
|
||||
<?php include('../jsinclude.php'); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var isMobile = window.innerWidth < 768;
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
locale: 'it',
|
||||
initialView: isMobile ? 'listWeek' : 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: isMobile ? 'listWeek,dayGridMonth' : 'dayGridMonth,listWeek'
|
||||
},
|
||||
height: 'auto',
|
||||
navLinks: true,
|
||||
eventSources: [{
|
||||
url: 'scadenzario/ajax/get_calendar_events.php',
|
||||
extraParams: function() {
|
||||
return {
|
||||
status: document.getElementById('filterStatus').value,
|
||||
department: document.getElementById('filterDepartment').value,
|
||||
employee: document.getElementById('filterEmployee').value
|
||||
};
|
||||
},
|
||||
failure: function() {
|
||||
Swal.fire('Errore', 'Impossibile caricare gli eventi.', 'error');
|
||||
}
|
||||
}],
|
||||
eventClick: function(info) {
|
||||
info.jsEvent.preventDefault();
|
||||
if (info.event.url) {
|
||||
window.location.href = info.event.url;
|
||||
}
|
||||
},
|
||||
windowResize: function(view) {
|
||||
if (window.innerWidth < 768) {
|
||||
calendar.changeView('listWeek');
|
||||
} else {
|
||||
calendar.changeView('dayGridMonth');
|
||||
}
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
|
||||
// Filters
|
||||
document.querySelectorAll('#filterStatus, #filterDepartment, #filterEmployee').forEach(function(el) {
|
||||
el.addEventListener('change', function() { calendar.refetchEvents(); });
|
||||
});
|
||||
document.getElementById('btnResetFilters').addEventListener('click', function() {
|
||||
document.getElementById('filterStatus').value = 'non-completata';
|
||||
document.getElementById('filterDepartment').value = '';
|
||||
document.getElementById('filterEmployee').value = '';
|
||||
calendar.refetchEvents();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
220
public/userarea/scadenzario/cron/send_notifications.php
Normal file
220
public/userarea/scadenzario/cron/send_notifications.php
Normal file
@ -0,0 +1,220 @@
|
||||
<?php
|
||||
/**
|
||||
* Scadenzario — Email notification cron script
|
||||
* Run daily: 0 7 * * * php /var/www/html/public/userarea/scadenzario/cron/send_notifications.php
|
||||
*
|
||||
* Sends "approaching" emails N days before due_date (per-deadline notification_days).
|
||||
* Sends "overdue" emails when due_date has passed.
|
||||
* Skips completed deadlines and already-sent notifications (same deadline+employee+type+date).
|
||||
* Email is taken from employees.auth_user_id → auth_users.email.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../class/db-functions.php';
|
||||
require_once __DIR__ . '/../../../../vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../../../');
|
||||
$dotenv->load();
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
|
||||
|
||||
$sent = 0;
|
||||
$skipped = 0;
|
||||
$errors = 0;
|
||||
|
||||
// Get active deadlines that are approaching or overdue
|
||||
$stmt = $pdo->query("
|
||||
SELECT d.id, d.topic, d.category, d.due_date, d.notification_days
|
||||
FROM scad_deadlines d
|
||||
WHERE d.status = 'active'
|
||||
AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)
|
||||
");
|
||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($deadlines)) {
|
||||
echo date('Y-m-d H:i:s') . " — Nessuna scadenza da notificare.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Prepare statements
|
||||
$getRecipients = $pdo->prepare("
|
||||
SELECT DISTINCT e.id as employee_id, au.email, e.first_name, e.last_name
|
||||
FROM scad_deadline_employee de
|
||||
JOIN employees e ON e.id = de.employee_id
|
||||
JOIN auth_users au ON au.id = e.auth_user_id
|
||||
WHERE de.deadline_id = ?
|
||||
AND e.auth_user_id IS NOT NULL
|
||||
AND au.email IS NOT NULL
|
||||
AND au.email != ''
|
||||
");
|
||||
|
||||
// Also get employees from assigned departments
|
||||
$getDeptRecipients = $pdo->prepare("
|
||||
SELECT DISTINCT e.id as employee_id, au.email, e.first_name, e.last_name
|
||||
FROM employees e
|
||||
JOIN auth_users au ON au.id = e.auth_user_id
|
||||
WHERE e.department IN (SELECT TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(d.departments, ',', n.n), ',', -1))
|
||||
FROM scad_deadlines d
|
||||
CROSS JOIN (SELECT 1 n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) n
|
||||
WHERE d.id = ?
|
||||
AND d.departments IS NOT NULL
|
||||
AND n.n <= 1 + LENGTH(d.departments) - LENGTH(REPLACE(d.departments, ',', '')))
|
||||
AND e.auth_user_id IS NOT NULL
|
||||
AND au.email IS NOT NULL
|
||||
AND au.email != ''
|
||||
");
|
||||
|
||||
$checkSent = $pdo->prepare("
|
||||
SELECT COUNT(*) FROM scad_deadline_notifications
|
||||
WHERE deadline_id = ? AND employee_id = ? AND type = ? AND DATE(sent_at) = CURDATE()
|
||||
");
|
||||
|
||||
$insertNotif = $pdo->prepare("
|
||||
INSERT INTO scad_deadline_notifications (deadline_id, employee_id, type) VALUES (?, ?, ?)
|
||||
");
|
||||
|
||||
$insertHistory = $pdo->prepare("
|
||||
INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, NULL, 'notification_sent', ?)
|
||||
");
|
||||
|
||||
foreach ($deadlines as $dl) {
|
||||
$isOverdue = $dl['due_date'] < $today;
|
||||
$type = $isOverdue ? 'overdue' : 'approaching';
|
||||
$daysLeft = (int)((strtotime($dl['due_date']) - strtotime($today)) / 86400);
|
||||
|
||||
// Collect all recipients (direct + department)
|
||||
$recipients = [];
|
||||
|
||||
$getRecipients->execute([$dl['id']]);
|
||||
foreach ($getRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$recipients[$r['employee_id']] = $r;
|
||||
}
|
||||
|
||||
$getDeptRecipients->execute([$dl['id']]);
|
||||
foreach ($getDeptRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$recipients[$r['employee_id']] = $r;
|
||||
}
|
||||
|
||||
if (empty($recipients)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($recipients as $emp) {
|
||||
// Check if already sent today
|
||||
$checkSent->execute([$dl['id'], $emp['employee_id'], $type]);
|
||||
if ($checkSent->fetchColumn() > 0) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Send email
|
||||
try {
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
// SMTP config from .env
|
||||
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
|
||||
if ($mailer === 'smtp') {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
|
||||
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
|
||||
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
||||
}
|
||||
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
|
||||
if ($enc && $enc !== 'null') {
|
||||
$mail->SMTPSecure = $enc;
|
||||
}
|
||||
}
|
||||
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->setFrom(
|
||||
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
|
||||
$_ENV['MAIL_FROM_NAME'] ?? 'Scadenzario ZIBOGOMMA'
|
||||
);
|
||||
$mail->addAddress($emp['email'], trim($emp['first_name'] . ' ' . $emp['last_name']));
|
||||
|
||||
$detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id'];
|
||||
$topicText = ($dl['category'] ? $dl['category'] . ' — ' : '') . $dl['topic'];
|
||||
|
||||
if ($isOverdue) {
|
||||
$mail->Subject = '⚠️ Scadenza superata: ' . $dl['topic'];
|
||||
$mail->Body = buildHtml(
|
||||
'Scadenza superata',
|
||||
$topicText,
|
||||
'La scadenza era prevista per il <strong>' . date('d/m/Y', strtotime($dl['due_date'])) . '</strong> ed è stata superata da <strong>' . abs($daysLeft) . ' giorni</strong>.',
|
||||
'#dc3545',
|
||||
$detailUrl
|
||||
);
|
||||
} else {
|
||||
$mail->Subject = '📅 Scadenza in arrivo: ' . $dl['topic'];
|
||||
$daysText = $daysLeft === 0 ? 'oggi' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
|
||||
$mail->Body = buildHtml(
|
||||
'Scadenza in arrivo',
|
||||
$topicText,
|
||||
'La scadenza è prevista per il <strong>' . date('d/m/Y', strtotime($dl['due_date'])) . '</strong> (' . $daysText . ').',
|
||||
'#e8930c',
|
||||
$detailUrl
|
||||
);
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
|
||||
|
||||
$mail->send();
|
||||
|
||||
// Record notification
|
||||
$insertNotif->execute([$dl['id'], $emp['employee_id'], $type]);
|
||||
$sent++;
|
||||
|
||||
echo date('H:i:s') . " ✓ {$type} → {$emp['email']} — {$dl['topic']}\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
$errors++;
|
||||
echo date('H:i:s') . " ✗ Errore {$emp['email']}: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
// History (one per deadline, not per recipient)
|
||||
$recipientNames = implode(', ', array_map(fn($r) => trim($r['first_name'] . ' ' . $r['last_name']), $recipients));
|
||||
$insertHistory->execute([$dl['id'], "Notifica {$type} inviata a: {$recipientNames}"]);
|
||||
}
|
||||
|
||||
echo "\n" . date('Y-m-d H:i:s') . " — Completato. Inviate: {$sent}, Saltate: {$skipped}, Errori: {$errors}\n";
|
||||
|
||||
// --- HTML email template ---
|
||||
function buildHtml(string $title, string $topic, string $message, string $accentColor, string $url): string
|
||||
{
|
||||
return '
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body style="margin:0;padding:0;background:#f4f6f9;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="padding:30px 0">
|
||||
<tr><td align="center">
|
||||
<table width="560" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06)">
|
||||
<tr><td style="background:' . $accentColor . ';padding:20px 30px">
|
||||
<h1 style="margin:0;color:#fff;font-size:18px">' . htmlspecialchars($title) . '</h1>
|
||||
</td></tr>
|
||||
<tr><td style="padding:30px">
|
||||
<h2 style="margin:0 0 15px;color:#2c3e6b;font-size:16px">' . htmlspecialchars($topic) . '</h2>
|
||||
<p style="margin:0 0 20px;color:#444;font-size:14px;line-height:1.6">' . $message . '</p>
|
||||
<a href="' . htmlspecialchars($url) . '" style="display:inline-block;background:#5a8fd8;color:#fff;padding:10px 24px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px">Vai alla scadenza</a>
|
||||
</td></tr>
|
||||
<tr><td style="padding:15px 30px;background:#f8f9fb;border-top:1px solid #eee">
|
||||
<p style="margin:0;color:#999;font-size:11px">ZIBOGOMMA — Scadenzario</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>';
|
||||
}
|
||||
463
public/userarea/scadenzario/detail.php
Normal file
463
public/userarea/scadenzario/detail.php
Normal file
@ -0,0 +1,463 @@
|
||||
<?php include('../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$error = null;
|
||||
$deadline = null;
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
$error = 'ID non valido.';
|
||||
} else {
|
||||
$id = (int)$_GET['id'];
|
||||
$stmt = $pdo->prepare("SELECT * FROM scad_deadlines WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$deadline = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$deadline) {
|
||||
$error = 'Scadenza non trovata.';
|
||||
} else {
|
||||
$empStmt = $pdo->prepare("
|
||||
SELECT e.first_name, e.last_name, e.department
|
||||
FROM scad_deadline_employee de
|
||||
JOIN employees e ON e.id = de.employee_id
|
||||
WHERE de.deadline_id = ?
|
||||
ORDER BY e.first_name
|
||||
");
|
||||
$empStmt->execute([$id]);
|
||||
$employees = $empStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$attStmt = $pdo->prepare("SELECT * FROM scad_deadline_attachments WHERE deadline_id = ? ORDER BY created_at DESC");
|
||||
$attStmt->execute([$id]);
|
||||
$attachments = $attStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$histStmt = $pdo->prepare("
|
||||
SELECT h.*, au.first_name as user_fname, au.last_name as user_lname
|
||||
FROM scad_deadline_histories h
|
||||
LEFT JOIN auth_users au ON au.id = h.user_id
|
||||
WHERE h.deadline_id = ?
|
||||
ORDER BY h.created_at DESC
|
||||
");
|
||||
$histStmt->execute([$id]);
|
||||
$history = $histStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$isCompleted = $deadline['status'] === 'completed';
|
||||
$isOverdue = !$isCompleted && $deadline['due_date'] < $today;
|
||||
$approachDate = date('Y-m-d', strtotime($today . ' + ' . (int)$deadline['notification_days'] . ' days'));
|
||||
$isApproaching = !$isCompleted && !$isOverdue && $deadline['due_date'] <= $approachDate;
|
||||
|
||||
if ($isCompleted) { $statusLabel = 'Completata'; $statusClass = 'badge-completata'; }
|
||||
elseif ($isOverdue) { $statusLabel = 'Scaduta'; $statusClass = 'badge-scaduta'; }
|
||||
elseif ($isApproaching) { $statusLabel = 'In scadenza'; $statusClass = 'badge-in-scadenza'; }
|
||||
else { $statusLabel = 'Attiva'; $statusClass = 'badge-attiva'; }
|
||||
|
||||
$recurrenceLabels = ['once'=>'Una tantum','monthly'=>'Mensile','quarterly'=>'Trimestrale','semiannual'=>'Semestrale','annual'=>'Annuale','biennial'=>'Biennale','triennial'=>'Triennale','quinquennial'=>'Quinquennale'];
|
||||
$actionLabels = ['created'=>'Creata','updated'=>'Modificata','completed'=>'Completata','attachment_added'=>'Allegato aggiunto','attachment_removed'=>'Allegato rimosso','notification_sent'=>'Notifica inviata'];
|
||||
$actionColors = ['created'=>'#198754','updated'=>'#5a8fd8','completed'=>'#6f42c1','attachment_added'=>'#e8930c','attachment_removed'=>'#e8930c','notification_sent'=>'#adb5bd'];
|
||||
$actionIcons = ['created'=>'fa-plus','updated'=>'fa-pen','completed'=>'fa-check','attachment_added'=>'fa-paperclip','attachment_removed'=>'fa-trash','notification_sent'=>'fa-bell'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<base href="/userarea/">
|
||||
<?php include('../cssinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<title><?= $deadline ? htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') . ' — ' : '' ?>Scadenzario</title>
|
||||
<script>if(window.innerWidth>1024)document.addEventListener('DOMContentLoaded',function(){document.getElementById('appWrapper').classList.add('toggled')})</script>
|
||||
<style>
|
||||
:root {
|
||||
--scad-primary: #5a8fd8;
|
||||
--scad-primary-hover: #4578c0;
|
||||
--scad-heading: #2c3e6b;
|
||||
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
||||
--scad-card-border: #dde4f0;
|
||||
--scad-red: #dc3545;
|
||||
--scad-orange: #e8930c;
|
||||
--scad-green: #198754;
|
||||
}
|
||||
.scad-card {
|
||||
border: none; border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden;
|
||||
}
|
||||
.scad-card .card-header {
|
||||
background: var(--scad-card-bg);
|
||||
border-bottom: 1px solid var(--scad-card-border);
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
.scad-card .card-header h5 {
|
||||
font-weight: 700; color: var(--scad-heading);
|
||||
margin: 0; font-size: 1.05rem;
|
||||
}
|
||||
.scad-card .card-body { padding: 1.25rem; }
|
||||
|
||||
.btn-scad-primary {
|
||||
background: var(--scad-primary); border: none; color: #fff;
|
||||
font-weight: 600; font-size: 0.85rem; padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem; transition: all 0.2s;
|
||||
}
|
||||
.btn-scad-primary:hover { background: var(--scad-primary-hover); color: #fff; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(90,143,216,0.35); }
|
||||
.btn-scad-outline {
|
||||
background: transparent; border: 1.5px solid var(--scad-primary); color: var(--scad-primary);
|
||||
font-weight: 600; font-size: 0.85rem; padding: 0.45rem 1rem; border-radius: 0.5rem; transition: all 0.2s;
|
||||
}
|
||||
.btn-scad-outline:hover { background: var(--scad-primary); color: #fff; transform: translateY(-1px); }
|
||||
.btn-scad-green {
|
||||
background: var(--scad-green); border: none; color: #fff;
|
||||
font-weight: 600; font-size: 0.85rem; padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem; transition: all 0.2s;
|
||||
}
|
||||
.btn-scad-green:hover { background: #157347; color: #fff; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(25,135,84,0.35); }
|
||||
|
||||
.badge-status {
|
||||
font-weight: 600; font-size: 0.8rem; padding: 0.4em 0.75em;
|
||||
border-radius: 2rem; display: inline-block;
|
||||
}
|
||||
.badge-attiva { background: #e8eeff; color: #3a6bb5; }
|
||||
.badge-scaduta { background: #fde8e8; color: #b91c1c; }
|
||||
.badge-in-scadenza{ background: #fef3cd; color: #92600a; }
|
||||
.badge-completata { background: #d1f2e0; color: #0f5132; }
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.75rem; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: 0.04em; color: #8e99b0; margin-bottom: 0.2rem;
|
||||
}
|
||||
.detail-value {
|
||||
font-size: 0.95rem; color: var(--scad-heading); margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.detail-value.text-danger-date { color: var(--scad-red); font-weight: 600; }
|
||||
.detail-value.text-warning-date { color: var(--scad-orange); font-weight: 600; }
|
||||
|
||||
.person-chip {
|
||||
display: inline-flex; align-items: center; gap: 0.4rem;
|
||||
background: #f0f4ff; border: 1px solid #dde4f0; border-radius: 2rem;
|
||||
padding: 0.3rem 0.75rem 0.3rem 0.5rem; font-size: 0.85rem;
|
||||
margin: 0.2rem 0.15rem; color: var(--scad-heading);
|
||||
}
|
||||
.person-chip i { color: var(--scad-primary); font-size: 0.75rem; }
|
||||
.person-chip .chip-dept { color: #8e99b0; font-size: 0.78rem; }
|
||||
.dept-chip {
|
||||
display: inline-flex; align-items: center; gap: 0.4rem;
|
||||
background: #eef6ee; border: 1px solid #c8e6c9; border-radius: 2rem;
|
||||
padding: 0.3rem 0.75rem 0.3rem 0.5rem; font-size: 0.85rem;
|
||||
margin: 0.2rem 0.15rem; color: #2e5e2e;
|
||||
}
|
||||
.dept-chip i { color: #4caf50; font-size: 0.75rem; }
|
||||
|
||||
/* Attachments */
|
||||
.att-row {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.65rem 0; border-bottom: 1px solid #f0f2f5;
|
||||
}
|
||||
.att-row:last-child { border-bottom: none; }
|
||||
.att-icon { width: 36px; height: 36px; border-radius: 0.4rem; display: flex; align-items: center; justify-content: center; font-size: 1rem; flex-shrink: 0; }
|
||||
.att-icon-pdf { background: #fde8e8; color: #b91c1c; }
|
||||
.att-icon-img { background: #e8f5e9; color: #2e7d32; }
|
||||
.att-icon-file { background: #e8eeff; color: #3a6bb5; }
|
||||
.att-info { flex: 1; min-width: 0; }
|
||||
.att-name { font-weight: 600; color: var(--scad-heading); font-size: 0.9rem; text-decoration: none; word-break: break-all; }
|
||||
.att-name:hover { color: var(--scad-primary); }
|
||||
.att-meta { font-size: 0.78rem; color: #8e99b0; }
|
||||
|
||||
/* Timeline */
|
||||
.timeline { position: relative; padding-left: 2rem; }
|
||||
.timeline::before {
|
||||
content: ''; position: absolute; left: 0.55rem; top: 0.5rem; bottom: 0.5rem;
|
||||
width: 2px; background: #e2e8f0;
|
||||
}
|
||||
.timeline-item { position: relative; padding-bottom: 1.25rem; }
|
||||
.timeline-item:last-child { padding-bottom: 0; }
|
||||
.timeline-dot {
|
||||
position: absolute; left: -1.7rem; top: 0.15rem;
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.6rem; color: #fff; z-index: 1;
|
||||
box-shadow: 0 0 0 3px #fff;
|
||||
}
|
||||
.timeline-header {
|
||||
display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
.timeline-action { font-weight: 700; font-size: 0.88rem; color: var(--scad-heading); }
|
||||
.timeline-user { font-size: 0.82rem; color: #6c757d; }
|
||||
.timeline-date { font-size: 0.78rem; color: #adb5bd; }
|
||||
.timeline-notes { font-size: 0.83rem; color: #6c757d; margin-top: 0.15rem; }
|
||||
.timeline-changes { font-size: 0.82rem; margin-top: 0.3rem; background: #f8f9fb; border-radius: 0.4rem; padding: 0.5rem 0.75rem; }
|
||||
.timeline-changes .change-field { font-weight: 600; color: var(--scad-heading); }
|
||||
.timeline-changes .change-old { color: var(--scad-red); text-decoration: line-through; }
|
||||
.timeline-changes .change-new { color: var(--scad-green); }
|
||||
|
||||
.scad-breadcrumb { background: transparent; padding: 0; margin-bottom: 1rem; }
|
||||
.scad-breadcrumb .breadcrumb-item a { color: var(--scad-primary); text-decoration: none; font-weight: 500; transition: color 0.2s; }
|
||||
.scad-breadcrumb .breadcrumb-item a:hover { color: var(--scad-primary-hover); }
|
||||
.scad-breadcrumb .breadcrumb-item.active { color: #6c757d; font-weight: 600; }
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.action-bar { flex-direction: column; }
|
||||
.action-bar .btn { width: 100%; justify-content: center; }
|
||||
}
|
||||
|
||||
@media print {
|
||||
.sidebar-wrapper, .topbar, .page-footer, .action-bar, .scad-breadcrumb { display: none !important; }
|
||||
.page-wrapper { margin: 0 !important; }
|
||||
.scad-card { box-shadow: none; border: 1px solid #ddd; }
|
||||
@page { size: portrait; margin: 1cm; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('../include/navbar.php'); ?>
|
||||
<?php include('../include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa-solid fa-triangle-exclamation me-2"></i><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
||||
<a href="scadenzario/index.php" class="alert-link ms-2">Torna alla lista</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="scad-breadcrumb" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="scadenzario/index.php">Scadenzario</a></li>
|
||||
<li class="breadcrumb-item"><a href="scadenzario/index.php">Lista Scadenze</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><?= htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Action Bar -->
|
||||
<div class="action-bar d-flex gap-2 mb-3 flex-wrap">
|
||||
<a href="scadenzario/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-arrow-left"></i><span>Torna alla lista</span>
|
||||
</a>
|
||||
<?php if (!$isCompleted): ?>
|
||||
<button class="btn btn-scad-primary d-inline-flex align-items-center gap-2" id="btnModifica">
|
||||
<i class="fa-solid fa-pen"></i><span>Modifica</span>
|
||||
</button>
|
||||
<button class="btn btn-scad-green d-inline-flex align-items-center gap-2" id="btnCompleta">
|
||||
<i class="fa-solid fa-check"></i><span>Completa</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-scad-outline d-inline-flex align-items-center gap-2" onclick="window.print()">
|
||||
<i class="fa-solid fa-print"></i><span>Stampa</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Main Detail Card -->
|
||||
<div class="card scad-card mb-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa-solid fa-file-lines me-2"></i>Dettagli Scadenza</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<!-- Left Column -->
|
||||
<div class="col-12 col-md-6">
|
||||
<?php if ($deadline['category']): ?>
|
||||
<div class="detail-label">Argomento</div>
|
||||
<div class="detail-value" style="font-weight:600;"><?= htmlspecialchars($deadline['category'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="detail-label">Dettaglio</div>
|
||||
<div class="detail-value" style="font-size:1.15rem; font-weight:700;">
|
||||
<?= htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</div>
|
||||
|
||||
<?php if ($deadline['law_regulation']): ?>
|
||||
<div class="detail-label">Legge / Articolo</div>
|
||||
<div class="detail-value"><?= htmlspecialchars($deadline['law_regulation'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="detail-label">Periodicità</div>
|
||||
<div class="detail-value"><?= htmlspecialchars($recurrenceLabels[$deadline['recurrence_type']] ?? $deadline['recurrence_type'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="detail-label">Stato</div>
|
||||
<div class="detail-value">
|
||||
<span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span>
|
||||
<?php if ($isCompleted && $deadline['completed_at']): ?>
|
||||
<span class="text-muted ms-2" style="font-size:0.82rem"><?= date('d/m/Y H:i', strtotime($deadline['completed_at'])) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="detail-label">Data scadenza</div>
|
||||
<div class="detail-value <?= $isOverdue ? 'text-danger-date' : ($isApproaching ? 'text-warning-date' : '') ?>">
|
||||
<i class="fa-regular fa-calendar me-1"></i><?= date('d/m/Y', strtotime($deadline['due_date'])) ?>
|
||||
<?php if ($isOverdue): ?><span class="ms-1" style="font-size:0.8rem">(scaduta)</span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($deadline['document_date']): ?>
|
||||
<div class="detail-label">Data documento</div>
|
||||
<div class="detail-value"><?= date('d/m/Y', strtotime($deadline['document_date'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="detail-label">Data ultimo controllo</div>
|
||||
<div class="detail-value"><?= $deadline['check_date'] ? date('d/m/Y', strtotime($deadline['check_date'])) : '—' ?></div>
|
||||
|
||||
<div class="detail-label">Giorni preavviso notifica</div>
|
||||
<div class="detail-value"><?= (int)$deadline['notification_days'] ?> giorni</div>
|
||||
|
||||
<?php if ($deadline['storage_location']): ?>
|
||||
<div class="detail-label">Luogo archiviazione</div>
|
||||
<div class="detail-value"><i class="fa-regular fa-folder-open me-1"></i><?= htmlspecialchars($deadline['storage_location'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($deadline['notes']): ?>
|
||||
<div class="detail-label">Note</div>
|
||||
<div class="detail-value"><?= nl2br(htmlspecialchars($deadline['notes'], ENT_QUOTES, 'UTF-8')) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsabili -->
|
||||
<?php if ($deadline['departments'] || !empty($employees)): ?>
|
||||
<hr class="my-3" style="border-color:#e8eeff">
|
||||
<?php if ($deadline['departments']): ?>
|
||||
<div class="detail-label">Reparti responsabili</div>
|
||||
<div class="detail-value">
|
||||
<?php foreach (array_map('trim', explode(',', $deadline['departments'])) as $dept): ?>
|
||||
<span class="dept-chip"><i class="fa-solid fa-building"></i><?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($employees)): ?>
|
||||
<div class="detail-label">Singoli responsabili</div>
|
||||
<div class="detail-value">
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<span class="person-chip">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<?= htmlspecialchars(trim($emp['first_name'] . ' ' . $emp['last_name']), ENT_QUOTES, 'UTF-8') ?>
|
||||
<?php if ($emp['department']): ?>
|
||||
<span class="chip-dept">(<?= htmlspecialchars($emp['department'], ENT_QUOTES, 'UTF-8') ?>)</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attachments Card -->
|
||||
<?php if (!empty($attachments)): ?>
|
||||
<div class="card scad-card mb-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa-solid fa-paperclip me-2"></i>Allegati (<?= count($attachments) ?>)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php foreach ($attachments as $att):
|
||||
$mime = $att['mime_type'] ?? '';
|
||||
if (strpos($mime, 'pdf') !== false) { $iconClass = 'att-icon-pdf'; $icon = 'fa-file-pdf'; }
|
||||
elseif (strpos($mime, 'image') !== false) { $iconClass = 'att-icon-img'; $icon = 'fa-file-image'; }
|
||||
else { $iconClass = 'att-icon-file'; $icon = 'fa-file'; }
|
||||
$sizeKB = round(($att['size'] ?? 0) / 1024, 1);
|
||||
$sizeStr = $sizeKB >= 1024 ? round($sizeKB / 1024, 1) . ' MB' : $sizeKB . ' KB';
|
||||
?>
|
||||
<div class="att-row">
|
||||
<div class="att-icon <?= $iconClass ?>"><i class="fa-solid <?= $icon ?>"></i></div>
|
||||
<div class="att-info">
|
||||
<a href="scadenzario/ajax/download_attachment.php?id=<?= (int)$att['id'] ?>" class="att-name"><?= htmlspecialchars($att['original_name'], ENT_QUOTES, 'UTF-8') ?></a>
|
||||
<div class="att-meta"><?= $sizeStr ?> · <?= date('d/m/Y', strtotime($att['created_at'])) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- History Card -->
|
||||
<?php if (!empty($history)): ?>
|
||||
<div class="card scad-card mb-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fa-solid fa-clock-rotate-left me-2"></i>Cronologia</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="timeline">
|
||||
<?php foreach ($history as $h):
|
||||
$color = $actionColors[$h['action']] ?? '#adb5bd';
|
||||
$iconCls = $actionIcons[$h['action']] ?? 'fa-circle';
|
||||
$label = $actionLabels[$h['action']] ?? $h['action'];
|
||||
$userName = trim(($h['user_fname'] ?? '') . ' ' . ($h['user_lname'] ?? '')) ?: 'Sistema';
|
||||
$changes = $h['changes'] ? json_decode($h['changes'], true) : null;
|
||||
?>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot" style="background:<?= $color ?>"><i class="fa-solid <?= $iconCls ?>"></i></div>
|
||||
<div class="timeline-header">
|
||||
<span class="timeline-action"><?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span class="timeline-user">da <?= htmlspecialchars($userName, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span class="timeline-date"><?= date('d/m/Y H:i', strtotime($h['created_at'])) ?></span>
|
||||
</div>
|
||||
<?php if ($h['notes']): ?>
|
||||
<div class="timeline-notes"><i class="fa-regular fa-comment me-1"></i><?= htmlspecialchars($h['notes'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($changes && is_array($changes)): ?>
|
||||
<div class="timeline-changes">
|
||||
<?php foreach ($changes as $field => $vals): ?>
|
||||
<div>
|
||||
<span class="change-field"><?= htmlspecialchars($field, ENT_QUOTES, 'UTF-8') ?>:</span>
|
||||
<span class="change-old"><?= htmlspecialchars($vals['old'] ?? '—', ENT_QUOTES, 'UTF-8') ?></span>
|
||||
→
|
||||
<span class="change-new"><?= htmlspecialchars($vals['new'] ?? '—', ENT_QUOTES, 'UTF-8') ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include('../include/footer.php'); ?>
|
||||
</div>
|
||||
<?php include('../jsinclude.php'); ?>
|
||||
<?php if ($deadline && !$isCompleted): ?>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#btnModifica').on('click', function() {
|
||||
window.location.href = 'scadenzario/index.php?edit=<?= (int)$deadline['id'] ?>';
|
||||
});
|
||||
|
||||
$('#btnCompleta').on('click', function() {
|
||||
Swal.fire({
|
||||
title: 'Completare la scadenza?',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#198754',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Completa'
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
fetch('scadenzario/ajax/complete_deadline.php?id=<?= (int)$deadline['id'] ?>')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
Swal.fire({ icon: 'success', title: 'Completata', text: data.message, timer: 2500, showConfirmButton: false })
|
||||
.then(function() { window.location.href = 'scadenzario/index.php'; });
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() { Swal.fire('Errore', 'Errore di connessione.', 'error'); });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
</html>
|
||||
964
public/userarea/scadenzario/index.php
Normal file
964
public/userarea/scadenzario/index.php
Normal file
@ -0,0 +1,964 @@
|
||||
<?php include('../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->query("
|
||||
SELECT d.*,
|
||||
GROUP_CONCAT(DISTINCT CONCAT(e.first_name, ' ', e.last_name) ORDER BY e.first_name SEPARATOR ', ') as responsabili,
|
||||
GROUP_CONCAT(DISTINCT e.department ORDER BY e.department SEPARATOR ', ') as reparti_persone,
|
||||
d.departments as reparti_assegnati,
|
||||
(SELECT COUNT(*) FROM scad_deadline_attachments att WHERE att.deadline_id = d.id) as attachment_count
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
|
||||
LEFT JOIN employees e ON e.id = de.employee_id
|
||||
GROUP BY d.id
|
||||
ORDER BY (d.status = 'completed') ASC, d.due_date ASC
|
||||
");
|
||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$employees = $pdo->query("SELECT id, first_name, last_name, department FROM employees WHERE status = 'active' ORDER BY first_name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$departments = $pdo->query("SELECT DISTINCT department FROM employees WHERE department IS NOT NULL AND department != '' ORDER BY department")->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<base href="/userarea/">
|
||||
<?php include('../cssinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet">
|
||||
<title>Scadenzario - Lista Scadenze</title>
|
||||
<script>if(window.innerWidth>1024)document.addEventListener('DOMContentLoaded',function(){document.getElementById('appWrapper').classList.add('toggled')})</script>
|
||||
<style>
|
||||
:root {
|
||||
--scad-primary: #5a8fd8;
|
||||
--scad-primary-hover: #4578c0;
|
||||
--scad-heading: #2c3e6b;
|
||||
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
||||
--scad-card-border: #dde4f0;
|
||||
--scad-red: #dc3545;
|
||||
--scad-orange: #e8930c;
|
||||
--scad-green: #198754;
|
||||
--scad-blue: #5a8fd8;
|
||||
}
|
||||
.scad-card {
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.scad-card .card-header {
|
||||
background: var(--scad-card-bg);
|
||||
border-bottom: 1px solid var(--scad-card-border);
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
.scad-card .card-header h5 {
|
||||
font-weight: 700;
|
||||
color: var(--scad-heading);
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.scad-card .card-body { padding: 1.25rem; }
|
||||
|
||||
.btn-scad-primary {
|
||||
background: var(--scad-primary);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-scad-primary:hover {
|
||||
background: var(--scad-primary-hover);
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(90,143,216,0.35);
|
||||
}
|
||||
.btn-scad-outline {
|
||||
background: transparent;
|
||||
border: 1.5px solid var(--scad-primary);
|
||||
color: var(--scad-primary);
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.45rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-scad-outline:hover {
|
||||
background: var(--scad-primary);
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.btn-action {
|
||||
width: 32px; height: 32px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 0.4rem; border: none;
|
||||
transition: all 0.2s; font-size: 0.78rem;
|
||||
}
|
||||
.btn-action-edit { background: #eef3ff; color: var(--scad-primary); }
|
||||
.btn-action-edit:hover { background: var(--scad-primary); color: #fff; transform: translateY(-1px); box-shadow: 0 3px 8px rgba(90,143,216,0.3); }
|
||||
.btn-action-complete { background: #e8f5e9; color: var(--scad-green); }
|
||||
.btn-action-complete:hover { background: var(--scad-green); color: #fff; transform: translateY(-1px); box-shadow: 0 3px 8px rgba(25,135,84,0.3); }
|
||||
.btn-action-delete { background: #fff0f0; color: var(--scad-red); }
|
||||
.btn-action-delete:hover { background: var(--scad-red); color: #fff; transform: translateY(-1px); box-shadow: 0 3px 8px rgba(220,53,69,0.3); }
|
||||
|
||||
/* Status badges */
|
||||
.badge-status {
|
||||
font-weight: 600; font-size: 0.75rem;
|
||||
padding: 0.35em 0.65em; border-radius: 2rem;
|
||||
display: inline-block; white-space: nowrap;
|
||||
}
|
||||
.badge-attiva { background: #e8eeff; color: #3a6bb5; }
|
||||
.badge-scaduta { background: #fde8e8; color: #b91c1c; }
|
||||
.badge-in-scadenza{ background: #fef3cd; color: #92600a; }
|
||||
.badge-completata { background: #d1f2e0; color: #0f5132; }
|
||||
|
||||
/* Row coloring */
|
||||
#deadlinesTable tbody tr.row-overdue { background-color: #fff5f5; }
|
||||
#deadlinesTable tbody tr.row-approaching { background-color: #fffbeb; }
|
||||
#deadlinesTable tbody tr.row-completed { opacity: 0.6; }
|
||||
#deadlinesTable tbody tr:hover { filter: brightness(0.97); }
|
||||
|
||||
/* Filter bar */
|
||||
.filter-bar .form-select {
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
border-color: #d0d9e8;
|
||||
min-width: 160px;
|
||||
}
|
||||
.filter-bar .form-select:focus {
|
||||
border-color: var(--scad-primary);
|
||||
box-shadow: 0 0 0 0.2rem rgba(90,143,216,0.15);
|
||||
}
|
||||
|
||||
/* Modal groups */
|
||||
.form-section-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--scad-heading);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 2px solid #e8eeff;
|
||||
}
|
||||
.modal-backdrop { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; }
|
||||
.modal { position: fixed; }
|
||||
.modal-content { background: #fff !important; }
|
||||
.modal-body { background: #fff !important; }
|
||||
.modal-footer { background: #fff !important; }
|
||||
.modal-header {
|
||||
background: var(--scad-card-bg);
|
||||
border-bottom: 1px solid var(--scad-card-border);
|
||||
}
|
||||
.modal-title { font-weight: 700; color: var(--scad-heading); }
|
||||
|
||||
/* Empty state */
|
||||
.empty-state { text-align: center; padding: 3rem 1rem; color: #8e99b0; }
|
||||
.empty-state i { font-size: 2.5rem; margin-bottom: 0.75rem; color: #c5cfe0; display: block; }
|
||||
|
||||
/* Table text overflow */
|
||||
#deadlinesTable td { max-width: 250px; overflow: hidden; text-overflow: ellipsis; }
|
||||
#deadlinesTable td:first-child { max-width: 150px; }
|
||||
|
||||
/* Attachment list in modal */
|
||||
.att-item {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0.5rem 0.75rem; background: #f8f9fa; border-radius: 0.4rem;
|
||||
margin-bottom: 0.4rem; font-size: 0.85rem;
|
||||
}
|
||||
.att-item .att-name { color: var(--scad-heading); font-weight: 500; word-break: break-all; }
|
||||
.att-item .att-actions { display: flex; gap: 0.4rem; flex-shrink: 0; margin-left: 0.5rem; }
|
||||
.att-item .att-actions a, .att-item .att-actions button {
|
||||
width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 0.3rem; border: none; font-size: 0.75rem; transition: all 0.15s;
|
||||
}
|
||||
.att-item .att-download { background: #eef3ff; color: var(--scad-primary); text-decoration: none; }
|
||||
.att-item .att-download:hover { background: var(--scad-primary); color: #fff; }
|
||||
.att-item .att-remove { background: #fff0f0; color: var(--scad-red); cursor: pointer; }
|
||||
.att-item .att-remove:hover { background: var(--scad-red); color: #fff; }
|
||||
|
||||
/* Select2 tweaks */
|
||||
.select2-container--bootstrap-5 .select2-selection { min-height: 38px; border-color: #d0d9e8; }
|
||||
|
||||
/* Mobile cards */
|
||||
.deadline-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||
padding: 1rem 1rem 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
transition: box-shadow 0.2s;
|
||||
border-left: 5px solid var(--scad-blue);
|
||||
background: #fff;
|
||||
}
|
||||
.deadline-card:active { box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||
.deadline-card.card-overdue { border-left-color: var(--scad-red); background: #fff8f8; border-color: #fcd5d5; }
|
||||
.deadline-card.card-approaching { border-left-color: var(--scad-orange); background: #fffdf5; border-color: #fce9b8; }
|
||||
.deadline-card.card-completed { border-left-color: #adb5bd; opacity: 0.55; border-color: #dee2e6; }
|
||||
.deadline-card .card-topic {
|
||||
font-weight: 700; font-size: 0.95rem; color: var(--scad-heading);
|
||||
margin-bottom: 0.35rem; line-height: 1.3;
|
||||
}
|
||||
.deadline-card .card-meta {
|
||||
font-size: 0.8rem; color: #6c757d;
|
||||
display: flex; flex-wrap: wrap; gap: 0.3rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.deadline-card .card-meta i { width: 14px; text-align: center; }
|
||||
.deadline-card .card-actions {
|
||||
display: flex; gap: 0.5rem; align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.scad-card .card-header { flex-direction: column; gap: 0.75rem; }
|
||||
.header-actions { width: 100%; }
|
||||
.header-actions .btn { width: 100%; justify-content: center; }
|
||||
.filter-bar .form-select { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('../include/navbar.php'); ?>
|
||||
<?php include('../include/topbar.php'); ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="filter-bar mb-3">
|
||||
<div class="row g-2 align-items-center mb-2">
|
||||
<div class="col-12 col-sm-6 col-md-auto">
|
||||
<select id="filterStatus" class="form-select">
|
||||
<option value="non-completata" selected>Stato: Non completate</option>
|
||||
<option value="">Stato: Tutti</option>
|
||||
<option value="attiva">Attive</option>
|
||||
<option value="in-scadenza">In scadenza</option>
|
||||
<option value="scaduta">Scadute</option>
|
||||
<option value="completata">Completate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-auto">
|
||||
<select id="filterDepartment" class="form-select">
|
||||
<option value="">Reparto: Tutti</option>
|
||||
<?php foreach ($departments as $dept): ?>
|
||||
<option value="<?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-auto">
|
||||
<select id="filterEmployee" class="form-select">
|
||||
<option value="">Responsabile: Tutti</option>
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<option value="<?= htmlspecialchars(trim($emp['first_name'] . ' ' . $emp['last_name']), ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars(trim($emp['first_name'] . ' ' . $emp['last_name']), ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-12 col-sm-6 col-md-auto">
|
||||
<input type="text" class="form-control" id="filterDueRange" placeholder="Scadenza: da — a" readonly style="font-size:0.85rem;border-color:#d0d9e8;min-width:220px;background:#fff">
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-auto">
|
||||
<input type="text" class="form-control" id="filterCheckRange" placeholder="Controllo: da — a" readonly style="font-size:0.85rem;border-color:#d0d9e8;min-width:220px;background:#fff">
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto">
|
||||
<button id="btnResetFilters" class="btn btn-sm btn-light border w-100 w-sm-auto" title="Reset filtri">
|
||||
<i class="fa-solid fa-rotate-left me-1"></i> Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Card -->
|
||||
<div class="card scad-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5><i class="fa-solid fa-calendar-check me-2"></i>Lista Scadenze</h5>
|
||||
<div class="header-actions d-flex gap-2 flex-wrap">
|
||||
<a href="scadenzario/calendar.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-calendar-days"></i><span>Calendario</span>
|
||||
</a>
|
||||
<button class="btn btn-scad-outline d-inline-flex align-items-center gap-2" id="btnStampa">
|
||||
<i class="fa-solid fa-print"></i><span>Stampa</span>
|
||||
</button>
|
||||
<button class="btn btn-scad-primary d-inline-flex align-items-center gap-2" id="btnAddDeadline">
|
||||
<i class="fa-solid fa-plus"></i><span>Nuova Scadenza</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (count($deadlines) === 0): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-calendar-xmark"></i>
|
||||
<p>Nessuna scadenza registrata.<br>Clicca <strong>"Nuova Scadenza"</strong> per aggiungere la prima.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<?php
|
||||
// Pre-compute status for each row (used by both views)
|
||||
$processedDeadlines = [];
|
||||
foreach ($deadlines as $row) {
|
||||
$dueDate = $row['due_date'];
|
||||
$nDays = (int)$row['notification_days'];
|
||||
$approachDate = date('Y-m-d', strtotime($today . ' + ' . $nDays . ' days'));
|
||||
$isCompleted = $row['status'] === 'completed';
|
||||
$isOverdue = !$isCompleted && $dueDate < $today;
|
||||
$isApproaching = !$isCompleted && !$isOverdue && $dueDate <= $approachDate;
|
||||
|
||||
if ($isCompleted) { $statusLabel = 'Completata'; $statusClass = 'badge-completata'; $statusKey = 'completata'; $rowClass = 'row-completed'; $cardClass = 'card-completed'; }
|
||||
elseif ($isOverdue) { $statusLabel = 'Scaduta'; $statusClass = 'badge-scaduta'; $statusKey = 'scaduta'; $rowClass = 'row-overdue'; $cardClass = 'card-overdue'; }
|
||||
elseif ($isApproaching) { $statusLabel = 'In scadenza'; $statusClass = 'badge-in-scadenza'; $statusKey = 'in-scadenza'; $rowClass = 'row-approaching'; $cardClass = 'card-approaching'; }
|
||||
else { $statusLabel = 'Attiva'; $statusClass = 'badge-attiva'; $statusKey = 'attiva'; $rowClass = ''; $cardClass = ''; }
|
||||
|
||||
$row['_dueFmt'] = date('d/m/Y', strtotime($dueDate));
|
||||
$row['_checkFmt'] = $row['check_date'] ? date('d/m/Y', strtotime($row['check_date'])) : '—';
|
||||
$row['_statusLabel'] = $statusLabel;
|
||||
$row['_statusClass'] = $statusClass;
|
||||
$row['_statusKey'] = $statusKey;
|
||||
$row['_rowClass'] = $rowClass;
|
||||
$row['_cardClass'] = $cardClass;
|
||||
$row['_isCompleted'] = $isCompleted;
|
||||
|
||||
// Merge assigned departments + employee departments
|
||||
$allDepts = [];
|
||||
if (!empty($row['reparti_assegnati'])) $allDepts = array_map('trim', explode(',', $row['reparti_assegnati']));
|
||||
if (!empty($row['reparti_persone'])) $allDepts = array_merge($allDepts, array_map('trim', explode(',', $row['reparti_persone'])));
|
||||
$row['reparti'] = implode(', ', array_unique(array_filter($allDepts)));
|
||||
|
||||
$processedDeadlines[] = $row;
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- MOBILE: Cards (visible < md) -->
|
||||
<div class="d-md-none" id="mobileCards">
|
||||
<?php foreach ($processedDeadlines as $row): ?>
|
||||
<div class="deadline-card <?= $row['_cardClass'] ?>"
|
||||
data-id="<?= (int)$row['id'] ?>"
|
||||
data-status="<?= $row['_statusKey'] ?>"
|
||||
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?php if ($row['category']): ?>
|
||||
<div class="text-muted mb-1" style="font-size:0.75rem;font-weight:600;text-transform:uppercase;letter-spacing:0.03em"><?= htmlspecialchars($row['category'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex justify-content-between align-items-start mb-1">
|
||||
<a href="scadenzario/detail.php?id=<?= (int)$row['id'] ?>" class="card-topic text-decoration-none"><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></a>
|
||||
<span class="badge-status <?= $row['_statusClass'] ?> ms-2 flex-shrink-0"><?= $row['_statusLabel'] ?></span>
|
||||
</div>
|
||||
<div class="card-meta">
|
||||
<span><i class="fa-regular fa-calendar"></i> <?= $row['_dueFmt'] ?></span>
|
||||
<?php if ($row['reparti']): ?>
|
||||
<span><i class="fa-regular fa-building"></i> <?= htmlspecialchars($row['reparti'], ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($row['responsabili']): ?>
|
||||
<span><i class="fa-regular fa-user"></i> <?= htmlspecialchars($row['responsabili'], ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($row['law_regulation']): ?>
|
||||
<span><i class="fa-regular fa-file-lines"></i> <?= htmlspecialchars($row['law_regulation'], ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ((int)$row['attachment_count'] > 0): ?>
|
||||
<span><i class="fa-solid fa-paperclip"></i> <?= (int)$row['attachment_count'] ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if (!$row['_isCompleted']): ?>
|
||||
<div class="card-actions">
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn-action btn-action-edit btn-edit" title="Modifica"><i class="fa-solid fa-pen"></i></button>
|
||||
<button class="btn-action btn-action-complete btn-complete" title="Completa"><i class="fa-solid fa-check"></i></button>
|
||||
<button class="btn-action btn-action-delete btn-delete" title="Elimina"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- DESKTOP: DataTable (visible >= md) -->
|
||||
<div class="d-none d-md-block">
|
||||
<div class="table-responsive">
|
||||
<table id="deadlinesTable" class="table table-hover align-middle mb-0" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Argomento</th>
|
||||
<th>Dettaglio</th>
|
||||
<th class="d-none d-lg-table-cell">Legge/Art.</th>
|
||||
<th>Scadenza</th>
|
||||
<th class="d-none d-lg-table-cell">Verifica</th>
|
||||
<th>Responsabili</th>
|
||||
<th>Stato</th>
|
||||
<th class="text-center" style="width:120px">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($processedDeadlines as $row): ?>
|
||||
<tr class="<?= $row['_rowClass'] ?>"
|
||||
data-id="<?= (int)$row['id'] ?>"
|
||||
data-status="<?= $row['_statusKey'] ?>"
|
||||
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
|
||||
<td><?= htmlspecialchars($row['category'] ?? '—', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td>
|
||||
<a href="scadenzario/detail.php?id=<?= (int)$row['id'] ?>" class="fw-semibold text-decoration-none" style="color:var(--scad-heading)"><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></a>
|
||||
<?php if ((int)$row['attachment_count'] > 0): ?>
|
||||
<span class="text-muted ms-1" title="<?= (int)$row['attachment_count'] ?> allegati"><i class="fa-solid fa-paperclip"></i> <?= (int)$row['attachment_count'] ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="d-none d-lg-table-cell text-muted"><?= htmlspecialchars($row['law_regulation'] ?? '—', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><span class="text-nowrap"><?= $row['_dueFmt'] ?></span></td>
|
||||
<td class="d-none d-lg-table-cell text-muted"><?= $row['_checkFmt'] ?></td>
|
||||
<td>
|
||||
<?php if ($row['reparti']): ?><span class="text-muted"><i class="fa-regular fa-building me-1"></i><?= htmlspecialchars($row['reparti'], ENT_QUOTES, 'UTF-8') ?></span><?php endif; ?>
|
||||
<?php if ($row['reparti'] && $row['responsabili']): ?><br><?php endif; ?>
|
||||
<?php if ($row['responsabili']): ?><span><i class="fa-regular fa-user me-1"></i><?= htmlspecialchars($row['responsabili'], ENT_QUOTES, 'UTF-8') ?></span><?php endif; ?>
|
||||
<?php if (!$row['reparti'] && !$row['responsabili']): ?>—<?php endif; ?>
|
||||
</td>
|
||||
<td><span class="badge-status <?= $row['_statusClass'] ?>"><?= $row['_statusLabel'] ?></span></td>
|
||||
<td class="text-center">
|
||||
<?php if (!$row['_isCompleted']): ?>
|
||||
<div class="d-inline-flex gap-1">
|
||||
<button class="btn-action btn-action-edit btn-edit" title="Modifica"><i class="fa-solid fa-pen"></i></button>
|
||||
<button class="btn-action btn-action-complete btn-complete" title="Completa"><i class="fa-solid fa-check"></i></button>
|
||||
<button class="btn-action btn-action-delete btn-delete" title="Elimina"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include('../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<!-- Deadline Modal -->
|
||||
<div class="modal fade" id="deadlineModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">Nuova Scadenza</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<form id="deadlineForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="dlId" name="id" value="">
|
||||
|
||||
<!-- Group 1: Informazioni principali -->
|
||||
<div class="form-section-title">Informazioni principali</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="dlCategory" class="form-label fw-semibold">Argomento</label>
|
||||
<input type="text" class="form-control" id="dlCategory" name="category" maxlength="100" list="categoryList" placeholder="es. Sicurezza, Ambiente, Antincendio...">
|
||||
<datalist id="categoryList">
|
||||
<?php
|
||||
$cats = $pdo->query("SELECT DISTINCT category FROM scad_deadlines WHERE category IS NOT NULL AND category != '' ORDER BY category")->fetchAll(PDO::FETCH_COLUMN);
|
||||
foreach ($cats as $c): ?>
|
||||
<option value="<?= htmlspecialchars($c, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
|
||||
<input type="text" class="form-control" id="dlLaw" name="law_regulation" maxlength="500" placeholder="es. D.Lgs. 81/2008, D.M. 10.03.1998...">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlTopic" class="form-label fw-semibold">Dettaglio <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="dlTopic" name="topic" required maxlength="500" rows="2" placeholder="es. Verifica estintori, Autorizzazione trasporto rifiuti..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 2: Date e frequenza -->
|
||||
<div class="form-section-title">Date e frequenza</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlRecurrence" class="form-label fw-semibold">Periodicità</label>
|
||||
<select class="form-select" id="dlRecurrence" name="recurrence_type">
|
||||
<option value="once">Una tantum</option>
|
||||
<option value="monthly">Mensile</option>
|
||||
<option value="quarterly">Trimestrale</option>
|
||||
<option value="semiannual">Semestrale</option>
|
||||
<option value="annual">Annuale</option>
|
||||
<option value="biennial">Biennale</option>
|
||||
<option value="triennial">Triennale</option>
|
||||
<option value="quinquennial">Quinquennale</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlDocDate" class="form-label fw-semibold">Data documento</label>
|
||||
<input type="date" class="form-control" id="dlDocDate" name="document_date">
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlDueDate" class="form-label fw-semibold">Data scadenza <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" id="dlDueDate" name="due_date" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlCheckDate" class="form-label fw-semibold">Data ultimo controllo</label>
|
||||
<input type="date" class="form-control" id="dlCheckDate" name="check_date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 3: Responsabili -->
|
||||
<div class="form-section-title">Responsabili</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
|
||||
<select class="form-select" id="dlDepartments" name="department_names[]" multiple>
|
||||
<?php foreach ($departments as $dept): ?>
|
||||
<option value="<?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($dept, ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">Tutto il reparto sarà responsabile</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlEmployees" class="form-label fw-semibold">Singoli responsabili</label>
|
||||
<select class="form-select" id="dlEmployees" name="employee_ids[]" multiple>
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<option value="<?= (int)$emp['id'] ?>">
|
||||
<?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name'], ENT_QUOTES, 'UTF-8') ?><?php if ($emp['department']): ?> (<?= htmlspecialchars($emp['department'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 4: Dettagli aggiuntivi -->
|
||||
<div class="form-section-title">Dettagli aggiuntivi</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlNotifDays" class="form-label fw-semibold">Giorni preavviso</label>
|
||||
<input type="number" class="form-control" id="dlNotifDays" name="notification_days" value="7" min="1" max="365">
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<label for="dlStorage" class="form-label fw-semibold">Luogo archiviazione</label>
|
||||
<input type="text" class="form-control" id="dlStorage" name="storage_location" maxlength="500" placeholder="es. Armadio A3, Server/Documenti/Sicurezza...">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlNotes" class="form-label fw-semibold">Note</label>
|
||||
<textarea class="form-control" id="dlNotes" name="notes" rows="3" placeholder="es. Scadenza 09/06/2026, Attività in appalto a Ditta specializzata..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 5: Allegati -->
|
||||
<div class="form-section-title mt-4">Allegati</div>
|
||||
<div id="attachmentsList" class="mb-3"></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label for="dlFiles" class="form-label fw-semibold">Carica file</label>
|
||||
<input type="file" class="form-control" id="dlFiles" multiple>
|
||||
<div class="form-text">Puoi selezionare più file contemporaneamente</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-scad-primary">
|
||||
<i class="fa-solid fa-check me-1"></i> Salva
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('../jsinclude.php'); ?>
|
||||
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.7/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/i18n/it.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/it.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
// --- Flatpickr date range ---
|
||||
function formatDate(d) {
|
||||
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
|
||||
}
|
||||
var fpOpts = {
|
||||
mode: 'range',
|
||||
dateFormat: 'd/m/Y',
|
||||
locale: 'it',
|
||||
allowInput: false,
|
||||
onChange: function() {
|
||||
if (table) table.draw();
|
||||
filterCards();
|
||||
}
|
||||
};
|
||||
var fpDue = flatpickr('#filterDueRange', fpOpts);
|
||||
var fpCheck = flatpickr('#filterCheckRange', fpOpts);
|
||||
|
||||
// --- Select2 ---
|
||||
$('#dlDepartments').select2({
|
||||
theme: 'bootstrap-5',
|
||||
placeholder: 'Seleziona reparti...',
|
||||
allowClear: true,
|
||||
dropdownParent: $('#deadlineModal .modal-body'),
|
||||
language: 'it',
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
$('#dlEmployees').select2({
|
||||
theme: 'bootstrap-5',
|
||||
placeholder: 'Seleziona persone...',
|
||||
allowClear: true,
|
||||
dropdownParent: $('#deadlineModal .modal-body'),
|
||||
language: 'it',
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
// --- DataTables custom filters ---
|
||||
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
|
||||
if (settings.nTable.id !== 'deadlinesTable') return true;
|
||||
var row = settings.aoData[dataIndex].nTr;
|
||||
var statusFilter = $('#filterStatus').val();
|
||||
var deptFilter = $('#filterDepartment').val();
|
||||
|
||||
if (statusFilter === 'non-completata') { if (row.getAttribute('data-status') === 'completata') return false; }
|
||||
else if (statusFilter && row.getAttribute('data-status') !== statusFilter) return false;
|
||||
if (deptFilter && (row.getAttribute('data-department') || '').indexOf(deptFilter) === -1) return false;
|
||||
var empFilter = $('#filterEmployee').val();
|
||||
if (empFilter && (row.getAttribute('data-employees') || '').indexOf(empFilter) === -1) return false;
|
||||
var dueDates = fpDue.selectedDates;
|
||||
var dueVal = row.getAttribute('data-due-date') || '';
|
||||
if (dueDates.length >= 1 && dueVal < formatDate(dueDates[0])) return false;
|
||||
if (dueDates.length >= 2 && dueVal > formatDate(dueDates[1])) return false;
|
||||
var chkDates = fpCheck.selectedDates;
|
||||
var chkVal = row.getAttribute('data-check-date') || '';
|
||||
if (chkDates.length >= 1 && (!chkVal || chkVal < formatDate(chkDates[0]))) return false;
|
||||
if (chkDates.length >= 2 && (!chkVal || chkVal > formatDate(chkDates[1]))) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// --- DataTables init ---
|
||||
var table = null;
|
||||
if ($('#deadlinesTable').length) {
|
||||
table = $('#deadlinesTable').DataTable({
|
||||
order: [[3, 'asc']],
|
||||
pageLength: 25,
|
||||
language: {
|
||||
url: 'https://cdn.datatables.net/plug-ins/1.13.7/i18n/it-IT.json'
|
||||
},
|
||||
columnDefs: [
|
||||
{ orderable: false, targets: -1 }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// --- Filters ---
|
||||
function filterCards() {
|
||||
var statusVal = $('#filterStatus').val();
|
||||
var deptVal = $('#filterDepartment').val();
|
||||
$('#mobileCards .deadline-card').each(function() {
|
||||
var show = true;
|
||||
if (statusVal === 'non-completata') { if ($(this).data('status') === 'completata') show = false; }
|
||||
else if (statusVal && $(this).data('status') !== statusVal) show = false;
|
||||
if (deptVal && ($(this).data('department') || '').indexOf(deptVal) === -1) show = false;
|
||||
var empVal = $('#filterEmployee').val();
|
||||
if (empVal && ($(this).data('employees') || '').indexOf(empVal) === -1) show = false;
|
||||
var dueDates = fpDue.selectedDates;
|
||||
var dueVal = $(this).data('due-date') || '';
|
||||
if (dueDates.length >= 1 && dueVal < formatDate(dueDates[0])) show = false;
|
||||
if (dueDates.length >= 2 && dueVal > formatDate(dueDates[1])) show = false;
|
||||
var chkDates = fpCheck.selectedDates;
|
||||
var chkVal = $(this).data('check-date') || '';
|
||||
if (chkDates.length >= 1 && (!chkVal || chkVal < formatDate(chkDates[0]))) show = false;
|
||||
if (chkDates.length >= 2 && (!chkVal || chkVal > formatDate(chkDates[1]))) show = false;
|
||||
$(this).toggle(show);
|
||||
});
|
||||
}
|
||||
|
||||
$('#filterStatus, #filterDepartment, #filterEmployee').on('change', function() {
|
||||
if (table) table.draw();
|
||||
filterCards();
|
||||
});
|
||||
$('#btnResetFilters').on('click', function() {
|
||||
$('#filterStatus').val('non-completata');
|
||||
$('#filterDepartment').val('');
|
||||
$('#filterEmployee').val('');
|
||||
fpDue.clear();
|
||||
fpCheck.clear();
|
||||
if (table) table.draw();
|
||||
filterCards();
|
||||
});
|
||||
|
||||
// Apply default filter on load
|
||||
if (table) table.draw();
|
||||
filterCards();
|
||||
|
||||
// --- Modal ---
|
||||
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
|
||||
var form = document.getElementById('deadlineForm');
|
||||
|
||||
// Add
|
||||
document.getElementById('btnAddDeadline').addEventListener('click', function() {
|
||||
form.reset();
|
||||
document.getElementById('dlId').value = '';
|
||||
document.getElementById('dlNotifDays').value = '7';
|
||||
document.getElementById('modalTitle').textContent = 'Nuova Scadenza';
|
||||
document.getElementById('dlFiles').value = '';
|
||||
$('#dlDepartments').val(null).trigger('change');
|
||||
$('#dlEmployees').val(null).trigger('change');
|
||||
renderAttachments([]);
|
||||
modal.show();
|
||||
});
|
||||
|
||||
// Save
|
||||
var isSaving = false;
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
if (isSaving) return;
|
||||
isSaving = true;
|
||||
var saveBtn = form.querySelector('[type="submit"]');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin me-1"></i> Salvataggio...';
|
||||
var formData = new FormData(form);
|
||||
|
||||
fetch('scadenzario/ajax/save_deadline.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
var deadlineId = data.id;
|
||||
var fileInput = document.getElementById('dlFiles');
|
||||
if (fileInput.files.length > 0) {
|
||||
// Upload files
|
||||
var fileData = new FormData();
|
||||
fileData.append('deadline_id', deadlineId);
|
||||
for (var i = 0; i < fileInput.files.length; i++) {
|
||||
fileData.append('files[]', fileInput.files[i]);
|
||||
}
|
||||
return fetch('scadenzario/ajax/upload_attachment.php', { method: 'POST', body: fileData })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(upData) {
|
||||
modal.hide();
|
||||
Swal.fire({ icon: 'success', title: 'Salvato', text: data.message + ' ' + upData.message, timer: 2000, showConfirmButton: false })
|
||||
.then(function() { location.reload(); });
|
||||
});
|
||||
} else {
|
||||
modal.hide();
|
||||
Swal.fire({ icon: 'success', title: 'Salvato', text: data.message, timer: 1500, showConfirmButton: false })
|
||||
.then(function() { location.reload(); });
|
||||
}
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
isSaving = false; saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
isSaving = false; saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
|
||||
});
|
||||
});
|
||||
|
||||
// Render attachments list
|
||||
function renderAttachments(attachments) {
|
||||
var container = document.getElementById('attachmentsList');
|
||||
if (!attachments || attachments.length === 0) {
|
||||
container.innerHTML = '<div class="text-muted" style="font-size:0.85rem">Nessun allegato</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = attachments.map(function(a) {
|
||||
return '<div class="att-item" data-att-id="' + a.id + '">' +
|
||||
'<span class="att-name"><i class="fa-solid fa-paperclip me-1"></i>' + $('<span>').text(a.original_name).html() + '</span>' +
|
||||
'<span class="att-actions">' +
|
||||
'<a href="scadenzario/ajax/download_attachment.php?id=' + a.id + '" class="att-download" title="Scarica"><i class="fa-solid fa-download"></i></a>' +
|
||||
'<button type="button" class="att-remove" title="Elimina" data-att-id="' + a.id + '"><i class="fa-solid fa-xmark"></i></button>' +
|
||||
'</span></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Delete attachment
|
||||
$(document).on('click', '.att-remove', function(e) {
|
||||
e.preventDefault();
|
||||
var btn = $(this);
|
||||
var attId = btn.data('att-id');
|
||||
Swal.fire({
|
||||
title: 'Eliminare allegato?',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Elimina'
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
fetch('scadenzario/ajax/delete_attachment.php?id=' + attId)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
btn.closest('.att-item').remove();
|
||||
if ($('#attachmentsList .att-item').length === 0) {
|
||||
renderAttachments([]);
|
||||
}
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Edit
|
||||
$(document).on('click', '.btn-edit', function() {
|
||||
var id = $(this).closest('[data-id]').data('id');
|
||||
fetch('scadenzario/ajax/get_deadline.php?id=' + id)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (!data.success) { Swal.fire('Errore', data.message, 'error'); return; }
|
||||
var d = data.data;
|
||||
document.getElementById('dlId').value = d.id;
|
||||
document.getElementById('dlCategory').value = d.category || '';
|
||||
document.getElementById('dlTopic').value = d.topic || '';
|
||||
document.getElementById('dlLaw').value = d.law_regulation || '';
|
||||
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
|
||||
document.getElementById('dlDocDate').value = d.document_date || '';
|
||||
document.getElementById('dlDueDate').value = d.due_date || '';
|
||||
document.getElementById('dlCheckDate').value = d.check_date || '';
|
||||
document.getElementById('dlNotifDays').value = d.notification_days || 7;
|
||||
document.getElementById('dlStorage').value = d.storage_location || '';
|
||||
document.getElementById('dlNotes').value = d.notes || '';
|
||||
document.getElementById('dlFiles').value = '';
|
||||
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
|
||||
$('#dlDepartments').val(d.department_names || []).trigger('change');
|
||||
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
|
||||
renderAttachments(d.attachments || []);
|
||||
modal.show();
|
||||
})
|
||||
.catch(function() { Swal.fire('Errore', 'Errore di connessione.', 'error'); });
|
||||
});
|
||||
|
||||
// Complete
|
||||
$(document).on('click', '.btn-complete', function() {
|
||||
var el = $(this).closest('[data-id]');
|
||||
var id = el.data('id');
|
||||
Swal.fire({
|
||||
title: 'Completare la scadenza?',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#198754',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Completa'
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
fetch('scadenzario/ajax/complete_deadline.php?id=' + id)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
Swal.fire({ icon: 'success', title: 'Completata', text: data.message, timer: 2500, showConfirmButton: false })
|
||||
.then(function() { location.reload(); });
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() { Swal.fire('Errore', 'Errore di connessione.', 'error'); });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete
|
||||
$(document).on('click', '.btn-delete', function() {
|
||||
var el = $(this).closest('[data-id]');
|
||||
var id = el.data('id');
|
||||
Swal.fire({
|
||||
title: 'Eliminare la scadenza?',
|
||||
text: 'Questa azione non può essere annullata.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Elimina'
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
fetch('scadenzario/ajax/delete_deadline.php?id=' + id)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
Swal.fire({ icon: 'success', title: 'Eliminata', text: data.message, timer: 1500, showConfirmButton: false })
|
||||
.then(function() { location.reload(); });
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() { Swal.fire('Errore', 'Errore di connessione.', 'error'); });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-open edit modal from ?edit=ID
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var editId = urlParams.get('edit');
|
||||
if (editId) {
|
||||
history.replaceState(null, '', 'scadenzario/index.php');
|
||||
fetch('scadenzario/ajax/get_deadline.php?id=' + editId)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (!data.success) return;
|
||||
var d = data.data;
|
||||
document.getElementById('dlId').value = d.id;
|
||||
document.getElementById('dlCategory').value = d.category || '';
|
||||
document.getElementById('dlTopic').value = d.topic || '';
|
||||
document.getElementById('dlLaw').value = d.law_regulation || '';
|
||||
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
|
||||
document.getElementById('dlDocDate').value = d.document_date || '';
|
||||
document.getElementById('dlDueDate').value = d.due_date || '';
|
||||
document.getElementById('dlCheckDate').value = d.check_date || '';
|
||||
document.getElementById('dlNotifDays').value = d.notification_days || 7;
|
||||
document.getElementById('dlStorage').value = d.storage_location || '';
|
||||
document.getElementById('dlNotes').value = d.notes || '';
|
||||
document.getElementById('dlFiles').value = '';
|
||||
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
|
||||
$('#dlDepartments').val(d.department_names || []).trigger('change');
|
||||
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
|
||||
renderAttachments(d.attachments || []);
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Stampa
|
||||
document.getElementById('btnStampa').addEventListener('click', function() {
|
||||
var params = [];
|
||||
var s = $('#filterStatus').val();
|
||||
var d = $('#filterDepartment').val();
|
||||
var emp = $('#filterEmployee').val();
|
||||
if (s) params.push('status=' + encodeURIComponent(s));
|
||||
if (d) params.push('department=' + encodeURIComponent(d));
|
||||
if (emp) params.push('employee=' + encodeURIComponent(emp));
|
||||
var dueDates = fpDue.selectedDates;
|
||||
if (dueDates.length >= 1) params.push('due_from=' + formatDate(dueDates[0]));
|
||||
if (dueDates.length >= 2) params.push('due_to=' + formatDate(dueDates[1]));
|
||||
var chkDates = fpCheck.selectedDates;
|
||||
if (chkDates.length >= 1) params.push('check_from=' + formatDate(chkDates[0]));
|
||||
if (chkDates.length >= 2) params.push('check_to=' + formatDate(chkDates[1]));
|
||||
window.open('scadenzario/print.php' + (params.length ? '?' + params.join('&') : ''), '_blank');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
199
public/userarea/scadenzario/print.php
Normal file
199
public/userarea/scadenzario/print.php
Normal file
@ -0,0 +1,199 @@
|
||||
<?php include('../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$sql = "
|
||||
SELECT d.*,
|
||||
GROUP_CONCAT(DISTINCT CONCAT(e.first_name, ' ', e.last_name) ORDER BY e.first_name SEPARATOR ', ') as responsabili,
|
||||
GROUP_CONCAT(DISTINCT e.department ORDER BY e.department SEPARATOR ', ') as reparti_persone,
|
||||
d.departments as reparti_assegnati
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
|
||||
LEFT JOIN employees e ON e.id = de.employee_id
|
||||
";
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
$filterStatus = $_GET['status'] ?? '';
|
||||
$filterDept = $_GET['department'] ?? '';
|
||||
$filterEmployee = $_GET['employee'] ?? '';
|
||||
|
||||
if ($filterStatus === 'non-completata') {
|
||||
$where[] = "d.status != 'completed'";
|
||||
} elseif ($filterStatus === 'completata') {
|
||||
$where[] = "d.status = 'completed'";
|
||||
} elseif ($filterStatus === 'scaduta') {
|
||||
$where[] = "d.status = 'active' AND d.due_date < CURDATE()";
|
||||
} elseif ($filterStatus === 'in-scadenza') {
|
||||
$where[] = "d.status = 'active' AND d.due_date >= CURDATE() AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
} elseif ($filterStatus === 'attiva') {
|
||||
$where[] = "d.status = 'active' AND d.due_date > DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)";
|
||||
}
|
||||
|
||||
if ($filterEmployee) {
|
||||
$where[] = "EXISTS (SELECT 1 FROM scad_deadline_employee de2 JOIN employees e2 ON e2.id = de2.employee_id WHERE de2.deadline_id = d.id AND CONCAT(e2.first_name, ' ', e2.last_name) = ?)";
|
||||
$params[] = $filterEmployee;
|
||||
}
|
||||
|
||||
$dueFrom = $_GET['due_from'] ?? '';
|
||||
$dueTo = $_GET['due_to'] ?? '';
|
||||
$checkFrom = $_GET['check_from'] ?? '';
|
||||
$checkTo = $_GET['check_to'] ?? '';
|
||||
|
||||
if ($dueFrom) { $where[] = "d.due_date >= ?"; $params[] = $dueFrom; }
|
||||
if ($dueTo) { $where[] = "d.due_date <= ?"; $params[] = $dueTo; }
|
||||
if ($checkFrom) { $where[] = "d.check_date >= ?"; $params[] = $checkFrom; }
|
||||
if ($checkTo) { $where[] = "d.check_date <= ?"; $params[] = $checkTo; }
|
||||
|
||||
if (!empty($where)) {
|
||||
$sql .= " WHERE " . implode(' AND ', $where);
|
||||
}
|
||||
|
||||
$sql .= " GROUP BY d.id ORDER BY (d.status = 'completed') ASC, d.due_date ASC";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$recurrenceLabels = ['once'=>'Una tantum','monthly'=>'Mensile','quarterly'=>'Trimestrale','semiannual'=>'Semestrale','annual'=>'Annuale','biennial'=>'Biennale','triennial'=>'Triennale','quinquennial'=>'Quinquennale'];
|
||||
|
||||
$filterLabel = '';
|
||||
if ($filterStatus) {
|
||||
$statusLabels = ['non-completata'=>'Non completate','attiva'=>'Attive','in-scadenza'=>'In scadenza','scaduta'=>'Scadute','completata'=>'Completate'];
|
||||
$filterLabel = $statusLabels[$filterStatus] ?? '';
|
||||
}
|
||||
if ($filterDept) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Reparto: ' . $filterDept;
|
||||
}
|
||||
if ($filterEmployee) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Responsabile: ' . $filterEmployee;
|
||||
}
|
||||
if ($dueFrom || $dueTo) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Scadenza: ' . ($dueFrom ? date('d/m/Y', strtotime($dueFrom)) : '...') . ' → ' . ($dueTo ? date('d/m/Y', strtotime($dueTo)) : '...');
|
||||
}
|
||||
if ($checkFrom || $checkTo) {
|
||||
$filterLabel .= ($filterLabel ? ' — ' : '') . 'Controllo: ' . ($checkFrom ? date('d/m/Y', strtotime($checkFrom)) : '...') . ' → ' . ($checkTo ? date('d/m/Y', strtotime($checkTo)) : '...');
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Stampa Scadenzario</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 11px; color: #222; background: #fff; }
|
||||
.print-header { padding: 20px 20px 10px; border-bottom: 2px solid #2c3e6b; margin-bottom: 10px; }
|
||||
.print-header h1 { font-size: 18px; color: #2c3e6b; margin: 0; }
|
||||
.print-header .print-meta { font-size: 10px; color: #666; margin-top: 4px; }
|
||||
.print-actions { padding: 10px 20px; display: flex; gap: 10px; }
|
||||
.print-actions button { padding: 8px 20px; font-size: 13px; font-weight: 600; border-radius: 6px; cursor: pointer; border: none; }
|
||||
.btn-print { background: #5a8fd8; color: #fff; }
|
||||
.btn-back { background: #f0f0f0; color: #333; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 0 20px; }
|
||||
table { width: calc(100% - 40px); }
|
||||
th { background: #2c3e6b; color: #fff; font-weight: 600; font-size: 10px; text-transform: uppercase; letter-spacing: 0.03em; padding: 6px 8px; text-align: left; white-space: nowrap; }
|
||||
td { padding: 5px 8px; border-bottom: 1px solid #e0e0e0; vertical-align: top; font-size: 10.5px; line-height: 1.4; }
|
||||
tr:nth-child(even) { background: #f9fafb; }
|
||||
tr.row-overdue { background: #fff5f5; }
|
||||
tr.row-overdue td { color: #991b1b; }
|
||||
tr.row-approaching { background: #fffbeb; }
|
||||
tr.row-completed { opacity: 0.5; }
|
||||
.status-badge { font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 10px; white-space: nowrap; }
|
||||
.st-attiva { background: #e8eeff; color: #3a6bb5; }
|
||||
.st-scaduta { background: #fde8e8; color: #b91c1c; }
|
||||
.st-in-scadenza { background: #fef3cd; color: #92600a; }
|
||||
.st-completata { background: #d1f2e0; color: #0f5132; }
|
||||
.print-footer { padding: 10px 20px; font-size: 9px; color: #999; border-top: 1px solid #e0e0e0; margin-top: 10px; text-align: right; }
|
||||
@media print {
|
||||
.print-actions { display: none; }
|
||||
body { font-size: 9px; }
|
||||
th { font-size: 8.5px; padding: 4px 6px; }
|
||||
td { font-size: 9px; padding: 3px 6px; }
|
||||
@page { size: landscape; margin: 0.8cm; }
|
||||
}
|
||||
@media screen {
|
||||
table { margin: 0 auto; width: calc(100% - 40px); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="print-header">
|
||||
<h1>ELENCO PRESCRIZIONI LEGALI ED ALTRE PRESCRIZIONI APPLICABILI<br>SICUREZZA ED AMBIENTE CON VERIFICA DEL RISPETTO DELLE STESSE</h1>
|
||||
<div class="print-meta">
|
||||
Stampato il <?= date('d/m/Y H:i') ?>
|
||||
<?php if ($filterLabel): ?> — Filtro: <?= htmlspecialchars($filterLabel, ENT_QUOTES, 'UTF-8') ?><?php endif; ?>
|
||||
— Totale: <?= count($deadlines) ?> scadenze
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="print-actions">
|
||||
<button class="btn-print" onclick="window.print()"><i class="fa-solid fa-print"></i> Stampa</button>
|
||||
<button class="btn-back" onclick="window.close()">Chiudi</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Argomento</th>
|
||||
<th>Legge / Art.</th>
|
||||
<th>Dettaglio</th>
|
||||
<th>Periodicità Scadenza</th>
|
||||
<th>Data Documento</th>
|
||||
<th>Data Scadenza</th>
|
||||
<th>Data Ultimo Controllo</th>
|
||||
<th>Responsabilità</th>
|
||||
<th>Luogo di Archiviazione</th>
|
||||
<th>Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($deadlines as $row):
|
||||
$dueDate = $row['due_date'];
|
||||
$nDays = (int)$row['notification_days'];
|
||||
$approachDate = date('Y-m-d', strtotime($today . ' + ' . $nDays . ' days'));
|
||||
$isCompleted = $row['status'] === 'completed';
|
||||
$isOverdue = !$isCompleted && $dueDate < $today;
|
||||
$isApproaching = !$isCompleted && !$isOverdue && $dueDate <= $approachDate;
|
||||
|
||||
if ($isCompleted) { $statusLabel = 'Completata'; $stClass = 'st-completata'; $rowClass = 'row-completed'; }
|
||||
elseif ($isOverdue) { $statusLabel = 'Scaduta'; $stClass = 'st-scaduta'; $rowClass = 'row-overdue'; }
|
||||
elseif ($isApproaching) { $statusLabel = 'In scadenza'; $stClass = 'st-in-scadenza'; $rowClass = 'row-approaching'; }
|
||||
else { $statusLabel = 'Attiva'; $stClass = 'st-attiva'; $rowClass = ''; }
|
||||
|
||||
// Merge departments
|
||||
$allDepts = [];
|
||||
if (!empty($row['reparti_assegnati'])) $allDepts = array_map('trim', explode(',', $row['reparti_assegnati']));
|
||||
if (!empty($row['reparti_persone'])) $allDepts = array_merge($allDepts, array_map('trim', explode(',', $row['reparti_persone'])));
|
||||
$reparti = implode(', ', array_unique(array_filter($allDepts)));
|
||||
?>
|
||||
<tr class="<?= $rowClass ?>">
|
||||
<td><strong><?= htmlspecialchars($row['category'] ?? '', ENT_QUOTES, 'UTF-8') ?></strong></td>
|
||||
<td><?= htmlspecialchars($row['law_regulation'] ?? '', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($recurrenceLabels[$row['recurrence_type']] ?? $row['recurrence_type'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td style="white-space:nowrap"><?= $row['document_date'] ? date('d/m/Y', strtotime($row['document_date'])) : '' ?></td>
|
||||
<td style="white-space:nowrap"><?= date('d/m/Y', strtotime($dueDate)) ?></td>
|
||||
<td style="white-space:nowrap"><?= $row['check_date'] ? date('d/m/Y', strtotime($row['check_date'])) : '' ?></td>
|
||||
<td><?php
|
||||
$resp = [];
|
||||
if ($reparti) $resp[] = $reparti;
|
||||
if ($row['responsabili']) $resp[] = $row['responsabili'];
|
||||
echo htmlspecialchars(implode(', ', $resp), ENT_QUOTES, 'UTF-8');
|
||||
?></td>
|
||||
<td><?= htmlspecialchars($row['storage_location'] ?? '', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($row['notes'] ?? '', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="print-footer">
|
||||
ZIBOGOMMA — Scadenzario — <?= date('d/m/Y') ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
70
public/userarea/scadenzario/sql/create_tables.sql
Normal file
70
public/userarea/scadenzario/sql/create_tables.sql
Normal file
@ -0,0 +1,70 @@
|
||||
-- Scadenzario tables
|
||||
-- Responsible persons = employees (existing table)
|
||||
-- Departments = employees.department (varchar field)
|
||||
-- Notification email = employees.auth_user_id -> auth_users.email
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadlines (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
category VARCHAR(100) NULL,
|
||||
topic VARCHAR(500) NOT NULL,
|
||||
law_regulation VARCHAR(500) NULL,
|
||||
details TEXT NULL,
|
||||
recurrence_type VARCHAR(20) NOT NULL DEFAULT 'once',
|
||||
due_date DATE NOT NULL,
|
||||
check_date DATE NULL,
|
||||
document_date DATE NULL,
|
||||
notification_days SMALLINT UNSIGNED NOT NULL DEFAULT 7,
|
||||
storage_location VARCHAR(500) NULL,
|
||||
notes TEXT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
completed_at TIMESTAMP NULL,
|
||||
completed_by INT UNSIGNED NULL,
|
||||
created_by INT UNSIGNED NOT NULL,
|
||||
departments VARCHAR(500) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_due_date (due_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_employee (
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
employee_id INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (deadline_id, employee_id),
|
||||
CONSTRAINT fk_de_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_de_employee FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_attachments (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
original_name VARCHAR(500) NOT NULL,
|
||||
stored_name VARCHAR(500) NOT NULL,
|
||||
mime_type VARCHAR(100) NULL,
|
||||
size INT UNSIGNED NULL,
|
||||
uploaded_by INT UNSIGNED NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_att_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_histories (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
user_id INT UNSIGNED NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
changes JSON NULL,
|
||||
notes TEXT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_hist_deadline (deadline_id),
|
||||
CONSTRAINT fk_hist_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scad_deadline_notifications (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
deadline_id INT UNSIGNED NOT NULL,
|
||||
employee_id INT UNSIGNED NOT NULL,
|
||||
sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
type VARCHAR(30) NOT NULL,
|
||||
INDEX idx_notif_deadline (deadline_id),
|
||||
CONSTRAINT fk_notif_deadline FOREIGN KEY (deadline_id) REFERENCES scad_deadlines(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
Loading…
x
Reference in New Issue
Block a user