Files
zibo-dashboard/public/userarea/trainings.php
T
2026-05-24 01:04:41 +03:00

754 lines
44 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
include('include/headscript.php');
$pdo = DBHandlerSelect::getInstance()->getConnection();
/* ==========================================
PERMISSIONS
========================================== */
$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;
}
/* ==========================================
FILTERS (from GET)
========================================== */
$fEmployeeId = isset($_GET['employee_id']) && $_GET['employee_id'] !== '' ? (int)$_GET['employee_id'] : 0;
$fTopicId = isset($_GET['topic_id']) && $_GET['topic_id'] !== '' ? (int)$_GET['topic_id'] : 0;
$fStatus = isset($_GET['status']) ? trim($_GET['status']) : '';
$fType = isset($_GET['type']) ? trim($_GET['type']) : '';
$fDepartmentId = isset($_GET['department_id'])&& $_GET['department_id']!== '' ? (int)$_GET['department_id']: 0;
/* ==========================================
LOAD DATA
========================================== */
$where = [];
$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 ($fTopicId > 0) { $where[] = 'et.training_topic_id = :tid'; $params['tid'] = $fTopicId; }
if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) {
$where[] = 'et.training_type = :ty';
$params['ty'] = $fType;
}
if ($fDepartmentId > 0) { $where[] = 'e.department_id = :did'; $params['did'] = $fDepartmentId; }
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
$stmt = $pdo->prepare("
SELECT et.*,
tt.name AS topic_name,
tt.default_reminder_days AS topic_default_rem,
e.first_name, e.last_name, e.employee_code,
d.name AS department_name, d.color AS department_color,
(SELECT COUNT(*) FROM employee_training_attachments a WHERE a.training_id = et.id) AS attachments_count
FROM employee_trainings et
JOIN training_topics tt ON tt.id = et.training_topic_id
JOIN employees e ON e.id = et.employee_id
LEFT JOIN departments d ON d.id = e.department_id
$whereSql
ORDER BY et.next_due_date IS NULL, et.next_due_date ASC, e.last_name, e.first_name
");
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
/* Filter by computed status */
function trainingStatus(?string $nextDue, ?int $reminderDays, ?int $topicDefaultRem): array {
if (!$nextDue) {
return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success'];
}
$rem = $reminderDays !== null ? $reminderDays : ($topicDefaultRem !== null ? $topicDefaultRem : 30);
$today = new DateTime('today');
$due = DateTime::createFromFormat('Y-m-d', $nextDue);
if (!$due) return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success'];
$daysLeft = (int)$today->diff($due)->format('%r%a');
if ($daysLeft < 0) return ['code' => 'expired', 'label' => 'Scaduto', 'class' => 'danger', 'days' => $daysLeft];
if ($daysLeft <= $rem) return ['code' => 'due_soon', 'label' => 'Da aggiornare', 'class' => 'warning', 'days' => $daysLeft];
return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success', 'days' => $daysLeft];
}
$filtered = [];
$counters = ['compliant' => 0, 'due_soon' => 0, 'expired' => 0, 'not_present' => 0, 'all' => 0];
foreach ($rows as $r) {
$s = trainingStatus($r['next_due_date'] ?: null,
$r['reminder_days'] !== null ? (int)$r['reminder_days'] : null,
$r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : null);
$r['_status'] = $s;
$counters['all']++;
$counters[$s['code']] = ($counters[$s['code']] ?? 0) + 1;
if ($fStatus !== '' && $fStatus !== $s['code']) continue;
$filtered[] = $r;
}
/* ==========================================
"NOT PRESENT" — mandatory topics without any record for an employee.
Apply the same filters (employee_id / topic_id / department_id / type=initial).
========================================== */
if ($fType === '' || $fType === 'initial') {
$missingWhere = [];
$missingParams = [];
if ($fEmployeeId > 0) { $missingWhere[] = 'e.id = :eid'; $missingParams['eid'] = $fEmployeeId; }
if ($fTopicId > 0) { $missingWhere[] = 'tt.id = :tid'; $missingParams['tid'] = $fTopicId; }
if ($fDepartmentId > 0) { $missingWhere[] = 'e.department_id = :did'; $missingParams['did'] = $fDepartmentId; }
$missingWhereSql = $missingWhere ? ('AND ' . implode(' AND ', $missingWhere)) : '';
$missingStmt = $pdo->prepare("
SELECT e.id AS employee_id, e.first_name, e.last_name, e.employee_code,
d.name AS department_name, d.color AS department_color,
tt.id AS topic_id, tt.name AS topic_name
FROM employees e
CROSS JOIN training_topics tt
LEFT JOIN departments d ON d.id = e.department_id
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
)
$missingWhereSql
ORDER BY e.last_name, e.first_name, tt.name
");
$missingStmt->execute($missingParams);
$missingRows = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($missingRows as $m) {
$counters['all']++;
$counters['not_present']++;
if ($fStatus !== '' && $fStatus !== 'not_present') continue;
$filtered[] = [
'id' => null,
'_virtual' => true,
'employee_id' => $m['employee_id'],
'first_name' => $m['first_name'],
'last_name' => $m['last_name'],
'employee_code' => $m['employee_code'],
'department_name' => $m['department_name'],
'department_color' => $m['department_color'],
'training_topic_id' => $m['topic_id'],
'topic_name' => $m['topic_name'],
'training_type' => null,
'completed_date' => null,
'next_due_date' => null,
'attachments_count' => 0,
'_status' => ['code' => 'not_present', 'label' => 'Non presente', 'class' => 'secondary', 'days' => null],
];
}
}
/* Dropdown data */
$employees = $pdo->query("
SELECT id, first_name, last_name, employee_code, department_id
FROM employees
ORDER BY last_name, first_name
")->fetchAll(PDO::FETCH_ASSOC);
$topics = $pdo->query("
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);
$departments = $pdo->query("
SELECT id, name, color FROM departments WHERE is_active = 1 ORDER BY sort_order, name
")->fetchAll(PDO::FETCH_ASSOC);
function fmtDate(?string $d): string {
if (!$d || $d === '0000-00-00') return '—';
$ts = strtotime($d);
return $ts ? date('d/m/Y', $ts) : '—';
}
?>
<!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>Storico Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></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>
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;
}
.stat-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 20px; }
@media (max-width: 991.98px) { .stat-row { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 575.98px) { .stat-row { grid-template-columns: repeat(2, 1fr); } }
.stat-card {
border-radius: 14px; padding: 14px 16px; text-align: center;
background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,.05);
cursor: pointer; transition: transform .15s;
}
.stat-card:hover { transform: translateY(-2px); }
.stat-card.active { outline: 3px solid #0d6efd; }
.stat-card .stat-num { font-size: 1.8rem; font-weight: 700; line-height: 1; }
.stat-card .stat-label { font-size: 0.85rem; color: #64748b; margin-top: 4px; }
.stat-card.all .stat-num { color: #1f2937; }
.stat-card.compliant .stat-num { color: #16a34a; }
.stat-card.due_soon .stat-num { color: #d97706; }
.stat-card.expired .stat-num { color: #dc2626; }
.stat-card.not_present .stat-num { color: #6b7280; }
.pill { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 0.85rem; font-weight: 600; }
.pill-success { background: #d1fae5; color: #065f46; }
.pill-warning { background: #fef3c7; color: #92400e; }
.pill-danger { background: #fee2e2; color: #991b1b; }
.pill-secondary { background: #e5e7eb; color: #374151; }
.pill-role { background: #fff; color: #334155; border: 1px solid #cbd5e1; }
.pill-dept-inline { padding: 2px 8px; }
.tr-card {
border: 1px solid #e2e8f0; border-radius: 14px;
padding: 14px 16px; margin-bottom: 12px;
background: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
}
.tr-card .name a { color: #1f2937; font-weight: 600; text-decoration: none; }
.tr-card .topic { color: #475569; }
.tr-card .meta { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 0.85rem; color: #64748b; margin-top: 8px; }
.tr-card .meta b { color: #1f2937; font-weight: 600; }
@media (max-width: 767.98px) {
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
.back-dashboard { width: 100%; }
}
</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">📚 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'">
↩️ Torna alla Dashboard
</button>
</div>
</div>
<div class="card-body">
<!-- COUNTERS -->
<div class="stat-row">
<a class="stat-card all <?= $fStatus === '' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)$counters['all'] ?></div>
<div class="stat-label">Tutte</div>
</a>
<a class="stat-card compliant <?= $fStatus === 'compliant' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'compliant', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['compliant'] ?? 0) ?></div>
<div class="stat-label">Conformi</div>
</a>
<a class="stat-card due_soon <?= $fStatus === 'due_soon' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'due_soon', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['due_soon'] ?? 0) ?></div>
<div class="stat-label">Da aggiornare</div>
</a>
<a class="stat-card expired <?= $fStatus === 'expired' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'expired', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'type' => $fType, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['expired'] ?? 0) ?></div>
<div class="stat-label">Scaduti</div>
</a>
<a class="stat-card not_present <?= $fStatus === 'not_present' ? 'active' : '' ?>" href="?<?= http_build_query(array_filter(['status' => 'not_present', 'employee_id' => $fEmployeeId, 'topic_id' => $fTopicId, 'department_id' => $fDepartmentId])) ?>">
<div class="stat-num"><?= (int)($counters['not_present'] ?? 0) ?></div>
<div class="stat-label">Non presenti</div>
</a>
</div>
<!-- FILTERS -->
<form method="get" class="row g-2 mb-3" id="filtersForm">
<input type="hidden" name="status" value="<?= htmlspecialchars($fStatus, ENT_QUOTES) ?>">
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Dipendente</label>
<select name="employee_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="">— Tutti —</option>
<?php foreach ($employees as $e): ?>
<option value="<?= (int)$e['id'] ?>" <?= $fEmployeeId === (int)$e['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars(trim($e['first_name'] . ' ' . $e['last_name'])) ?>
<?php if (!empty($e['employee_code'])): ?>(<?= htmlspecialchars($e['employee_code']) ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Corso</label>
<select name="topic_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="">— Tutti —</option>
<?php foreach ($topics as $t): ?>
<option value="<?= (int)$t['id'] ?>" <?= $fTopicId === (int)$t['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($t['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Reparto</label>
<select name="department_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="">— Tutti —</option>
<?php foreach ($departments as $d): ?>
<option value="<?= (int)$d['id'] ?>" <?= $fDepartmentId === (int)$d['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($d['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-3">
<label class="form-label small fw-semibold">Tipo</label>
<select name="type" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="">— Tutti —</option>
<option value="initial" <?= $fType === 'initial' ? 'selected' : '' ?>>Iniziale</option>
<option value="refresher" <?= $fType === 'refresher' ? 'selected' : '' ?>>Aggiornamento</option>
</select>
</div>
<?php if ($fEmployeeId || $fTopicId || $fDepartmentId || $fType || $fStatus): ?>
<div class="col-12">
<a href="trainings.php" class="btn btn-sm btn-outline-secondary">✖️ Pulisci filtri</a>
</div>
<?php endif; ?>
</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)): ?>
<div class="text-center text-muted py-4">
Nessuna formazione corrispondente ai filtri.
</div>
<?php else: ?>
<!-- DESKTOP TABLE -->
<div class="table-responsive d-none d-md-block">
<table class="table table-striped align-middle">
<thead style="background-color:#cfe3ff;">
<tr>
<th style="width:36px"><input type="checkbox" class="form-check-input" id="checkAll" title="Seleziona tutti"></th>
<th>Dipendente</th>
<th>Reparto</th>
<th>Corso</th>
<th>Tipo</th>
<th>Completato</th>
<th>Prossimo agg.</th>
<th>Stato</th>
<th>Giorni</th>
</tr>
</thead>
<tbody>
<?php foreach ($filtered as $r): ?>
<?php
$fullName = trim($r['first_name'] . ' ' . $r['last_name']);
$typeLbl = $r['training_type'] === 'refresher' ? 'Aggiornamento' : ($r['training_type'] === 'initial' ? 'Iniziale' : '—');
$days = $r['_status']['days'] ?? null;
?>
<tr>
<td>
<?php if (!empty($r['id'])): ?>
<input type="checkbox" class="form-check-input row-check" value="<?= (int)$r['id'] ?>">
<?php endif; ?>
</td>
<td>
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training" class="fw-semibold text-decoration-none">
<?= htmlspecialchars($fullName) ?>
</a>
<?php if (!empty($r['employee_code'])): ?>
<div class="small text-muted"><?= htmlspecialchars($r['employee_code']) ?></div>
<?php endif; ?>
</td>
<td>
<?php if (!empty($r['department_name'])): ?>
<span class="pill pill-dept-inline" style="background:<?= htmlspecialchars($r['department_color'] ?? '#e5e7eb', ENT_QUOTES) ?>20; color:<?= htmlspecialchars($r['department_color'] ?? '#374151', ENT_QUOTES) ?>;">
<?= htmlspecialchars($r['department_name']) ?>
</span>
<?php else: ?>—<?php endif; ?>
</td>
<td><?= htmlspecialchars($r['topic_name']) ?></td>
<td><span class="pill pill-role"><?= $typeLbl ?></span></td>
<td><?= fmtDate($r['completed_date']) ?></td>
<td><?= fmtDate($r['next_due_date']) ?></td>
<td><span class="pill pill-<?= $r['_status']['class'] ?>"><?= $r['_status']['label'] ?></span></td>
<td>
<?php if ($days === null): ?>—
<?php elseif ($days < 0): ?>
<span class="text-danger fw-semibold"><?= $days ?></span>
<?php else: ?>
+<?= $days ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- MOBILE CARDS -->
<div class="d-block d-md-none">
<?php foreach ($filtered as $r): ?>
<?php
$fullName = trim($r['first_name'] . ' ' . $r['last_name']);
$typeLbl = $r['training_type'] === 'refresher' ? 'Aggiornamento' : ($r['training_type'] === 'initial' ? 'Iniziale' : '—');
$days = $r['_status']['days'] ?? null;
?>
<div class="tr-card">
<div class="d-flex justify-content-between align-items-start gap-2 mb-1">
<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">
<?= htmlspecialchars($fullName) ?>
</a>
</div>
<span class="pill pill-<?= $r['_status']['class'] ?>"><?= $r['_status']['label'] ?></span>
</div>
<div class="topic">📖 <?= htmlspecialchars($r['topic_name']) ?></div>
<div class="meta">
<span><b>Tipo:</b> <?= $typeLbl ?></span>
<span><b>Completato:</b> <?= fmtDate($r['completed_date']) ?></span>
<?php if ($r['next_due_date']): ?>
<span><b>Prossimo:</b> <?= fmtDate($r['next_due_date']) ?>
<?php if ($days !== null && $days < 0): ?>
<span class="text-danger fw-semibold">(<?= $days ?>g)</span>
<?php elseif ($days !== null): ?>
(+<?= $days ?>g)
<?php endif; ?>
</span>
<?php endif; ?>
<?php if (!empty($r['department_name'])): ?>
<span><b>Reparto:</b> <?= htmlspecialchars($r['department_name']) ?></span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</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'); ?>
<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>
</html>