15 Commits

Author SHA1 Message Date
solocla fdc3af01f3 Merge branch 'feature/user_profile' of https://gitea.solocla.synology.me/solocla/zibo-dashboard into feature/user_profile 2026-05-26 16:05:26 +02:00
solocla 3d54140280 fixed employee profile 2026-05-26 16:05:24 +02:00
RMubarakzyanov bfdbbbfc8f Merge branch 'main' into feature/user_profile 2026-05-26 16:52:41 +03:00
RMubarakzyanov 40a5771a4b bulk operations 2026-05-24 01:04:41 +03:00
RMubarakzyanov 9f5a585717 Merge branch 'main' into feature/user_profile 2026-05-24 00:16:28 +03:00
RMubarakzyanov 9ec5419a86 dlFunction fix 2026-05-24 00:15:09 +03:00
RMubarakzyanov c05091e020 Merge branch 'main' into feature/20260520_scadenziario
# Conflicts:
#	public/userarea/scadenzario/index.php
2026-05-24 00:02:20 +03:00
RMubarakzyanov 0b470f290e fix auto-open 2026-05-23 23:56:43 +03:00
solocla e74870c8d3 added functions 2026-05-22 09:16:46 +02:00
RMubarakzyanov 9001eff317 file repo, cc, auto-open 2026-05-21 23:31:36 +03:00
RMubarakzyanov 7cbd74111d Merge branch 'main' into feature/user_profile
# Conflicts:
#	public/userarea/include/topbar.php
#	public/userarea/scadenzario/include/my_deadlines_widget.php
2026-05-21 22:44:42 +03:00
solocla 650676037a fixed date format 2026-05-20 14:48:45 +02:00
solocla 2fc34c3cf4 fixed redirection 2026-05-18 13:49:22 +02:00
solocla 955a7ed9e9 fixed user setting 2026-05-18 13:31:34 +02:00
RMubarakzyanov cb221a8039 fix initial+refresher 2026-05-17 21:13:57 +03:00
30 changed files with 4692 additions and 1322 deletions
+2
View File
@@ -31,6 +31,8 @@ MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MANAGER_USER_ID=
PUSHER_APP_ID= PUSHER_APP_ID=
PUSHER_APP_KEY= PUSHER_APP_KEY=
PUSHER_APP_SECRET= PUSHER_APP_SECRET=
@@ -111,6 +111,14 @@ class LoginController extends Controller
return redirect()->to('userarea/production_dashboard.php'); return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('User')) { } elseif ($user->hasRole('User')) {
return redirect()->to('userarea/production_dashboard.php'); return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('HR')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('SuperUser')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('Management')) {
return redirect()->to('userarea/production_dashboard.php');
} elseif ($user->hasRole('Quality')) {
return redirect()->to('userarea/production_dashboard.php');
} }
// Se il ruolo non è specificato, reindirizza alla home predefinita // Se il ruolo non è specificato, reindirizza alla home predefinita
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddFunctionsToScadDeadlines extends AbstractMigration
{
public function change(): void
{
$this->table('scad_functions', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
])
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
])
->addColumn('status', 'string', [
'limit' => 20,
'null' => false,
'default' => 'active',
])
->addColumn('created_at', 'timestamp', [
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['name'], [
'unique' => true,
'name' => 'uniq_scad_functions_name',
])
->create();
$this->table('scad_deadlines')
->addColumn('function_id', 'integer', [
'signed' => false,
'null' => true,
'after' => 'subject_id',
])
->addIndex(['function_id'], [
'name' => 'idx_scad_deadlines_function_id',
])
->addForeignKey('function_id', 'scad_functions', 'id', [
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_scad_deadlines_function',
])
->update();
}
}
@@ -0,0 +1,104 @@
<?php
/**
* Bulk "renew": set a common completed_date on the selected training records
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
// $pdo and $currentUserId from hr_auth_check.php
$completedDate = trim($_POST['completed_date'] ?? '');
$ids = $_POST['training_ids'] ?? [];
if (!is_array($ids)) {
$ids = [];
}
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0)));
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
echo json_encode(['success' => false, 'message' => 'Indicare una data valida.']);
exit;
}
if (empty($ids)) {
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un record.']);
exit;
}
try {
$pdo->beginTransaction();
// Load each record with its topic default frequency
$rowStmt = $pdo->prepare("
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
et.update_frequency_months, tt.default_frequency_months
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
WHERE et.id = :id
");
$upd = $pdo->prepare("
UPDATE employee_trainings
SET completed_date = :cd, next_due_date = :nd, updated_at = NOW()
WHERE id = :id
");
$logStmt = $pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
");
$updated = 0;
foreach ($ids as $id) {
$rowStmt->execute(['id' => $id]);
$row = $rowStmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
continue;
}
// Effective frequency: per-record override, else topic default
$effFreq = $row['update_frequency_months'] !== null
? (int)$row['update_frequency_months']
: ($row['default_frequency_months'] !== null ? (int)$row['default_frequency_months'] : null);
$nextDue = null;
if ($effFreq !== null && $effFreq > 0) {
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
if ($d) {
$d->modify('+' . $effFreq . ' months');
$nextDue = $d->format('Y-m-d');
}
}
$upd->execute(['cd' => $completedDate, 'nd' => $nextDue, 'id' => $id]);
if ((string)$row['completed_date'] !== (string)$completedDate) {
$logStmt->execute([
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'completed_date',
'old_v' => $row['completed_date'], 'new_v' => $completedDate, 'cb' => $currentUserId,
]);
}
if ((string)$row['next_due_date'] !== (string)$nextDue) {
$logStmt->execute([
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'next_due_date',
'old_v' => $row['next_due_date'], 'new_v' => $nextDue, 'cb' => $currentUserId,
]);
}
$updated++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'updated' => $updated,
'message' => $updated . ' record aggiornat' . ($updated === 1 ? 'o' : 'i') . '.',
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,92 @@
<?php
/**
* Calendar events for the training calendar (training_calendar.php).
* Returns FullCalendar event objects for the *current* training record per
* (employee, topic) that has a next_due_date, colored by computed status.
* HR-only.
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
try {
// $pdo and $currentUserId provided by hr_auth_check.php
$start = $_GET['start'] ?? null;
$end = $_GET['end'] ?? null;
$fStatus = isset($_GET['status']) ? trim($_GET['status']) : '';
$fDept = isset($_GET['department_id']) && $_GET['department_id'] !== '' ? (int)$_GET['department_id'] : 0;
$fTopic = isset($_GET['topic_id']) && $_GET['topic_id'] !== '' ? (int)$_GET['topic_id'] : 0;
$fEmp = isset($_GET['employee_id']) && $_GET['employee_id'] !== '' ? (int)$_GET['employee_id'] : 0;
$where = [];
$params = [];
// Deadlines only (one-time trainings have no next_due_date)
$where[] = "et.next_due_date IS NOT NULL";
// Only the most recent record per (employee, topic)
$where[] = "NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)";
if ($start && $end) {
$where[] = "et.next_due_date >= :start AND et.next_due_date <= :end";
$params['start'] = $start;
$params['end'] = $end;
}
if ($fDept > 0) { $where[] = "e.department_id = :did"; $params['did'] = $fDept; }
if ($fTopic > 0) { $where[] = "et.training_topic_id = :tid"; $params['tid'] = $fTopic; }
if ($fEmp > 0) { $where[] = "et.employee_id = :eid"; $params['eid'] = $fEmp; }
$whereSql = 'WHERE ' . implode(' AND ', $where);
$stmt = $pdo->prepare("
SELECT et.id, et.employee_id, et.next_due_date, et.reminder_days,
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
e.first_name, e.last_name
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
JOIN employees e ON e.id = et.employee_id
$whereSql
");
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$today = new DateTime('today');
$events = [];
foreach ($rows as $r) {
$rem = $r['reminder_days'] !== null
? (int)$r['reminder_days']
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
$due = DateTime::createFromFormat('Y-m-d', $r['next_due_date']);
if (!$due) continue;
$daysLeft = (int)$today->diff($due)->format('%r%a');
if ($daysLeft < 0) { $code = 'expired'; $color = '#dc3545'; }
elseif ($daysLeft <= $rem){ $code = 'due_soon'; $color = '#e8930c'; }
else { $code = 'compliant'; $color = '#198754'; }
if ($fStatus !== '' && $fStatus !== $code) continue;
$name = trim($r['first_name'] . ' ' . $r['last_name']);
$events[] = [
'id' => (int)$r['id'],
'title' => $name . ' — ' . $r['topic_name'],
'start' => $r['next_due_date'],
'allDay' => true,
'backgroundColor' => $color,
'borderColor' => $color,
'url' => 'employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training',
];
}
echo json_encode($events);
} catch (Exception $e) {
echo json_encode([]);
}
@@ -0,0 +1,131 @@
<?php
/**
* Bulk-create training records: one employee_trainings row per selected employee,
* all sharing the same course + parameters (a single training "session").
* Mirrors the next_due_date logic of ajax/employee_profile/save_training.php.
* HR-only.
*/
require_once(__DIR__ . '/../hr_auth_check.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
// $pdo and $currentUserId from hr_auth_check.php
$topicId = (int)($_POST['training_topic_id'] ?? 0);
$completedDate = trim($_POST['completed_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$description = trim($_POST['description'] ?? '');
$trainingType = trim($_POST['training_type'] ?? 'initial');
$freqRaw = $_POST['update_frequency_months'] ?? '';
$remRaw = $_POST['reminder_days'] ?? '';
$employeeIds = $_POST['employee_ids'] ?? [];
if (!is_array($employeeIds)) {
$employeeIds = [];
}
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
if ($topicId <= 0) {
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
exit;
}
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
exit;
}
if (empty($employeeIds)) {
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
exit;
}
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
$trainingType = 'initial';
}
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
$topicStmt->execute(['id' => $topicId]);
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
if (!$topic) {
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
exit;
}
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
/* Effective frequency → next_due_date (same for every employee: same date + same frequency) */
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
$nextDue = null;
if ($effFreq !== null && $effFreq > 0) {
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
if ($d) {
$d->modify('+' . (int)$effFreq . ' months');
$nextDue = $d->format('Y-m-d');
}
}
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$description = $description !== '' ? $description : null;
try {
$pdo->beginTransaction();
// Only insert for employees that actually exist
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
$ins = $pdo->prepare("
INSERT INTO employee_trainings
(employee_id, training_topic_id, completed_date,
delivered_by, description,
training_type, update_frequency_months, reminder_days, next_due_date,
created_by, created_at, updated_at)
VALUES
(:eid, :tid, :completed_date,
:delivered_by, :description,
:training_type, :freq, :rem, :next_due,
:cb, NOW(), NOW())
");
$logStmt = $pdo->prepare("
INSERT INTO employee_training_log
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
VALUES
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
");
$created = 0;
foreach ($employeeIds as $eid) {
$checkEmp->execute(['id' => $eid]);
if (!$checkEmp->fetchColumn()) {
continue;
}
$ins->execute([
'eid' => $eid,
'tid' => $topicId,
'completed_date' => $completedDate,
'delivered_by' => $deliveredBy,
'description' => $description,
'training_type' => $trainingType,
'freq' => $freq,
'rem' => $rem,
'next_due' => $nextDue,
'cb' => $currentUserId,
]);
$newId = (int)$pdo->lastInsertId();
$logStmt->execute(['eid' => $eid, 'tid' => $newId, 'cb' => $currentUserId]);
$created++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'created' => $created,
'message' => $created . ' formazion' . ($created === 1 ? 'e registrata' : 'i registrate') . '.',
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -47,7 +47,8 @@ $sent = 0;
$skipped = 0; $skipped = 0;
$errors = 0; $errors = 0;
/* Candidate trainings (with optional override reminder + topic default) */ /* Candidate trainings (with optional override reminder + topic default).
Only the most recent record per (employee, topic) older history rows skipped. */
$stmt = $pdo->query(" $stmt = $pdo->query("
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date, SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
et.reminder_days, et.delivered_by, et.reminder_days, et.delivered_by,
@@ -60,6 +61,13 @@ $stmt = $pdo->query("
JOIN employees e ON e.id = et.employee_id JOIN employees e ON e.id = et.employee_id
LEFT JOIN auth_users au ON au.id = e.auth_user_id LEFT JOIN auth_users au ON au.id = e.auth_user_id
WHERE et.next_due_date IS NOT NULL WHERE et.next_due_date IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)
"); ");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
File diff suppressed because it is too large Load Diff
+5
View File
@@ -321,6 +321,11 @@
<i class='bx bx-radio-circle'></i>Storico Formazione <i class='bx bx-radio-circle'></i>Storico Formazione
</a> </a>
</li> </li>
<li>
<a href="training_calendar.php">
<i class='bx bx-radio-circle'></i>Calendario Formazione
</a>
</li>
<?php endif; ?> <?php endif; ?>
<?php if (userCan('hr.skills.view')) : ?> <?php if (userCan('hr.skills.view')) : ?>
+1 -1
View File
@@ -100,7 +100,7 @@
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item d-flex align-items-center" href="../users"> <a class="dropdown-item d-flex align-items-center" href="user_settings.php">
<i class="bx bx-user fs-5"></i><span>Utente</span> <i class="bx bx-user fs-5"></i><span>Utente</span>
</a> </a>
</li> </li>
@@ -19,6 +19,7 @@ if (!$__trWidgetHr) {
return; return;
} }
/* Only the most recent record per (employee, topic) — older history rows ignored. */
$__trRows = $pdo->query(" $__trRows = $pdo->query("
SELECT et.id, SELECT et.id,
et.next_due_date, et.next_due_date,
@@ -27,6 +28,13 @@ $__trRows = $pdo->query("
FROM employee_trainings et FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id JOIN training_topics tt ON tt.id = et.training_topic_id
WHERE et.next_due_date IS NOT NULL WHERE et.next_due_date IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)
")->fetchAll(PDO::FETCH_ASSOC); ")->fetchAll(PDO::FETCH_ASSOC);
$__expiredCount = 0; $__expiredCount = 0;
+3 -3
View File
@@ -51,7 +51,7 @@ $dashboardSections = [
'permission' => 'warehouse.dashboard.view', 'permission' => 'warehouse.dashboard.view',
], ],
[ [
'label' => 'Scadenziario', 'label' => 'Smart-Alert',
'icon' => '⏰', 'icon' => '⏰',
'class' => 'btn-scadenziario', 'class' => 'btn-scadenziario',
'url' => 'scadenzario/index.php', 'url' => 'scadenzario/index.php',
@@ -187,7 +187,7 @@ $dashboardSections = [
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" /> <link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?> <?php include('cssinclude.php'); ?>
<title>Dashboard Produzione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title> <title>Dashboard <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<!-- Bootstrap + jQuery --> <!-- Bootstrap + jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
@@ -539,7 +539,7 @@ $dashboardSections = [
<?php include(__DIR__ . '/include/training_widget.php'); ?> <?php include(__DIR__ . '/include/training_widget.php'); ?>
</div> </div>
<h3 class="dashboard-title">Dashboard Produzione</h3> <h3 class="dashboard-title">Dashboard</h3>
<!-- ===== STATISTICHE PRINCIPALI ===== --> <!-- ===== STATISTICHE PRINCIPALI ===== -->
<div class="stats-row"> <div class="stats-row">
@@ -4,12 +4,19 @@ header('Content-Type: application/json');
require_once(__DIR__ . '/../../class/db-functions.php'); require_once(__DIR__ . '/../../class/db-functions.php');
try { 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.']); echo json_encode(['success' => false, 'message' => 'ID non valido.']);
exit; 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(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
@@ -34,11 +41,13 @@ try {
->execute([$id, $currentUserId]); ->execute([$id, $currentUserId]);
$newId = null; $newId = null;
$newDueDate = null;
// If recurring, create next deadline // If recurring AND the user asked for it, create the next deadline
if ($deadline['recurrence_type'] !== 'once') { if ($deadline['recurrence_type'] !== 'once' && $createNext) {
$dueDate = new DateTime($deadline['due_date']); $dueDate = new DateTime($deadline['due_date']);
$checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null; $checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null;
$documentDate = $deadline['document_date'] ? new DateTime($deadline['document_date']) : null;
switch ($deadline['recurrence_type']) { switch ($deadline['recurrence_type']) {
case 'monthly': $interval = new DateInterval('P1M'); break; case 'monthly': $interval = new DateInterval('P1M'); break;
@@ -57,23 +66,25 @@ try {
if ($interval) { if ($interval) {
$dueDate->add($interval); $dueDate->add($interval);
if ($checkDate) $checkDate->add($interval); if ($checkDate) $checkDate->add($interval);
if ($documentDate) $documentDate->add($interval);
$ins = $pdo->prepare(" $ins = $pdo->prepare("
INSERT INTO scad_deadlines 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) document_date, notification_days, storage_location, notes, created_by, departments)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"); ");
$ins->execute([ $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'), $deadline['recurrence_type'], $dueDate->format('Y-m-d'),
$checkDate ? $checkDate->format('Y-m-d') : null, $checkDate ? $checkDate->format('Y-m-d') : null,
$deadline['document_date'], $documentDate ? $documentDate->format('Y-m-d') : null,
$deadline['notification_days'], $deadline['storage_location'], $deadline['notification_days'], $deadline['storage_location'],
$deadline['notes'], $deadline['created_by'], $deadline['departments'] $deadline['notes'], $deadline['created_by'], $deadline['departments']
]); ]);
$newId = $pdo->lastInsertId(); $newId = $pdo->lastInsertId();
$newDueDate = $dueDate;
// Copy employee assignments // Copy employee assignments
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?"); $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 // History for new
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)") $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)")
->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]); ->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]);
@@ -97,7 +133,7 @@ try {
$msg = 'Scadenza completata con successo.'; $msg = 'Scadenza completata con successo.';
if ($newId) { 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]); echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]);
@@ -23,20 +23,32 @@ try {
exit; 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']; $filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
if (file_exists($filePath)) { if (file_exists($filePath)) {
unlink($filePath); unlink($filePath);
} }
$action = 'attachment_removed';
// Delete DB record $message = 'Allegato eliminato.';
$pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]); }
// History // History
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_removed', ?)") $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, ?, ?)")
->execute([$att['deadline_id'], $currentUserId, $att['original_name']]); ->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) { } catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]); echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
@@ -13,10 +13,29 @@ try {
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $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 = $pdo->prepare("DELETE FROM scad_deadlines WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
if ($stmt->rowCount() > 0) { 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.']); echo json_encode(['success' => true, 'message' => 'Scadenza eliminata con successo.']);
} else { } else {
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']); echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
@@ -9,6 +9,7 @@ try {
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null; $id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
$subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null; $subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null;
$function_id = isset($_POST['function_id']) && is_numeric($_POST['function_id']) && (int)$_POST['function_id'] > 0 ? (int)$_POST['function_id'] : null;
$topic = trim($_POST['topic'] ?? ''); $topic = trim($_POST['topic'] ?? '');
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null; $law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
$recurrence_type = $_POST['recurrence_type'] ?? 'once'; $recurrence_type = $_POST['recurrence_type'] ?? 'once';
@@ -52,15 +53,25 @@ try {
if ($id) { if ($id) {
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
UPDATE scad_deadlines SET UPDATE scad_deadlines SET
subject_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?, subject_id = ?, function_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
due_date = ?, check_date = ?, document_date = ?, notification_days = ?, due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
storage_location = ?, notes = ?, departments = ? storage_location = ?, notes = ?, departments = ?
WHERE id = ? WHERE id = ?
"); ");
$stmt->execute([ $stmt->execute([
$subject_id, $topic, $law_regulation, $recurrence_type, $subject_id,
$due_date, $check_date, $document_date, $notification_days, $function_id,
$storage_location, $notes, $departmentsStr, $id $topic,
$law_regulation,
$recurrence_type,
$due_date,
$check_date,
$document_date,
$notification_days,
$storage_location,
$notes,
$departmentsStr,
$id
]); ]);
// Re-link employees // Re-link employees
@@ -75,14 +86,24 @@ try {
// INSERT // INSERT
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO scad_deadlines 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) document_date, notification_days, storage_location, notes, created_by, departments)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"); ");
$stmt->execute([ $stmt->execute([
$subject_id, $topic, $law_regulation, $recurrence_type, $subject_id,
$due_date, $check_date, $document_date, $notification_days, $function_id,
$storage_location, $notes, $currentUserId, $departmentsStr $topic,
$law_regulation,
$recurrence_type,
$due_date,
$check_date,
$document_date,
$notification_days,
$storage_location,
$notes,
$currentUserId,
$departmentsStr
]); ]);
$deadlineId = $pdo->lastInsertId(); $deadlineId = $pdo->lastInsertId();
@@ -107,7 +128,6 @@ try {
'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.', 'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.',
'id' => $deadlineId 'id' => $deadlineId
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
if (isset($pdo) && $pdo->inTransaction()) { if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack(); $pdo->rollBack();
@@ -25,6 +25,17 @@ $pdo = $db->getConnection();
$today = date('Y-m-d'); $today = date('Y-m-d');
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/'); $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; $sent = 0;
$skipped = 0; $skipped = 0;
$errors = 0; $errors = 0;
@@ -143,6 +154,11 @@ foreach ($deadlines as $dl) {
); );
$mail->addAddress($emp['email'], trim($emp['first_name'] . ' ' . $emp['last_name'])); $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']; $detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id'];
$topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic']; $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']; $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']; $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', 'notification_sent' => '#adb5bd']; $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', 'notification_sent' => 'fa-bell']; $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 ?>"> <base href="<?= $baseHref ?>">
<?php include('../cssinclude.php'); ?> <?php include('../cssinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <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> <title><?= $deadline ? htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') . ' — ' : '' ?>Scadenzario</title>
<script> <script>
if (window.innerWidth > 1024) document.addEventListener('DOMContentLoaded', function() { if (window.innerWidth > 1024) document.addEventListener('DOMContentLoaded', function() {
@@ -755,25 +763,33 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
</div> </div>
<?php include('../include/footer.php'); ?> <?php include('../include/footer.php'); ?>
</div> </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 include('../jsinclude.php'); ?>
<?php if ($deadline && !$isCompleted): ?> <?php if ($deadline && !$isCompleted): ?>
<script> <script>
$(document).ready(function() { $(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() { $('#btnModifica').on('click', function() {
window.location.href = 'scadenzario/index.php?edit=<?= (int)$deadline['id'] ?>'; window.openDeadlineEdit(<?= (int)$deadline['id'] ?>);
}); });
$('#btnCompleta').on('click', function() { function detailSubmitComplete(createNext, copyAttachments) {
Swal.fire({ var fd = new FormData();
title: 'Completare la scadenza?', fd.append('id', '<?= (int)$deadline['id'] ?>');
icon: 'question', fd.append('create_next', createNext ? '1' : '0');
showCancelButton: true, fd.append('copy_attachments', copyAttachments ? '1' : '0');
confirmButtonColor: '#198754',
cancelButtonText: 'Annulla', fetch('scadenzario/ajax/complete_deadline.php', {
confirmButtonText: 'Completa' method: 'POST',
}).then(function(result) { body: fd
if (result.isConfirmed) { })
fetch('scadenzario/ajax/complete_deadline.php?id=<?= (int)$deadline['id'] ?>')
.then(function(r) { .then(function(r) {
return r.json(); return r.json();
}) })
@@ -783,11 +799,15 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
icon: 'success', icon: 'success',
title: 'Completata', title: 'Completata',
text: data.message, text: data.message,
timer: 2500, timer: 1800,
showConfirmButton: false showConfirmButton: false
}) })
.then(function() { .then(function() {
if (data.new_id) {
window.location.href = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
} else {
window.location.href = 'scadenzario/index.php'; window.location.href = 'scadenzario/index.php';
}
}); });
} else { } else {
Swal.fire('Errore', data.message, 'error'); 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'); 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> </script>
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
<?php endif; ?> <?php endif; ?>
</body> </body>
@@ -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 function_id = ?");
$stmt->execute([$id]);
$inUse = (int)$stmt->fetchColumn();
if ($inUse > 0) {
echo json_encode([
'success' => false,
'message' => "Impossibile eliminare: la funzione è utilizzata in $inUse scadenz" . ($inUse === 1 ? 'a' : 'e') . '.',
]);
exit;
}
$pdo->prepare("DELETE FROM scad_functions WHERE id = ?")->execute([$id]);
echo json_encode(['success' => true, 'message' => 'Funzione eliminata.']);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
}
@@ -0,0 +1,63 @@
<?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'] ?? '');
$description = trim($_POST['description'] ?? '') ?: null;
if ($name === '') {
echo json_encode(['success' => false, 'message' => 'Il nome è obbligatorio.']);
exit;
}
if (mb_strlen($name) > 255) {
echo json_encode(['success' => false, 'message' => 'Il nome supera 255 caratteri.']);
exit;
}
if ($id) {
$stmt = $pdo->prepare("SELECT id FROM scad_functions WHERE name = ? AND id <> ?");
$stmt->execute([$name, $id]);
} else {
$stmt = $pdo->prepare("SELECT id FROM scad_functions WHERE name = ?");
$stmt->execute([$name]);
}
if ($stmt->fetch()) {
echo json_encode(['success' => false, 'message' => 'Esiste già una funzione con questo nome.']);
exit;
}
if ($id) {
$stmt = $pdo->prepare("
UPDATE scad_functions
SET name = ?, description = ?
WHERE id = ?
");
$stmt->execute([$name, $description, $id]);
$savedId = $id;
} else {
$stmt = $pdo->prepare("
INSERT INTO scad_functions (name, description, status)
VALUES (?, ?, 'active')
");
$stmt->execute([$name, $description]);
$savedId = (int)$pdo->lastInsertId();
}
echo json_encode([
'success' => true,
'message' => $id ? 'Funzione aggiornata.' : 'Funzione creata.',
'id' => $savedId,
'name' => $name,
'description' => $description,
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
}
@@ -0,0 +1,464 @@
<?php include('../../include/headscript.php'); ?>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$functions = $pdo->query("
SELECT f.*,
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id) AS deadline_count,
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id AND d.status <> 'completed') AS open_count
FROM scad_functions f
ORDER BY f.name ASC
")->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']);
$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 - Funzioni</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;
}
.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;
}
.btn-scad-primary:hover {
background: var(--scad-primary-hover);
color: #fff;
}
.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;
}
.btn-scad-outline:hover {
background: var(--scad-primary);
color: #fff;
}
.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;
}
.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;
}
.function-card {
background: #fff;
border: 1px solid var(--scad-card-border);
border-radius: 0.6rem;
padding: 0.85rem 0.95rem;
margin-bottom: 0.6rem;
}
.function-card .fc-name {
font-weight: 700;
color: var(--scad-heading);
font-size: 0.95rem;
}
.function-card .fc-stats {
display: flex;
gap: 0.75rem;
font-size: 0.8rem;
color: #6c757d;
margin: 0.5rem 0;
}
.function-card .fc-stats strong {
color: var(--scad-heading);
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #6c757d;
}
.empty-state i {
font-size: 3rem;
opacity: 0.3;
margin-bottom: 1rem;
}
@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">Funzioni</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-briefcase me-2"></i>Funzioni</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="btnAddFunction">
<i class="fa-solid fa-plus"></i><span>Nuova Funzione</span>
</button>
</div>
</div>
<div class="card-body">
<?php if (count($functions) === 0): ?>
<div class="empty-state">
<i class="fa-solid fa-briefcase"></i>
<p>Nessuna funzione definita.<br>Clicca <strong>"Nuova Funzione"</strong> per aggiungere la prima.</p>
</div>
<?php else: ?>
<div id="functionsList">
<div class="d-md-none">
<?php foreach ($functions as $f): ?>
<div class="function-card"
data-id="<?= (int)$f['id'] ?>"
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
data-in-use="<?= (int)$f['deadline_count'] ?>">
<div class="fc-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
<?php if (!empty($f['description'])): ?>
<div class="text-muted small mt-1"><?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<div class="fc-stats">
<span>Scadenze: <strong><?= (int)$f['deadline_count'] ?></strong></span>
<span>Aperte: <strong><?= (int)$f['open_count'] ?></strong></span>
</div>
<div class="d-flex gap-1 justify-content-end">
<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>
</div>
<?php endforeach; ?>
</div>
<div class="d-none d-md-block">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Nome</th>
<th>Descrizione</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:120px">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($functions as $f): ?>
<tr data-id="<?= (int)$f['id'] ?>"
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
data-in-use="<?= (int)$f['deadline_count'] ?>">
<td class="fw-semibold" style="color:var(--scad-heading)">
<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>
</td>
<td class="text-muted">
<?= htmlspecialchars($f['description'] ?? '—', ENT_QUOTES, 'UTF-8') ?>
</td>
<td class="text-center"><?= (int)$f['deadline_count'] ?></td>
<td class="text-center"><?= (int)$f['open_count'] ?></td>
<td class="text-center">
<div class="d-inline-flex gap-1">
<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>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include('../../include/footer.php'); ?>
</div>
<div class="modal fade" id="functionModal" 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="functionModalTitle">Nuova Funzione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<form id="functionForm">
<div class="modal-body">
<input type="hidden" id="functionId" name="id" value="">
<div class="mb-3">
<label for="functionName" class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="functionName" name="name" maxlength="255" required>
</div>
<div class="mb-3">
<label for="functionDescription" class="form-label fw-semibold">Descrizione</label>
<textarea class="form-control" id="functionDescription" name="description" rows="3"></textarea>
</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() {
function openModal(data) {
const isEdit = !!data;
$('#functionModalTitle').text(isEdit ? 'Modifica Funzione' : 'Nuova Funzione');
$('#functionId').val(isEdit ? data.id : '');
$('#functionName').val(isEdit ? data.name : '');
$('#functionDescription').val(isEdit ? data.description : '');
new bootstrap.Modal('#functionModal').show();
}
$('#btnAddFunction').on('click', function() {
openModal(null);
});
$('#functionsList').on('click', '.btn-edit', function() {
const $row = $(this).closest('[data-id]');
openModal({
id: $row.data('id'),
name: $row.data('name'),
description: $row.data('description')
});
});
$('#functionsList').on('click', '.btn-delete', function() {
const $row = $(this).closest('[data-id]');
const inUse = parseInt($row.data('in-use') || 0, 10);
const name = $row.data('name');
if (inUse > 0) {
Swal.fire({
icon: 'warning',
title: 'Impossibile eliminare',
text: `La funzione "${name}" è utilizzata in ${inUse} scadenz${inUse === 1 ? 'a' : 'e'}.`
});
return;
}
Swal.fire({
title: `Eliminare "${name}"?`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Elimina',
cancelButtonText: 'Annulla',
confirmButtonColor: '#dc3545'
}).then(function(result) {
if (!result.isConfirmed) return;
$.post('scadenzario/functions/ajax/delete_function.php', {
id: $row.data('id')
})
.done(function(res) {
if (res.success) {
location.reload();
} else {
Swal.fire({
icon: 'error',
title: 'Errore',
text: res.message
});
}
})
.fail(function() {
Swal.fire({
icon: 'error',
title: 'Errore di rete'
});
});
});
});
$('#functionForm').on('submit', function(e) {
e.preventDefault();
const payload = {
id: $('#functionId').val(),
name: $('#functionName').val().trim(),
description: $('#functionDescription').val().trim()
};
if (!payload.name) {
Swal.fire({
icon: 'warning',
title: 'Nome obbligatorio'
});
return;
}
$.post('scadenzario/functions/ajax/save_function.php', payload)
.done(function(res) {
if (res.success) {
location.reload();
} else {
Swal.fire({
icon: 'error',
title: 'Errore',
text: res.message
});
}
})
.fail(function() {
Swal.fire({
icon: 'error',
title: 'Errore di rete'
});
});
});
});
</script>
</body>
</html>
@@ -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>
@@ -1,4 +1,5 @@
<?php <?php
/** /**
* Renders two status banners for the current user: * Renders two status banners for the current user:
* - red -> overdue deadlines (scaduta) * - red -> overdue deadlines (scaduta)
@@ -44,63 +45,90 @@ if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
?> ?>
<style> <style>
.my-deadlines-widgets { .my-deadlines-widgets {
display: flex; flex-wrap: wrap; gap: 0.75rem; display: flex;
margin-bottom: 1rem; width: 100%; gap: 0.75rem;
} margin-bottom: 1rem;
.my-deadlines-widgets:empty { display: none; } flex-wrap: wrap;
/* When two widget containers are nested inside an outer .my-deadlines-widgets
(e.g. on the production dashboard), let their children flow into the outer flex. */
.my-deadlines-widgets .my-deadlines-widgets {
display: contents;
} }
.my-deadlines-widgets .mdw { .my-deadlines-widgets .mdw {
flex: 1 1 0; min-width: 0; flex: 1 1 260px;
display: flex; align-items: center; gap: 0.75rem; display: flex;
padding: 0.8rem 0.9rem; border-radius: 0.6rem; align-items: center;
text-decoration: none; color: #fff; gap: 0.9rem;
box-shadow: 0 2px 6px rgba(0,0,0,0.08); padding: 0.85rem 1rem;
border-radius: 0.6rem;
text-decoration: none;
color: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
transition: transform 0.15s, box-shadow 0.15s; transition: transform 0.15s, box-shadow 0.15s;
} }
.my-deadlines-widgets .mdw:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); color: #fff; }
.my-deadlines-widgets .mdw-red { background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%); } .my-deadlines-widgets .mdw:hover {
.my-deadlines-widgets .mdw-orange { background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%); } transform: translateY(-1px);
.my-deadlines-widgets .mdw-gray { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); } box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
color: #fff;
}
.my-deadlines-widgets .mdw-red {
background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%);
}
.my-deadlines-widgets .mdw-orange {
background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%);
}
.my-deadlines-widgets .mdw-icon { .my-deadlines-widgets .mdw-icon {
width: 38px; height: 38px; border-radius: 50%; width: 42px;
display: flex; align-items: center; justify-content: center; height: 42px;
background: rgba(255,255,255,0.22); font-size: 1.05rem; flex-shrink: 0; border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.22);
font-size: 1.2rem;
flex-shrink: 0;
} }
.my-deadlines-widgets .mdw-body { flex: 1; line-height: 1.2; min-width: 0; }
.my-deadlines-widgets .mdw-count { font-size: 1.5rem; font-weight: 700; } .my-deadlines-widgets .mdw-body {
.my-deadlines-widgets .mdw-label { font-size: 0.78rem; opacity: 0.95; flex: 1;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } line-height: 1.2;
.my-deadlines-widgets .mdw-arrow { opacity: 0.7; font-size: 0.85rem; flex-shrink: 0; }
@media (max-width: 991.98px) {
.my-deadlines-widgets .mdw { flex: 1 1 calc(50% - 0.375rem); }
} }
@media (max-width: 575.98px) {
.my-deadlines-widgets .mdw { flex: 1 1 100%; } .my-deadlines-widgets .mdw-count {
font-size: 1.6rem;
font-weight: 700;
}
.my-deadlines-widgets .mdw-label {
font-size: 0.8rem;
opacity: 0.95;
}
.my-deadlines-widgets .mdw-arrow {
opacity: 0.7;
font-size: 0.9rem;
} }
</style> </style>
<div class="my-deadlines-widgets"> <div class="my-deadlines-widgets">
<?php if ($_overdue > 0): ?> <?php if ($_overdue > 0): ?>
<a class="mdw mdw-red" href="scadenzario/index.php?filter_my=1&filter_status=scaduta"> <a class="mdw mdw-red" href="scadenzario/index.php?filter_my=1&filter_status=scaduta">
<span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span> <span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
<span class="mdw-body"> <span class="mdw-body">
<span class="mdw-count"><?= $_overdue ?></span> <span class="mdw-count"><?= $_overdue ?></span>
<span class="mdw-label d-block">Scadenz<?= $_overdue === 1 ? 'a' : 'e' ?> scadut<?= $_overdue === 1 ? 'a' : 'e' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span> <span class="mdw-label d-block">Task<?= $_overdue === 1 ? '' : 's' ?> scadut<?= $_overdue === 1 ? 'o' : 'i' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
</span> </span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span> <span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a> </a>
<?php endif; ?> <?php endif; ?>
<?php if ($_approaching > 0): ?> <?php if ($_approaching > 0): ?>
<a class="mdw mdw-orange" href="scadenzario/index.php?filter_my=1&filter_status=in-scadenza"> <a class="mdw mdw-orange" href="scadenzario/index.php?filter_my=1&filter_status=in-scadenza">
<span class="mdw-icon"><i class="fa-solid fa-clock"></i></span> <span class="mdw-icon"><i class="fa-solid fa-clock"></i></span>
<span class="mdw-body"> <span class="mdw-body">
<span class="mdw-count"><?= $_approaching ?></span> <span class="mdw-count"><?= $_approaching ?></span>
<span class="mdw-label d-block">In scadenza a breve <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span> <span class="mdw-label d-block">In scadenza a breve <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
</span> </span>
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span> <span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
</a> </a>
<?php endif; ?> <?php endif; ?>
</div> </div>
+103 -472
View File
@@ -37,12 +37,14 @@ $sql = "
SELECT d.*, SELECT d.*,
s.name AS subject_name, s.name AS subject_name,
s.color AS subject_color, s.color AS subject_color,
f.name AS function_name,
GROUP_CONCAT(DISTINCT CONCAT(e.first_name, ' ', e.last_name) ORDER BY e.first_name SEPARATOR ', ') as responsabili, GROUP_CONCAT(DISTINCT CONCAT(e.first_name, ' ', e.last_name) ORDER BY e.first_name SEPARATOR ', ') as responsabili,
GROUP_CONCAT(DISTINCT dep.name ORDER BY dep.name SEPARATOR ', ') as reparti_persone, GROUP_CONCAT(DISTINCT dep.name ORDER BY dep.name SEPARATOR ', ') as reparti_persone,
d.departments as reparti_assegnati, d.departments as reparti_assegnati,
(SELECT COUNT(*) FROM scad_deadline_attachments att WHERE att.deadline_id = d.id) as attachment_count (SELECT COUNT(*) FROM scad_deadline_attachments att WHERE att.deadline_id = d.id) as attachment_count
FROM scad_deadlines d FROM scad_deadlines d
LEFT JOIN scad_subjects s ON s.id = d.subject_id LEFT JOIN scad_subjects s ON s.id = d.subject_id
LEFT JOIN scad_functions f ON f.id = d.function_id
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
LEFT JOIN employees e ON e.id = de.employee_id LEFT JOIN employees e ON e.id = de.employee_id
LEFT JOIN departments dep ON dep.id = e.department_id LEFT JOIN departments dep ON dep.id = e.department_id
@@ -69,27 +71,7 @@ $stmt = $pdo->prepare($sql);
$stmt->execute($params); $stmt->execute($params);
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC); $deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
$employees = $pdo->query(" require __DIR__ . '/include/deadline_form_data.php';
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);
$today = date('Y-m-d'); $today = date('Y-m-d');
@@ -494,7 +476,8 @@ function getContrastTextColor($hexColor)
} }
#deadlinesTable td:first-child { #deadlinesTable td:first-child {
max-width: 150px; max-width: 110px;
width: 110px;
} }
/* Attachment list in modal */ /* Attachment list in modal */
@@ -824,6 +807,9 @@ function getContrastTextColor($hexColor)
<a href="scadenzario/subjects/index.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2"> <a href="scadenzario/subjects/index.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
<i class="fa-solid fa-tags"></i><span>Argomenti</span> <i class="fa-solid fa-tags"></i><span>Argomenti</span>
</a> </a>
<a href="scadenzario/functions/index.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
<i class="fa-solid fa-briefcase"></i><span>Funzioni</span>
</a>
<a href="scadenzario/calendar.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2"> <a href="scadenzario/calendar.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
<i class="fa-solid fa-calendar-days"></i><span>Calendario</span> <i class="fa-solid fa-calendar-days"></i><span>Calendario</span>
</a> </a>
@@ -842,6 +828,7 @@ function getContrastTextColor($hexColor)
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/subjects/index.php"><i class="fa-solid fa-tags"></i> Argomenti</a></li> <li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/subjects/index.php"><i class="fa-solid fa-tags"></i> Argomenti</a></li>
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/functions/index.php"><i class="fa-solid fa-briefcase"></i> Funzioni</a></li>
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/calendar.php"><i class="fa-solid fa-calendar-days"></i> Calendario</a></li> <li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/calendar.php"><i class="fa-solid fa-calendar-days"></i> Calendario</a></li>
<li><button type="button" class="dropdown-item d-flex align-items-center gap-2" id="btnStampaMobile"><i class="fa-solid fa-print"></i> Stampa</button></li> <li><button type="button" class="dropdown-item d-flex align-items-center gap-2" id="btnStampaMobile"><i class="fa-solid fa-print"></i> Stampa</button></li>
</ul> </ul>
@@ -923,7 +910,9 @@ function getContrastTextColor($hexColor)
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', 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-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'])): ?> <?php if (!empty($row['subject_name'])): ?>
<div class="mb-1"><?php <div class="mb-1"><?php
$subjectBadgeBg = $row['subject_color'] ?: '#6c757d'; $subjectBadgeBg = $row['subject_color'] ?: '#6c757d';
@@ -972,11 +961,12 @@ function getContrastTextColor($hexColor)
<table id="deadlinesTable" class="table table-hover align-middle mb-0" style="width:100%"> <table id="deadlinesTable" class="table table-hover align-middle mb-0" style="width:100%">
<thead> <thead>
<tr> <tr>
<th>Argomento</th> <th style="width:110px">Argomento</th>
<th>Dettaglio</th> <th>Dettaglio</th>
<th class="d-none d-lg-table-cell">Legge/Art.</th> <th class="d-none d-lg-table-cell">Legge/Art.</th>
<th>Scadenza</th> <th>Scadenza</th>
<th class="d-none d-lg-table-cell">Verifica</th> <th class="d-none d-lg-table-cell">Verifica</th>
<th>Funzione</th>
<th>Responsabili</th> <th>Responsabili</th>
<th>Stato</th> <th>Stato</th>
<th class="text-center" style="width:120px">Azioni</th> <th class="text-center" style="width:120px">Azioni</th>
@@ -1000,7 +990,9 @@ function getContrastTextColor($hexColor)
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', 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-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> <td>
<?php if (!empty($row['subject_name'])): ?> <?php if (!empty($row['subject_name'])): ?>
<?php <?php
@@ -1014,6 +1006,7 @@ function getContrastTextColor($hexColor)
<span class="text-muted"></span> <span class="text-muted"></span>
<?php endif; ?> <?php endif; ?>
</td> </td>
<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> <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): ?> <?php if ((int)$row['attachment_count'] > 0): ?>
@@ -1023,6 +1016,17 @@ function getContrastTextColor($hexColor)
<td class="d-none d-lg-table-cell text-muted"><?= htmlspecialchars($row['law_regulation'] ?? '—', ENT_QUOTES, 'UTF-8') ?></td> <td class="d-none d-lg-table-cell text-muted"><?= htmlspecialchars($row['law_regulation'] ?? '—', ENT_QUOTES, 'UTF-8') ?></td>
<td><span class="text-nowrap"><?= $row['_dueFmt'] ?></span></td> <td><span class="text-nowrap"><?= $row['_dueFmt'] ?></span></td>
<td class="d-none d-lg-table-cell text-muted"><?= $row['_checkFmt'] ?></td> <td class="d-none d-lg-table-cell text-muted"><?= $row['_checkFmt'] ?></td>
<td>
<?php if (!empty($row['function_name'])): ?>
<span class="text-muted">
<i class="fa-solid fa-briefcase me-1"></i>
<?= htmlspecialchars($row['function_name'], ENT_QUOTES, 'UTF-8') ?>
</span>
<?php else: ?>
<span class="text-muted"></span>
<?php endif; ?>
</td>
<td> <td>
<?php if ($row['reparti']): ?><span class="text-muted"><i class="fa-regular fa-building me-1"></i><?= htmlspecialchars($row['reparti'], ENT_QUOTES, 'UTF-8') ?></span><?php endif; ?> <?php if ($row['reparti']): ?><span class="text-muted"><i class="fa-regular fa-building me-1"></i><?= htmlspecialchars($row['reparti'], ENT_QUOTES, 'UTF-8') ?></span><?php endif; ?>
<?php if ($row['reparti'] && $row['responsabili']): ?><br><?php endif; ?> <?php if ($row['reparti'] && $row['responsabili']): ?><br><?php endif; ?>
@@ -1055,143 +1059,7 @@ function getContrastTextColor($hexColor)
<?php include('../include/footer.php'); ?> <?php include('../include/footer.php'); ?>
</div> </div>
<!-- Deadline Modal --> <?php include __DIR__ . '/include/deadline_modal.php'; ?>
<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="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="date" class="form-control" id="dlDocDate" name="document_date">
</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="date" class="form-control" id="dlDueDate" name="due_date" required>
</div>
<div class="col-12 col-md-4">
<label for="dlCheckDate" class="form-label fw-semibold">Data ultimo controllo</label>
<input type="date" class="form-control" id="dlCheckDate" name="check_date">
</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('../jsinclude.php'); ?> <?php include('../jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
@@ -1221,84 +1089,6 @@ function getContrastTextColor($hexColor)
var fpDue = flatpickr('#filterDueRange', fpOpts); var fpDue = flatpickr('#filterDueRange', fpOpts);
var fpCheck = flatpickr('#filterCheckRange', fpOpts); 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...',
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');
document.getElementById('dlDueDate').value = iso;
}
$('#dlDocDate, #dlRecurrence').on('change', computeDueDate);
// --- DataTables custom filters --- // --- DataTables custom filters ---
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) { $.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
if (settings.nTable.id !== 'deadlinesTable') return true; if (settings.nTable.id !== 'deadlinesTable') return true;
@@ -1460,148 +1250,8 @@ function getContrastTextColor($hexColor)
// Apply default filter on load // Apply default filter on load
applyFiltersRefresh(); applyFiltersRefresh();
// --- Modal ---
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
var form = document.getElementById('deadlineForm');
// Add
document.getElementById('btnAddDeadline').addEventListener('click', function() { document.getElementById('btnAddDeadline').addEventListener('click', function() {
form.reset(); if (window.openDeadlineCreate) window.openDeadlineCreate();
document.getElementById('dlId').value = '';
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([]);
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');
}
});
}
});
}); });
// Edit with confirmation // Edit with confirmation
@@ -1618,69 +1268,23 @@ function getContrastTextColor($hexColor)
confirmButtonText: 'Sì, modifica', confirmButtonText: 'Sì, modifica',
reverseButtons: true reverseButtons: true
}).then(function(result) { }).then(function(result) {
if (!result.isConfirmed) { if (result.isConfirmed && window.openDeadlineEdit) {
return; 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');
document.getElementById('dlTopic').value = d.topic || '';
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
document.getElementById('dlDocDate').value = d.document_date || '';
document.getElementById('dlDueDate').value = d.due_date || '';
document.getElementById('dlCheckDate').value = d.check_date || '';
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 // Complete
$(document).on('click', '.btn-complete', function() { function submitComplete(id, createNext, copyAttachments) {
var el = $(this).closest('[data-id]'); var fd = new FormData();
var id = el.data('id'); fd.append('id', id);
Swal.fire({ fd.append('create_next', createNext ? '1' : '0');
title: 'Completare la scadenza?', fd.append('copy_attachments', copyAttachments ? '1' : '0');
icon: 'question',
showCancelButton: true, fetch('scadenzario/ajax/complete_deadline.php', {
confirmButtonColor: '#198754', method: 'POST',
cancelButtonText: 'Annulla', body: fd
confirmButtonText: 'Completa' })
}).then(function(result) {
if (result.isConfirmed) {
fetch('scadenzario/ajax/complete_deadline.php?id=' + id)
.then(function(r) { .then(function(r) {
return r.json(); return r.json();
}) })
@@ -1690,11 +1294,16 @@ function getContrastTextColor($hexColor)
icon: 'success', icon: 'success',
title: 'Completata', title: 'Completata',
text: data.message, text: data.message,
timer: 2500, timer: 1800,
showConfirmButton: false showConfirmButton: false
}) })
.then(function() { .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(); location.reload();
}
}); });
} else { } else {
Swal.fire('Errore', data.message, 'error'); Swal.fire('Errore', data.message, 'error');
@@ -1704,6 +1313,59 @@ function getContrastTextColor($hexColor)
Swal.fire('Errore', 'Errore di connessione.', 'error'); 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);
}
}); });
}); });
@@ -1748,38 +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');
document.getElementById('dlTopic').value = d.topic || '';
document.getElementById('dlLaw').value = d.law_regulation || '';
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
document.getElementById('dlDocDate').value = d.document_date || '';
document.getElementById('dlDueDate').value = d.due_date || '';
document.getElementById('dlCheckDate').value = d.check_date || '';
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 // Stampa
function doStampa() { function doStampa() {
var params = []; var params = [];
@@ -1802,6 +1432,7 @@ function getContrastTextColor($hexColor)
if (btnStampaMobile) btnStampaMobile.addEventListener('click', doStampa); if (btnStampaMobile) btnStampaMobile.addEventListener('click', doStampa);
}); });
</script> </script>
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
</body> </body>
</html> </html>
+217
View File
@@ -0,0 +1,217 @@
<?php
include('include/headscript.php');
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* ==========================================
PERMISSIONS (mirror trainings.php)
========================================== */
$isHrManager = Auth::user()->hasRole('Admin')
|| Auth::user()->hasRole('Superuser')
|| Auth::user()->hasRole('employee-hr')
|| Auth::user()->hasRole('manager');
if (!$isHrManager) {
header('Location: employee-profile.php');
exit;
}
/* Dropdown data */
$employees = $pdo->query("
SELECT id, first_name, last_name, employee_code
FROM employees
ORDER BY last_name, first_name
")->fetchAll(PDO::FETCH_ASSOC);
$topics = $pdo->query("
SELECT id, name FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC);
$departments = $pdo->query("
SELECT id, name FROM departments WHERE is_active = 1 ORDER BY sort_order, 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">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.9/index.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.9/locales/it.global.min.js"></script>
<title>Calendario Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
body { font-size: 1.05rem; background: #f8fafc; }
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
.back-dashboard {
background-color: #cfe3ff !important; color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important; border-radius: 10px;
font-weight: 600; padding: 10px 18px;
}
.legend { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; }
.legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: #64748b; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
/* FullCalendar overrides */
.fc { font-size: 0.95rem; }
.fc .fc-toolbar-title { font-size: 1.15rem; font-weight: 700; color: #2c3e6b; }
.fc .fc-button-primary {
background: #5a8fd8; border-color: #5a8fd8;
font-weight: 600; font-size: 0.82rem; border-radius: 0.4rem;
}
.fc .fc-button-primary:hover { background: #4578c0; border-color: #4578c0; }
.fc .fc-button-primary:disabled { background: #9bbce6; border-color: #9bbce6; }
.fc .fc-button-primary:not(:disabled).fc-button-active { background: #2c3e6b; border-color: #2c3e6b; }
.fc .fc-daygrid-day-number { color: #2c3e6b; font-weight: 500; }
.fc .fc-daygrid-day.fc-day-today { background: #f0f4ff; }
.fc .fc-event { border-radius: 0.3rem; padding: 2px 4px; font-weight: 600; cursor: pointer; }
.fc .fc-event:hover { filter: brightness(0.92); }
.fc .fc-list-event:hover td { background: #f0f4ff; }
@media (max-width: 767.98px) {
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
.fc .fc-toolbar { flex-direction: column; gap: 0.5rem; }
.fc .fc-toolbar-title { font-size: 1rem; }
}
</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">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">📅 Calendario Formazione</h5>
<div class="d-flex gap-2 flex-wrap">
<a href="trainings.php" class="btn btn-light border d-inline-flex align-items-center gap-2">
📚 <span>Storico Formazione</span>
</a>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
</div>
<div class="card-body">
<!-- FILTERS -->
<div class="row g-2 mb-3">
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Stato</label>
<select id="filterStatus" class="form-select">
<option value="">Tutti</option>
<option value="expired">Scaduti</option>
<option value="due_soon">Da aggiornare</option>
<option value="compliant">Conformi</option>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Corso</label>
<select id="filterTopic" class="form-select">
<option value="">Tutti</option>
<?php foreach ($topics as $t): ?>
<option value="<?= (int)$t['id'] ?>"><?= htmlspecialchars($t['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Reparto</label>
<select id="filterDepartment" class="form-select">
<option value="">Tutti</option>
<?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>"><?= htmlspecialchars($d['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label small text-muted mb-1">Dipendente</label>
<div class="d-flex gap-2">
<select id="filterEmployee" class="form-select">
<option value="">Tutti</option>
<?php foreach ($employees as $e): ?>
<option value="<?= (int)$e['id'] ?>"><?= htmlspecialchars(trim($e['last_name'] . ' ' . $e['first_name']), ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
<button id="btnResetFilters" type="button" class="btn btn-light border flex-shrink-0" title="Reset filtri">
<i class="fa-solid fa-rotate-left"></i>
</button>
</div>
</div>
</div>
<!-- LEGEND -->
<div class="legend">
<div class="legend-item"><span class="legend-dot" style="background:#dc3545"></span> Scaduto</div>
<div class="legend-item"><span class="legend-dot" style="background:#e8930c"></span> Da aggiornare</div>
<div class="legend-item"><span class="legend-dot" style="background:#198754"></span> Conforme</div>
</div>
<div id="calendar"></div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var isMobile = window.innerWidth < 768;
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
locale: 'it',
initialView: isMobile ? 'listMonth' : 'dayGridMonth',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: isMobile ? 'listMonth,dayGridMonth' : 'dayGridMonth,listMonth'
},
height: 'auto',
navLinks: true,
eventSources: [{
url: 'ajax/trainings/calendar_events.php',
extraParams: function() {
return {
status: document.getElementById('filterStatus').value,
topic_id: document.getElementById('filterTopic').value,
department_id: document.getElementById('filterDepartment').value,
employee_id: document.getElementById('filterEmployee').value
};
},
failure: function() {
if (window.Swal) Swal.fire('Errore', 'Impossibile caricare gli eventi.', 'error');
}
}],
eventClick: function(info) {
info.jsEvent.preventDefault();
if (info.event.url) window.location.href = info.event.url;
},
windowResize: function() {
calendar.changeView(window.innerWidth < 768 ? 'listMonth' : 'dayGridMonth');
}
});
calendar.render();
document.querySelectorAll('#filterStatus, #filterTopic, #filterDepartment, #filterEmployee').forEach(function(el) {
el.addEventListener('change', function() { calendar.refetchEvents(); });
});
document.getElementById('btnResetFilters').addEventListener('click', function() {
['filterStatus', 'filterTopic', 'filterDepartment', 'filterEmployee'].forEach(function(id) {
document.getElementById(id).value = '';
});
calendar.refetchEvents();
});
});
</script>
</body>
</html>
+326 -3
View File
@@ -30,6 +30,15 @@ $fDepartmentId = isset($_GET['department_id'])&& $_GET['department_id']!== '' ?
========================================== */ ========================================== */
$where = []; $where = [];
$params = []; $params = [];
// Only the most recent record per (employee, topic) — older initial/refresher
// rows stay as history on the employee profile, not in this overview.
$where[] = "NOT EXISTS (
SELECT 1 FROM employee_trainings et2
WHERE et2.employee_id = et.employee_id
AND et2.training_topic_id = et.training_topic_id
AND (et2.completed_date > et.completed_date
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
)";
if ($fEmployeeId > 0) { $where[] = 'et.employee_id = :eid'; $params['eid'] = $fEmployeeId; } if ($fEmployeeId > 0) { $where[] = 'et.employee_id = :eid'; $params['eid'] = $fEmployeeId; }
if ($fTopicId > 0) { $where[] = 'et.training_topic_id = :tid'; $params['tid'] = $fTopicId; } if ($fTopicId > 0) { $where[] = 'et.training_topic_id = :tid'; $params['tid'] = $fTopicId; }
if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) { if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) {
@@ -142,12 +151,13 @@ if ($fType === '' || $fType === 'initial') {
/* Dropdown data */ /* Dropdown data */
$employees = $pdo->query(" $employees = $pdo->query("
SELECT id, first_name, last_name, employee_code SELECT id, first_name, last_name, employee_code, department_id
FROM employees FROM employees
ORDER BY last_name, first_name ORDER BY last_name, first_name
")->fetchAll(PDO::FETCH_ASSOC); ")->fetchAll(PDO::FETCH_ASSOC);
$topics = $pdo->query(" $topics = $pdo->query("
SELECT id, name FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name SELECT id, name, default_frequency_months, default_reminder_days
FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC); ")->fetchAll(PDO::FETCH_ASSOC);
$departments = $pdo->query(" $departments = $pdo->query("
SELECT id, name, color FROM departments WHERE is_active = 1 ORDER BY sort_order, name SELECT id, name, color FROM departments WHERE is_active = 1 ORDER BY sort_order, name
@@ -171,6 +181,9 @@ function fmtDate(?string $d): string {
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></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">
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<style> <style>
body { font-size: 1.05rem; background: #f8fafc; } body { font-size: 1.05rem; background: #f8fafc; }
@@ -234,10 +247,15 @@ function fmtDate(?string $d): string {
<div class="card p-3"> <div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2"> <div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">📚 Storico Formazione</h5> <h5 class="mb-0">📚 Storico Formazione</h5>
<div class="d-flex gap-2 flex-wrap">
<button type="button" class="btn btn-primary" id="btnBulkTraining">
Aggiungi sessione
</button>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'"> <button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard ↩️ Torna alla Dashboard
</button> </button>
</div> </div>
</div>
<div class="card-body"> <div class="card-body">
<!-- COUNTERS --> <!-- COUNTERS -->
@@ -316,6 +334,12 @@ function fmtDate(?string $d): string {
<?php endif; ?> <?php endif; ?>
</form> </form>
<div id="bulkBar" class="d-none align-items-center flex-wrap gap-2 mb-3 p-2" style="background:#fff6e5;border:1px solid #ffe0a6;border-radius:10px;">
<span class="fw-semibold"><span id="bulkSelCount">0</span> selezionati</span>
<button type="button" class="btn btn-sm btn-warning" id="btnBulkRenew">🔄 Aggiorna scadenza</button>
<button type="button" class="btn btn-sm btn-link text-decoration-none" id="btnBulkDeselect">Deseleziona</button>
</div>
<?php if (empty($filtered)): ?> <?php if (empty($filtered)): ?>
<div class="text-center text-muted py-4"> <div class="text-center text-muted py-4">
Nessuna formazione corrispondente ai filtri. Nessuna formazione corrispondente ai filtri.
@@ -326,6 +350,7 @@ function fmtDate(?string $d): string {
<table class="table table-striped align-middle"> <table class="table table-striped align-middle">
<thead style="background-color:#cfe3ff;"> <thead style="background-color:#cfe3ff;">
<tr> <tr>
<th style="width:36px"><input type="checkbox" class="form-check-input" id="checkAll" title="Seleziona tutti"></th>
<th>Dipendente</th> <th>Dipendente</th>
<th>Reparto</th> <th>Reparto</th>
<th>Corso</th> <th>Corso</th>
@@ -344,6 +369,11 @@ function fmtDate(?string $d): string {
$days = $r['_status']['days'] ?? null; $days = $r['_status']['days'] ?? null;
?> ?>
<tr> <tr>
<td>
<?php if (!empty($r['id'])): ?>
<input type="checkbox" class="form-check-input row-check" value="<?= (int)$r['id'] ?>">
<?php endif; ?>
</td>
<td> <td>
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training" class="fw-semibold text-decoration-none"> <a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training" class="fw-semibold text-decoration-none">
<?= htmlspecialchars($fullName) ?> <?= htmlspecialchars($fullName) ?>
@@ -388,7 +418,10 @@ function fmtDate(?string $d): string {
?> ?>
<div class="tr-card"> <div class="tr-card">
<div class="d-flex justify-content-between align-items-start gap-2 mb-1"> <div class="d-flex justify-content-between align-items-start gap-2 mb-1">
<div class="name"> <div class="name d-flex align-items-start gap-2">
<?php if (!empty($r['id'])): ?>
<input type="checkbox" class="form-check-input row-check mt-1" value="<?= (int)$r['id'] ?>">
<?php endif; ?>
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training"> <a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training">
<?= htmlspecialchars($fullName) ?> <?= htmlspecialchars($fullName) ?>
</a> </a>
@@ -425,6 +458,296 @@ function fmtDate(?string $d): string {
<?php include('include/footer.php'); ?> <?php include('include/footer.php'); ?>
</div> </div>
<!-- BULK TRAINING SESSION MODAL -->
<div class="modal fade" id="bulkTrainingModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"> Nuova sessione formativa</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<form id="bulkTrainingForm">
<div class="modal-body" style="max-height:65vh; overflow-y:auto;">
<p class="text-muted small">Registra lo stesso corso, con gli stessi parametri, per più dipendenti contemporaneamente.</p>
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label fw-semibold">Corso <span class="text-danger">*</span></label>
<select id="bulkTopic" class="form-select" required>
<option value=""> Seleziona </option>
<?php foreach ($topics as $t): ?>
<option value="<?= (int)$t['id'] ?>"
data-freq="<?= $t['default_frequency_months'] !== null ? (int)$t['default_frequency_months'] : '' ?>"
data-rem="<?= $t['default_reminder_days'] !== null ? (int)$t['default_reminder_days'] : '' ?>">
<?= htmlspecialchars($t['name'], ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Data completamento <span class="text-danger">*</span></label>
<input type="date" id="bulkCompletedDate" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Tipo</label>
<select id="bulkType" class="form-select">
<option value="initial">Iniziale</option>
<option value="refresher">Aggiornamento</option>
</select>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Frequenza (mesi)</label>
<input type="number" id="bulkFreq" class="form-control" min="0" max="600" placeholder="default corso">
<div class="form-text">Vuoto = una tantum</div>
</div>
<div class="col-6 col-md-3">
<label class="form-label fw-semibold">Promemoria (giorni)</label>
<input type="number" id="bulkRem" class="form-control" min="0" max="365" placeholder="default corso">
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-semibold">Erogato da</label>
<input type="text" id="bulkDeliveredBy" class="form-control" maxlength="255" placeholder="es. Ente formatore, docente interno...">
</div>
<div class="col-12">
<label class="form-label fw-semibold">Descrizione / note</label>
<textarea id="bulkDescription" class="form-control" rows="2"></textarea>
</div>
<div class="col-12"><hr class="my-1"></div>
<div class="col-12">
<label class="form-label fw-semibold">Dipendenti <span class="text-danger">*</span></label>
<div class="d-flex flex-wrap gap-2 mb-2 align-items-end">
<div>
<select id="bulkDept" class="form-select form-select-sm" style="min-width:180px">
<option value=""> Reparto </option>
<?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>"><?= htmlspecialchars($d['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="bulkAddDept">+ Aggiungi reparto</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="bulkSelectAll">Tutti</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="bulkClear">Pulisci</button>
</div>
<select id="bulkEmployees" class="form-select" multiple required>
<?php foreach ($employees as $e): ?>
<option value="<?= (int)$e['id'] ?>" data-dept="<?= (int)($e['department_id'] ?? 0) ?>">
<?= htmlspecialchars(trim($e['last_name'] . ' ' . $e['first_name']), ENT_QUOTES, 'UTF-8') ?><?php if (!empty($e['employee_code'])): ?> (<?= htmlspecialchars($e['employee_code'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
<div class="form-text"><span id="bulkCount">0</span> dipendenti selezionati</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary" id="bulkSaveBtn">Registra formazione</button>
</div>
</form>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?> <?php include('jsinclude.php'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var bulkModal = new bootstrap.Modal(document.getElementById('bulkTrainingModal'));
var $emp = $('#bulkEmployees');
$emp.select2({
theme: 'bootstrap-5',
placeholder: 'Seleziona dipendenti...',
dropdownParent: $('#bulkTrainingModal'),
closeOnSelect: false,
width: '100%'
});
function updateCount() {
document.getElementById('bulkCount').textContent = ($emp.val() || []).length;
}
$emp.on('change', updateCount);
document.getElementById('btnBulkTraining').addEventListener('click', function() {
document.getElementById('bulkTrainingForm').reset();
$emp.val(null).trigger('change');
document.getElementById('bulkTopic').value = '';
document.getElementById('bulkType').value = 'initial';
updateCount();
bulkModal.show();
});
// Prefill frequency/reminder from the selected course
document.getElementById('bulkTopic').addEventListener('change', function() {
var opt = this.options[this.selectedIndex];
document.getElementById('bulkFreq').value = opt ? (opt.getAttribute('data-freq') || '') : '';
document.getElementById('bulkRem').value = opt ? (opt.getAttribute('data-rem') || '') : '';
});
// Add all employees of the chosen department to the selection
document.getElementById('bulkAddDept').addEventListener('click', function() {
var dept = document.getElementById('bulkDept').value;
if (!dept) return;
var current = ($emp.val() || []).map(String);
$emp.find('option').each(function() {
if (this.getAttribute('data-dept') === String(dept) && current.indexOf(this.value) === -1) {
current.push(this.value);
}
});
$emp.val(current).trigger('change');
});
document.getElementById('bulkSelectAll').addEventListener('click', function() {
var all = $emp.find('option').map(function() { return this.value; }).get();
$emp.val(all).trigger('change');
});
document.getElementById('bulkClear').addEventListener('click', function() {
$emp.val(null).trigger('change');
});
document.getElementById('bulkTrainingForm').addEventListener('submit', function(e) {
e.preventDefault();
var topicId = document.getElementById('bulkTopic').value;
var completed = document.getElementById('bulkCompletedDate').value;
var emps = $emp.val() || [];
if (!topicId) { Swal.fire('Attenzione', 'Selezionare un corso.', 'warning'); return; }
if (!completed) { Swal.fire('Attenzione', 'Indicare la data di completamento.', 'warning'); return; }
if (emps.length === 0) { Swal.fire('Attenzione', 'Selezionare almeno un dipendente.', 'warning'); return; }
var btn = document.getElementById('bulkSaveBtn');
btn.disabled = true;
var orig = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Salvataggio...';
var fd = new FormData();
fd.append('training_topic_id', topicId);
fd.append('completed_date', completed);
fd.append('training_type', document.getElementById('bulkType').value);
fd.append('delivered_by', document.getElementById('bulkDeliveredBy').value);
fd.append('description', document.getElementById('bulkDescription').value);
fd.append('update_frequency_months', document.getElementById('bulkFreq').value);
fd.append('reminder_days', document.getElementById('bulkRem').value);
emps.forEach(function(id) { fd.append('employee_ids[]', id); });
fetch('ajax/trainings/save_bulk_training.php', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
bulkModal.hide();
Swal.fire({ icon: 'success', title: 'Fatto', text: data.message, timer: 1800, showConfirmButton: false })
.then(function() { location.reload(); });
} else {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', data.message || 'Errore.', 'error');
}
})
.catch(function() {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
});
});
</script>
<!-- BULK RENEW DEADLINE MODAL -->
<div class="modal fade" id="bulkRenewModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form id="bulkRenewForm">
<div class="modal-header">
<h5 class="modal-title">🔄 Aggiorna scadenza</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body">
<p class="text-muted small">Imposta la data di completamento per <b id="renewCount">0</b> record selezionati. Le prossime scadenze verranno ricalcolate in base alla frequenza di ciascun corso.</p>
<label class="form-label fw-semibold">Nuova data di completamento</label>
<input type="date" id="renewDate" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-warning" id="renewSaveBtn">Aggiorna</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var renewModal = new bootstrap.Modal(document.getElementById('bulkRenewModal'));
var checkAll = document.getElementById('checkAll');
function checkedIds() {
return Array.prototype.slice.call(document.querySelectorAll('.row-check:checked'))
.map(function(c) { return c.value; });
}
function refreshBulkBar() {
var ids = checkedIds();
var bar = document.getElementById('bulkBar');
document.getElementById('bulkSelCount').textContent = ids.length;
if (ids.length > 0) { bar.classList.remove('d-none'); bar.classList.add('d-flex'); }
else { bar.classList.add('d-none'); bar.classList.remove('d-flex'); }
var all = document.querySelectorAll('.row-check');
if (checkAll) checkAll.checked = (all.length > 0 && ids.length === all.length);
}
document.addEventListener('change', function(e) {
if (e.target && e.target.classList && e.target.classList.contains('row-check')) refreshBulkBar();
});
if (checkAll) checkAll.addEventListener('change', function() {
document.querySelectorAll('.row-check').forEach(function(c) { c.checked = checkAll.checked; });
refreshBulkBar();
});
document.getElementById('btnBulkDeselect').addEventListener('click', function() {
document.querySelectorAll('.row-check').forEach(function(c) { c.checked = false; });
if (checkAll) checkAll.checked = false;
refreshBulkBar();
});
document.getElementById('btnBulkRenew').addEventListener('click', function() {
var ids = checkedIds();
if (ids.length === 0) return;
document.getElementById('renewCount').textContent = ids.length;
renewModal.show();
});
document.getElementById('bulkRenewForm').addEventListener('submit', function(e) {
e.preventDefault();
var ids = checkedIds();
var date = document.getElementById('renewDate').value;
if (ids.length === 0) { renewModal.hide(); return; }
if (!date) { Swal.fire('Attenzione', 'Indicare la data.', 'warning'); return; }
var btn = document.getElementById('renewSaveBtn');
btn.disabled = true;
var orig = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Salvataggio...';
var fd = new FormData();
fd.append('completed_date', date);
ids.forEach(function(id) { fd.append('training_ids[]', id); });
fetch('ajax/trainings/bulk_update_deadline.php', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
renewModal.hide();
Swal.fire({ icon: 'success', title: 'Fatto', text: data.message, timer: 1800, showConfirmButton: false })
.then(function() { location.reload(); });
} else {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', data.message || 'Errore.', 'error');
}
})
.catch(function() {
btn.disabled = false; btn.innerHTML = orig;
Swal.fire('Errore', 'Errore di connessione.', 'error');
});
});
});
</script>
</body> </body>
</html> </html>
+868
View File
@@ -0,0 +1,868 @@
<?php include('include/headscript.php'); ?>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$userId = (int)($iduserlogin ?? 0);
if ($userId <= 0) {
die('Utente non valido.');
}
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['user_settings_csrf'])) {
$_SESSION['user_settings_csrf'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['user_settings_csrf'];
$successMessage = '';
$errorMessage = '';
// Load countries.
$countries = [];
try {
$stmtCountries = $pdo->query("
SELECT id, name, iso_3166_2
FROM auth_countries
ORDER BY name ASC
");
$countries = $stmtCountries->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
$countries = [];
}
// Load current user.
$stmtProfileUser = $pdo->prepare("
SELECT
id,
email,
password,
first_name,
last_name,
phone,
avatar,
address,
country_id,
birthday,
role_id,
status,
last_login
FROM auth_users
WHERE id = ?
LIMIT 1
");
$stmtProfileUser->execute([$userId]);
$profileUser = $stmtProfileUser->fetch(PDO::FETCH_ASSOC);
if (!$profileUser) {
die('Utente non trovato.');
}
function e($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
function normalizeAvatarPath($avatar)
{
$avatar = trim((string)$avatar);
if ($avatar === '') {
return '';
}
// If the database already contains a complete relative path, use it as it is.
if (
str_starts_with($avatar, '../') ||
str_starts_with($avatar, './') ||
str_starts_with($avatar, '/') ||
str_starts_with($avatar, 'http://') ||
str_starts_with($avatar, 'https://')
) {
return $avatar;
}
// If the database contains only the filename, build the expected user upload path.
return '../upload/users/' . $avatar;
}
function getAvatarInitials($profileUser)
{
$first = trim((string)($profileUser['first_name'] ?? ''));
$last = trim((string)($profileUser['last_name'] ?? ''));
$email = trim((string)($profileUser['email'] ?? ''));
$initials = '';
if ($first !== '') {
$initials .= mb_substr($first, 0, 1);
}
if ($last !== '') {
$initials .= mb_substr($last, 0, 1);
}
if ($initials === '' && $email !== '') {
$initials = mb_substr($email, 0, 1);
}
return strtoupper($initials ?: 'U');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$postedToken = $_POST['csrf_token'] ?? '';
if (!hash_equals($csrfToken, $postedToken)) {
$errorMessage = 'Sessione non valida. Ricarica la pagina e riprova.';
} else {
$email = trim($_POST['email'] ?? '');
$firstName = trim($_POST['first_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$address = trim($_POST['address'] ?? '');
$countryId = $_POST['country_id'] !== '' ? (int)$_POST['country_id'] : null;
$birthday = trim($_POST['birthday'] ?? '');
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
$birthdayValue = null;
$avatarToSave = $profileUser['avatar'];
if ($birthday !== '') {
$dateObj = DateTime::createFromFormat('Y-m-d', $birthday);
if (!$dateObj || $dateObj->format('Y-m-d') !== $birthday) {
$errorMessage = 'La data di nascita non è valida.';
} else {
$birthdayValue = $birthday;
}
}
if (!$errorMessage && $email === '') {
$errorMessage = 'Lemail è obbligatoria.';
}
if (!$errorMessage && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errorMessage = 'Lemail inserita non è valida.';
}
// Check unique email.
if (!$errorMessage) {
$stmtCheckEmail = $pdo->prepare("
SELECT id
FROM auth_users
WHERE email = ? AND id <> ?
LIMIT 1
");
$stmtCheckEmail->execute([$email, $userId]);
if ($stmtCheckEmail->fetchColumn()) {
$errorMessage = 'Questa email è già utilizzata da un altro utente.';
}
}
// Avatar upload.
if (!$errorMessage && isset($_FILES['avatar']) && $_FILES['avatar']['error'] !== UPLOAD_ERR_NO_FILE) {
if ($_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
$errorMessage = 'Errore durante il caricamento dellavatar.';
} else {
$maxFileSize = 2 * 1024 * 1024; // 2 MB
if ($_FILES['avatar']['size'] > $maxFileSize) {
$errorMessage = 'Lavatar non può superare 2 MB.';
} else {
$tmpFile = $_FILES['avatar']['tmp_name'];
$originalName = $_FILES['avatar']['name'];
$allowedMimeTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif',
];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($tmpFile);
if (!array_key_exists($mimeType, $allowedMimeTypes)) {
$errorMessage = 'Formato avatar non valido. Sono consentiti JPG, PNG, WEBP o GIF.';
} else {
$uploadDir = __DIR__ . '/../upload/users/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$extension = $allowedMimeTypes[$mimeType];
$safeOriginalName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', pathinfo($originalName, PATHINFO_FILENAME));
$fileName = time() . '_' . $userId . '_' . $safeOriginalName . '.' . $extension;
$destination = $uploadDir . $fileName;
if (!move_uploaded_file($tmpFile, $destination)) {
$errorMessage = 'Impossibile salvare il file avatar.';
} else {
// Path used by pages inside userarea, for example:
// <img src="../upload/users/file.jpg">
$avatarToSave = $fileName;
}
}
}
}
}
$passwordToSave = null;
$wantsPasswordChange = ($currentPassword !== '' || $newPassword !== '' || $confirmPassword !== '');
if (!$errorMessage && $wantsPasswordChange) {
if ($currentPassword === '') {
$errorMessage = 'Inserisci la password attuale.';
} elseif ($newPassword === '') {
$errorMessage = 'Inserisci la nuova password.';
} elseif (strlen($newPassword) < 8) {
$errorMessage = 'La nuova password deve contenere almeno 8 caratteri.';
} elseif ($newPassword !== $confirmPassword) {
$errorMessage = 'La conferma password non corrisponde.';
} elseif (!password_verify($currentPassword, $profileUser['password'])) {
$errorMessage = 'La password attuale non è corretta.';
} else {
// Password is encrypted before saving.
$passwordToSave = password_hash($newPassword, PASSWORD_DEFAULT);
}
}
if (!$errorMessage) {
try {
$pdo->beginTransaction();
$stmtUpdate = $pdo->prepare("
UPDATE auth_users
SET
email = :email,
first_name = :first_name,
last_name = :last_name,
phone = :phone,
avatar = :avatar,
address = :address,
country_id = :country_id,
birthday = :birthday,
updated_at = NOW()
WHERE id = :id
LIMIT 1
");
$stmtUpdate->execute([
':email' => $email,
':first_name' => $firstName !== '' ? $firstName : null,
':last_name' => $lastName !== '' ? $lastName : null,
':phone' => $phone !== '' ? $phone : null,
':avatar' => $avatarToSave !== '' ? $avatarToSave : null,
':address' => $address !== '' ? $address : null,
':country_id' => $countryId,
':birthday' => $birthdayValue,
':id' => $userId,
]);
if ($passwordToSave !== null) {
$stmtPassword = $pdo->prepare("
UPDATE auth_users
SET password = ?, updated_at = NOW()
WHERE id = ?
LIMIT 1
");
$stmtPassword->execute([$passwordToSave, $userId]);
}
$pdo->commit();
$successMessage = $passwordToSave !== null
? 'Profilo, avatar e password aggiornati correttamente.'
: 'Profilo aggiornato correttamente.';
// Reload updated user.
$stmtProfileUser->execute([$userId]);
$profileUser = $stmtProfileUser->fetch(PDO::FETCH_ASSOC);
$_SESSION['user_settings_csrf'] = bin2hex(random_bytes(32));
$csrfToken = $_SESSION['user_settings_csrf'];
} catch (Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$errorMessage = 'Errore durante il salvataggio delle impostazioni.';
}
}
}
}
$avatarPath = normalizeAvatarPath($profileUser['avatar'] ?? '');
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Impostazioni Utente <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
body {
background: linear-gradient(135deg, #f3f6f8, #e8eef3);
font-family: 'Segoe UI', sans-serif;
color: #2b3e50;
}
.page-content {
padding: 1.4rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
}
.settings-wrap {
width: 100%;
max-width: 1280px;
}
h3.settings-title {
text-align: center;
font-weight: 800;
color: #2b3e50;
margin-bottom: 1.2rem;
letter-spacing: 0.3px;
}
.settings-card {
background: #fff;
border-radius: 16px;
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
margin-bottom: 16px;
overflow: hidden;
}
.settings-header {
padding: 16px 18px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
gap: 12px;
}
.settings-icon {
font-size: 1.8rem;
line-height: 1;
}
.settings-heading {
margin: 0;
font-weight: 800;
font-size: 1.1rem;
}
.settings-subtitle {
margin: 0;
color: #6b7a89;
font-size: 0.92rem;
}
.settings-body {
padding: 20px;
}
.profile-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 24px;
align-items: flex-start;
}
.avatar-panel {
background: linear-gradient(135deg, #f7fbff, #edf5ff);
border: 1px solid #dbeafe;
border-radius: 18px;
padding: 20px;
text-align: center;
}
.avatar-box {
width: 132px;
height: 132px;
border-radius: 34px;
background: linear-gradient(135deg, #cde5ff, #dff0ff);
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
font-weight: 800;
color: #2b3e50;
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
margin: 0 auto 14px auto;
}
.avatar-box img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-name {
font-size: 1.1rem;
font-weight: 800;
margin: 0;
color: #2b3e50;
}
.avatar-email {
color: #6b7a89;
margin: 4px 0 14px 0;
font-size: 0.92rem;
word-break: break-word;
}
.avatar-upload-label {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
border-radius: 14px;
padding: 11px 14px;
cursor: pointer;
font-weight: 800;
color: #2b3e50;
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
margin-top: 8px;
}
.avatar-upload-label:hover {
transform: translateY(-2px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
}
.avatar-upload-input {
display: none;
}
.avatar-help {
font-size: 0.82rem;
color: #6b7a89;
margin-top: 10px;
line-height: 1.35;
}
.profile-meta {
color: #6b7a89;
font-size: 0.88rem;
margin-top: 14px;
}
.form-label {
font-weight: 700;
color: #2b3e50;
margin-bottom: 6px;
}
.form-control,
.form-select {
border-radius: 12px;
border: 1px solid #d8e0e7;
padding: 10px 12px;
font-size: 0.95rem;
}
.form-control:focus,
.form-select:focus {
border-color: #8bbcf7;
box-shadow: 0 0 0 0.18rem rgba(139, 188, 247, 0.25);
}
.help-text {
font-size: 0.86rem;
color: #6b7a89;
margin-top: 6px;
}
.password-box {
background: linear-gradient(135deg, #fff7e6, #fffaf0);
border-radius: 16px;
padding: 16px;
border: 1px solid rgba(255, 184, 107, 0.45);
}
.btn-save-settings {
border: 0;
border-radius: 16px;
padding: 13px 24px;
font-size: 1rem;
font-weight: 800;
color: #fff;
background: linear-gradient(135deg, #61ce5dff, #61ce5dff);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
}
.btn-save-settings:hover {
transform: translateY(-2px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
color: #fff;
}
.btn-back {
border: 0;
border-radius: 16px;
padding: 13px 20px;
font-size: 1rem;
font-weight: 800;
color: #2b3e50;
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.btn-back:hover {
transform: translateY(-2px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
color: #2b3e50;
}
.alert {
border-radius: 16px;
border: 0;
font-weight: 600;
}
.readonly-note {
background: linear-gradient(135deg, #cde5ff, #dff0ff);
border-radius: 16px;
padding: 14px 16px;
color: #2b3e50;
font-weight: 600;
margin-bottom: 18px;
}
.actions-row {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-top: 18px;
}
.selected-file-name {
font-size: 0.84rem;
color: #2b3e50;
margin-top: 8px;
font-weight: 600;
word-break: break-word;
}
@media (max-width: 992px) {
.profile-layout {
grid-template-columns: 1fr;
}
.avatar-panel {
max-width: 420px;
margin: 0 auto;
width: 100%;
}
}
@media (max-width: 768px) {
.settings-body {
padding: 16px;
}
.actions-row {
flex-direction: column;
}
.btn-save-settings,
.btn-back {
width: 100%;
justify-content: center;
}
}
</style>
</head>
<body>
<div class="wrapper toggled">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="settings-wrap">
<h3 class="settings-title">Impostazioni Utente</h3>
<?php if ($successMessage): ?>
<div class="alert alert-success">
<?= e($successMessage); ?>
</div>
<?php endif; ?>
<?php if ($errorMessage): ?>
<div class="alert alert-danger">
<?= e($errorMessage); ?>
</div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= e($csrfToken); ?>">
<div class="settings-card">
<div class="settings-header">
<div class="settings-icon">👤</div>
<div>
<p class="settings-heading">Profilo personale</p>
<p class="settings-subtitle">Dati anagrafici, contatti e avatar utente</p>
</div>
</div>
<div class="settings-body">
<div class="readonly-note">
Ruolo, stato account e impostazioni di sicurezza avanzate non sono modificabili da questa pagina.
</div>
<div class="profile-layout">
<div class="avatar-panel">
<div class="avatar-box" id="avatarPreviewBox">
<?php if (!empty($avatarPath)): ?>
<img src="<?= e($avatarPath); ?>" class="user-img" alt="user avatar" id="avatarPreviewImage">
<?php else: ?>
<span id="avatarInitials"><?= e(getAvatarInitials($profileUser)); ?></span>
<?php endif; ?>
</div>
<p class="avatar-name">
<?= e(trim(($profileUser['first_name'] ?? '') . ' ' . ($profileUser['last_name'] ?? '')) ?: 'Utente'); ?>
</p>
<p class="avatar-email">
<?= e($profileUser['email']); ?>
</p>
<label for="avatar" class="avatar-upload-label">
Carica avatar
</label>
<input type="file"
class="avatar-upload-input"
id="avatar"
name="avatar"
accept="image/jpeg,image/png,image/webp,image/gif">
<div id="selectedFileName" class="selected-file-name"></div>
<div class="avatar-help">
Formati consentiti: JPG, PNG, WEBP, GIF.<br>
Dimensione massima: 2 MB.
</div>
<div class="profile-meta">
Stato account: <?= e($profileUser['status']); ?>
<?php if (!empty($profileUser['last_login'])): ?>
<br>Ultimo accesso: <?= e(date('d/m/Y H:i', strtotime($profileUser['last_login']))); ?>
<?php endif; ?>
</div>
</div>
<div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="first_name">Nome</label>
<input type="text"
class="form-control"
id="first_name"
name="first_name"
value="<?= e($profileUser['first_name']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="last_name">Cognome</label>
<input type="text"
class="form-control"
id="last_name"
name="last_name"
value="<?= e($profileUser['last_name']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="email">Email</label>
<input type="email"
class="form-control"
id="email"
name="email"
value="<?= e($profileUser['email']); ?>"
required>
</div>
<div class="col-md-6">
<label class="form-label" for="phone">Telefono</label>
<input type="text"
class="form-control"
id="phone"
name="phone"
value="<?= e($profileUser['phone']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="birthday">Data di nascita</label>
<input type="date"
class="form-control"
id="birthday"
name="birthday"
value="<?= e($profileUser['birthday']); ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="country_id">Paese</label>
<select class="form-select" id="country_id" name="country_id">
<option value="">Seleziona...</option>
<?php foreach ($countries as $country): ?>
<option value="<?= (int)$country['id']; ?>"
<?= ((int)$profileUser['country_id'] === (int)$country['id']) ? 'selected' : ''; ?>>
<?= e($country['name'] . (!empty($country['iso_3166_2']) ? ' (' . $country['iso_3166_2'] . ')' : '')); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-12">
<label class="form-label" for="address">Indirizzo</label>
<input type="text"
class="form-control"
id="address"
name="address"
value="<?= e($profileUser['address']); ?>">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="settings-card">
<div class="settings-header">
<div class="settings-icon">🔐</div>
<div>
<p class="settings-heading">Cambio password</p>
<p class="settings-subtitle">Compila questa sezione solo se vuoi modificare la password</p>
</div>
</div>
<div class="settings-body">
<div class="password-box">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label" for="current_password">Password attuale</label>
<input type="password"
class="form-control"
id="current_password"
name="current_password"
autocomplete="current-password">
</div>
<div class="col-md-4">
<label class="form-label" for="new_password">Nuova password</label>
<input type="password"
class="form-control"
id="new_password"
name="new_password"
autocomplete="new-password">
</div>
<div class="col-md-4">
<label class="form-label" for="confirm_password">Conferma nuova password</label>
<input type="password"
class="form-control"
id="confirm_password"
name="confirm_password"
autocomplete="new-password">
</div>
</div>
<div class="help-text">
Se lasci questi campi vuoti, la password attuale rimane invariata.
La nuova password deve avere almeno 8 caratteri.
</div>
</div>
<div class="actions-row">
<a href="production_dashboard.php" class="btn-back"> Torna alla dashboard</a>
<button type="submit" class="btn-save-settings">Salva impostazioni</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<?php include('include/footer.php'); ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const avatarInput = document.getElementById('avatar');
const previewBox = document.getElementById('avatarPreviewBox');
const selectedFileName = document.getElementById('selectedFileName');
if (!avatarInput || !previewBox) {
return;
}
avatarInput.addEventListener('change', function() {
const file = this.files && this.files[0] ? this.files[0] : null;
if (!file) {
selectedFileName.textContent = '';
return;
}
selectedFileName.textContent = file.name;
if (!file.type.startsWith('image/')) {
return;
}
const reader = new FileReader();
reader.onload = function(event) {
previewBox.innerHTML = '';
const img = document.createElement('img');
img.src = event.target.result;
img.className = 'user-img';
img.alt = 'user avatar';
previewBox.appendChild(img);
};
reader.readAsDataURL(file);
});
});
</script>
</body>
</html>