added assorbimento corso
This commit is contained in:
@@ -74,6 +74,15 @@ $stmt = $pdo->prepare("
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
require_once __DIR__ . '/include/training_coverage.php';
|
||||
|
||||
/* Mappa di tutti i corsi (serve all'helper coperture) */
|
||||
$allTopicsById = [];
|
||||
foreach ($pdo->query("SELECT id, name, default_frequency_months, default_reminder_days, is_active FROM training_topics")->fetchAll(PDO::FETCH_ASSOC) as $t) {
|
||||
if ((int)$t['is_active'] !== 1) continue;
|
||||
$allTopicsById[(int)$t['id']] = $t;
|
||||
}
|
||||
|
||||
/* Filter by computed status */
|
||||
function trainingStatus(?string $nextDue, ?int $reminderDays, ?int $topicDefaultRem): array
|
||||
{
|
||||
@@ -90,6 +99,60 @@ function trainingStatus(?string $nextDue, ?int $reminderDays, ?int $topicDefault
|
||||
return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success', 'days' => $daysLeft];
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
COPERTURE — righe virtuali derivate.
|
||||
Vanno calcolate sui record NON filtrati per topic,
|
||||
altrimenti filtrando su "Antincendio" perderemmo l'RSPP che lo copre.
|
||||
========================================== */
|
||||
$coverageBase = $rows;
|
||||
if ($fTopicId > 0) {
|
||||
$baseWhere = ["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))
|
||||
)"];
|
||||
$baseParams = [];
|
||||
if ($fEmployeeId > 0) {
|
||||
$baseWhere[] = 'et.employee_id = :eid';
|
||||
$baseParams['eid'] = $fEmployeeId;
|
||||
}
|
||||
if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) {
|
||||
$baseWhere[] = 'et.training_type = :ty';
|
||||
$baseParams['ty'] = $fType;
|
||||
}
|
||||
if ($fDepartmentId > 0) {
|
||||
$baseWhere[] = 'e.department_id = :did';
|
||||
$baseParams['did'] = $fDepartmentId;
|
||||
}
|
||||
$baseSql = 'WHERE ' . implode(' AND ', $baseWhere);
|
||||
|
||||
$bstmt = $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
|
||||
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
|
||||
$baseSql
|
||||
");
|
||||
$bstmt->execute($baseParams);
|
||||
$coverageBase = $bstmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$virtualRows = tc_buildVirtualRows($pdo, $coverageBase, $allTopicsById);
|
||||
|
||||
/* Applica i filtri attivi alle righe virtuali */
|
||||
if ($fTopicId > 0) {
|
||||
$virtualRows = array_values(array_filter($virtualRows, function ($v) use ($fTopicId) {
|
||||
return (int)$v['training_topic_id'] === $fTopicId;
|
||||
}));
|
||||
}
|
||||
|
||||
$coveredKeys = tc_coveredKeys($virtualRows);
|
||||
|
||||
$filtered = [];
|
||||
$counters = ['compliant' => 0, 'due_soon' => 0, 'expired' => 0, 'not_present' => 0, 'all' => 0];
|
||||
foreach ($rows as $r) {
|
||||
@@ -106,6 +169,21 @@ foreach ($rows as $r) {
|
||||
$filtered[] = $r;
|
||||
}
|
||||
|
||||
/* Righe coperte: stesso calcolo di stato delle righe reali */
|
||||
foreach ($virtualRows as $v) {
|
||||
$s = trainingStatus(
|
||||
$v['next_due_date'] ?: null,
|
||||
null,
|
||||
$v['topic_default_rem'] !== null ? (int)$v['topic_default_rem'] : null
|
||||
);
|
||||
$v['_status'] = $s;
|
||||
$counters['all']++;
|
||||
$counters[$s['code']] = ($counters[$s['code']] ?? 0) + 1;
|
||||
|
||||
if ($fStatus !== '' && $fStatus !== $s['code']) continue;
|
||||
$filtered[] = $v;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
"NOT PRESENT" — mandatory topics without any record for an employee.
|
||||
Apply the same filters (employee_id / topic_id / department_id / type=initial).
|
||||
@@ -146,6 +224,9 @@ if ($fType === '' || $fType === 'initial') {
|
||||
$missingRows = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($missingRows as $m) {
|
||||
// Se il corso obbligatorio è assorbito da un altro già svolto, non è "Non presente"
|
||||
if (isset($coveredKeys[$m['employee_id'] . ':' . $m['topic_id']])) continue;
|
||||
|
||||
$counters['all']++;
|
||||
$counters['not_present']++;
|
||||
if ($fStatus !== '' && $fStatus !== 'not_present') continue;
|
||||
@@ -421,6 +502,19 @@ function fmtDate(?string $d): string
|
||||
border: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.pill-covered {
|
||||
background: #ede9fe;
|
||||
color: #5b21b6;
|
||||
border: 1px solid #ddd6fe;
|
||||
font-size: .78rem;
|
||||
margin-left: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tr.row-covered {
|
||||
background-color: #faf5ff !important;
|
||||
}
|
||||
|
||||
.pill-dept-inline {
|
||||
padding: 2px 8px;
|
||||
}
|
||||
@@ -617,7 +711,7 @@ function fmtDate(?string $d): string
|
||||
$typeLbl = $r['training_type'] === 'refresher' ? 'Aggiornamento' : ($r['training_type'] === 'initial' ? 'Iniziale' : '—');
|
||||
$days = $r['_status']['days'] ?? null;
|
||||
?>
|
||||
<tr>
|
||||
<tr class="<?= !empty($r['_covered']) ? 'row-covered' : '' ?>">
|
||||
<td>
|
||||
<?php if (!empty($r['id'])): ?>
|
||||
<input type="checkbox" class="form-check-input row-check" value="<?= (int)$r['id'] ?>">
|
||||
@@ -638,7 +732,14 @@ function fmtDate(?string $d): string
|
||||
</span>
|
||||
<?php else: ?>—<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($r['topic_name']) ?></td>
|
||||
<td>
|
||||
<?= htmlspecialchars($r['topic_name']) ?>
|
||||
<?php if (!empty($r['_covered'])): ?>
|
||||
<span class="pill pill-covered" title="Assorbito automaticamente dal corso <?= htmlspecialchars($r['_covered_by_name'], ENT_QUOTES) ?>">
|
||||
⛓️ Coperto da <?= htmlspecialchars($r['_covered_by_name']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><span class="pill pill-role"><?= $typeLbl ?></span></td>
|
||||
<td><?= fmtDate($r['completed_date']) ?></td>
|
||||
<td><?= fmtDate($r['next_due_date']) ?></td>
|
||||
@@ -677,7 +778,14 @@ function fmtDate(?string $d): string
|
||||
</div>
|
||||
<span class="pill pill-<?= $r['_status']['class'] ?>"><?= $r['_status']['label'] ?></span>
|
||||
</div>
|
||||
<div class="topic">📖 <?= htmlspecialchars($r['topic_name']) ?></div>
|
||||
<div class="topic">
|
||||
📖 <?= htmlspecialchars($r['topic_name']) ?>
|
||||
<?php if (!empty($r['_covered'])): ?>
|
||||
<div class="mt-1">
|
||||
<span class="pill pill-covered">⛓️ Coperto da <?= htmlspecialchars($r['_covered_by_name']) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span><b>Tipo:</b> <?= $typeLbl ?></span>
|
||||
<span><b>Completato:</b> <?= fmtDate($r['completed_date']) ?></span>
|
||||
|
||||
Reference in New Issue
Block a user