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: = htmlspecialchars($employee['employee_code']) ?>
-
-
-
💼 = htmlspecialchars($jobName) ?>
-
-
-
- 🏢 = htmlspecialchars($deptName) ?>
-
-
-
- = htmlspecialchars($status['label']) ?>
-
+
+
+
Reparto
+
+
+
+ 🏢 = htmlspecialchars($deptName) ?>
+
+
+ —
+
+
+
+
+
+
Mansioni / Sottomansioni
+
+
+
+
+ 💼 = htmlspecialchars($roleGroup['job_role_name']) ?>
+
+ 🧩 = htmlspecialchars($sr['job_sub_role_name']) ?>
+
+
+
+
+
+
—
+
Nessuna sottomansione associata
+
+
+
+
+
Ruolo accesso
+
+
+ 🔐 = htmlspecialchars($employee['auth_role_display_name'] ?: $employee['auth_role_name']) ?>
+
+ —
+
+
+
+
= htmlspecialchars($employee['auth_username']) ?>
+
+
+
+
+
Stato
+
+
+ = htmlspecialchars($status['label']) ?>
+
+
+
@@ -853,9 +1166,26 @@ function fmtFileSize(?int $bytes): string
Reparto
= valOrDash($deptName) ?>
-
-
Mansione
-
= valOrDash($jobName) ?>
+
+
Mansioni / Sottomansioni
+
+
+
+
+
+
💼 = htmlspecialchars($roleGroup['job_role_name']) ?>
+
+
+ 🧩 = htmlspecialchars($sr['job_sub_role_name']) ?>
+
+
+
+
+
+
+
Nessuna mansione/sottomansione associata
+
+
Stato
@@ -1001,6 +1331,54 @@ function fmtFileSize(?int $bytes): string
+
+
+
+ 🦺 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.
+
+
+
@@ -1018,31 +1396,62 @@ function fmtFileSize(?int $bytes): string
| DPI |
+ Categoria |
Data Consegna |
+ Scadenza |
Consegnato da |
+ Stato |
Note |
Azioni |
-
+
- | = htmlspecialchars($p['item_name']) ?> |
- = fmtDate($p['delivery_date']) ?> |
- = valOrDash($p['delivered_by']) ?> |
+ = 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']) ?> |
-
+
+
+
+
|
@@ -1054,13 +1463,41 @@ function fmtFileSize(?int $bytes): string
-
+
- 🦺 = htmlspecialchars($p['item_name']) ?>
- = fmtDate($p['delivery_date']) ?>
+ 🦺 = 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']) ?>
@@ -1068,17 +1505,23 @@ function fmtFileSize(?int $bytes): string
Note: = htmlspecialchars($p['notes']) ?>
+
-
+
+
+
+
@@ -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
Aggiungi DPI
+
-
-
@@ -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);
-
-
@@ -656,28 +1195,42 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Puoi selezionare più sottomansioni anche appartenenti a mansioni diverse.
+
+
+
@@ -704,7 +1257,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
-
+
@@ -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.
+
+
+
@@ -823,7 +1389,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
-
+
@@ -837,7 +1403,6 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
-
@@ -864,7 +1429,96 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
-
+
+
+
+
@@ -879,7 +1533,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
-
+
+
+
+
+
@@ -936,10 +1598,213 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Mansioni e Sottomansioni
+
Gestione compatta delle mansioni, sottomansioni e DPI necessari.
+
+
+
+
+
+
+
+
+
+
+
+
+
Mansioni totali
+
= (int)$totalRoles; ?>
+
+
+
+
+
+
Mansioni attive
+
= (int)$totalActiveRoles; ?>
+
+
+
+
+
+
Sottomansioni totali
+
= (int)$totalSubRoles; ?>
+
+
+
+
+
+
Sottomansioni attive
+
= (int)$totalActiveSubRoles; ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ Mansione |
+ Sottomansioni |
+ Stato |
+ Azioni |
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+
+ = htmlspecialchars($role['name'], ENT_QUOTES, 'UTF-8'); ?>
+
+
+
+
+ = htmlspecialchars($role['description'], ENT_QUOTES, 'UTF-8'); ?>
+
+
+ |
+
+
+
+ = (int)$role['sub_roles_count']; ?>
+
+ |
+
+
+
+ Attiva
+
+ Inattiva
+
+ |
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+ Usa la freccia per espandere la mansione e gestire le sottomansioni. I DPI si associano dalla singola sottomansione con selezione multipla.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+