getConnection();
/* ==========================================
PERMISSIONS
========================================== */
$isHrManager = Auth::user()->hasRole('Admin')
|| Auth::user()->hasRole('Superuser')
|| Auth::user()->hasRole('employee-hr')
|| Auth::user()->hasRole('manager');
/* ==========================================
RESOLVE TARGET EMPLOYEE
========================================== */
$requestedId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$isOwnProfile = ($requestedId === 0);
if ($isOwnProfile) {
$stmt = $pdo->prepare("SELECT id FROM employees WHERE auth_user_id = :uid LIMIT 1");
$stmt->execute(['uid' => $iduserlogin]);
$employeeId = (int)$stmt->fetchColumn();
} else {
if (!$isHrManager) {
// Non-HR users can only view their own profile.
header('Location: employee-profile.php');
exit;
}
$employeeId = $requestedId;
}
/* ==========================================
LOAD EMPLOYEE DATA
========================================== */
$employee = null;
if ($employeeId > 0) {
$stmt = $pdo->prepare("
SELECT e.*,
d.name AS department_name,
d.color AS department_color,
jr.name AS job_role_name,
jsr.name AS job_sub_role_name,
au.first_name AS auth_first_name,
au.last_name AS auth_last_name,
au.email AS auth_email,
au.username AS auth_username,
au.avatar AS auth_avatar,
ar.name AS auth_role_name,
ar.display_name AS auth_role_display_name
FROM employees e
LEFT JOIN departments d ON d.id = e.department_id
LEFT JOIN job_roles jr ON jr.id = e.job_role_id
LEFT JOIN job_sub_roles jsr ON jsr.id = e.job_sub_role_id
LEFT JOIN auth_users au ON au.id = e.auth_user_id
LEFT JOIN auth_roles ar ON ar.id = au.role_id
WHERE e.id = :id
LIMIT 1
");
$stmt->execute(['id' => $employeeId]);
$employee = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
/* Authorization: own profile must match auth_user_id (defence in depth) */
if (!$isHrManager && $employee && (int)$employee['auth_user_id'] !== (int)$iduserlogin) {
header('Location: employee-profile.php');
exit;
}
$canEdit = $isHrManager;
/* ==========================================
EMPLOYEE JOB ROLES / SUB ROLES (multi assignment)
========================================== */
$employeeSubRoles = [];
$employeeJobRoleNames = [];
$employeeSubRoleNames = [];
$employeeSubRoleIds = [];
$employeeSubRolesByRole = [];
if ($employee) {
$stmt = $pdo->prepare("
SELECT
ejsr.job_sub_role_id,
ejsr.is_primary,
jsr.name AS job_sub_role_name,
jsr.job_role_id,
jr.name AS job_role_name
FROM employee_job_sub_roles ejsr
INNER JOIN job_sub_roles jsr ON jsr.id = ejsr.job_sub_role_id
LEFT JOIN job_roles jr ON jr.id = jsr.job_role_id
WHERE ejsr.employee_id = :eid
ORDER BY ejsr.is_primary DESC, jr.sort_order ASC, jr.name ASC, jsr.sort_order ASC, jsr.name ASC
");
$stmt->execute(['eid' => $employeeId]);
$employeeSubRoles = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Fallback: if the bridge table is empty but legacy employees.job_sub_role_id is filled, show the legacy value.
if (!$employeeSubRoles && !empty($employee['job_sub_role_id'])) {
$stmt = $pdo->prepare("
SELECT
jsr.id AS job_sub_role_id,
1 AS is_primary,
jsr.name AS job_sub_role_name,
jsr.job_role_id,
jr.name AS job_role_name
FROM job_sub_roles jsr
LEFT JOIN job_roles jr ON jr.id = jsr.job_role_id
WHERE jsr.id = :sid
LIMIT 1
");
$stmt->execute(['sid' => (int)$employee['job_sub_role_id']]);
$legacySubRole = $stmt->fetch(PDO::FETCH_ASSOC);
if ($legacySubRole) {
$employeeSubRoles = [$legacySubRole];
}
}
foreach ($employeeSubRoles as $sr) {
$employeeSubRoleIds[] = (int)$sr['job_sub_role_id'];
if (!empty($sr['job_role_name'])) {
$employeeJobRoleNames[(int)$sr['job_role_id']] = $sr['job_role_name'];
}
if (!empty($sr['job_sub_role_name'])) {
$employeeSubRoleNames[(int)$sr['job_sub_role_id']] = $sr['job_sub_role_name'];
}
$roleKey = (int)($sr['job_role_id'] ?? 0);
if (!isset($employeeSubRolesByRole[$roleKey])) {
$employeeSubRolesByRole[$roleKey] = [
'job_role_name' => $sr['job_role_name'] ?: 'Senza mansione',
'items' => [],
];
}
$employeeSubRolesByRole[$roleKey]['items'][] = $sr;
}
}
/* ==========================================
DOCUMENTS (File Repository)
========================================== */
$documents = [];
if ($employee) {
$stmt = $pdo->prepare("
SELECT d.*,
TRIM(CONCAT(COALESCE(au.first_name,''),' ',COALESCE(au.last_name,''))) AS uploader_name,
au.email AS uploader_email
FROM employee_documents d
LEFT JOIN auth_users au ON au.id = d.uploaded_by
WHERE d.employee_id = :eid
ORDER BY d.created_at DESC
");
$stmt->execute(['eid' => $employeeId]);
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/* ==========================================
TRAINING HISTORY
========================================== */
$trainings = [];
$trainingTopicsAll = [];
$missingMandatoryTopics = [];
if ($employee) {
$stmt = $pdo->prepare("
SELECT et.*,
tt.name AS topic_name,
tt.default_frequency_months AS topic_default_freq,
tt.default_reminder_days AS topic_default_rem,
(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
WHERE et.employee_id = :eid
ORDER BY et.completed_date DESC, et.id DESC
");
$stmt->execute(['eid' => $employeeId]);
$trainings = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Mark the most recent record per topic — older ones are history, not "expired".
$seenTopics = [];
foreach ($trainings as &$t) {
$tid = (int)$t['training_topic_id'];
$t['_is_latest'] = !isset($seenTopics[$tid]);
$seenTopics[$tid] = true;
}
unset($t);
if ($canEdit) {
$trainingTopicsAll = $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);
}
$missingStmt = $pdo->prepare("
SELECT tt.id, tt.name
FROM training_topics tt
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
AND NOT EXISTS (
SELECT 1 FROM employee_trainings et
WHERE et.employee_id = :eid AND et.training_topic_id = tt.id
)
ORDER BY tt.sort_order, tt.name
");
$missingStmt->execute(['eid' => $employeeId]);
$missingMandatoryTopics = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
}
/* ==========================================
PPE (Assigned + Required by sub role)
========================================== */
$ppeList = [];
$ppeItemsAll = [];
$requiredPpeList = [];
$assignedPpeIds = [];
if ($employee) {
// Assigned PPE history from the normalized table.
$stmt = $pdo->prepare("
SELECT
epi.*,
pi.name AS ppe_name,
pi.category AS ppe_category,
pi.photo AS ppe_photo,
pi.standard_reference,
pi.validity_months
FROM employee_ppe_items epi
INNER JOIN ppe_items pi ON pi.id = epi.ppe_item_id
WHERE epi.employee_id = :eid
ORDER BY
CASE epi.status
WHEN 'assigned' THEN 1
WHEN 'expired' THEN 2
WHEN 'damaged' THEN 3
WHEN 'lost' THEN 4
WHEN 'returned' THEN 5
ELSE 9
END,
epi.assigned_date DESC,
epi.created_at DESC
");
$stmt->execute(['eid' => $employeeId]);
$ppeList = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($ppeList as $p) {
if (($p['status'] ?? '') === 'assigned') {
$assignedPpeIds[(int)$p['ppe_item_id']] = true;
}
}
// All active PPE for manual assignment dropdown.
if ($canEdit) {
$ppeItemsAll = $pdo->query("
SELECT id, name, category, standard_reference, validity_months
FROM ppe_items
WHERE is_active = 1
ORDER BY sort_order ASC, name ASC
")->fetchAll(PDO::FETCH_ASSOC);
}
// Required PPE based on all employee sub roles.
// DISTINCT avoids duplicated PPE when two sub roles require the same item.
$stmt = $pdo->prepare("
SELECT
pi.id,
pi.name,
pi.category,
pi.photo,
pi.standard_reference,
pi.validity_months,
GROUP_CONCAT(DISTINCT CONCAT(COALESCE(jr.name, 'Senza mansione'), ' / ', jsr.name) ORDER BY jr.sort_order ASC, jr.name ASC, jsr.sort_order ASC, jsr.name ASC SEPARATOR ' | ') AS source_sub_roles
FROM employee_job_sub_roles ejsr
INNER JOIN job_sub_roles jsr ON jsr.id = ejsr.job_sub_role_id
LEFT JOIN job_roles jr ON jr.id = jsr.job_role_id
INNER JOIN job_sub_role_ppe_items jsp ON jsp.job_sub_role_id = ejsr.job_sub_role_id
INNER JOIN ppe_items pi ON pi.id = jsp.ppe_item_id
WHERE ejsr.employee_id = :eid
AND jsp.is_active = 1
AND pi.is_active = 1
GROUP BY pi.id, pi.name, pi.category, pi.photo, pi.standard_reference, pi.validity_months
ORDER BY pi.category ASC, pi.name ASC
");
$stmt->execute(['eid' => $employeeId]);
$requiredPpeList = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/* ==========================================
DROPDOWN DATA FOR EDIT MODAL
========================================== */
$departments = $isHrManager
? $pdo->query("SELECT id, name FROM departments WHERE is_active = 1 ORDER BY sort_order, name")->fetchAll(PDO::FETCH_ASSOC)
: [];
$jobRoles = $isHrManager
? $pdo->query("SELECT id, name FROM job_roles WHERE is_active = 1 ORDER BY sort_order, name")->fetchAll(PDO::FETCH_ASSOC)
: [];
$jobSubRolesAll = $isHrManager
? $pdo->query("
SELECT
jsr.id,
jsr.job_role_id,
jsr.name,
jr.name AS job_role_name
FROM job_sub_roles jsr
LEFT JOIN job_roles jr ON jr.id = jsr.job_role_id
WHERE jsr.is_active = 1
ORDER BY jr.sort_order ASC, jr.name ASC, jsr.sort_order ASC, jsr.name ASC
")->fetchAll(PDO::FETCH_ASSOC)
: [];
$jobSubRoleToRoleMap = [];
foreach ($jobSubRolesAll as $sr) {
$jobSubRoleToRoleMap[(int)$sr['id']] = (int)$sr['job_role_id'];
}
$authUsers = $isHrManager
? $pdo->query("SELECT id, username, first_name, last_name, email, role_id FROM auth_users ORDER BY first_name, last_name")->fetchAll(PDO::FETCH_ASSOC)
: [];
$rolesList = $isHrManager
? $pdo->query("SELECT id, name, display_name FROM auth_roles ORDER BY display_name, name")->fetchAll(PDO::FETCH_ASSOC)
: [];
/* ==========================================
HELPERS
========================================== */
function statusBadge(string $status): array
{
switch ($status) {
case 'active':
return ['label' => 'Attivo', 'class' => 'success'];
case 'inactive':
return ['label' => 'Cessato', 'class' => 'secondary'];
case 'suspended':
return ['label' => 'Sospeso', 'class' => 'warning'];
default:
return ['label' => htmlspecialchars($status), 'class' => 'secondary'];
}
}
function fmtDate(?string $d): string
{
if (!$d || $d === '0000-00-00') return '—';
$ts = strtotime($d);
return $ts ? date('d/m/Y', $ts) : '—';
}
function valOrDash($v): string
{
$v = (string)($v ?? '');
return $v !== '' ? htmlspecialchars($v) : '—';
}
function categoryLabel(string $c): string
{
switch ($c) {
case 'job_description':
return 'Mansionario';
case 'contract':
return 'Contratto';
case 'rules':
return 'Regolamento';
case 'other':
return 'Altro';
default:
return htmlspecialchars($c);
}
}
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'];
if ($daysLeft <= $rem) return ['code' => 'due_soon', 'label' => 'Da aggiornare', 'class' => 'warning'];
return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success'];
}
function fmtFileSize(?int $bytes): string
{
if ($bytes === null || $bytes <= 0) return '—';
if ($bytes < 1024) return $bytes . ' B';
if ($bytes < 1024 * 1024) return number_format($bytes / 1024, 1) . ' KB';
if ($bytes < 1024 * 1024 * 1024) return number_format($bytes / 1024 / 1024, 1) . ' MB';
return number_format($bytes / 1024 / 1024 / 1024, 1) . ' GB';
}
?>
Profilo Dipendente - = htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?>
= $isOwnProfile
? 'Il tuo profilo dipendente non è ancora stato creato'
: 'Dipendente non trovato' ?>
= $isOwnProfile
? 'Contatta il responsabile HR per la creazione del tuo profilo.'
: 'Verifica l\'ID o torna alla lista dipendenti.' ?>
Nome
= valOrDash($employee['first_name']) ?>
Cognome
= valOrDash($employee['last_name']) ?>
Codice Dipendente
= valOrDash($employee['employee_code']) ?>
Data di Assunzione
= fmtDate($employee['hire_date']) ?>
Indirizzo
= valOrDash($employee['address']) ?>
Reparto
= valOrDash($deptName) ?>
Mansioni / Sottomansioni
💼 = htmlspecialchars($roleGroup['job_role_name']) ?>
🧩 = htmlspecialchars($sr['job_sub_role_name']) ?>
Nessuna mansione/sottomansione associata
Stato
= htmlspecialchars($status['label']) ?>
Utente collegato
= htmlspecialchars($employee['auth_username']) ?>
= htmlspecialchars($employee['auth_email']) ?>
Nessun utente collegato
Nessun documento
= $canEdit
? 'Carica il primo documento (mansionario, contratto, regolamento ecc.).'
: 'Nessun documento disponibile al momento.' ?>
| Categoria |
Nome File |
Dimensione |
Caricato da |
Data |
Azioni |
| = categoryLabel((string)$d['category']) ?> |
= htmlspecialchars($d['original_name']) ?>
= htmlspecialchars($d['notes']) ?>
|
= fmtFileSize($d['size'] !== null ? (int)$d['size'] : null) ?> |
= htmlspecialchars($upBy) ?> |
= fmtDate(substr((string)$d['created_at'], 0, 10)) ?> |
⬇️ Scarica
|
= categoryLabel((string)$d['category']) ?>
= fmtDate(substr((string)$d['created_at'], 0, 10)) ?>
= htmlspecialchars($d['original_name']) ?>
= htmlspecialchars($d['notes']) ?>
Dim.: = fmtFileSize($d['size'] !== null ? (int)$d['size'] : null) ?>
Da: = htmlspecialchars($upBy) ?>
🦺 DPI richiesti dalle sottomansioni associate
— calcolati su = count($employeeSubRoleNames) ?> sottomansion= count($employeeSubRoleNames) === 1 ? 'e' : 'i' ?>
= htmlspecialchars($rp['name']) ?>
= !empty($rp['category']) ? htmlspecialchars($rp['category']) : 'Senza categoria' ?>
· = htmlspecialchars($rp['standard_reference']) ?>
Da: = htmlspecialchars($rp['source_sub_roles']) ?>
Assegnato
Mancante
Nessun DPI obbligatorio configurato per le sottomansioni associate al dipendente.
Nessuna sottomansione associata al dipendente: non è possibile suggerire DPI obbligatori.
Nessun DPI assegnato
= $canEdit
? 'Aggiungi il primo dispositivo di protezione individuale.'
: 'Nessun DPI consegnato al momento.' ?>
| DPI |
Categoria |
Data Consegna |
Scadenza |
Consegnato da |
Stato |
Note |
Azioni |
| = htmlspecialchars($p['ppe_name']) ?> |
= valOrDash($p['ppe_category']) ?> |
= fmtDate($p['assigned_date']) ?> |
= fmtDate($p['expiry_date']) ?> |
= valOrDash($p['delivered_by'] ?? null) ?> |
= $statusLabel ?> |
= valOrDash($p['notes']) ?> |
|
🦺 = htmlspecialchars($p['ppe_name']) ?>
= $statusLabel ?>
Categoria: = htmlspecialchars($p['ppe_category']) ?>
Consegna: = fmtDate($p['assigned_date']) ?>
Scadenza: = fmtDate($p['expiry_date']) ?>
Consegnato da: = htmlspecialchars($p['delivered_by']) ?>
Note: = htmlspecialchars($p['notes']) ?>
⚠️ = count($missingMandatoryTopics) ?>
obbligator= count($missingMandatoryTopics) === 1 ? 'ia non presente' : 'ie non presenti' ?>:
= htmlspecialchars($mt['name']) ?>
Nessuna formazione registrata
= $canEdit
? 'Aggiungi la prima registrazione di formazione (iniziale o aggiornamento).'
: 'Nessuna formazione registrata al momento.' ?>
| Corso |
Tipo |
Completato |
Prossimo agg. |
Stato |
Allegati |
Azioni |
'storico', 'label' => 'Storico', 'class' => 'secondary'];
$typeLabel = $t['training_type'] === 'refresher' ? 'Aggiornamento' : 'Iniziale';
?>
| = htmlspecialchars($t['topic_name']) ?> |
= $typeLabel ?> |
= fmtDate($t['completed_date']) ?> |
= fmtDate($t['next_due_date']) ?> |
= $s['label'] ?> |
= (int)$t['attachments_count'] ?> |
|
'storico', 'label' => 'Storico', 'class' => 'secondary'];
$typeLabel = $t['training_type'] === 'refresher' ? 'Aggiornamento' : 'Iniziale';
?>
📖 = htmlspecialchars($t['topic_name']) ?>
= $s['label'] ?>
Tipo: = $typeLabel ?>
Completato: = fmtDate($t['completed_date']) ?>
Prossimo: = fmtDate($t['next_due_date']) ?>
0): ?>
Allegati: = (int)$t['attachments_count'] ?>