diff --git a/public/userarea/include/headscript.php b/public/userarea/include/headscript.php index 248df1e..2114c5f 100644 --- a/public/userarea/include/headscript.php +++ b/public/userarea/include/headscript.php @@ -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 diff --git a/public/userarea/include/navbar.php b/public/userarea/include/navbar.php index b92acc7..fa7b0c9 100644 --- a/public/userarea/include/navbar.php +++ b/public/userarea/include/navbar.php @@ -59,6 +59,22 @@ +
  • + +
    +
    + +
    + +
  • + diff --git a/public/userarea/production_dashboard.php b/public/userarea/production_dashboard.php index 25bda26..0bd0c60 100644 --- a/public/userarea/production_dashboard.php +++ b/public/userarea/production_dashboard.php @@ -381,7 +381,7 @@
    Magazzino
    - diff --git a/public/userarea/scadenzario/INSTALLATION.md b/public/userarea/scadenzario/INSTALLATION.md new file mode 100644 index 0000000..4616871 --- /dev/null +++ b/public/userarea/scadenzario/INSTALLATION.md @@ -0,0 +1,75 @@ +# Installation — Scadenzario + +## 1. Database + +Run the schema script: + +```bash +mysql -u -p < 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 = WHERE id = ; + ``` + +Employees without `auth_user_id` are silently skipped by the cron. diff --git a/public/userarea/scadenzario/ajax/auth_check.php b/public/userarea/scadenzario/ajax/auth_check.php new file mode 100644 index 0000000..c5c78b0 --- /dev/null +++ b/public/userarea/scadenzario/ajax/auth_check.php @@ -0,0 +1,18 @@ + false, 'message' => 'Non autorizzato. Effettua il login.']); + exit; +} + +$currentUserId = (int)$_SESSION['iduserlogin']; diff --git a/public/userarea/scadenzario/ajax/complete_deadline.php b/public/userarea/scadenzario/ajax/complete_deadline.php new file mode 100644 index 0000000..31dd90e --- /dev/null +++ b/public/userarea/scadenzario/ajax/complete_deadline.php @@ -0,0 +1,107 @@ + 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()]); +} diff --git a/public/userarea/scadenzario/ajax/delete_attachment.php b/public/userarea/scadenzario/ajax/delete_attachment.php new file mode 100644 index 0000000..ba9d2d0 --- /dev/null +++ b/public/userarea/scadenzario/ajax/delete_attachment.php @@ -0,0 +1,43 @@ + 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()]); +} diff --git a/public/userarea/scadenzario/ajax/delete_deadline.php b/public/userarea/scadenzario/ajax/delete_deadline.php new file mode 100644 index 0000000..d81b041 --- /dev/null +++ b/public/userarea/scadenzario/ajax/delete_deadline.php @@ -0,0 +1,26 @@ + 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()]); +} diff --git a/public/userarea/scadenzario/ajax/download_attachment.php b/public/userarea/scadenzario/ajax/download_attachment.php new file mode 100644 index 0000000..eb158f4 --- /dev/null +++ b/public/userarea/scadenzario/ajax/download_attachment.php @@ -0,0 +1,36 @@ +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; diff --git a/public/userarea/scadenzario/ajax/get_calendar_events.php b/public/userarea/scadenzario/ajax/get_calendar_events.php new file mode 100644 index 0000000..faaceaa --- /dev/null +++ b/public/userarea/scadenzario/ajax/get_calendar_events.php @@ -0,0 +1,92 @@ +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([]); +} diff --git a/public/userarea/scadenzario/ajax/get_deadline.php b/public/userarea/scadenzario/ajax/get_deadline.php new file mode 100644 index 0000000..baab80b --- /dev/null +++ b/public/userarea/scadenzario/ajax/get_deadline.php @@ -0,0 +1,45 @@ + 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()]); +} diff --git a/public/userarea/scadenzario/ajax/get_history.php b/public/userarea/scadenzario/ajax/get_history.php new file mode 100644 index 0000000..b75a06a --- /dev/null +++ b/public/userarea/scadenzario/ajax/get_history.php @@ -0,0 +1,49 @@ + 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()]); +} diff --git a/public/userarea/scadenzario/ajax/save_deadline.php b/public/userarea/scadenzario/ajax/save_deadline.php new file mode 100644 index 0000000..ce47ac3 --- /dev/null +++ b/public/userarea/scadenzario/ajax/save_deadline.php @@ -0,0 +1,116 @@ +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()]); +} diff --git a/public/userarea/scadenzario/ajax/upload_attachment.php b/public/userarea/scadenzario/ajax/upload_attachment.php new file mode 100644 index 0000000..770a92c --- /dev/null +++ b/public/userarea/scadenzario/ajax/upload_attachment.php @@ -0,0 +1,72 @@ + 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()]); +} diff --git a/public/userarea/scadenzario/attachments/.gitignore b/public/userarea/scadenzario/attachments/.gitignore new file mode 100644 index 0000000..e24a60f --- /dev/null +++ b/public/userarea/scadenzario/attachments/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!.htaccess diff --git a/public/userarea/scadenzario/attachments/.htaccess b/public/userarea/scadenzario/attachments/.htaccess new file mode 100644 index 0000000..3a42882 --- /dev/null +++ b/public/userarea/scadenzario/attachments/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/public/userarea/scadenzario/calendar.php b/public/userarea/scadenzario/calendar.php new file mode 100644 index 0000000..29b0fe2 --- /dev/null +++ b/public/userarea/scadenzario/calendar.php @@ -0,0 +1,222 @@ + +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); +?> + + + + + + + + + + + Calendario - Scadenzario + + + + +
    + + +
    +
    + + + + + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    Calendario Scadenze
    +
    +
    +
    +
    Attiva
    +
    In scadenza
    +
    Scaduta
    +
    Completata
    +
    +
    +
    +
    + +
    +
    + +
    + + + + diff --git a/public/userarea/scadenzario/cron/send_notifications.php b/public/userarea/scadenzario/cron/send_notifications.php new file mode 100644 index 0000000..dca2536 --- /dev/null +++ b/public/userarea/scadenzario/cron/send_notifications.php @@ -0,0 +1,220 @@ +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 ' . date('d/m/Y', strtotime($dl['due_date'])) . ' ed è stata superata da ' . abs($daysLeft) . ' giorni.', + '#dc3545', + $detailUrl + ); + } else { + $mail->Subject = '📅 Scadenza in arrivo: ' . $dl['topic']; + $daysText = $daysLeft === 0 ? 'oggi' : 'tra ' . $daysLeft . ' giorni'; + $mail->Body = buildHtml( + 'Scadenza in arrivo', + $topicText, + 'La scadenza è prevista per il ' . date('d/m/Y', strtotime($dl['due_date'])) . ' (' . $daysText . ').', + '#e8930c', + $detailUrl + ); + } + + $mail->isHTML(true); + $mail->AltBody = strip_tags(str_replace('
    ', "\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 ' + + + + + + +
    + + + + +
    +

    ' . htmlspecialchars($title) . '

    +
    +

    ' . htmlspecialchars($topic) . '

    +

    ' . $message . '

    + Vai alla scadenza +
    +

    ZIBOGOMMA — Scadenzario

    +
    +
    + +'; +} diff --git a/public/userarea/scadenzario/detail.php b/public/userarea/scadenzario/detail.php new file mode 100644 index 0000000..60a9003 --- /dev/null +++ b/public/userarea/scadenzario/detail.php @@ -0,0 +1,463 @@ + +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']; + } +} +?> + + + + + + + + + <?= $deadline ? htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') . ' — ' : '' ?>Scadenzario + + + + +
    + + +
    +
    + + + + + + + + + +
    + + Torna alla lista + + + + + + +
    + + +
    +
    +
    Dettagli Scadenza
    +
    +
    +
    + +
    + +
    Argomento
    +
    + + +
    Dettaglio
    +
    + +
    + + +
    Legge / Articolo
    +
    + + +
    Periodicità
    +
    +
    + + +
    +
    Stato
    +
    + + + + +
    + +
    Data scadenza
    +
    + + (scaduta) +
    + + +
    Data documento
    +
    + + +
    Data ultimo controllo
    +
    + +
    Giorni preavviso notifica
    +
    giorni
    + + +
    Luogo archiviazione
    +
    + + + +
    Note
    +
    + +
    +
    + + + +
    + +
    Reparti responsabili
    +
    + + + +
    + + + +
    Singoli responsabili
    +
    + + + + + + () + + + +
    + + +
    +
    + + + +
    +
    +
    Allegati ()
    +
    +
    + = 1024 ? round($sizeKB / 1024, 1) . ' MB' : $sizeKB . ' KB'; + ?> +
    +
    +
    + +
    ·
    +
    +
    + +
    +
    + + + + +
    +
    +
    Cronologia
    +
    +
    +
    + +
    +
    +
    + + da + +
    + +
    + + +
    + $vals): ?> +
    + : + + → + +
    + +
    + +
    + +
    +
    +
    + + + + +
    +
    + +
    + + + + + + diff --git a/public/userarea/scadenzario/index.php b/public/userarea/scadenzario/index.php new file mode 100644 index 0000000..54470e7 --- /dev/null +++ b/public/userarea/scadenzario/index.php @@ -0,0 +1,964 @@ + +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'); +?> + + + + + + + + + + + + + Scadenzario - Lista Scadenze + + + + +
    + + +
    +
    + + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + + +
    +
    +
    Lista Scadenze
    +
    + + Calendario + + + +
    +
    +
    + +
    + +

    Nessuna scadenza registrata.
    Clicca "Nuova Scadenza" per aggiungere la prima.

    +
    + + + + + +
    + +
    + +
    + +
    + + +
    +
    + + + + + + + + + + + 0): ?> + + +
    + +
    +
    + + + +
    +
    + +
    + +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ArgomentoDettaglioLegge/Art.ScadenzaVerificaResponsabiliStatoAzioni
    + + 0): ?> + + + + +
    + + +
    + +
    + + + +
    + +
    +
    +
    + + +
    +
    + +
    +
    + +
    + + + + + + + + + + + + + + diff --git a/public/userarea/scadenzario/print.php b/public/userarea/scadenzario/print.php new file mode 100644 index 0000000..93d1a8c --- /dev/null +++ b/public/userarea/scadenzario/print.php @@ -0,0 +1,199 @@ + +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)) : '...'); +} +?> + + + + + + Stampa Scadenzario + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ArgomentoLegge / Art.DettaglioPeriodicità ScadenzaData DocumentoData ScadenzaData Ultimo ControlloResponsabilitàLuogo di ArchiviazioneNote
    + + + + diff --git a/public/userarea/scadenzario/sql/create_tables.sql b/public/userarea/scadenzario/sql/create_tables.sql new file mode 100644 index 0000000..a12792f --- /dev/null +++ b/public/userarea/scadenzario/sql/create_tables.sql @@ -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;