From 9001eff3174d8ad2d1574fa9c9688fad75265591 Mon Sep 17 00:00:00 2001 From: "r.mubarakzyanov" Date: Thu, 21 May 2026 23:31:36 +0300 Subject: [PATCH] file repo, cc, auto-open --- .env.example | 6 +- .../scadenzario/ajax/complete_deadline.php | 48 ++++++- .../scadenzario/ajax/delete_attachment.php | 34 +++-- .../scadenzario/ajax/delete_deadline.php | 19 +++ .../scadenzario/cron/send_notifications.php | 16 +++ public/userarea/scadenzario/detail.php | 106 +++++++++++---- public/userarea/scadenzario/index.php | 124 ++++++++++++++---- 7 files changed, 279 insertions(+), 74 deletions(-) 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..1f6e9fe 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,6 +66,7 @@ try { if ($interval) { $dueDate->add($interval); if ($checkDate) $checkDate->add($interval); + if ($documentDate) $documentDate->add($interval); $ins = $pdo->prepare(" INSERT INTO scad_deadlines @@ -68,12 +78,13 @@ try { $deadline['subject_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..78c55ea 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']; } } ?> @@ -763,39 +763,91 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { window.location.href = 'scadenzario/index.php?edit='; }); + function detailSubmitComplete(createNext, copyAttachments) { + var fd = new FormData(); + fd.append('id', ''); + fd.append('create_next', createNext ? '1' : '0'); + fd.append('copy_attachments', copyAttachments ? '1' : '0'); + + fetch('scadenzario/ajax/complete_deadline.php', { + method: 'POST', + body: fd + }) + .then(function(r) { + return r.json(); + }) + .then(function(data) { + if (data.success) { + Swal.fire({ + icon: 'success', + title: 'Completata', + text: data.message, + timer: 1800, + showConfirmButton: false + }) + .then(function() { + if (data.new_id) { + window.location.href = 'scadenzario/index.php?edit=' + data.new_id; + } else { + window.location.href = 'scadenzario/index.php'; + } + }); + } else { + Swal.fire('Errore', data.message, 'error'); + } + }) + .catch(function() { + Swal.fire('Errore', 'Errore di connessione.', 'error'); + }); + } + $('#btnCompleta').on('click', function() { + var recurrence = ; + var attCount = ; + + if (recurrence === 'once') { + Swal.fire({ + title: 'Completare la scadenza?', + text: 'La scadenza verrà contrassegnata come completata.', + icon: 'question', + showCancelButton: true, + confirmButtonColor: '#198754', + cancelButtonText: 'Annulla', + confirmButtonText: 'Completa', + reverseButtons: true + }).then(function(result) { + if (result.isConfirmed) { + detailSubmitComplete(false, false); + } + }); + return; + } + + var attCheckbox = attCount > 0 ? + '
' + + '' + + '' + + '
' : + ''; + Swal.fire({ title: 'Completare la scadenza?', + html: 'Vuoi creare automaticamente la prossima scadenza ricorrente?' + attCheckbox, icon: 'question', showCancelButton: true, + showDenyButton: true, confirmButtonColor: '#198754', + denyButtonColor: '#6c757d', + confirmButtonText: 'Completa e crea la prossima', + denyButtonText: 'Completa senza nuova', cancelButtonText: 'Annulla', - confirmButtonText: 'Completa' + reverseButtons: true }).then(function(result) { if (result.isConfirmed) { - fetch('scadenzario/ajax/complete_deadline.php?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'); - }); + var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false; + detailSubmitComplete(true, copy); + } else if (result.isDenied) { + detailSubmitComplete(false, false); } }); }); diff --git a/public/userarea/scadenzario/index.php b/public/userarea/scadenzario/index.php index c273b6b..8fe672f 100644 --- a/public/userarea/scadenzario/index.php +++ b/public/userarea/scadenzario/index.php @@ -923,7 +923,9 @@ function getContrastTextColor($hexColor) data-department="" data-employees="" data-due-date="" - data-check-date=""> + data-check-date="" + data-recurrence="" + data-att-count="">
" data-employees="" data-due-date="" - data-check-date=""> + data-check-date="" + data-recurrence="" + data-att-count=""> 0 ? + '
' + + '' + + '' + + '
' : + ''; + Swal.fire({ title: 'Completare la scadenza?', + html: 'Vuoi creare automaticamente la prossima scadenza ricorrente?' + attCheckbox, icon: 'question', showCancelButton: true, + showDenyButton: true, confirmButtonColor: '#198754', + denyButtonColor: '#6c757d', + confirmButtonText: 'Completa e crea la prossima', + denyButtonText: 'Completa senza nuova', cancelButtonText: 'Annulla', - confirmButtonText: 'Completa' + reverseButtons: true }).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'); - }); + var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false; + submitComplete(id, true, copy); + } else if (result.isDenied) { + submitComplete(id, false, false); } }); });