Subject CRUD

This commit is contained in:
r.mubarakzyanov 2026-04-18 15:26:04 +03:00
parent d2e5cc8b2b
commit 0550ffe923
11 changed files with 557 additions and 36 deletions

View File

@ -57,12 +57,12 @@ try {
$ins = $pdo->prepare("
INSERT INTO scad_deadlines
(category, topic, law_regulation, recurrence_type, due_date, check_date,
(subject_id, 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['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'],

View File

@ -13,8 +13,10 @@ try {
$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
$sql = "SELECT DISTINCT d.id, d.topic, d.due_date, d.status, d.notification_days, d.departments,
s.name AS subject_name, s.color AS subject_color
FROM scad_deadlines d
LEFT JOIN scad_subjects s ON s.id = d.subject_id
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
LEFT JOIN employees e ON e.id = de.employee_id";
@ -73,14 +75,14 @@ try {
else { $color = '#5a8fd8'; }
$title = $d['topic'];
if ($d['category']) $title = $d['category'] . ': ' . $title;
if (!empty($d['subject_name'])) $title = $d['subject_name'] . ': ' . $title;
$events[] = [
'id' => $d['id'],
'title' => $title,
'start' => $d['due_date'],
'backgroundColor' => $color,
'borderColor' => $color,
'borderColor' => !empty($d['subject_color']) ? $d['subject_color'] : $color,
'url' => 'scadenzario/detail.php?id=' . $d['id'],
];
}

View File

@ -8,7 +8,7 @@ try {
$pdo = $db->getConnection();
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
$category = trim($_POST['category'] ?? '') ?: null;
$subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null;
$topic = trim($_POST['topic'] ?? '');
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
$recurrence_type = $_POST['recurrence_type'] ?? 'once';
@ -52,13 +52,13 @@ try {
if ($id) {
$stmt = $pdo->prepare("
UPDATE scad_deadlines SET
category = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
subject_id = ?, 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,
$subject_id, $topic, $law_regulation, $recurrence_type,
$due_date, $check_date, $document_date, $notification_days,
$storage_location, $notes, $departmentsStr, $id
]);
@ -75,12 +75,12 @@ try {
// INSERT
$stmt = $pdo->prepare("
INSERT INTO scad_deadlines
(category, topic, law_regulation, recurrence_type, due_date, check_date,
(subject_id, 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,
$subject_id, $topic, $law_regulation, $recurrence_type,
$due_date, $check_date, $document_date, $notification_days,
$storage_location, $notes, $currentUserId, $departmentsStr
]);

View File

@ -31,8 +31,9 @@ $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
SELECT d.id, d.topic, s.name AS subject_name, d.due_date, d.notification_days
FROM scad_deadlines d
LEFT JOIN scad_subjects s ON s.id = d.subject_id
WHERE d.status = 'active'
AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)
");
@ -143,7 +144,7 @@ foreach ($deadlines as $dl) {
$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'];
$topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic'];
if ($isOverdue) {
$mail->Subject = '⚠️ Scadenza superata: ' . $dl['topic'];

View File

@ -9,7 +9,12 @@ 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 = $pdo->prepare("
SELECT d.*, s.name AS subject_name, s.color AS subject_color
FROM scad_deadlines d
LEFT JOIN scad_subjects s ON s.id = d.subject_id
WHERE d.id = ?
");
$stmt->execute([$id]);
$deadline = $stmt->fetch(PDO::FETCH_ASSOC);
@ -263,9 +268,13 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
<div class="row">
<!-- Left Column -->
<div class="col-12 col-md-6">
<?php if ($deadline['category']): ?>
<?php if (!empty($deadline['subject_name'])): ?>
<div class="detail-label">Argomento</div>
<div class="detail-value" style="font-weight:600;"><?= htmlspecialchars($deadline['category'], ENT_QUOTES, 'UTF-8') ?></div>
<div class="detail-value">
<span style="display:inline-block;padding:0.25rem 0.7rem;border-radius:1rem;color:#fff;font-weight:600;font-size:0.85rem;background: <?= htmlspecialchars($deadline['subject_color'] ?: '#6c757d', ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($deadline['subject_name'], ENT_QUOTES, 'UTF-8') ?>
</span>
</div>
<?php endif; ?>
<div class="detail-label">Dettaglio</div>

View File

@ -3,24 +3,46 @@
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->query("
// Optional filter: show only deadlines of a given subject (used by "Storico" link from subjects CRUD)
$filterSubjectId = isset($_GET['subject_id']) && is_numeric($_GET['subject_id']) ? (int)$_GET['subject_id'] : null;
$filterSubjectName = null;
if ($filterSubjectId) {
$s = $pdo->prepare("SELECT name FROM scad_subjects WHERE id = ?");
$s->execute([$filterSubjectId]);
$filterSubjectName = $s->fetchColumn() ?: null;
if (!$filterSubjectName) { $filterSubjectId = null; }
}
$sql = "
SELECT d.*,
s.name AS subject_name,
s.color AS subject_color,
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_subjects s ON s.id = d.subject_id
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
");
";
$params = [];
if ($filterSubjectId) {
$sql .= " WHERE d.subject_id = ?";
$params[] = $filterSubjectId;
}
$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);
$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);
$subjects = $pdo->query("SELECT id, name, color FROM scad_subjects ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
$today = date('Y-m-d');
?>
<!doctype html>
@ -137,6 +159,32 @@ $today = date('Y-m-d');
#deadlinesTable tbody tr.row-completed { opacity: 0.6; }
#deadlinesTable tbody tr:hover { filter: brightness(0.97); }
/* Subject color stripe */
#deadlinesTable tbody tr[data-subject-color] td:first-child {
border-left: 4px solid var(--subject-color, transparent);
}
.subject-chip {
display: inline-block;
padding: 0.15rem 0.55rem;
border-radius: 1rem;
font-size: 0.72rem;
font-weight: 600;
color: #fff;
letter-spacing: 0.02em;
white-space: nowrap;
}
.deadline-card[data-subject-color] {
border-left: 4px solid var(--subject-color, var(--scad-card-border));
}
/* Filter banner (subject history mode) */
.subject-filter-banner {
display: flex; align-items: center; justify-content: space-between;
gap: 0.75rem; padding: 0.75rem 1rem;
background: var(--scad-card-bg); border: 1px solid var(--scad-card-border);
border-radius: 0.5rem; margin-bottom: 1rem; font-size: 0.9rem;
}
/* Filter bar */
.filter-bar .form-select {
border-radius: 0.5rem;
@ -244,19 +292,46 @@ $today = date('Y-m-d');
<div class="page-wrapper">
<div class="page-content">
<?php if ($filterSubjectId): ?>
<div class="subject-filter-banner">
<div>
<i class="fa-solid fa-filter me-2"></i>
<strong>Storico per argomento:</strong>
<?= htmlspecialchars($filterSubjectName, ENT_QUOTES, 'UTF-8') ?>
<span class="text-muted ms-2">(tutte le scadenze aperte e chiuse)</span>
</div>
<a href="scadenzario/index.php" class="btn btn-sm btn-light border">
<i class="fa-solid fa-xmark me-1"></i> Rimuovi filtro
</a>
</div>
<?php endif; ?>
<!-- Filter Bar -->
<div class="filter-bar mb-3">
<div class="row g-2 align-items-center mb-2">
<div class="col-12 col-sm-6 col-md-auto">
<select id="filterStatus" class="form-select">
<?php if ($filterSubjectId): ?>
<option value="" selected>Stato: Tutti</option>
<option value="non-completata">Stato: Non completate</option>
<?php else: ?>
<option value="non-completata" selected>Stato: Non completate</option>
<option value="">Stato: Tutti</option>
<?php endif; ?>
<option value="attiva">Attive</option>
<option value="in-scadenza">In scadenza</option>
<option value="scaduta">Scadute</option>
<option value="completata">Completate</option>
</select>
</div>
<div class="col-12 col-sm-6 col-md-auto">
<select id="filterSubject" class="form-select">
<option value="">Argomento: Tutti</option>
<?php foreach ($subjects as $s): ?>
<option value="<?= (int)$s['id'] ?>" <?= $filterSubjectId === (int)$s['id'] ? 'selected' : '' ?>><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-sm-6 col-md-auto">
<select id="filterDepartment" class="form-select">
<option value="">Reparto: Tutti</option>
@ -294,6 +369,9 @@ $today = date('Y-m-d');
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
<h5><i class="fa-solid fa-calendar-check me-2"></i>Lista Scadenze</h5>
<div class="header-actions d-flex gap-2 flex-wrap">
<a href="scadenzario/subjects/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
<i class="fa-solid fa-tags"></i><span>Argomenti</span>
</a>
<a href="scadenzario/calendar.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
<i class="fa-solid fa-calendar-days"></i><span>Calendario</span>
</a>
@ -354,12 +432,14 @@ $today = date('Y-m-d');
<div class="deadline-card <?= $row['_cardClass'] ?>"
data-id="<?= (int)$row['id'] ?>"
data-status="<?= $row['_statusKey'] ?>"
data-subject-id="<?= (int)($row['subject_id'] ?? 0) ?>"
<?php if (!empty($row['subject_color'])): ?>data-subject-color="<?= htmlspecialchars($row['subject_color'], ENT_QUOTES, 'UTF-8') ?>" style="--subject-color: <?= htmlspecialchars($row['subject_color'], ENT_QUOTES, 'UTF-8') ?>"<?php endif; ?>
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<?php if ($row['category']): ?>
<div class="text-muted mb-1" style="font-size:0.75rem;font-weight:600;text-transform:uppercase;letter-spacing:0.03em"><?= htmlspecialchars($row['category'], ENT_QUOTES, 'UTF-8') ?></div>
<?php if (!empty($row['subject_name'])): ?>
<div class="mb-1"><span class="subject-chip" style="background: <?= htmlspecialchars($row['subject_color'] ?: '#6c757d', ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($row['subject_name'], ENT_QUOTES, 'UTF-8') ?></span></div>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-start mb-1">
<a href="scadenzario/detail.php?id=<?= (int)$row['id'] ?>" class="card-topic text-decoration-none"><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></a>
@ -414,11 +494,19 @@ $today = date('Y-m-d');
<tr class="<?= $row['_rowClass'] ?>"
data-id="<?= (int)$row['id'] ?>"
data-status="<?= $row['_statusKey'] ?>"
data-subject-id="<?= (int)($row['subject_id'] ?? 0) ?>"
<?php if (!empty($row['subject_color'])): ?>data-subject-color="<?= htmlspecialchars($row['subject_color'], ENT_QUOTES, 'UTF-8') ?>" style="--subject-color: <?= htmlspecialchars($row['subject_color'], ENT_QUOTES, 'UTF-8') ?>"<?php endif; ?>
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<td><?= htmlspecialchars($row['category'] ?? '—', ENT_QUOTES, 'UTF-8') ?></td>
<td>
<?php if (!empty($row['subject_name'])): ?>
<span class="subject-chip" style="background: <?= htmlspecialchars($row['subject_color'] ?: '#6c757d', ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($row['subject_name'], ENT_QUOTES, 'UTF-8') ?></span>
<?php else: ?>
<span class="text-muted"></span>
<?php endif; ?>
</td>
<td>
<a href="scadenzario/detail.php?id=<?= (int)$row['id'] ?>" class="fw-semibold text-decoration-none" style="color:var(--scad-heading)"><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></a>
<?php if ((int)$row['attachment_count'] > 0): ?>
@ -476,15 +564,18 @@ $today = date('Y-m-d');
<div class="form-section-title">Informazioni principali</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6">
<label for="dlCategory" class="form-label fw-semibold">Argomento</label>
<input type="text" class="form-control" id="dlCategory" name="category" maxlength="100" list="categoryList" placeholder="es. Sicurezza, Ambiente, Antincendio...">
<datalist id="categoryList">
<?php
$cats = $pdo->query("SELECT DISTINCT category FROM scad_deadlines WHERE category IS NOT NULL AND category != '' ORDER BY category")->fetchAll(PDO::FETCH_COLUMN);
foreach ($cats as $c): ?>
<option value="<?= htmlspecialchars($c, ENT_QUOTES, 'UTF-8') ?>">
<?php endforeach; ?>
</datalist>
<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="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
@ -617,6 +708,15 @@ $today = date('Y-m-d');
var fpCheck = flatpickr('#filterCheckRange', fpOpts);
// --- Select2 ---
$('#dlSubject').select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona argomento...',
allowClear: true,
dropdownParent: $('#deadlineModal .modal-body'),
language: 'it',
width: '100%'
});
$('#dlDepartments').select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona reparti...',
@ -645,6 +745,8 @@ $today = date('Y-m-d');
if (statusFilter === 'non-completata') { if (row.getAttribute('data-status') === 'completata') return false; }
else if (statusFilter && row.getAttribute('data-status') !== statusFilter) return false;
if (deptFilter && (row.getAttribute('data-department') || '').indexOf(deptFilter) === -1) return false;
var subjFilter = $('#filterSubject').val();
if (subjFilter && String(row.getAttribute('data-subject-id')) !== String(subjFilter)) return false;
var empFilter = $('#filterEmployee').val();
if (empFilter && (row.getAttribute('data-employees') || '').indexOf(empFilter) === -1) return false;
var dueDates = fpDue.selectedDates;
@ -677,11 +779,13 @@ $today = date('Y-m-d');
function filterCards() {
var statusVal = $('#filterStatus').val();
var deptVal = $('#filterDepartment').val();
var subjVal = $('#filterSubject').val();
$('#mobileCards .deadline-card').each(function() {
var show = true;
if (statusVal === 'non-completata') { if ($(this).data('status') === 'completata') show = false; }
else if (statusVal && $(this).data('status') !== statusVal) show = false;
if (deptVal && ($(this).data('department') || '').indexOf(deptVal) === -1) show = false;
if (subjVal && String($(this).data('subject-id')) !== String(subjVal)) show = false;
var empVal = $('#filterEmployee').val();
if (empVal && ($(this).data('employees') || '').indexOf(empVal) === -1) show = false;
var dueDates = fpDue.selectedDates;
@ -696,7 +800,7 @@ $today = date('Y-m-d');
});
}
$('#filterStatus, #filterDepartment, #filterEmployee').on('change', function() {
$('#filterStatus, #filterDepartment, #filterEmployee, #filterSubject').on('change', function() {
if (table) table.draw();
filterCards();
});
@ -704,6 +808,7 @@ $today = date('Y-m-d');
$('#filterStatus').val('non-completata');
$('#filterDepartment').val('');
$('#filterEmployee').val('');
$('#filterSubject').val('');
fpDue.clear();
fpCheck.clear();
if (table) table.draw();
@ -725,6 +830,7 @@ $today = date('Y-m-d');
document.getElementById('dlNotifDays').value = '7';
document.getElementById('modalTitle').textContent = 'Nuova Scadenza';
document.getElementById('dlFiles').value = '';
$('#dlSubject').val('').trigger('change');
$('#dlDepartments').val(null).trigger('change');
$('#dlEmployees').val(null).trigger('change');
renderAttachments([]);
@ -839,7 +945,7 @@ $today = date('Y-m-d');
if (!data.success) { Swal.fire('Errore', data.message, 'error'); return; }
var d = data.data;
document.getElementById('dlId').value = d.id;
document.getElementById('dlCategory').value = d.category || '';
$('#dlSubject').val(d.subject_id || '').trigger('change');
document.getElementById('dlTopic').value = d.topic || '';
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
@ -927,7 +1033,7 @@ $today = date('Y-m-d');
if (!data.success) return;
var d = data.data;
document.getElementById('dlId').value = d.id;
document.getElementById('dlCategory').value = d.category || '';
$('#dlSubject').val(d.subject_id || '').trigger('change');
document.getElementById('dlTopic').value = d.topic || '';
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';

View File

@ -5,10 +5,13 @@ $pdo = $db->getConnection();
$sql = "
SELECT d.*,
s.name AS subject_name,
s.color AS subject_color,
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_subjects s ON s.id = d.subject_id
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
LEFT JOIN employees e ON e.id = de.employee_id
";
@ -171,8 +174,8 @@ if ($checkFrom || $checkTo) {
if (!empty($row['reparti_persone'])) $allDepts = array_merge($allDepts, array_map('trim', explode(',', $row['reparti_persone'])));
$reparti = implode(', ', array_unique(array_filter($allDepts)));
?>
<tr class="<?= $rowClass ?>">
<td><strong><?= htmlspecialchars($row['category'] ?? '', ENT_QUOTES, 'UTF-8') ?></strong></td>
<tr class="<?= $rowClass ?>"<?= !empty($row['subject_color']) ? ' style="border-left: 4px solid ' . htmlspecialchars($row['subject_color'], ENT_QUOTES, 'UTF-8') . '"' : '' ?>>
<td><strong><?= htmlspecialchars($row['subject_name'] ?? '', ENT_QUOTES, 'UTF-8') ?></strong></td>
<td><?= htmlspecialchars($row['law_regulation'] ?? '', ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($recurrenceLabels[$row['recurrence_type']] ?? $row['recurrence_type'], ENT_QUOTES, 'UTF-8') ?></td>

View File

@ -0,0 +1,25 @@
-- One-time migration: move scad_deadlines.category into dedicated scad_subjects table
CREATE TABLE IF NOT EXISTS scad_subjects (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
color VARCHAR(7) NOT NULL DEFAULT '#6c757d',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uniq_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO scad_subjects (name)
SELECT DISTINCT TRIM(category) FROM scad_deadlines
WHERE category IS NOT NULL AND TRIM(category) <> '';
ALTER TABLE scad_deadlines
ADD COLUMN subject_id INT UNSIGNED NULL AFTER id,
ADD INDEX idx_subject (subject_id),
ADD CONSTRAINT fk_deadlines_subject FOREIGN KEY (subject_id) REFERENCES scad_subjects(id) ON DELETE SET NULL;
UPDATE scad_deadlines d
JOIN scad_subjects s ON s.name = TRIM(d.category)
SET d.subject_id = s.id;
ALTER TABLE scad_deadlines DROP COLUMN category;

View File

@ -0,0 +1,34 @@
<?php
require_once(__DIR__ . '/../../ajax/auth_check.php');
header('Content-Type: application/json');
require_once(__DIR__ . '/../../../class/db-functions.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : 0;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
exit;
}
$stmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadlines WHERE subject_id = ?");
$stmt->execute([$id]);
$inUse = (int)$stmt->fetchColumn();
if ($inUse > 0) {
echo json_encode([
'success' => false,
'message' => "Impossibile eliminare: l'argomento è utilizzato in $inUse scadenz" . ($inUse === 1 ? 'a' : 'e') . '.',
]);
exit;
}
$pdo->prepare("DELETE FROM scad_subjects WHERE id = ?")->execute([$id]);
echo json_encode(['success' => true, 'message' => 'Argomento eliminato.']);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
}

View File

@ -0,0 +1,59 @@
<?php
require_once(__DIR__ . '/../../ajax/auth_check.php');
header('Content-Type: application/json');
require_once(__DIR__ . '/../../../class/db-functions.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
$name = trim($_POST['name'] ?? '');
$color = trim($_POST['color'] ?? '');
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Il nome è obbligatorio.']);
exit;
}
if (mb_strlen($name) > 100) {
echo json_encode(['success' => false, 'message' => 'Il nome supera 100 caratteri.']);
exit;
}
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
$color = '#6c757d';
}
// Uniqueness check
if ($id) {
$stmt = $pdo->prepare("SELECT id FROM scad_subjects WHERE name = ? AND id <> ?");
$stmt->execute([$name, $id]);
} else {
$stmt = $pdo->prepare("SELECT id FROM scad_subjects WHERE name = ?");
$stmt->execute([$name]);
}
if ($stmt->fetch()) {
echo json_encode(['success' => false, 'message' => 'Esiste già un argomento con questo nome.']);
exit;
}
if ($id) {
$stmt = $pdo->prepare("UPDATE scad_subjects SET name = ?, color = ? WHERE id = ?");
$stmt->execute([$name, $color, $id]);
$savedId = $id;
} else {
$stmt = $pdo->prepare("INSERT INTO scad_subjects (name, color) VALUES (?, ?)");
$stmt->execute([$name, $color]);
$savedId = (int)$pdo->lastInsertId();
}
echo json_encode([
'success' => true,
'message' => $id ? 'Argomento aggiornato.' : 'Argomento creato.',
'id' => $savedId,
'name' => $name,
'color' => $color,
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
}

View File

@ -0,0 +1,282 @@
<?php include('../../include/headscript.php'); ?>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$subjects = $pdo->query("
SELECT s.*,
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.subject_id = s.id) AS deadline_count,
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.subject_id = s.id AND d.status <> 'completed') AS open_count
FROM scad_subjects s
ORDER BY s.name
")->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
// subjects/index.php -> scadenzario -> userarea
$baseHref = dirname(dirname($scriptDir)) . '/';
?>
<base href="<?= $baseHref ?>">
<?php include('../../cssinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<title>Scadenzario - Argomenti</title>
<script>if(window.innerWidth>1024)document.addEventListener('DOMContentLoaded',function(){document.getElementById('appWrapper').classList.add('toggled')})</script>
<style>
:root {
--scad-primary: #5a8fd8;
--scad-primary-hover: #4578c0;
--scad-heading: #2c3e6b;
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
--scad-card-border: #dde4f0;
}
.scad-card { border: none; border-radius: 0.75rem; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; }
.scad-card .card-header { background: var(--scad-card-bg); border-bottom: 1px solid var(--scad-card-border); padding: 1rem 1.25rem; }
.scad-card .card-header h5 { font-weight: 700; color: var(--scad-heading); margin: 0; font-size: 1.1rem; letter-spacing: -0.01em; }
.scad-card .card-body { padding: 1.25rem; }
.btn-scad-primary { background: var(--scad-primary); border: none; color: #fff; font-weight: 600; font-size: 0.85rem; padding: 0.5rem 1rem; border-radius: 0.5rem; transition: all 0.2s; }
.btn-scad-primary:hover { background: var(--scad-primary-hover); color: #fff; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(90,143,216,0.35); }
.btn-scad-outline { background: transparent; border: 1.5px solid var(--scad-primary); color: var(--scad-primary); font-weight: 600; font-size: 0.85rem; padding: 0.45rem 1rem; border-radius: 0.5rem; transition: all 0.2s; }
.btn-scad-outline:hover { background: var(--scad-primary); color: #fff; transform: translateY(-1px); }
.btn-action { width: 32px; height: 32px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border: none; border-radius: 0.4rem; font-size: 0.85rem; transition: all 0.15s; }
.btn-action-edit { background: rgba(90,143,216,0.12); color: var(--scad-primary); }
.btn-action-edit:hover { background: var(--scad-primary); color: #fff; }
.btn-action-delete { background: rgba(220,53,69,0.12); color: #dc3545; }
.btn-action-delete:hover { background: #dc3545; color: #fff; }
.btn-action-history { background: rgba(108,117,125,0.12); color: #495057; }
.btn-action-history:hover { background: #495057; color: #fff; }
.color-swatch { width: 28px; height: 28px; border-radius: 6px; display: inline-block; border: 1px solid rgba(0,0,0,0.08); vertical-align: middle; }
.subject-row { border-left: 4px solid var(--row-color, #e9ecef); }
.empty-state { text-align: center; padding: 3rem 1rem; color: #6c757d; }
.empty-state i { font-size: 3rem; opacity: 0.3; margin-bottom: 1rem; }
/* Color picker swatches */
.color-picker-grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 0.4rem; margin-bottom: 0.75rem; }
.color-picker-swatch { width: 100%; aspect-ratio: 1; border-radius: 6px; cursor: pointer; border: 2px solid transparent; transition: all 0.15s; }
.color-picker-swatch:hover { transform: scale(1.1); }
.color-picker-swatch.selected { border-color: #2c3e6b; transform: scale(1.1); box-shadow: 0 2px 8px rgba(44,62,107,0.3); }
@media (max-width: 575.98px) {
.scad-card .card-header { flex-direction: column; gap: 0.75rem; align-items: flex-start !important; }
.header-actions { width: 100%; }
.header-actions .btn { width: 100%; justify-content: center; }
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('../../include/navbar.php'); ?>
<?php include('../../include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb" style="background:transparent;padding:0;margin:0;font-size:0.85rem">
<li class="breadcrumb-item"><a href="scadenzario/index.php">Scadenzario</a></li>
<li class="breadcrumb-item active" aria-current="page">Argomenti</li>
</ol>
</nav>
<div class="card scad-card">
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
<h5><i class="fa-solid fa-tags me-2"></i>Argomenti</h5>
<div class="header-actions d-flex gap-2 flex-wrap">
<a href="scadenzario/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
<i class="fa-solid fa-arrow-left"></i><span>Scadenzario</span>
</a>
<button class="btn btn-scad-primary d-inline-flex align-items-center gap-2" id="btnAddSubject">
<i class="fa-solid fa-plus"></i><span>Nuovo Argomento</span>
</button>
</div>
</div>
<div class="card-body">
<?php if (count($subjects) === 0): ?>
<div class="empty-state">
<i class="fa-solid fa-tags"></i>
<p>Nessun argomento definito.<br>Clicca <strong>"Nuovo Argomento"</strong> per aggiungere il primo.</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:60px">Colore</th>
<th>Nome</th>
<th class="text-center" style="width:120px">Scadenze</th>
<th class="text-center" style="width:120px">Aperte</th>
<th class="text-center" style="width:180px">Azioni</th>
</tr>
</thead>
<tbody id="subjectsTbody">
<?php foreach ($subjects as $s): ?>
<tr class="subject-row"
style="--row-color: <?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"
data-id="<?= (int)$s['id'] ?>"
data-name="<?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?>"
data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"
data-in-use="<?= (int)$s['deadline_count'] ?>">
<td><span class="color-swatch" style="background: <?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"></span></td>
<td class="fw-semibold" style="color:var(--scad-heading)"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></td>
<td class="text-center"><?= (int)$s['deadline_count'] ?></td>
<td class="text-center"><?= (int)$s['open_count'] ?></td>
<td class="text-center">
<div class="d-inline-flex gap-1">
<a href="scadenzario/index.php?subject_id=<?= (int)$s['id'] ?>" class="btn-action btn-action-history" title="Storico scadenze">
<i class="fa-solid fa-clock-rotate-left"></i>
</a>
<button class="btn-action btn-action-edit btn-edit" title="Modifica"><i class="fa-solid fa-pen"></i></button>
<button class="btn-action btn-action-delete btn-delete" title="Elimina"><i class="fa-solid fa-trash"></i></button>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include('../../include/footer.php'); ?>
</div>
<!-- Subject Modal -->
<div class="modal fade" id="subjectModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="subjectModalTitle">Nuovo Argomento</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<form id="subjectForm">
<div class="modal-body">
<input type="hidden" id="subjId" name="id" value="">
<div class="mb-3">
<label for="subjName" class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="subjName" name="name" maxlength="100" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Colore</label>
<div class="color-picker-grid" id="colorPickerGrid"></div>
<div class="d-flex align-items-center gap-2">
<input type="color" class="form-control form-control-color" id="subjColor" name="color" value="#6c757d" style="width:56px;height:38px;padding:2px">
<input type="text" class="form-control" id="subjColorText" maxlength="7" placeholder="#RRGGBB" style="max-width:130px;font-family:monospace">
<span class="text-muted small">Personalizzato</span>
</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">Salva</button>
</div>
</form>
</div>
</div>
</div>
<?php include('../../jsinclude.php'); ?>
<script>
$(function () {
const PRESET_COLORS = [
'#dc3545','#e8930c','#ffc107','#198754','#20c997',
'#0dcaf0','#0d6efd','#5a8fd8','#6f42c1','#d63384',
'#6c757d','#495057','#212529','#8b4513','#795548',
'#b88a44','#e83e8c','#17a2b8','#28a745','#343a40'
];
function buildPicker(selected) {
const $grid = $('#colorPickerGrid').empty();
PRESET_COLORS.forEach(c => {
const $sw = $('<div class="color-picker-swatch"></div>')
.css('background', c)
.attr('data-color', c);
if (c.toLowerCase() === (selected || '').toLowerCase()) $sw.addClass('selected');
$sw.on('click', function () {
$('#colorPickerGrid .color-picker-swatch').removeClass('selected');
$(this).addClass('selected');
$('#subjColor').val(c);
$('#subjColorText').val(c);
});
$grid.append($sw);
});
}
function openModal(data) {
const isEdit = !!data;
$('#subjectModalTitle').text(isEdit ? 'Modifica Argomento' : 'Nuovo Argomento');
$('#subjId').val(isEdit ? data.id : '');
$('#subjName').val(isEdit ? data.name : '');
const color = isEdit ? data.color : '#6c757d';
$('#subjColor').val(color);
$('#subjColorText').val(color);
buildPicker(color);
new bootstrap.Modal('#subjectModal').show();
}
$('#btnAddSubject').on('click', () => openModal(null));
$('#subjectsTbody').on('click', '.btn-edit', function () {
const $tr = $(this).closest('tr');
openModal({ id: $tr.data('id'), name: $tr.data('name'), color: $tr.data('color') });
});
$('#subjectsTbody').on('click', '.btn-delete', function () {
const $tr = $(this).closest('tr');
const inUse = parseInt($tr.data('in-use') || 0, 10);
const name = $tr.data('name');
if (inUse > 0) {
Swal.fire({ icon: 'warning', title: 'Impossibile eliminare',
text: `L'argomento "${name}" è utilizzato in ${inUse} scadenz${inUse === 1 ? 'a' : 'e'}.` });
return;
}
Swal.fire({
title: `Eliminare "${name}"?`,
icon: 'warning', showCancelButton: true,
confirmButtonText: 'Elimina', cancelButtonText: 'Annulla',
confirmButtonColor: '#dc3545'
}).then(r => {
if (!r.isConfirmed) return;
$.post('scadenzario/subjects/ajax/delete_subject.php', { id: $tr.data('id') })
.done(res => {
if (res.success) { location.reload(); }
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
})
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
});
});
$('#subjColor').on('input', function () { $('#subjColorText').val($(this).val()); });
$('#subjColorText').on('input', function () {
const v = $(this).val();
if (/^#[0-9A-Fa-f]{6}$/.test(v)) $('#subjColor').val(v);
});
$('#subjectForm').on('submit', function (e) {
e.preventDefault();
const payload = {
id: $('#subjId').val(),
name: $('#subjName').val().trim(),
color: $('#subjColor').val()
};
if (!payload.name) { Swal.fire({ icon: 'warning', title: 'Nome obbligatorio' }); return; }
$.post('scadenzario/subjects/ajax/save_subject.php', payload)
.done(res => {
if (res.success) { location.reload(); }
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
})
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
});
});
</script>
</body>
</html>