added subroles and dpi association fixed all pages and migration

This commit is contained in:
2026-06-04 12:17:17 +02:00
parent f7e97f55e9
commit b1f2bb60e3
20 changed files with 6125 additions and 638 deletions
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateJobSubRolesTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('job_sub_roles', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('job_role_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
'default' => null,
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 999,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => 1,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['job_role_id'], [
'name' => 'idx_job_sub_roles_job_role_id',
])
->addIndex(['is_active'], [
'name' => 'idx_job_sub_roles_is_active',
])
->addIndex(['sort_order'], [
'name' => 'idx_job_sub_roles_sort_order',
])
->addForeignKey(
'job_role_id',
'job_roles',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_job_sub_roles_job_role',
]
)
->create();
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreatePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('ppe_items', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('name', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('description', 'text', [
'null' => true,
'default' => null,
])
->addColumn('category', 'string', [
'limit' => 100,
'null' => true,
'default' => null,
'comment' => 'PPE category, for example Head, Hands, Eyes, Feet, Respiratory',
])
->addColumn('photo', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'comment' => 'PPE image path or filename',
])
->addColumn('standard_reference', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'comment' => 'Reference standard, for example EN ISO 20345',
])
->addColumn('validity_months', 'integer', [
'signed' => false,
'null' => true,
'default' => null,
'comment' => 'Default validity in months after assignment',
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 999,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => 1,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['category'], [
'name' => 'idx_ppe_items_category',
])
->addIndex(['is_active'], [
'name' => 'idx_ppe_items_is_active',
])
->addIndex(['sort_order'], [
'name' => 'idx_ppe_items_sort_order',
])
->create();
}
}
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateEmployeePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('employee_ppe_items', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('employee_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('ppe_item_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('assigned_date', 'date', [
'null' => true,
'default' => null,
])
->addColumn('expiry_date', 'date', [
'null' => true,
'default' => null,
])
->addColumn('quantity', 'integer', [
'signed' => false,
'null' => false,
'default' => 1,
])
->addColumn('status', 'enum', [
'values' => [
'assigned',
'returned',
'expired',
'lost',
'damaged',
],
'null' => false,
'default' => 'assigned',
])
->addColumn('notes', 'text', [
'null' => true,
'default' => null,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['employee_id'], [
'name' => 'idx_employee_ppe_items_employee_id',
])
->addIndex(['ppe_item_id'], [
'name' => 'idx_employee_ppe_items_ppe_item_id',
])
->addIndex(['status'], [
'name' => 'idx_employee_ppe_items_status',
])
->addIndex(['expiry_date'], [
'name' => 'idx_employee_ppe_items_expiry_date',
])
->addForeignKey(
'employee_id',
'employees',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_employee_ppe_items_employee',
]
)
->addForeignKey(
'ppe_item_id',
'ppe_items',
'id',
[
'delete' => 'RESTRICT',
'update' => 'CASCADE',
'constraint' => 'fk_employee_ppe_items_ppe_item',
]
)
->create();
}
}
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateJobSubRolePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('job_sub_role_ppe_items', [
'id' => false,
'primary_key' => ['id'],
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('job_sub_role_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('ppe_item_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('requirement_type', 'enum', [
'values' => [
'mandatory',
'recommended',
'optional',
],
'null' => false,
'default' => 'mandatory',
'comment' => 'Defines if the PPE is mandatory, recommended or optional for the sub role',
])
->addColumn('notes', 'text', [
'null' => true,
'default' => null,
])
->addColumn('sort_order', 'integer', [
'signed' => false,
'null' => false,
'default' => 999,
])
->addColumn('is_active', 'boolean', [
'null' => false,
'default' => 1,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['job_sub_role_id'], [
'name' => 'idx_job_sub_role_ppe_items_sub_role_id',
])
->addIndex(['ppe_item_id'], [
'name' => 'idx_job_sub_role_ppe_items_ppe_item_id',
])
->addIndex(['requirement_type'], [
'name' => 'idx_job_sub_role_ppe_items_requirement_type',
])
->addIndex(['is_active'], [
'name' => 'idx_job_sub_role_ppe_items_is_active',
])
->addIndex(['job_sub_role_id', 'ppe_item_id'], [
'unique' => true,
'name' => 'uq_job_sub_role_ppe_item',
])
->addForeignKey(
'job_sub_role_id',
'job_sub_roles',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_job_sub_role_ppe_items_sub_role',
]
)
->addForeignKey(
'ppe_item_id',
'ppe_items',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_job_sub_role_ppe_items_ppe_item',
]
)
->create();
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddJobSubRoleIdToEmployeesTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('employees');
$table
->addColumn('job_sub_role_id', 'integer', [
'signed' => false,
'null' => true,
'default' => null,
'after' => 'job_role_id',
])
->addIndex(['job_sub_role_id'], [
'name' => 'idx_employees_job_sub_role_id',
])
->addForeignKey(
'job_sub_role_id',
'job_sub_roles',
'id',
[
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_employees_job_sub_role',
]
)
->update();
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddDeliveryFieldsToEmployeePpeItemsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('employee_ppe_items');
$table
->addColumn('delivered_by', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'after' => 'expiry_date',
])
->addColumn('created_by', 'integer', [
'signed' => false,
'null' => true,
'default' => null,
'after' => 'notes',
])
->addIndex(['created_by'], [
'name' => 'idx_employee_ppe_items_created_by',
])
->addForeignKey(
'created_by',
'auth_users',
'id',
[
'delete' => 'SET_NULL',
'update' => 'CASCADE',
'constraint' => 'fk_employee_ppe_items_created_by',
]
)
->update();
}
}
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateEmployeeJobSubRolesTable extends AbstractMigration
{
public function up(): void
{
if (!$this->hasTable('employee_job_sub_roles')) {
$table = $this->table('employee_job_sub_roles', [
'id' => false,
'primary_key' => ['id'],
'signed' => false,
'collation' => 'utf8mb4_general_ci',
'encoding' => 'utf8mb4',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('employee_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('job_sub_role_id', 'integer', [
'signed' => false,
'null' => false,
])
->addColumn('is_primary', 'boolean', [
'null' => false,
'default' => false,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addIndex(['employee_id', 'job_sub_role_id'], [
'unique' => true,
'name' => 'uq_employee_subrole',
])
->addIndex(['employee_id'], [
'name' => 'idx_employee_job_sub_roles_employee',
])
->addIndex(['job_sub_role_id'], [
'name' => 'idx_employee_job_sub_roles_subrole',
])
->addForeignKey(
'employee_id',
'employees',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_employee_job_sub_roles_employee',
]
)
->addForeignKey(
'job_sub_role_id',
'job_sub_roles',
'id',
[
'delete' => 'CASCADE',
'update' => 'CASCADE',
'constraint' => 'fk_employee_job_sub_roles_subrole',
]
)
->create();
}
// Import existing single sub-role assignments from employees.job_sub_role_id
// into the new bridge table.
$this->execute("
INSERT IGNORE INTO employee_job_sub_roles
(employee_id, job_sub_role_id, is_primary, created_at)
SELECT
e.id,
e.job_sub_role_id,
1,
NOW()
FROM employees e
WHERE e.job_sub_role_id IS NOT NULL
AND e.job_sub_role_id > 0
");
}
public function down(): void
{
if ($this->hasTable('employee_job_sub_roles')) {
$this->table('employee_job_sub_roles')->drop()->save();
}
}
}
@@ -1,26 +1,38 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
include('../../include/headscript.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 = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'ID DPI non valido.']);
exit;
}
header('Content-Type: application/json; charset=utf-8');
try {
$stmt = $pdo->prepare("DELETE FROM employee_ppe WHERE id = :id");
$stmt->execute(['id' => $id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => 'ID DPI non valido.'
]);
exit;
}
$stmt = $pdo->prepare("
UPDATE employee_ppe_items
SET status = 'returned',
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$id]);
echo json_encode([
'success' => true,
'message' => 'DPI rimosso correttamente.'
]);
exit;
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
exit;
}
@@ -1,82 +1,153 @@
<?php
require_once(__DIR__ . '/../hr_auth_check.php');
include('../../include/headscript.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 = DBHandlerSelect::getInstance()->getConnection();
$id = (int)($_POST['id'] ?? 0);
$employeeId = (int)($_POST['employee_id'] ?? 0);
$itemName = trim($_POST['item_name'] ?? '');
$deliveryDate = trim($_POST['delivery_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$notes = trim($_POST['notes'] ?? '');
if ($employeeId <= 0) {
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
exit;
}
if ($itemName === '') {
echo json_encode(['success' => false, 'message' => 'Il nome del DPI è obbligatorio.']);
exit;
}
$deliveryDate = $deliveryDate === '' ? null : $deliveryDate;
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
$notes = $notes !== '' ? $notes : null;
header('Content-Type: application/json; charset=utf-8');
try {
if ($id > 0) {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : null;
$employeeId = (int)($_POST['employee_id'] ?? 0);
$ppeItemId = (int)($_POST['ppe_item_id'] ?? 0);
$assignedDate = trim($_POST['assigned_date'] ?? '');
$expiryDate = trim($_POST['expiry_date'] ?? '');
$deliveredBy = trim($_POST['delivered_by'] ?? '');
$status = trim($_POST['status'] ?? 'assigned');
$notes = trim($_POST['notes'] ?? '');
$allowedStatuses = [
'assigned',
'returned',
'expired',
'lost',
'damaged',
];
if ($employeeId <= 0) {
echo json_encode([
'success' => false,
'message' => 'Dipendente non valido.'
]);
exit;
}
if ($ppeItemId <= 0) {
echo json_encode([
'success' => false,
'message' => 'Selezionare un DPI.'
]);
exit;
}
if (!in_array($status, $allowedStatuses, true)) {
$status = 'assigned';
}
$checkEmployee = $pdo->prepare("SELECT id FROM employees WHERE id = ? LIMIT 1");
$checkEmployee->execute([$employeeId]);
if (!$checkEmployee->fetchColumn()) {
echo json_encode([
'success' => false,
'message' => 'Dipendente non trovato.'
]);
exit;
}
$checkPpe = $pdo->prepare("SELECT id FROM ppe_items WHERE id = ? LIMIT 1");
$checkPpe->execute([$ppeItemId]);
if (!$checkPpe->fetchColumn()) {
echo json_encode([
'success' => false,
'message' => 'DPI non trovato.'
]);
exit;
}
if ($id) {
$stmt = $pdo->prepare("
UPDATE employee_ppe
SET item_name = :item_name,
delivery_date = :delivery_date,
UPDATE employee_ppe_items
SET ppe_item_id = :ppe_item_id,
assigned_date = :assigned_date,
expiry_date = :expiry_date,
delivered_by = :delivered_by,
status = :status,
notes = :notes,
updated_at = NOW()
WHERE id = :id AND employee_id = :eid
WHERE id = :id
AND employee_id = :employee_id
");
$stmt->execute([
'item_name' => $itemName,
'delivery_date' => $deliveryDate,
'delivered_by' => $deliveredBy,
'notes' => $notes,
'id' => $id,
'eid' => $employeeId,
]);
echo json_encode(['success' => true, 'id' => $id]);
exit;
}
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE id = :id");
$check->execute(['id' => $employeeId]);
if ((int)$check->fetchColumn() === 0) {
echo json_encode(['success' => false, 'message' => 'Dipendente non trovato.']);
$stmt->execute([
'ppe_item_id' => $ppeItemId,
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
'expiry_date' => $expiryDate !== '' ? $expiryDate : null,
'delivered_by' => $deliveredBy !== '' ? $deliveredBy : null,
'status' => $status,
'notes' => $notes !== '' ? $notes : null,
'id' => $id,
'employee_id' => $employeeId,
]);
echo json_encode([
'success' => true,
'message' => 'DPI aggiornato.'
]);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO employee_ppe
(employee_id, item_name, delivery_date, delivered_by, notes, created_by, created_at, updated_at)
INSERT INTO employee_ppe_items
(
employee_id,
ppe_item_id,
assigned_date,
expiry_date,
delivered_by,
quantity,
status,
notes,
created_by,
created_at,
updated_at
)
VALUES
(:employee_id, :item_name, :delivery_date, :delivered_by, :notes, :created_by, NOW(), NOW())
(
:employee_id,
:ppe_item_id,
:assigned_date,
:expiry_date,
:delivered_by,
1,
:status,
:notes,
:created_by,
NOW(),
NOW()
)
");
$stmt->execute([
'employee_id' => $employeeId,
'item_name' => $itemName,
'delivery_date' => $deliveryDate,
'delivered_by' => $deliveredBy,
'notes' => $notes,
'created_by' => $currentUserId,
'ppe_item_id' => $ppeItemId,
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
'expiry_date' => $expiryDate !== '' ? $expiryDate : null,
'delivered_by' => $deliveredBy !== '' ? $deliveredBy : null,
'status' => $status,
'notes' => $notes !== '' ? $notes : null,
'created_by' => isset($iduserlogin) ? (int)$iduserlogin : null,
]);
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
echo json_encode([
'success' => true,
'message' => 'DPI assegnato.'
]);
exit;
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
exit;
}
+688 -48
View File
@@ -40,15 +40,20 @@ if ($employeeId > 0) {
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
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
");
@@ -64,6 +69,75 @@ if (!$isHrManager && $employee && (int)$employee['auth_user_id'] !== (int)$iduse
$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)
========================================== */
@@ -136,20 +210,84 @@ if ($employee) {
}
/* ==========================================
PPE (Assigned)
PPE (Assigned + Required by sub role)
========================================== */
$ppeList = [];
$ppeItemsAll = [];
$requiredPpeList = [];
$assignedPpeIds = [];
if ($employee) {
// Assigned PPE history from the normalized table.
$stmt = $pdo->prepare("
SELECT *
FROM employee_ppe
WHERE employee_id = :eid
ORDER BY delivery_date DESC, created_at DESC
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
========================================== */
@@ -159,6 +297,23 @@ $departments = $isHrManager
$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)
: [];
@@ -240,6 +395,8 @@ function fmtFileSize(?int $bytes): string
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/dataTables.bootstrap5.min.css">
<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" />
<title>Profilo Dipendente - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
@@ -335,6 +492,79 @@ function fmtFileSize(?int $bytes): string
margin: 4px 0 8px 0;
}
.profile-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 10px;
margin-top: 12px;
}
.profile-summary-card {
background: rgba(255, 255, 255, .72);
border: 1px solid rgba(148, 163, 184, .45);
border-radius: 14px;
padding: 10px 12px;
min-height: 68px;
}
.profile-summary-label {
font-size: .72rem;
text-transform: uppercase;
letter-spacing: .06em;
color: #64748b;
font-weight: 800;
margin-bottom: 5px;
}
.profile-summary-value {
color: #1f2937;
font-weight: 700;
line-height: 1.25;
}
.profile-summary-muted {
color: #64748b;
font-size: .84rem;
margin-top: 2px;
}
.profile-role-stack {
display: flex;
flex-direction: column;
gap: 6px;
}
.profile-role-group {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
}
.profile-role-main {
display: inline-flex;
align-items: center;
background: #e0f2fe;
color: #075985;
border: 1px solid #bae6fd;
border-radius: 999px;
padding: 3px 9px;
font-size: .78rem;
font-weight: 800;
}
.profile-subrole-chip {
display: inline-flex;
align-items: center;
background: #ffffff;
color: #334155;
border: 1px solid #cbd5e1;
border-radius: 999px;
padding: 3px 9px;
font-size: .78rem;
font-weight: 650;
}
.profile-badges {
display: flex;
flex-wrap: wrap;
@@ -530,6 +760,43 @@ function fmtFileSize(?int $bytes): string
word-break: break-word;
}
.job-role-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.job-role-group {
border: 1px solid #e2e8f0;
background: #fff;
border-radius: 12px;
padding: 10px 12px;
}
.job-role-group-title {
font-weight: 700;
color: #1f2937;
margin-bottom: 6px;
}
.job-subrole-chip-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.job-subrole-chip {
display: inline-flex;
align-items: center;
border-radius: 999px;
background: #eff6ff;
color: #1d4ed8;
border: 1px solid #bfdbfe;
padding: 4px 10px;
font-size: 0.85rem;
font-weight: 600;
}
.empty-profile {
text-align: center;
padding: 60px 20px;
@@ -714,7 +981,10 @@ function fmtFileSize(?int $bytes): string
$status = statusBadge((string)($employee['status'] ?? 'active'));
$deptName = $employee['department_name'] ?? null;
$deptColor = $employee['department_color'] ?? null;
$jobName = $employee['job_role_name'] ?? null;
$jobNames = array_values($employeeJobRoleNames);
$jobSubRoleNames = array_values($employeeSubRoleNames);
$jobName = $jobNames ? implode(', ', $jobNames) : ($employee['job_role_name'] ?? null);
$jobSubRoleName = $jobSubRoleNames ? implode(', ', $jobSubRoleNames) : ($employee['job_sub_role_name'] ?? null);
$avatar = trim((string)($employee['auth_avatar'] ?? ''));
@@ -746,20 +1016,63 @@ function fmtFileSize(?int $bytes): string
Codice: <code><?= htmlspecialchars($employee['employee_code']) ?></code>
</div>
<?php endif; ?>
<div class="profile-badges">
<?php if ($jobName): ?>
<span class="pill pill-role">💼 <?= htmlspecialchars($jobName) ?></span>
<?php endif; ?>
<div class="profile-summary-grid">
<div class="profile-summary-card">
<div class="profile-summary-label">Reparto</div>
<div class="profile-summary-value">
<?php if ($deptName): ?>
<span class="pill pill-dept" style="<?= $deptColor ? 'background:' . htmlspecialchars($deptColor, ENT_QUOTES) . '20; color:' . htmlspecialchars($deptColor, ENT_QUOTES) . ';' : '' ?>">
🏢 <?= htmlspecialchars($deptName) ?>
</span>
<?php else: ?>
<?php endif; ?>
</div>
</div>
<div class="profile-summary-card">
<div class="profile-summary-label">Mansioni / Sottomansioni</div>
<?php if (!empty($employeeSubRolesByRole)): ?>
<div class="profile-role-stack">
<?php foreach ($employeeSubRolesByRole as $roleGroup): ?>
<div class="profile-role-group">
<span class="profile-role-main">💼 <?= htmlspecialchars($roleGroup['job_role_name']) ?></span>
<?php foreach ($roleGroup['items'] as $sr): ?>
<span class="profile-subrole-chip">🧩 <?= htmlspecialchars($sr['job_sub_role_name']) ?></span>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="profile-summary-value"></div>
<div class="profile-summary-muted">Nessuna sottomansione associata</div>
<?php endif; ?>
</div>
<div class="profile-summary-card">
<div class="profile-summary-label">Ruolo accesso</div>
<div class="profile-summary-value">
<?php if (!empty($employee['auth_role_display_name']) || !empty($employee['auth_role_name'])): ?>
🔐 <?= htmlspecialchars($employee['auth_role_display_name'] ?: $employee['auth_role_name']) ?>
<?php else: ?>
<?php endif; ?>
</div>
<?php if (!empty($employee['auth_username'])): ?>
<div class="profile-summary-muted"><?= htmlspecialchars($employee['auth_username']) ?></div>
<?php endif; ?>
</div>
<div class="profile-summary-card">
<div class="profile-summary-label">Stato</div>
<div class="profile-summary-value">
<span class="pill pill-status-<?= htmlspecialchars($status['class']) ?>">
<?= htmlspecialchars($status['label']) ?>
</span>
</div>
</div>
</div>
</div>
<?php if ($canEdit): ?>
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#editPersonalModal">
✏️ Modifica
@@ -853,9 +1166,26 @@ function fmtFileSize(?int $bytes): string
<div class="info-label">Reparto</div>
<div class="info-value"><?= valOrDash($deptName) ?></div>
</div>
<div class="info-row">
<div class="info-label">Mansione</div>
<div class="info-value"><?= valOrDash($jobName) ?></div>
<div class="info-row" style="grid-column: 1 / -1;">
<div class="info-label">Mansioni / Sottomansioni</div>
<div class="info-value">
<?php if (!empty($employeeSubRolesByRole)): ?>
<div class="job-role-list">
<?php foreach ($employeeSubRolesByRole as $roleGroup): ?>
<div class="job-role-group">
<div class="job-role-group-title">💼 <?= htmlspecialchars($roleGroup['job_role_name']) ?></div>
<div class="job-subrole-chip-list">
<?php foreach ($roleGroup['items'] as $sr): ?>
<span class="job-subrole-chip">🧩 <?= htmlspecialchars($sr['job_sub_role_name']) ?></span>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<span class="text-muted">Nessuna mansione/sottomansione associata</span>
<?php endif; ?>
</div>
</div>
<div class="info-row">
<div class="info-label">Stato</div>
@@ -1001,6 +1331,54 @@ function fmtFileSize(?int $bytes): string
</div>
<?php endif; ?>
<?php if (!empty($requiredPpeList)): ?>
<div class="ppe-required-box">
<div class="ppe-required-title">
🦺 DPI richiesti dalle sottomansioni associate
<?php if (!empty($employeeSubRoleNames)): ?>
<span class="text-muted fw-normal"> calcolati su <?= count($employeeSubRoleNames) ?> sottomansion<?= count($employeeSubRoleNames) === 1 ? 'e' : 'i' ?></span>
<?php endif; ?>
</div>
<?php foreach ($requiredPpeList as $rp): ?>
<?php
$requiredPpeId = (int)$rp['id'];
$isAssigned = isset($assignedPpeIds[$requiredPpeId]);
?>
<div class="ppe-required-grid">
<div>
<div class="ppe-name-main"><?= htmlspecialchars($rp['name']) ?></div>
<div class="ppe-meta-small">
<?= !empty($rp['category']) ? htmlspecialchars($rp['category']) : 'Senza categoria' ?>
<?php if (!empty($rp['standard_reference'])): ?>
· <?= htmlspecialchars($rp['standard_reference']) ?>
<?php endif; ?>
<?php if (!empty($rp['source_sub_roles'])): ?>
<br><span class="text-primary">Da: <?= htmlspecialchars($rp['source_sub_roles']) ?></span>
<?php endif; ?>
</div>
</div>
<div>
<?php if ($isAssigned): ?>
<span class="ppe-status-assigned">Assegnato</span>
<?php else: ?>
<span class="ppe-status-missing">Mancante</span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php elseif (!empty($employeeSubRoleIds)): ?>
<div class="alert alert-light border">
Nessun DPI obbligatorio configurato per le sottomansioni associate al dipendente.
</div>
<?php else: ?>
<div class="alert alert-warning">
Nessuna sottomansione associata al dipendente: non è possibile suggerire DPI obbligatori.
</div>
<?php endif; ?>
<?php if (empty($ppeList)): ?>
<div class="placeholder-section">
<i class='bx bx-shield-quarter'></i>
@@ -1018,31 +1396,62 @@ function fmtFileSize(?int $bytes): string
<thead style="background-color:#cfe3ff;">
<tr>
<th>DPI</th>
<th>Categoria</th>
<th>Data Consegna</th>
<th>Scadenza</th>
<th>Consegnato da</th>
<th>Stato</th>
<th>Note</th>
<?php if ($canEdit): ?><th class="text-end">Azioni</th><?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($ppeList as $p): ?>
<?php $pid = (int)$p['id']; ?>
<?php
$pid = (int)$p['id'];
$ppeStatus = $p['status'] ?? 'assigned';
$statusClass = 'ppe-status-assigned';
$statusLabel = 'Assegnato';
if ($ppeStatus === 'returned') {
$statusClass = 'ppe-status-returned';
$statusLabel = 'Restituito';
} elseif ($ppeStatus === 'expired') {
$statusClass = 'ppe-status-problem';
$statusLabel = 'Scaduto';
} elseif ($ppeStatus === 'lost') {
$statusClass = 'ppe-status-problem';
$statusLabel = 'Perso';
} elseif ($ppeStatus === 'damaged') {
$statusClass = 'ppe-status-problem';
$statusLabel = 'Danneggiato';
}
?>
<tr>
<td class="fw-semibold"><?= htmlspecialchars($p['item_name']) ?></td>
<td><?= fmtDate($p['delivery_date']) ?></td>
<td><?= valOrDash($p['delivered_by']) ?></td>
<td class="fw-semibold"><?= htmlspecialchars($p['ppe_name']) ?></td>
<td><?= valOrDash($p['ppe_category']) ?></td>
<td><?= fmtDate($p['assigned_date']) ?></td>
<td><?= fmtDate($p['expiry_date']) ?></td>
<td><?= valOrDash($p['delivered_by'] ?? null) ?></td>
<td><span class="<?= $statusClass ?>"><?= $statusLabel ?></span></td>
<td><?= valOrDash($p['notes']) ?></td>
<?php if ($canEdit): ?>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary edit-ppe"
data-id="<?= $pid ?>"
data-item_name="<?= htmlspecialchars($p['item_name'], ENT_QUOTES) ?>"
data-delivery_date="<?= htmlspecialchars($p['delivery_date'] ?? '', ENT_QUOTES) ?>"
data-ppe_item_id="<?= (int)$p['ppe_item_id'] ?>"
data-assigned_date="<?= htmlspecialchars($p['assigned_date'] ?? '', ENT_QUOTES) ?>"
data-expiry_date="<?= htmlspecialchars($p['expiry_date'] ?? '', ENT_QUOTES) ?>"
data-delivered_by="<?= htmlspecialchars($p['delivered_by'] ?? '', ENT_QUOTES) ?>"
data-status="<?= htmlspecialchars($p['status'] ?? 'assigned', ENT_QUOTES) ?>"
data-notes="<?= htmlspecialchars($p['notes'] ?? '', ENT_QUOTES) ?>">✏️</button>
<?php if (($p['status'] ?? '') === 'assigned'): ?>
<button class="btn btn-sm btn-outline-danger delete-ppe"
data-id="<?= $pid ?>"
data-name="<?= htmlspecialchars($p['item_name'], ENT_QUOTES) ?>">🗑️</button>
data-name="<?= htmlspecialchars($p['ppe_name'], ENT_QUOTES) ?>">Rimuovi</button>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
@@ -1054,13 +1463,41 @@ function fmtFileSize(?int $bytes): string
<!-- MOBILE CARDS -->
<div class="d-block d-md-none">
<?php foreach ($ppeList as $p): ?>
<?php $pid = (int)$p['id']; ?>
<?php
$pid = (int)$p['id'];
$ppeStatus = $p['status'] ?? 'assigned';
$statusClass = 'ppe-status-assigned';
$statusLabel = 'Assegnato';
if ($ppeStatus === 'returned') {
$statusClass = 'ppe-status-returned';
$statusLabel = 'Restituito';
} elseif ($ppeStatus === 'expired') {
$statusClass = 'ppe-status-problem';
$statusLabel = 'Scaduto';
} elseif ($ppeStatus === 'lost') {
$statusClass = 'ppe-status-problem';
$statusLabel = 'Perso';
} elseif ($ppeStatus === 'damaged') {
$statusClass = 'ppe-status-problem';
$statusLabel = 'Danneggiato';
}
?>
<div class="doc-card">
<div class="d-flex justify-content-between align-items-start gap-2 mb-2">
<span class="doc-card-title">🦺 <?= htmlspecialchars($p['item_name']) ?></span>
<span class="small text-muted text-nowrap"><?= fmtDate($p['delivery_date']) ?></span>
<span class="doc-card-title">🦺 <?= htmlspecialchars($p['ppe_name']) ?></span>
<span class="<?= $statusClass ?>"><?= $statusLabel ?></span>
</div>
<div class="doc-card-meta">
<?php if (!empty($p['ppe_category'])): ?>
<span><b>Categoria:</b> <?= htmlspecialchars($p['ppe_category']) ?></span>
<?php endif; ?>
<span><b>Consegna:</b> <?= fmtDate($p['assigned_date']) ?></span>
<?php if (!empty($p['expiry_date'])): ?>
<span><b>Scadenza:</b> <?= fmtDate($p['expiry_date']) ?></span>
<?php endif; ?>
<?php if (!empty($p['delivered_by'])): ?>
<span><b>Consegnato da:</b> <?= htmlspecialchars($p['delivered_by']) ?></span>
<?php endif; ?>
@@ -1068,17 +1505,23 @@ function fmtFileSize(?int $bytes): string
<span><b>Note:</b> <?= htmlspecialchars($p['notes']) ?></span>
<?php endif; ?>
</div>
<?php if ($canEdit): ?>
<div class="doc-card-actions">
<button class="btn btn-sm btn-outline-secondary edit-ppe"
data-id="<?= $pid ?>"
data-item_name="<?= htmlspecialchars($p['item_name'], ENT_QUOTES) ?>"
data-delivery_date="<?= htmlspecialchars($p['delivery_date'] ?? '', ENT_QUOTES) ?>"
data-ppe_item_id="<?= (int)$p['ppe_item_id'] ?>"
data-assigned_date="<?= htmlspecialchars($p['assigned_date'] ?? '', ENT_QUOTES) ?>"
data-expiry_date="<?= htmlspecialchars($p['expiry_date'] ?? '', ENT_QUOTES) ?>"
data-delivered_by="<?= htmlspecialchars($p['delivered_by'] ?? '', ENT_QUOTES) ?>"
data-status="<?= htmlspecialchars($p['status'] ?? 'assigned', ENT_QUOTES) ?>"
data-notes="<?= htmlspecialchars($p['notes'] ?? '', ENT_QUOTES) ?>">✏️ Modifica</button>
<?php if (($p['status'] ?? '') === 'assigned'): ?>
<button class="btn btn-sm btn-outline-danger delete-ppe"
data-id="<?= $pid ?>"
data-name="<?= htmlspecialchars($p['item_name'], ENT_QUOTES) ?>">🗑️</button>
data-name="<?= htmlspecialchars($p['ppe_name'], ENT_QUOTES) ?>">Rimuovi</button>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
@@ -1363,6 +1806,80 @@ function fmtFileSize(?int $bytes): string
padding: 2px 8px;
border-radius: 4px;
}
.ppe-required-box {
border: 1px solid #bfdbfe;
background: #eff6ff;
border-radius: 14px;
padding: 14px;
margin-bottom: 18px;
}
.ppe-required-title {
font-weight: 700;
color: #1e3a8a;
margin-bottom: 10px;
}
.ppe-required-grid {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
align-items: center;
border-top: 1px solid #dbeafe;
padding-top: 8px;
margin-top: 8px;
}
.ppe-name-main {
font-weight: 700;
color: #1f2937;
}
.ppe-meta-small {
color: #64748b;
font-size: 0.85rem;
}
.ppe-status-assigned {
background: #dcfce7;
color: #166534;
border: 1px solid #bbf7d0;
border-radius: 999px;
padding: 4px 10px;
font-weight: 700;
font-size: 0.82rem;
}
.ppe-status-missing {
background: #fee2e2;
color: #991b1b;
border: 1px solid #fecaca;
border-radius: 999px;
padding: 4px 10px;
font-weight: 700;
font-size: 0.82rem;
}
.ppe-status-returned {
background: #e5e7eb;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 999px;
padding: 4px 10px;
font-weight: 700;
font-size: 0.82rem;
}
.ppe-status-problem {
background: #fef3c7;
color: #92400e;
border: 1px solid #fde68a;
border-radius: 999px;
padding: 4px 10px;
font-weight: 700;
font-size: 0.82rem;
}
</style>
<!-- TRAINING ATTACHMENTS MODAL (visible to everyone with profile access) -->
@@ -1537,28 +2054,60 @@ function fmtFileSize(?int $bytes): string
<h5 class="modal-title" id="ppeModalTitle">Aggiungi DPI</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="ppeForm">
<input type="hidden" id="ppeId">
<input type="hidden" name="employee_id" id="ppeEmployeeId" value="<?= (int)$employee['id'] ?>">
<div class="mb-3">
<label class="form-label fw-semibold">DPI *</label>
<input type="text" class="form-control" id="ppeItemName" placeholder="es. Casco, Guanti, Scarpe antinfortunistiche" required>
<select class="form-select" id="ppeItemId" required>
<option value=""> Seleziona DPI </option>
<?php foreach ($ppeItemsAll as $item): ?>
<option value="<?= (int)$item['id'] ?>"
data-validity_months="<?= $item['validity_months'] !== null ? (int)$item['validity_months'] : '' ?>">
<?= htmlspecialchars($item['name']) ?>
<?= !empty($item['category']) ? ' — ' . htmlspecialchars($item['category']) : '' ?>
<?= !empty($item['standard_reference']) ? ' — ' . htmlspecialchars($item['standard_reference']) : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Data Consegna</label>
<input type="date" class="form-control" id="ppeDeliveryDate">
<input type="date" class="form-control" id="ppeAssignedDate" value="<?= date('Y-m-d') ?>">
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Data Scadenza</label>
<input type="date" class="form-control" id="ppeExpiryDate">
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Consegnato da</label>
<input type="text" class="form-control" id="ppeDeliveredBy" placeholder="Nome o azienda">
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Stato</label>
<select class="form-select" id="ppeStatus">
<option value="assigned">Assegnato</option>
<option value="returned">Restituito</option>
<option value="expired">Scaduto</option>
<option value="lost">Perso</option>
<option value="damaged">Danneggiato</option>
</select>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Note</label>
<textarea class="form-control" id="ppeNotes" rows="2" placeholder="Opzionale"></textarea>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
@@ -1631,15 +2180,29 @@ function fmtFileSize(?int $bytes): string
</select>
</div>
<div class="col-12 col-md-6 mb-3">
<label class="form-label fw-semibold">Mansione</label>
<select class="form-select" id="editJobRoleId">
<option value=""> Nessuna </option>
<?php foreach ($jobRoles as $r): ?>
<option value="<?= (int)$r['id'] ?>" <?= ((int)($employee['job_role_id'] ?? 0) === (int)$r['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($r['name']) ?>
<label class="form-label fw-semibold">Sottomansioni</label>
<select class="form-select" id="editJobSubRoleIds" multiple style="width:100%;">
<?php
$currentGroup = null;
foreach ($jobSubRolesAll as $sr):
$groupName = $sr['job_role_name'] ?: 'Senza mansione';
if ($currentGroup !== $groupName):
if ($currentGroup !== null): ?>
</optgroup>
<?php endif; ?>
<optgroup label="<?= htmlspecialchars($groupName, ENT_QUOTES, 'UTF-8') ?>">
<?php $currentGroup = $groupName;
endif;
?>
<option value="<?= (int)$sr['id'] ?>" <?= in_array((int)$sr['id'], $employeeSubRoleIds, true) ? 'selected' : '' ?>>
<?= htmlspecialchars($sr['name']) ?>
</option>
<?php endforeach; ?>
<?php if ($currentGroup !== null): ?>
</optgroup>
<?php endif; ?>
</select>
<small class="text-muted">Puoi selezionare più sottomansioni anche appartenenti a mansioni diverse.</small>
</div>
</div>
@@ -1698,6 +2261,7 @@ function fmtFileSize(?int $bytes): string
<?php include('jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.8/js/dataTables.bootstrap5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.full.min.js"></script>
<script>
$(document).ready(function() {
@@ -2111,7 +2675,19 @@ function fmtFileSize(?int $bytes): string
<?php if ($employee && $canEdit): ?>
<script>
const jobSubRoleToRoleMap = <?= json_encode($jobSubRoleToRoleMap, JSON_UNESCAPED_UNICODE) ?>;
$(document).ready(function() {
if ($('#editJobSubRoleIds').length) {
$('#editJobSubRoleIds').select2({
theme: 'bootstrap-5',
dropdownParent: $('#editPersonalModal'),
placeholder: 'Seleziona una o più sottomansioni...',
closeOnSelect: false,
width: '100%'
});
}
// ---- UPLOAD DOCUMENT ----
$("#uploadDocumentForm").on("submit", function(e) {
e.preventDefault();
@@ -2315,35 +2891,83 @@ function fmtFileSize(?int $bytes): string
});
});
// ---- PPE: open modal (add or edit) ----
// ---- PPE: Select2 ----
if ($('#ppeItemId').length) {
$('#ppeItemId').select2({
theme: 'bootstrap-5',
dropdownParent: $('#ppeModal'),
placeholder: 'Cerca DPI...',
width: '100%',
allowClear: true
});
}
function addMonthsToDate(dateString, months) {
if (!dateString || !months) return '';
const date = new Date(dateString + 'T00:00:00');
if (isNaN(date.getTime())) return '';
date.setMonth(date.getMonth() + parseInt(months, 10));
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
$('#ppeItemId, #ppeAssignedDate').on('change', function() {
const validityMonths = $('#ppeItemId option:selected').data('validity_months');
const assignedDate = $('#ppeAssignedDate').val();
if (validityMonths && assignedDate && !$('#ppeExpiryDate').val()) {
$('#ppeExpiryDate').val(addMonthsToDate(assignedDate, validityMonths));
}
});
// ---- PPE: open modal add ----
window.openPpeModal = function() {
$("#ppeId").val('');
$("#ppeItemName").val('');
$("#ppeDeliveryDate").val('');
$("#ppeItemId").val('').trigger('change');
$("#ppeAssignedDate").val('<?= date('Y-m-d') ?>');
$("#ppeExpiryDate").val('');
$("#ppeDeliveredBy").val('');
$("#ppeStatus").val('assigned');
$("#ppeNotes").val('');
$("#ppeModalTitle").text('Aggiungi DPI');
};
// ---- PPE: open modal edit ----
$(document).on("click", ".edit-ppe", function() {
const b = $(this);
$("#ppeId").val(b.data("id"));
$("#ppeItemName").val(b.data("item_name"));
$("#ppeDeliveryDate").val(b.data("delivery_date"));
$("#ppeItemId").val(String(b.data("ppe_item_id"))).trigger('change');
$("#ppeAssignedDate").val(b.data("assigned_date"));
$("#ppeExpiryDate").val(b.data("expiry_date"));
$("#ppeDeliveredBy").val(b.data("delivered_by"));
$("#ppeStatus").val(b.data("status") || 'assigned');
$("#ppeNotes").val(b.data("notes"));
$("#ppeModalTitle").text('Modifica DPI');
$("#ppeModal").modal("show");
});
// ---- PPE: save ----
$("#ppeForm").on("submit", function(e) {
e.preventDefault();
const p = new URLSearchParams();
p.append('id', $("#ppeId").val());
p.append('employee_id', $("#ppeEmployeeId").val());
p.append('item_name', $("#ppeItemName").val().trim());
p.append('delivery_date', $("#ppeDeliveryDate").val());
p.append('ppe_item_id', $("#ppeItemId").val());
p.append('assigned_date', $("#ppeAssignedDate").val());
p.append('expiry_date', $("#ppeExpiryDate").val());
p.append('delivered_by', $("#ppeDeliveredBy").val().trim());
p.append('status', $("#ppeStatus").val());
p.append('notes', $("#ppeNotes").val().trim());
fetch("ajax/employee_profile/save_ppe.php", {
method: "POST",
headers: {
@@ -2378,22 +3002,26 @@ function fmtFileSize(?int $bytes): string
});
});
// ---- PPE: remove / mark as returned ----
$(document).on("click", ".delete-ppe", function() {
const id = $(this).data("id");
const name = $(this).data("name");
Swal.fire({
title: "Confermi la cancellazione?",
text: name ? ("DPI: " + name) : "Il DPI verrà cancellato.",
title: "Confermi la rimozione?",
text: name ? ("DPI: " + name) : "Il DPI verrà segnato come restituito.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#6c757d",
confirmButtonText: "Sì, cancella",
confirmButtonText: "Sì, rimuovi",
cancelButtonText: "Annulla"
}).then((result) => {
if (!result.isConfirmed) return;
const p = new URLSearchParams();
p.append('id', id);
fetch("ajax/employee_profile/delete_ppe.php", {
method: "POST",
headers: {
@@ -2406,7 +3034,7 @@ function fmtFileSize(?int $bytes): string
if (data.success) {
Swal.fire({
icon: "success",
title: "Cancellato!",
title: "Rimosso!",
confirmButtonColor: "#3085d6"
})
.then(() => location.reload());
@@ -2414,7 +3042,7 @@ function fmtFileSize(?int $bytes): string
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Impossibile cancellare."
text: data.message || "Impossibile rimuovere."
});
}
})
@@ -2510,7 +3138,19 @@ function fmtFileSize(?int $bytes): string
p.append('phone', $("#editPhone").val().trim());
p.append('email', $("#editEmail").val().trim());
p.append('department_id', $("#editDepartmentId").val());
p.append('job_role_id', $("#editJobRoleId").val());
const selectedSubRoles = $("#editJobSubRoleIds").val() || [];
selectedSubRoles.forEach(function(subRoleId) {
p.append('job_sub_role_ids[]', subRoleId);
});
// Backward compatibility for ajax/employee_profile/save_personal.php:
// keep sending a legacy primary job_role_id/job_sub_role_id based on the first selected sub role.
const primarySubRoleId = selectedSubRoles.length ? selectedSubRoles[0] : '';
const primaryJobRoleId = primarySubRoleId && jobSubRoleToRoleMap[primarySubRoleId] ? jobSubRoleToRoleMap[primarySubRoleId] : '';
p.append('job_role_id', primaryJobRoleId);
p.append('job_sub_role_id', primarySubRoleId);
p.append('status', $("#editStatus").val());
p.append('auth_user_id', $("#editAuthUserId").val());
p.append('role_id', $("#editAuthUserId").val() ? ($("#editRoleId").val() || '') : '');
File diff suppressed because it is too large Load Diff
+2 -14
View File
@@ -307,25 +307,13 @@
</li>
<?php endif; ?>
<?php if (userCan('hr.training_topics.view')) : ?>
<li>
<a href="training_topics.php">
<i class='bx bx-radio-circle'></i>Corsi di Formazione
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.trainings.view')) : ?>
<li>
<a href="trainings.php">
<i class='bx bx-radio-circle'></i>Storico Formazione
</a>
</li>
<li>
<a href="training_calendar.php">
<i class='bx bx-radio-circle'></i>Calendario Formazione
<i class='bx bx-radio-circle'></i>Gestione Formazione
</a>
</li>
<?php endif; ?>
<?php if (userCan('hr.skills.view')) : ?>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13 -6
View File
@@ -155,12 +155,19 @@ $dashboardSections = [
'open' => false,
'buttons' => [
[
'label' => 'Employees',
'label' => 'Dipendenti',
'icon' => '👥',
'class' => 'btn-employees',
'url' => 'employees.php',
'permission' => 'hr.employees.view',
],
[
'label' => 'Mansioni',
'icon' => '🧩',
'class' => 'btn-setup',
'url' => 'job-roles.php',
'permission' => 'hr.employees.view',
],
[
'label' => 'Departments',
'icon' => '🏢',
@@ -169,14 +176,14 @@ $dashboardSections = [
'permission' => 'hr.departments.view',
],
[
'label' => 'Corsi Formazione',
'icon' => '📚',
'label' => 'DPI',
'icon' => '🦺',
'class' => 'btn-setup',
'url' => 'training_topics.php',
'permission' => 'hr.training_topics.view',
'url' => 'ppe-items.php',
'permission' => 'hr.employees.view',
],
[
'label' => 'Storico Formazione',
'label' => 'Gestione Formazione',
'icon' => '🎓',
'class' => 'btn-setup',
'url' => 'trainings.php',
@@ -0,0 +1,154 @@
<?php
include('../../include/headscript.php');
header('Content-Type: application/json; charset=utf-8');
$pdo = DBHandlerSelect::getInstance()->getConnection();
function jsonResponse(array $data): void
{
echo json_encode($data);
exit;
}
function normalizeNullableInt($value): ?int
{
return (isset($value) && $value !== '') ? (int)$value : null;
}
try {
$isHrManager = Auth::user()->hasRole('Admin')
|| Auth::user()->hasRole('Superuser')
|| Auth::user()->hasRole('employee-hr')
|| Auth::user()->hasRole('manager');
if (!$isHrManager) {
jsonResponse(['success' => false, 'message' => 'Non autorizzato.']);
}
$employeeId = (int)($_POST['employee_id'] ?? 0);
$firstName = trim($_POST['first_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? '');
$employeeCode = trim($_POST['employee_code'] ?? '');
$hireDate = trim($_POST['hire_date'] ?? '');
$address = trim($_POST['address'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$email = trim($_POST['email'] ?? '');
$departmentId = normalizeNullableInt($_POST['department_id'] ?? '');
$status = trim($_POST['status'] ?? 'active');
$authUserId = normalizeNullableInt($_POST['auth_user_id'] ?? '');
$roleId = normalizeNullableInt($_POST['role_id'] ?? '');
$jobSubRoleIds = $_POST['job_sub_role_ids'] ?? [];
if (!is_array($jobSubRoleIds)) {
$jobSubRoleIds = [$jobSubRoleIds];
}
$jobSubRoleIds = array_values(array_unique(array_filter(array_map('intval', $jobSubRoleIds))));
if ($employeeId <= 0) {
jsonResponse(['success' => false, 'message' => 'ID dipendente non valido.']);
}
if ($firstName === '' || $lastName === '') {
jsonResponse(['success' => false, 'message' => 'Nome e cognome sono obbligatori.']);
}
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
}
if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
$status = 'active';
}
$stmtEmployee = $pdo->prepare('SELECT id FROM employees WHERE id = ? LIMIT 1');
$stmtEmployee->execute([$employeeId]);
if (!$stmtEmployee->fetchColumn()) {
jsonResponse(['success' => false, 'message' => 'Dipendente non trovato.']);
}
$primaryJobRoleId = null;
$primaryJobSubRoleId = null;
if ($jobSubRoleIds) {
$placeholders = implode(',', array_fill(0, count($jobSubRoleIds), '?'));
$stmtSubRoles = $pdo->prepare("\n SELECT id, job_role_id\n FROM job_sub_roles\n WHERE id IN ($placeholders)\n AND is_active = 1\n ");
$stmtSubRoles->execute($jobSubRoleIds);
$validRows = $stmtSubRoles->fetchAll(PDO::FETCH_ASSOC);
$validMap = [];
foreach ($validRows as $row) {
$validMap[(int)$row['id']] = (int)$row['job_role_id'];
}
$jobSubRoleIds = array_values(array_filter($jobSubRoleIds, static function ($id) use ($validMap) {
return isset($validMap[(int)$id]);
}));
if ($jobSubRoleIds) {
$primaryJobSubRoleId = (int)$jobSubRoleIds[0];
$primaryJobRoleId = $validMap[$primaryJobSubRoleId] ?? null;
}
}
$pdo->beginTransaction();
$stmt = $pdo->prepare("\n UPDATE employees\n SET first_name = :first_name,\n last_name = :last_name,\n employee_code = :employee_code,\n hire_date = :hire_date,\n address = :address,\n phone = :phone,\n email = :email,\n department_id = :department_id,\n job_role_id = :job_role_id,\n job_sub_role_id = :job_sub_role_id,\n status = :status,\n auth_user_id = :auth_user_id,\n updated_at = NOW()\n WHERE id = :employee_id\n ");
$stmt->execute([
'first_name' => $firstName,
'last_name' => $lastName,
'employee_code' => $employeeCode !== '' ? $employeeCode : null,
'hire_date' => $hireDate !== '' ? $hireDate : null,
'address' => $address !== '' ? $address : null,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'department_id' => $departmentId,
'job_role_id' => $primaryJobRoleId,
'job_sub_role_id' => $primaryJobSubRoleId,
'status' => $status,
'auth_user_id' => $authUserId,
'employee_id' => $employeeId,
]);
$stmtDelete = $pdo->prepare('DELETE FROM employee_job_sub_roles WHERE employee_id = ?');
$stmtDelete->execute([$employeeId]);
if ($jobSubRoleIds) {
$stmtInsert = $pdo->prepare("\n INSERT INTO employee_job_sub_roles\n (employee_id, job_sub_role_id, is_primary, created_at)\n VALUES\n (:employee_id, :job_sub_role_id, :is_primary, NOW())\n ");
foreach ($jobSubRoleIds as $index => $jobSubRoleId) {
$stmtInsert->execute([
'employee_id' => $employeeId,
'job_sub_role_id' => (int)$jobSubRoleId,
'is_primary' => $index === 0 ? 1 : 0,
]);
}
}
if ($authUserId !== null && $roleId !== null) {
$checkRole = $pdo->prepare('SELECT COUNT(*) FROM auth_roles WHERE id = ?');
$checkRole->execute([$roleId]);
if ((int)$checkRole->fetchColumn() > 0) {
$stmtRole = $pdo->prepare('UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :auth_user_id');
$stmtRole->execute([
'role_id' => $roleId,
'auth_user_id' => $authUserId,
]);
}
}
$pdo->commit();
jsonResponse(['success' => true]);
} catch (Throwable $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
jsonResponse([
'success' => false,
'message' => $e->getMessage(),
]);
}
@@ -96,7 +96,7 @@
</div>
<!-- Group 3: Responsabili -->
<div class="form-section-title">Responsabili</div>
<div class="form-section-title">Esecutore</div>
<div class="row g-3 mb-4">
<div class="col-12">
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
+1 -1
View File
@@ -967,7 +967,7 @@ function getContrastTextColor($hexColor)
<th>Scadenza</th>
<th class="d-none d-lg-table-cell">Verifica</th>
<th>Funzione</th>
<th>Responsabili</th>
<th>Esecutore</th>
<th>Stato</th>
<th class="text-center" style="width:120px">Azioni</th>
</tr>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB