diff --git a/.env.example b/.env.example
index 42476a2..fefcfa5 100644
--- a/.env.example
+++ b/.env.example
@@ -31,6 +31,8 @@ MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
+MANAGER_USER_ID=
+
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
@@ -55,5 +57,5 @@ AZURE_REDIRECT_URI=https://your-app.com/auth/azure/callback
AZURE_TENANT_ID=
MICROSOFT_CLIENT_ID=your_client_id_here
-MICROSOFT_CLIENT_SECRET=your_client_secret_here
-MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
\ No newline at end of file
+MICROSOFT_CLIENT_SECRET=your_client_secret_here
+MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
diff --git a/public/userarea/scadenzario/ajax/complete_deadline.php b/public/userarea/scadenzario/ajax/complete_deadline.php
index 7bfc4c8..a192fd5 100644
--- a/public/userarea/scadenzario/ajax/complete_deadline.php
+++ b/public/userarea/scadenzario/ajax/complete_deadline.php
@@ -4,12 +4,19 @@ header('Content-Type: application/json');
require_once(__DIR__ . '/../../class/db-functions.php');
try {
- if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
+ $rawId = $_POST['id'] ?? $_GET['id'] ?? null;
+ if ($rawId === null || !is_numeric($rawId)) {
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
exit;
}
- $id = (int)$_GET['id'];
+ $id = (int)$rawId;
+
+ // Whether to create the next (recurring) deadline. Absent or '1' => create; '0' => complete only.
+ $createNext = ($_POST['create_next'] ?? '1') !== '0';
+
+ // Whether to carry the attachment links over to the new deadline. Default ON ("default all activate").
+ $copyAttachments = ($_POST['copy_attachments'] ?? '1') !== '0';
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
@@ -34,11 +41,13 @@ try {
->execute([$id, $currentUserId]);
$newId = null;
+ $newDueDate = null;
- // If recurring, create next deadline
- if ($deadline['recurrence_type'] !== 'once') {
+ // If recurring AND the user asked for it, create the next deadline
+ if ($deadline['recurrence_type'] !== 'once' && $createNext) {
$dueDate = new DateTime($deadline['due_date']);
$checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null;
+ $documentDate = $deadline['document_date'] ? new DateTime($deadline['document_date']) : null;
switch ($deadline['recurrence_type']) {
case 'monthly': $interval = new DateInterval('P1M'); break;
@@ -57,23 +66,25 @@ try {
if ($interval) {
$dueDate->add($interval);
if ($checkDate) $checkDate->add($interval);
+ if ($documentDate) $documentDate->add($interval);
$ins = $pdo->prepare("
INSERT INTO scad_deadlines
- (subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
+ (subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
document_date, notification_days, storage_location, notes, created_by, departments)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$ins->execute([
- $deadline['subject_id'], $deadline['topic'], $deadline['law_regulation'],
+ $deadline['subject_id'], $deadline['function_id'], $deadline['topic'], $deadline['law_regulation'],
$deadline['recurrence_type'], $dueDate->format('Y-m-d'),
$checkDate ? $checkDate->format('Y-m-d') : null,
- $deadline['document_date'],
+ $documentDate ? $documentDate->format('Y-m-d') : null,
$deadline['notification_days'], $deadline['storage_location'],
$deadline['notes'], $deadline['created_by'], $deadline['departments']
]);
$newId = $pdo->lastInsertId();
+ $newDueDate = $dueDate;
// Copy employee assignments
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?");
@@ -87,6 +98,31 @@ try {
}
}
+ // Carry forward ALL attachment links from the source deadline (shared physical file, same stored_name).
+ // Individual links can later be removed on the new deadline without deleting the file.
+ if ($copyAttachments) {
+ $attSel = $pdo->prepare("
+ SELECT original_name, stored_name, mime_type, size
+ FROM scad_deadline_attachments
+ WHERE deadline_id = ?
+ ");
+ $attSel->execute([$id]);
+ $attRows = $attSel->fetchAll(PDO::FETCH_ASSOC);
+
+ if ($attRows) {
+ $attIns = $pdo->prepare("
+ INSERT INTO scad_deadline_attachments
+ (deadline_id, original_name, stored_name, mime_type, size, uploaded_by)
+ VALUES (?, ?, ?, ?, ?, ?)
+ ");
+ $attHist = $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_linked', ?)");
+ foreach ($attRows as $a) {
+ $attIns->execute([$newId, $a['original_name'], $a['stored_name'], $a['mime_type'], $a['size'], $currentUserId]);
+ $attHist->execute([$newId, $currentUserId, $a['original_name']]);
+ }
+ }
+ }
+
// 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]);
@@ -97,7 +133,7 @@ try {
$msg = 'Scadenza completata con successo.';
if ($newId) {
- $msg .= ' Nuova scadenza creata con data ' . $dueDate->format('d/m/Y') . '.';
+ $msg .= ' Nuova scadenza creata con data ' . $newDueDate->format('d/m/Y') . '.';
}
echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]);
diff --git a/public/userarea/scadenzario/ajax/delete_attachment.php b/public/userarea/scadenzario/ajax/delete_attachment.php
index ba9d2d0..da4df5d 100644
--- a/public/userarea/scadenzario/ajax/delete_attachment.php
+++ b/public/userarea/scadenzario/ajax/delete_attachment.php
@@ -23,20 +23,32 @@ try {
exit;
}
- // Delete file
- $filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
- if (file_exists($filePath)) {
- unlink($filePath);
- }
-
- // Delete DB record
+ // Remove this link (DB record) first
$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']]);
+ // The same physical file may be shared with other deadlines (carried forward on completion).
+ // Only unlink it when no other link references the same stored file.
+ $refStmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadline_attachments WHERE stored_name = ?");
+ $refStmt->execute([$att['stored_name']]);
+ $stillReferenced = (int)$refStmt->fetchColumn() > 0;
- echo json_encode(['success' => true, 'message' => 'Allegato eliminato.']);
+ if ($stillReferenced) {
+ $action = 'attachment_unlinked';
+ $message = 'Collegamento rimosso. Il file è conservato (usato da un\'altra scadenza).';
+ } else {
+ $filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
+ if (file_exists($filePath)) {
+ unlink($filePath);
+ }
+ $action = 'attachment_removed';
+ $message = 'Allegato eliminato.';
+ }
+
+ // History
+ $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, ?, ?)")
+ ->execute([$att['deadline_id'], $currentUserId, $action, $att['original_name']]);
+
+ echo json_encode(['success' => true, 'message' => $message]);
} 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
index d81b041..98de3d2 100644
--- a/public/userarea/scadenzario/ajax/delete_deadline.php
+++ b/public/userarea/scadenzario/ajax/delete_deadline.php
@@ -13,10 +13,29 @@ try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
+ // Collect the physical files referenced by this deadline before the FK cascade removes its links
+ $attStmt = $pdo->prepare("SELECT DISTINCT stored_name FROM scad_deadline_attachments WHERE deadline_id = ?");
+ $attStmt->execute([$id]);
+ $storedNames = $attStmt->fetchAll(PDO::FETCH_COLUMN);
+
+ // Deleting the deadline cascades to its attachment/employee/history rows (FK ON DELETE CASCADE)
$stmt = $pdo->prepare("DELETE FROM scad_deadlines WHERE id = ?");
$stmt->execute([$id]);
if ($stmt->rowCount() > 0) {
+ // Unlink physical files no longer referenced by any other deadline (shared-file safe)
+ if (!empty($storedNames)) {
+ $refStmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadline_attachments WHERE stored_name = ?");
+ foreach ($storedNames as $storedName) {
+ $refStmt->execute([$storedName]);
+ if ((int)$refStmt->fetchColumn() === 0) {
+ $filePath = __DIR__ . '/../attachments/' . $storedName;
+ if (file_exists($filePath)) {
+ unlink($filePath);
+ }
+ }
+ }
+ }
echo json_encode(['success' => true, 'message' => 'Scadenza eliminata con successo.']);
} else {
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
diff --git a/public/userarea/scadenzario/cron/send_notifications.php b/public/userarea/scadenzario/cron/send_notifications.php
index a8930b7..051e286 100644
--- a/public/userarea/scadenzario/cron/send_notifications.php
+++ b/public/userarea/scadenzario/cron/send_notifications.php
@@ -25,6 +25,17 @@ $pdo = $db->getConnection();
$today = date('Y-m-d');
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
+// Manager email for Cc — taken from MANAGER_USER_ID → auth_users.email
+$managerCcEmail = null;
+if (!empty($_ENV['MANAGER_USER_ID']) && is_numeric($_ENV['MANAGER_USER_ID'])) {
+ $mgrStmt = $pdo->prepare("SELECT email FROM auth_users WHERE id = ?");
+ $mgrStmt->execute([(int)$_ENV['MANAGER_USER_ID']]);
+ $mgrEmail = $mgrStmt->fetchColumn();
+ if (!empty($mgrEmail)) {
+ $managerCcEmail = $mgrEmail;
+ }
+}
+
$sent = 0;
$skipped = 0;
$errors = 0;
@@ -143,6 +154,11 @@ foreach ($deadlines as $dl) {
);
$mail->addAddress($emp['email'], trim($emp['first_name'] . ' ' . $emp['last_name']));
+ // Cc the manager (unless they are the direct recipient)
+ if ($managerCcEmail && strcasecmp($managerCcEmail, $emp['email']) !== 0) {
+ $mail->addCC($managerCcEmail);
+ }
+
$detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id'];
$topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic'];
diff --git a/public/userarea/scadenzario/detail.php b/public/userarea/scadenzario/detail.php
index c6bda13..c365288 100644
--- a/public/userarea/scadenzario/detail.php
+++ b/public/userarea/scadenzario/detail.php
@@ -66,9 +66,9 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
}
$recurrenceLabels = ['once' => 'Una tantum', 'monthly' => 'Mensile', 'quarterly' => 'Trimestrale', 'semiannual' => 'Semestrale', 'annual' => 'Annuale', 'biennial' => 'Biennale', 'triennial' => 'Triennale', 'quadriennial' => 'Quadriennale', 'quinquennial' => 'Quinquennale', 'decennial' => 'Decennale', 'quindecennial' => 'Quindicennale'];
- $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'];
+ $actionLabels = ['created' => 'Creata', 'updated' => 'Modificata', 'completed' => 'Completata', 'attachment_added' => 'Allegato aggiunto', 'attachment_removed' => 'Allegato rimosso', 'attachment_linked' => 'Allegato collegato', 'attachment_unlinked' => 'Collegamento rimosso', 'notification_sent' => 'Notifica inviata'];
+ $actionColors = ['created' => '#198754', 'updated' => '#5a8fd8', 'completed' => '#6f42c1', 'attachment_added' => '#e8930c', 'attachment_removed' => '#e8930c', 'attachment_linked' => '#0dcaf0', 'attachment_unlinked' => '#adb5bd', 'notification_sent' => '#adb5bd'];
+ $actionIcons = ['created' => 'fa-plus', 'updated' => 'fa-pen', 'completed' => 'fa-check', 'attachment_added' => 'fa-paperclip', 'attachment_removed' => 'fa-trash', 'attachment_linked' => 'fa-link', 'attachment_unlinked' => 'fa-link-slash', 'notification_sent' => 'fa-bell'];
}
}
?>
@@ -85,6 +85,14 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {