From b1f2bb60e329b118287c264763c63f39d72512d7 Mon Sep 17 00:00:00 2001 From: solocla Date: Thu, 4 Jun 2026 12:17:17 +0200 Subject: [PATCH] added subroles and dpi association fixed all pages and migration --- ...60603183520_create_job_sub_roles_table.php | 74 + .../20260604072729_create_ppe_items_table.php | 84 + ...072747_create_employee_ppe_items_table.php | 102 + ...05_create_job_sub_role_ppe_items_table.php | 101 + ...add_job_sub_role_id_to_employees_table.php | 35 + ...ery_fields_to_employee_ppe_items_table.php | 41 + ...28_create_employee_job_sub_roles_table.php | 96 + .../ajax/employee_profile/delete_ppe.php | 54 +- .../ajax/employee_profile/save_ppe.php | 199 +- public/userarea/employee-profile.php | 772 ++++++- public/userarea/employees.php | 1860 +++++++++++++---- public/userarea/include/navbar.php | 18 +- public/userarea/job-roles.php | 1697 +++++++++++++++ public/userarea/ppe-items.php | 1449 +++++++++++++ public/userarea/production_dashboard.php | 19 +- .../save_personal_multi_sottomansioni.php | 154 ++ .../scadenzario/include/deadline_modal.php | 4 +- public/userarea/scadenzario/index.php | 4 +- .../ppe/ppe_20260604_073458_3c4f44d9ccc4.jpg | Bin 0 -> 6129 bytes .../ppe/ppe_20260604_073605_8c69896caf19.jpg | Bin 0 -> 48513 bytes 20 files changed, 6125 insertions(+), 638 deletions(-) create mode 100644 db/migrations/20260603183520_create_job_sub_roles_table.php create mode 100644 db/migrations/20260604072729_create_ppe_items_table.php create mode 100644 db/migrations/20260604072747_create_employee_ppe_items_table.php create mode 100644 db/migrations/20260604072805_create_job_sub_role_ppe_items_table.php create mode 100644 db/migrations/20260604091640_add_job_sub_role_id_to_employees_table.php create mode 100644 db/migrations/20260604094405_add_delivery_fields_to_employee_ppe_items_table.php create mode 100644 db/migrations/20260604100828_create_employee_job_sub_roles_table.php create mode 100644 public/userarea/job-roles.php create mode 100644 public/userarea/ppe-items.php create mode 100644 public/userarea/save_personal_multi_sottomansioni.php create mode 100644 public/userarea/uploads/ppe/ppe_20260604_073458_3c4f44d9ccc4.jpg create mode 100644 public/userarea/uploads/ppe/ppe_20260604_073605_8c69896caf19.jpg diff --git a/db/migrations/20260603183520_create_job_sub_roles_table.php b/db/migrations/20260603183520_create_job_sub_roles_table.php new file mode 100644 index 0000000..c0c5bcc --- /dev/null +++ b/db/migrations/20260603183520_create_job_sub_roles_table.php @@ -0,0 +1,74 @@ +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(); + } +} diff --git a/db/migrations/20260604072729_create_ppe_items_table.php b/db/migrations/20260604072729_create_ppe_items_table.php new file mode 100644 index 0000000..c1ac268 --- /dev/null +++ b/db/migrations/20260604072729_create_ppe_items_table.php @@ -0,0 +1,84 @@ +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(); + } +} diff --git a/db/migrations/20260604072747_create_employee_ppe_items_table.php b/db/migrations/20260604072747_create_employee_ppe_items_table.php new file mode 100644 index 0000000..c8b2c7f --- /dev/null +++ b/db/migrations/20260604072747_create_employee_ppe_items_table.php @@ -0,0 +1,102 @@ +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(); + } +} diff --git a/db/migrations/20260604072805_create_job_sub_role_ppe_items_table.php b/db/migrations/20260604072805_create_job_sub_role_ppe_items_table.php new file mode 100644 index 0000000..e796d6a --- /dev/null +++ b/db/migrations/20260604072805_create_job_sub_role_ppe_items_table.php @@ -0,0 +1,101 @@ +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(); + } +} diff --git a/db/migrations/20260604091640_add_job_sub_role_id_to_employees_table.php b/db/migrations/20260604091640_add_job_sub_role_id_to_employees_table.php new file mode 100644 index 0000000..1797833 --- /dev/null +++ b/db/migrations/20260604091640_add_job_sub_role_id_to_employees_table.php @@ -0,0 +1,35 @@ +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(); + } +} diff --git a/db/migrations/20260604094405_add_delivery_fields_to_employee_ppe_items_table.php b/db/migrations/20260604094405_add_delivery_fields_to_employee_ppe_items_table.php new file mode 100644 index 0000000..69507b2 --- /dev/null +++ b/db/migrations/20260604094405_add_delivery_fields_to_employee_ppe_items_table.php @@ -0,0 +1,41 @@ +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(); + } +} diff --git a/db/migrations/20260604100828_create_employee_job_sub_roles_table.php b/db/migrations/20260604100828_create_employee_job_sub_roles_table.php new file mode 100644 index 0000000..cad6f1f --- /dev/null +++ b/db/migrations/20260604100828_create_employee_job_sub_roles_table.php @@ -0,0 +1,96 @@ +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(); + } + } +} diff --git a/public/userarea/ajax/employee_profile/delete_ppe.php b/public/userarea/ajax/employee_profile/delete_ppe.php index b4d0762..f3af814 100644 --- a/public/userarea/ajax/employee_profile/delete_ppe.php +++ b/public/userarea/ajax/employee_profile/delete_ppe.php @@ -1,26 +1,38 @@ 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; } diff --git a/public/userarea/ajax/employee_profile/save_ppe.php b/public/userarea/ajax/employee_profile/save_ppe.php index 1dfb320..061d370 100644 --- a/public/userarea/ajax/employee_profile/save_ppe.php +++ b/public/userarea/ajax/employee_profile/save_ppe.php @@ -1,82 +1,153 @@ 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) { - $stmt = $pdo->prepare(" - UPDATE employee_ppe - SET item_name = :item_name, - delivery_date = :delivery_date, - delivered_by = :delivered_by, - notes = :notes, - updated_at = NOW() - WHERE id = :id AND employee_id = :eid - "); - $stmt->execute([ - 'item_name' => $itemName, - 'delivery_date' => $deliveryDate, - 'delivered_by' => $deliveredBy, - 'notes' => $notes, - 'id' => $id, - 'eid' => $employeeId, + $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.' ]); - 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.']); + 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_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 = :employee_id + "); + + $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, + 'employee_id' => $employeeId, + '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; } diff --git a/public/userarea/employee-profile.php b/public/userarea/employee-profile.php index 3727328..f980a43 100644 --- a/public/userarea/employee-profile.php +++ b/public/userarea/employee-profile.php @@ -40,15 +40,20 @@ if ($employeeId > 0) { d.name AS department_name, d.color AS department_color, jr.name AS job_role_name, - au.first_name AS auth_first_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_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 + + Profilo Dipendente - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> @@ -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,18 +1016,61 @@ function fmtFileSize(?int $bytes): string Codice: -
- - ๐Ÿ’ผ - - - - ๐Ÿข - - - - - +
+
+
Reparto
+
+ + + ๐Ÿข + + + โ€” + +
+
+ +
+
Mansioni / Sottomansioni
+ +
+ +
+ ๐Ÿ’ผ + + ๐Ÿงฉ + +
+ +
+ +
โ€”
+
Nessuna sottomansione associata
+ +
+ +
+
Ruolo accesso
+
+ + ๐Ÿ” + + โ€” + +
+ +
+ +
+ +
+
Stato
+
+ + + +
+
@@ -853,9 +1166,26 @@ function fmtFileSize(?int $bytes): string
Reparto
-
-
Mansione
-
+
+
Mansioni / Sottomansioni
+
+ +
+ +
+
๐Ÿ’ผ
+
+ + ๐Ÿงฉ + +
+
+ +
+ + Nessuna mansione/sottomansione associata + +
Stato
@@ -1001,6 +1331,54 @@ function fmtFileSize(?int $bytes): string
+ +
+
+ ๐Ÿฆบ DPI richiesti dalle sottomansioni associate + + โ€” calcolati su sottomansion + +
+ + + +
+
+
+
+ + + ยท + + +
Da: + +
+
+ +
+ + Assegnato + + Mancante + +
+
+ +
+ +
+ Nessun DPI obbligatorio configurato per le sottomansioni associate al dipendente. +
+ +
+ Nessuna sottomansione associata al dipendente: non รจ possibile suggerire DPI obbligatori. +
+ +
@@ -1018,31 +1396,62 @@ function fmtFileSize(?int $bytes): string DPI + Categoria Data Consegna + Scadenza Consegnato da + Stato Note Azioni - + - - - + + + + + + - + + + + @@ -1054,13 +1463,41 @@ function fmtFileSize(?int $bytes): string
- +
- ๐Ÿฆบ - + ๐Ÿฆบ +
+
+ + Categoria: + + Consegna: + + Scadenza: + Consegnato da: @@ -1068,17 +1505,23 @@ function fmtFileSize(?int $bytes): string Note:
+
- + + + +
@@ -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; + } @@ -1537,28 +2054,60 @@ function fmtFileSize(?int $bytes): string
+
- - + + + + + + + + + + + Puoi selezionare piรน sottomansioni anche appartenenti a mansioni diverse.
@@ -1698,6 +2261,7 @@ function fmtFileSize(?int $bytes): string + - - @@ -445,6 +941,46 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC); font-size: 0.8rem; font-weight: 600; } + + .job-subrole-text { + color: #64748b; + font-size: 0.86rem; + } + + .ppe-required-card { + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 12px; + margin-bottom: 10px; + background: #ffffff; + } + + .ppe-required-card.missing { + border-color: #fecaca; + background: #fff7f7; + } + + .ppe-required-card.assigned { + border-color: #bbf7d0; + background: #f0fdf4; + } + + .ppe-status-pill { + border-radius: 999px; + padding: 4px 10px; + font-size: 0.78rem; + font-weight: 700; + } + + .ppe-status-pill.missing { + background: #fee2e2; + color: #991b1b; + } + + .ppe-status-pill.assigned { + background: #dcfce7; + color: #166534; + } @@ -479,125 +1015,128 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
-
- - + - - - - + - - - - + + + - - - - - + - - + + + + - - - + + + + + + -
ID Codice Nome Contatti RepartoMansioneMansioni / Sottomansioni Data Assunzione StatoUtente collegato Azioni
- - + } + ?> +
+ + + + + + + โœ‰๏ธ +
+ + + + ๐Ÿ“ž -
- - - โœ‰๏ธ -
- - - - ๐Ÿ“ž - - - - -
- - - - - - - - - - - + + - + + + + - - + + - + + +
+
+ +
+
+ + + + + - + - -
- @@ -656,28 +1195,42 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC); -
- - -
- - -
+
+ +
+ + + Puoi selezionare piรน sottomansioni anche appartenenti a mansioni diverse. +
+ +
- + @@ -718,7 +1271,6 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
- @@ -775,28 +1327,42 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC); -
- - -
- - -
+
+ +
+ + + Puoi selezionare piรน sottomansioni anche appartenenti a mansioni diverse. +
+ +
- + @@ -837,7 +1403,6 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
- @@ -864,7 +1429,96 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC); - + + + +