Merge branch 'main' into feature/user_profile

This commit is contained in:
2026-05-26 16:52:41 +03:00
11 changed files with 879 additions and 602 deletions
+2
View File
@@ -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=
@@ -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]);
@@ -23,20 +23,32 @@ try {
exit;
}
// Delete file
// Remove this link (DB record) first
$pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]);
// 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;
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);
}
// Delete DB record
$pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]);
$action = 'attachment_removed';
$message = 'Allegato eliminato.';
}
// 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']]);
$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' => 'Allegato eliminato.']);
echo json_encode(['success' => true, 'message' => $message]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
@@ -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.']);
@@ -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'];
+86 -16
View File
@@ -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'])) {
<base href="<?= $baseHref ?>">
<?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.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet">
<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>
<?php include __DIR__ . '/include/deadline_modal_css.php'; ?>
<title><?= $deadline ? htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') . ' — ' : '' ?>Scadenzario</title>
<script>
if (window.innerWidth > 1024) document.addEventListener('DOMContentLoaded', function() {
@@ -755,25 +763,33 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
</div>
<?php include('../include/footer.php'); ?>
</div>
<?php if ($deadline && !$isCompleted): ?>
<?php require __DIR__ . '/include/deadline_form_data.php'; ?>
<?php include __DIR__ . '/include/deadline_modal.php'; ?>
<?php endif; ?>
<?php include('../jsinclude.php'); ?>
<?php if ($deadline && !$isCompleted): ?>
<script>
$(document).ready(function() {
// Used by the shared modal JS to auto-open edit on "#edit"
window.SCAD_DETAIL_ID = <?= (int)$deadline['id'] ?>;
$('#btnModifica').on('click', function() {
window.location.href = 'scadenzario/index.php?edit=<?= (int)$deadline['id'] ?>';
window.openDeadlineEdit(<?= (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'] ?>')
function detailSubmitComplete(createNext, copyAttachments) {
var fd = new FormData();
fd.append('id', '<?= (int)$deadline['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();
})
@@ -783,11 +799,15 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
icon: 'success',
title: 'Completata',
text: data.message,
timer: 2500,
timer: 1800,
showConfirmButton: false
})
.then(function() {
if (data.new_id) {
window.location.href = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
} else {
window.location.href = 'scadenzario/index.php';
}
});
} else {
Swal.fire('Errore', data.message, 'error');
@@ -797,10 +817,60 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
}
$('#btnCompleta').on('click', function() {
var recurrence = <?= json_encode($deadline['recurrence_type'] ?? 'once') ?>;
var attCount = <?= count($attachments) ?>;
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 ?
'<div class="form-check d-flex align-items-center justify-content-center gap-2 mt-3">' +
'<input class="form-check-input" type="checkbox" id="swCopyAtt" checked>' +
'<label class="form-check-label" for="swCopyAtt">Copia gli allegati (' + attCount + ') sulla nuova scadenza</label>' +
'</div>' :
'';
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',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false;
detailSubmitComplete(true, copy);
} else if (result.isDenied) {
detailSubmitComplete(false, false);
}
});
});
});
</script>
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
<?php endif; ?>
</body>
@@ -0,0 +1,41 @@
<?php
/**
* Shared data for the deadline modal form (used by index.php and detail.php).
* Populates $employees, $departments, $subjects. Safe to include more than once.
*/
if (!isset($pdo) || !$pdo) {
$pdo = DBHandlerSelect::getInstance()->getConnection();
}
if (!isset($employees)) {
$employees = $pdo->query("
SELECT e.id, e.first_name, e.last_name, e.department_id, dep.name AS department_name
FROM employees e
LEFT JOIN departments dep ON dep.id = e.department_id
WHERE e.status = 'active'
ORDER BY e.first_name, e.last_name
")->fetchAll(PDO::FETCH_ASSOC);
}
if (!isset($departments)) {
$departments = $pdo->query("
SELECT id, name, code, color
FROM departments
WHERE is_active = 1
ORDER BY sort_order ASC, name ASC
")->fetchAll(PDO::FETCH_ASSOC);
}
if (!isset($subjects)) {
$subjects = $pdo->query("SELECT id, name, color FROM scad_subjects ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
}
if (!isset($functions)) {
$functions = $pdo->query("
SELECT id, name
FROM scad_functions
WHERE status = 'active'
ORDER BY name ASC
")->fetchAll(PDO::FETCH_ASSOC);
}
@@ -0,0 +1,162 @@
<?php
/**
* Shared "Nuova/Modifica Scadenza" modal markup (used by index.php and detail.php).
* Requires $subjects, $departments, $employees in scope (see deadline_form_data.php).
* The accompanying JS lives in deadline_modal_js.php.
*/
?>
<!-- Deadline Modal -->
<div class="modal fade" id="deadlineModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl 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="dlSubject" class="form-label fw-semibold">Argomento</label>
<div class="d-flex gap-2">
<select class="form-select" id="dlSubject" name="subject_id" style="flex:1">
<option value=""> Nessuno </option>
<?php foreach ($subjects as $s): ?>
<option value="<?= (int)$s['id'] ?>" data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
<a href="scadenzario/subjects/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci argomenti">
<i class="fa-solid fa-gear"></i>
</a>
</div>
</div>
<div class="col-12 col-md-6">
<label for="dlFunction" class="form-label fw-semibold">Funzione</label>
<div class="d-flex gap-2">
<select class="form-select" id="dlFunction" name="function_id" style="flex:1">
<option value=""> Nessuna </option>
<?php foreach ($functions as $fn): ?>
<option value="<?= (int)$fn['id'] ?>">
<?= htmlspecialchars($fn['name'], ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
<a href="scadenzario/functions/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci funzioni">
<i class="fa-solid fa-gear"></i>
</a>
</div>
</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="quadriennial">Quadriennale</option>
<option value="quinquennial">Quinquennale</option>
<option value="decennial">Decennale</option>
<option value="quindecennial">Quindicennale</option>
</select>
</div>
<div class="col-12 col-md-4">
<label for="dlDocDate" class="form-label fw-semibold">Data documento</label>
<input type="text" class="form-control js-date-it" id="dlDocDate" name="document_date" placeholder="gg/mm/aaaa" autocomplete="off">
</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="text" class="form-control js-date-it" id="dlDueDate" name="due_date" placeholder="gg/mm/aaaa" autocomplete="off" required>
</div>
<div class="col-12 col-md-4">
<label for="dlCheckDate" class="form-label fw-semibold">Data ultimo controllo</label>
<input type="text" class="form-control js-date-it" id="dlCheckDate" name="check_date" placeholder="gg/mm/aaaa" autocomplete="off">
</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['name'], ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>
<?= !empty($dept['code']) ? ' (' . htmlspecialchars($dept['code'], 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 (!empty($emp['department_name'])): ?> (<?= htmlspecialchars($emp['department_name'], 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>
@@ -0,0 +1,99 @@
<?php
/**
* Shared styles for the deadline modal (deadline_modal.php).
* Relies on the --scad-* CSS variables defined on each page's :root.
*/
?>
<style>
.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;
}
#deadlineModal.modal {
position: fixed;
}
#deadlineModal .modal-content,
#deadlineModal .modal-body,
#deadlineModal .modal-footer {
background: #fff !important;
}
#deadlineModal .modal-header {
background: var(--scad-card-bg);
border-bottom: 1px solid var(--scad-card-border);
}
#deadlineModal .modal-title {
font-weight: 700;
color: var(--scad-heading);
}
/* 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, #dc3545);
cursor: pointer;
}
.att-item .att-remove:hover {
background: var(--scad-red, #dc3545);
color: #fff;
}
</style>
@@ -0,0 +1,280 @@
<?php
/**
* Self-contained JS for the deadline modal (deadline_modal.php).
* Requires jQuery, Bootstrap, flatpickr, select2 and SweetAlert2 to be loaded first.
*
* Exposes:
* window.openDeadlineCreate() open the modal empty (new deadline)
* window.openDeadlineEdit(id) fetch a deadline and open the modal in edit mode
*
* Auto-open on load:
* #edit=<id> → opens edit for that id
* #edit → opens edit for window.SCAD_DETAIL_ID (used by detail.php)
*/
?>
<script>
$(document).ready(function() {
// --- Flatpickr date fields (visible dd/mm/yyyy, submitted yyyy-mm-dd) ---
var fpOptsDate = {
dateFormat: 'Y-m-d',
altInput: true,
altFormat: 'd/m/Y',
locale: 'it',
allowInput: true
};
var fpDocDate = flatpickr('#dlDocDate', fpOptsDate);
var fpDueDate = flatpickr('#dlDueDate', fpOptsDate);
var fpCheckDate = flatpickr('#dlCheckDate', fpOptsDate);
// --- Select2 ---
var s2Opts = {
theme: 'bootstrap-5',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
};
$('#dlSubject').select2($.extend({}, s2Opts, { placeholder: 'Seleziona argomento...' }));
$('#dlDepartments').select2($.extend({}, s2Opts, { placeholder: 'Seleziona reparti...' }));
$('#dlEmployees').select2($.extend({}, s2Opts, { placeholder: 'Seleziona persone...' }));
$('#dlFunction').select2($.extend({}, s2Opts, { placeholder: 'Seleziona funzione...' }));
// --- Auto-calc due_date from document_date + recurrence ---
var RECURRENCE_OFFSETS = {
monthly: { months: 1 },
quarterly: { months: 3 },
semiannual: { months: 6 },
annual: { years: 1 },
biennial: { years: 2 },
triennial: { years: 3 },
quadriennial: { years: 4 },
quinquennial: { years: 5 },
decennial: { years: 10 },
quindecennial: { years: 15 }
};
function computeDueDate() {
var docVal = document.getElementById('dlDocDate').value;
var recurrence = document.getElementById('dlRecurrence').value;
var offset = RECURRENCE_OFFSETS[recurrence];
if (!docVal || !offset) return;
var d = new Date(docVal + 'T00:00:00');
if (isNaN(d.getTime())) return;
if (offset.months) d.setMonth(d.getMonth() + offset.months);
if (offset.years) d.setFullYear(d.getFullYear() + offset.years);
var iso = d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
fpDueDate.setDate(iso, true, 'Y-m-d');
}
$('#dlDocDate, #dlRecurrence').on('change', computeDueDate);
// --- Modal instance ---
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
var form = document.getElementById('deadlineForm');
// --- 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('');
}
// --- Open modal (create) ---
window.openDeadlineCreate = function() {
form.reset();
document.getElementById('dlId').value = '';
document.getElementById('dlNotifDays').value = '7';
document.getElementById('modalTitle').textContent = 'Nuova Scadenza';
document.getElementById('dlFiles').value = '';
fpDocDate.clear();
fpDueDate.clear();
fpCheckDate.clear();
$('#dlSubject').val('').trigger('change');
$('#dlDepartments').val(null).trigger('change');
$('#dlEmployees').val(null).trigger('change');
$('#dlFunction').val('').trigger('change');
renderAttachments([]);
modal.show();
};
// --- Open modal (edit) ---
window.openDeadlineEdit = function(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;
$('#dlSubject').val(d.subject_id || '').trigger('change');
document.getElementById('dlTopic').value = d.topic || '';
$('#dlFunction').val(d.function_id || '').trigger('change');
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
fpDocDate.setDate(d.document_date || null, false, 'Y-m-d');
fpDueDate.setDate(d.due_date || null, false, 'Y-m-d');
fpCheckDate.setDate(d.check_date || null, false, 'Y-m-d');
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');
if (Array.isArray(d.employee_ids)) {
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
} else {
$('#dlEmployees').val(null).trigger('change');
}
renderAttachments(d.attachments || []);
modal.show();
})
.catch(function() {
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
};
// --- 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) {
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';
});
});
// --- Delete attachment ---
$(document).on('click', '.att-remove', function(e) {
e.preventDefault();
var btn = $(this);
var attId = btn.data('att-id');
Swal.fire({
title: 'Rimuovere l\'allegato?',
text: 'Il collegamento verrà rimosso da questa scadenza. Il file resta disponibile se è usato da altre scadenze.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
cancelButtonText: 'Annulla',
confirmButtonText: 'Rimuovi',
reverseButtons: true
}).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([]);
}
Swal.fire({
icon: 'success',
title: 'Fatto',
text: data.message,
timer: 1800,
showConfirmButton: false
});
} else {
Swal.fire('Errore', data.message, 'error');
}
});
}
});
});
// --- Auto-open from hash (#edit=ID or #edit for the current detail page) ---
var hash = window.location.hash;
var hashMatch = hash.match(/^#edit=(\d+)$/);
var autoEditId = hashMatch ? hashMatch[1] :
(hash === '#edit' && window.SCAD_DETAIL_ID ? window.SCAD_DETAIL_ID : null);
if (autoEditId) {
history.replaceState(null, '', window.location.pathname + window.location.search);
window.openDeadlineEdit(autoEditId);
}
});
</script>
+81 -541
View File
@@ -71,34 +71,7 @@ $stmt = $pdo->prepare($sql);
$stmt->execute($params);
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
$employees = $pdo->query("
SELECT
e.id,
e.first_name,
e.last_name,
e.department_id,
dep.name AS department_name
FROM employees e
LEFT JOIN departments dep ON dep.id = e.department_id
WHERE e.status = 'active'
ORDER BY e.first_name, e.last_name
")->fetchAll(PDO::FETCH_ASSOC);
$departments = $pdo->query("
SELECT id, name, code, color
FROM departments
WHERE is_active = 1
ORDER BY sort_order ASC, name ASC
")->fetchAll(PDO::FETCH_ASSOC);
$subjects = $pdo->query("SELECT id, name, color FROM scad_subjects ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
$functions = $pdo->query("
SELECT id, name
FROM scad_functions
WHERE status = 'active'
ORDER BY name ASC
")->fetchAll(PDO::FETCH_ASSOC);
require __DIR__ . '/include/deadline_form_data.php';
$today = date('Y-m-d');
@@ -937,7 +910,9 @@ function getContrastTextColor($hexColor)
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') ?>">
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-recurrence="<?= htmlspecialchars($row['recurrence_type'] ?? 'once', ENT_QUOTES, 'UTF-8') ?>"
data-att-count="<?= (int)$row['attachment_count'] ?>">
<?php if (!empty($row['subject_name'])): ?>
<div class="mb-1"><?php
$subjectBadgeBg = $row['subject_color'] ?: '#6c757d';
@@ -1015,7 +990,9 @@ function getContrastTextColor($hexColor)
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') ?>">
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-recurrence="<?= htmlspecialchars($row['recurrence_type'] ?? 'once', ENT_QUOTES, 'UTF-8') ?>"
data-att-count="<?= (int)$row['attachment_count'] ?>">
<td>
<?php if (!empty($row['subject_name'])): ?>
<?php
@@ -1082,160 +1059,7 @@ function getContrastTextColor($hexColor)
<?php include('../include/footer.php'); ?>
</div>
<!-- Deadline Modal -->
<div class="modal fade" id="deadlineModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl 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="dlSubject" class="form-label fw-semibold">Argomento</label>
<div class="d-flex gap-2">
<select class="form-select" id="dlSubject" name="subject_id" style="flex:1">
<option value=""> Nessuno </option>
<?php foreach ($subjects as $s): ?>
<option value="<?= (int)$s['id'] ?>" data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
<a href="scadenzario/subjects/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci argomenti">
<i class="fa-solid fa-gear"></i>
</a>
</div>
</div>
<div class="col-12 col-md-6">
<label for="dlFunction" class="form-label fw-semibold">Funzione</label>
<div class="d-flex gap-2">
<select class="form-select" id="dlFunction" name="function_id" style="flex:1">
<option value=""> Nessuna </option>
<?php foreach ($functions as $fn): ?>
<option value="<?= (int)$fn['id'] ?>">
<?= htmlspecialchars($fn['name'], ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
<a href="scadenzario/functions/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci funzioni">
<i class="fa-solid fa-gear"></i>
</a>
</div>
</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="quadriennial">Quadriennale</option>
<option value="quinquennial">Quinquennale</option>
<option value="decennial">Decennale</option>
<option value="quindecennial">Quindicennale</option>
</select>
</div>
<div class="col-12 col-md-4">
<label for="dlDocDate" class="form-label fw-semibold">Data documento</label>
<input type="text" class="form-control js-date-it" id="dlDocDate" name="document_date" placeholder="gg/mm/aaaa" autocomplete="off">
</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="text" class="form-control js-date-it" id="dlDueDate" name="due_date" placeholder="gg/mm/aaaa" autocomplete="off" required>
</div>
<div class="col-12 col-md-4">
<label for="dlCheckDate" class="form-label fw-semibold">Data ultimo controllo</label>
<input type="text" class="form-control js-date-it" id="dlCheckDate" name="check_date" placeholder="gg/mm/aaaa" autocomplete="off">
</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['name'], ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>
<?= !empty($dept['code']) ? ' (' . htmlspecialchars($dept['code'], 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 (!empty($emp['department_name'])): ?> (<?= htmlspecialchars($emp['department_name'], 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 __DIR__ . '/include/deadline_modal.php'; ?>
<?php include('../jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
@@ -1265,120 +1089,6 @@ function getContrastTextColor($hexColor)
var fpDue = flatpickr('#filterDueRange', fpOpts);
var fpCheck = flatpickr('#filterCheckRange', fpOpts);
// --- Flatpickr Italian date fields in deadline modal ---
// Visible format: dd/mm/yyyy
// Submitted format: yyyy-mm-dd, compatible with MySQL DATE
var fpDocDate = flatpickr('#dlDocDate', {
dateFormat: 'Y-m-d',
altInput: true,
altFormat: 'd/m/Y',
locale: 'it',
allowInput: true
});
var fpDueDate = flatpickr('#dlDueDate', {
dateFormat: 'Y-m-d',
altInput: true,
altFormat: 'd/m/Y',
locale: 'it',
allowInput: true
});
var fpCheckDate = flatpickr('#dlCheckDate', {
dateFormat: 'Y-m-d',
altInput: true,
altFormat: 'd/m/Y',
locale: 'it',
allowInput: true
});
// --- Select2 ---
$('#dlSubject').select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona argomento...',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
});
$('#dlFunction').select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona funzione...',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
});
$('#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%'
});
// --- Auto-calc due_date from document_date + recurrence ---
var RECURRENCE_OFFSETS = {
monthly: {
months: 1
},
quarterly: {
months: 3
},
semiannual: {
months: 6
},
annual: {
years: 1
},
biennial: {
years: 2
},
triennial: {
years: 3
},
quadriennial: {
years: 4
},
quinquennial: {
years: 5
},
decennial: {
years: 10
},
quindecennial: {
years: 15
}
};
function computeDueDate() {
var docVal = document.getElementById('dlDocDate').value;
var recurrence = document.getElementById('dlRecurrence').value;
var offset = RECURRENCE_OFFSETS[recurrence];
if (!docVal || !offset) return;
var d = new Date(docVal + 'T00:00:00');
if (isNaN(d.getTime())) return;
if (offset.months) d.setMonth(d.getMonth() + offset.months);
if (offset.years) d.setFullYear(d.getFullYear() + offset.years);
var iso = d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
fpDueDate.setDate(iso, true, 'Y-m-d');
}
$('#dlDocDate, #dlRecurrence').on('change', computeDueDate);
// --- DataTables custom filters ---
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
if (settings.nTable.id !== 'deadlinesTable') return true;
@@ -1540,157 +1250,8 @@ function getContrastTextColor($hexColor)
// Apply default filter on load
applyFiltersRefresh();
// --- Modal ---
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
var form = document.getElementById('deadlineForm');
// Add
// 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 = '';
fpDocDate.clear();
fpDueDate.clear();
fpCheckDate.clear();
$('#dlSubject').val('').trigger('change');
$('#dlFunction').val('').trigger('change');
$('#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');
}
});
}
});
if (window.openDeadlineCreate) window.openDeadlineCreate();
});
// Edit with confirmation
@@ -1707,70 +1268,23 @@ function getContrastTextColor($hexColor)
confirmButtonText: 'Sì, modifica',
reverseButtons: true
}).then(function(result) {
if (!result.isConfirmed) {
return;
if (result.isConfirmed && window.openDeadlineEdit) {
window.openDeadlineEdit(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;
$('#dlSubject').val(d.subject_id || '').trigger('change');
$('#dlFunction').val(d.function_id || '').trigger('change');
document.getElementById('dlTopic').value = d.topic || '';
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
fpDocDate.setDate(d.document_date || null, false, 'Y-m-d');
fpDueDate.setDate(d.due_date || null, false, 'Y-m-d');
fpCheckDate.setDate(d.check_date || null, false, 'Y-m-d');
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');
if (Array.isArray(d.employee_ids)) {
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
} else {
$('#dlEmployees').val(null).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)
function submitComplete(id, createNext, copyAttachments) {
var fd = new FormData();
fd.append('id', 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();
})
@@ -1780,11 +1294,16 @@ function getContrastTextColor($hexColor)
icon: 'success',
title: 'Completata',
text: data.message,
timer: 2500,
timer: 1800,
showConfirmButton: false
})
.then(function() {
// Open the new deadline's detail page with the edit modal auto-opening
if (data.new_id) {
window.location = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
} else {
location.reload();
}
});
} else {
Swal.fire('Errore', data.message, 'error');
@@ -1794,6 +1313,59 @@ function getContrastTextColor($hexColor)
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
}
$(document).on('click', '.btn-complete', function() {
var el = $(this).closest('[data-id]');
var id = el.data('id');
var recurrence = el.data('recurrence') || 'once';
var attCount = parseInt(el.data('att-count'), 10) || 0;
// Non-recurring: simple confirm, no new deadline is created
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) {
submitComplete(id, false, false);
}
});
return;
}
// Recurring: ask whether to create the next deadline; optionally carry attachments over
var attCheckbox = attCount > 0 ?
'<div class="form-check d-flex align-items-center justify-content-center gap-2 mt-3">' +
'<input class="form-check-input" type="checkbox" id="swCopyAtt" checked>' +
'<label class="form-check-label" for="swCopyAtt">Copia gli allegati (' + attCount + ') sulla nuova scadenza</label>' +
'</div>' :
'';
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',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false;
submitComplete(id, true, copy);
} else if (result.isDenied) {
submitComplete(id, false, false);
}
});
});
@@ -1838,39 +1410,6 @@ function getContrastTextColor($hexColor)
});
});
// 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;
$('#dlSubject').val(d.subject_id || '').trigger('change');
$('#dlFunction').val(d.function_id || '').trigger('change');
document.getElementById('dlTopic').value = d.topic || '';
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
fpDocDate.setDate(d.document_date || null, false, 'Y-m-d');
fpDueDate.setDate(d.due_date || null, false, 'Y-m-d');
fpCheckDate.setDate(d.check_date || null, false, 'Y-m-d');
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
function doStampa() {
var params = [];
@@ -1893,6 +1432,7 @@ function getContrastTextColor($hexColor)
if (btnStampaMobile) btnStampaMobile.addEventListener('click', doStampa);
});
</script>
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
</body>
</html>