diff --git a/docs/employee-profile-install.md b/docs/employee-profile-install.md new file mode 100644 index 0000000..2e1fe24 --- /dev/null +++ b/docs/employee-profile-install.md @@ -0,0 +1,246 @@ +# 1. Database migration + +```mysql +ALTER TABLE employees + ADD COLUMN address varchar(500) DEFAULT NULL AFTER last_name, + ADD COLUMN phone varchar(255) DEFAULT NULL AFTER address, + ADD COLUMN email varchar(255) DEFAULT NULL AFTER phone, + ADD COLUMN job_role_id int(10) UNSIGNED DEFAULT NULL AFTER department_id; + +-- Replace ENUM status with plain VARCHAR for easier maintenance. +ALTER TABLE employees + MODIFY status varchar(255) NOT NULL DEFAULT 'active'; + +CREATE TABLE IF NOT EXISTS job_roles ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL, + description text DEFAULT NULL, + sort_order int(10) UNSIGNED NOT NULL DEFAULT 999, + is_active tinyint(1) NOT NULL DEFAULT 1, + created_at timestamp NULL DEFAULT current_timestamp(), + updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (id), + UNIQUE KEY uniq_job_roles_name (name), + KEY idx_job_roles_active (is_active), + KEY idx_job_roles_sort_order (sort_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +ALTER TABLE employees + ADD KEY idx_employees_job_role_id (job_role_id); + +ALTER TABLE employees + ADD CONSTRAINT fk_employees_job_role + FOREIGN KEY (job_role_id) REFERENCES job_roles (id) + ON DELETE SET NULL + ON UPDATE CASCADE; + +-- 1) Seed job_roles with every distinct non-empty value of employees.position. +INSERT IGNORE INTO job_roles (name, is_active, sort_order, created_at, updated_at) +SELECT DISTINCT TRIM(position), 1, 999, NOW(), NOW() +FROM employees +WHERE position IS NOT NULL AND TRIM(position) <> ''; + +-- 2) Backfill employees.job_role_id by matching position text to job_roles.name. +UPDATE employees e +JOIN job_roles jr ON jr.name = TRIM(e.position) +SET e.job_role_id = jr.id +WHERE e.position IS NOT NULL AND TRIM(e.position) <> ''; + +-- 3) Drop the legacy column. +ALTER TABLE employees DROP COLUMN position; + +CREATE TABLE IF NOT EXISTS training_topics ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL, + description text DEFAULT NULL, + default_frequency_months int(10) UNSIGNED DEFAULT NULL, + default_reminder_days int(10) UNSIGNED NOT NULL DEFAULT 30, + sort_order int(10) UNSIGNED NOT NULL DEFAULT 999, + is_active tinyint(1) NOT NULL DEFAULT 1, + is_mandatory tinyint(1) NOT NULL DEFAULT 0, + created_at timestamp NULL DEFAULT current_timestamp(), + updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (id), + UNIQUE KEY uniq_training_topics_name (name), + KEY idx_training_topics_active (is_active), + KEY idx_training_topics_mandatory (is_mandatory), + KEY idx_training_topics_sort_order (sort_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS employee_documents ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + employee_id int(10) UNSIGNED NOT NULL, + category varchar(255) NOT NULL DEFAULT 'other', + original_name varchar(500) NOT NULL, + stored_name varchar(500) NOT NULL, + mime_type varchar(255) DEFAULT NULL, + size int(10) UNSIGNED DEFAULT NULL, + notes text DEFAULT NULL, + uploaded_by int(10) UNSIGNED DEFAULT NULL, + created_at timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (id), + KEY idx_employee_documents_employee (employee_id), + KEY idx_employee_documents_category (category), + KEY idx_employee_documents_uploaded_by (uploaded_by), + CONSTRAINT fk_employee_documents_employee + FOREIGN KEY (employee_id) REFERENCES employees (id) + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_employee_documents_uploaded_by + FOREIGN KEY (uploaded_by) REFERENCES auth_users (id) + ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS employee_ppe ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + employee_id int(10) UNSIGNED NOT NULL, + item_name varchar(255) NOT NULL, + delivery_date date DEFAULT NULL, + delivered_by varchar(255) DEFAULT NULL, + notes text DEFAULT NULL, + created_by int(10) UNSIGNED DEFAULT NULL, + created_at timestamp NULL DEFAULT current_timestamp(), + updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (id), + KEY idx_employee_ppe_employee (employee_id), + KEY idx_employee_ppe_delivery_date (delivery_date), + CONSTRAINT fk_employee_ppe_employee + FOREIGN KEY (employee_id) REFERENCES employees (id) + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_employee_ppe_created_by + FOREIGN KEY (created_by) REFERENCES auth_users (id) + ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS employee_trainings ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + employee_id int(10) UNSIGNED NOT NULL, + training_topic_id int(10) UNSIGNED NOT NULL, + completed_date date NOT NULL, + delivered_by varchar(255) DEFAULT NULL, + description text DEFAULT NULL, + training_type varchar(255) NOT NULL DEFAULT 'initial', + update_frequency_months int(10) UNSIGNED DEFAULT NULL, + reminder_days int(10) UNSIGNED DEFAULT NULL, + next_due_date date DEFAULT NULL, + created_by int(10) UNSIGNED DEFAULT NULL, + created_at timestamp NULL DEFAULT current_timestamp(), + updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (id), + KEY idx_employee_trainings_employee (employee_id), + KEY idx_employee_trainings_topic (training_topic_id), + KEY idx_employee_trainings_next_due (next_due_date), + KEY idx_employee_trainings_employee_topic (employee_id, training_topic_id), + KEY idx_employee_trainings_created_by (created_by), + CONSTRAINT fk_employee_trainings_employee + FOREIGN KEY (employee_id) REFERENCES employees (id) + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_employee_trainings_topic + FOREIGN KEY (training_topic_id) REFERENCES training_topics (id) + ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT fk_employee_trainings_created_by + FOREIGN KEY (created_by) REFERENCES auth_users (id) + ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS employee_training_attachments ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + training_id int(10) UNSIGNED NOT NULL, + original_name varchar(500) NOT NULL, + stored_name varchar(500) NOT NULL, + mime_type varchar(255) DEFAULT NULL, + size int(10) UNSIGNED DEFAULT NULL, + uploaded_by int(10) UNSIGNED DEFAULT NULL, + created_at timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (id), + KEY idx_employee_training_attachments_training (training_id), + KEY idx_employee_training_attachments_uploaded_by (uploaded_by), + CONSTRAINT fk_employee_training_attachments_training + FOREIGN KEY (training_id) REFERENCES employee_trainings (id) + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_employee_training_attachments_uploaded_by + FOREIGN KEY (uploaded_by) REFERENCES auth_users (id) + ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS employee_training_log ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + employee_id int(10) UNSIGNED DEFAULT NULL, + training_id int(10) UNSIGNED DEFAULT NULL, + action varchar(255) NOT NULL, + field varchar(255) DEFAULT NULL, + old_value text DEFAULT NULL, + new_value text DEFAULT NULL, + changed_by int(10) UNSIGNED DEFAULT NULL, + changed_at timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (id), + KEY idx_employee_training_log_employee (employee_id), + KEY idx_employee_training_log_training (training_id), + KEY idx_employee_training_log_changed_at (changed_at), + CONSTRAINT fk_employee_training_log_employee + FOREIGN KEY (employee_id) REFERENCES employees (id) + ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT fk_employee_training_log_training + FOREIGN KEY (training_id) REFERENCES employee_trainings (id) + ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT fk_employee_training_log_changed_by + FOREIGN KEY (changed_by) REFERENCES auth_users (id) + ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO auth_roles (name, display_name, description, removable, created_at, updated_at) VALUES + ('employee', 'Employee', 'Read-only access to own employee profile.', 1, NOW(), NOW()), + ('employee-hr', 'HR Manager', 'Can manage employee profiles, documents, PPE and training records.', 1, NOW(), NOW()), + ('manager', 'Manager', 'Same permissions as HR Manager.', 1, NOW(), NOW()) +ON DUPLICATE KEY UPDATE + display_name = VALUES(display_name), + description = VALUES(description), + updated_at = NOW(); + +CREATE TABLE IF NOT EXISTS training_reminder_log ( + id int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + training_id int(10) UNSIGNED DEFAULT NULL, + employee_id int(10) UNSIGNED DEFAULT NULL, + training_topic_id int(10) UNSIGNED DEFAULT NULL, + addressee_email varchar(255) NOT NULL, + next_due_date date DEFAULT NULL, + status_at_send varchar(255) NOT NULL, + sent_at timestamp NULL DEFAULT current_timestamp(), + PRIMARY KEY (id), + KEY idx_training_reminder_log_dedup (training_id, addressee_email, next_due_date), + KEY idx_training_reminder_log_dedup_missing (employee_id, training_topic_id, addressee_email), + KEY idx_training_reminder_log_sent_at (sent_at), + CONSTRAINT fk_training_reminder_log_training + FOREIGN KEY (training_id) REFERENCES employee_trainings (id) + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_training_reminder_log_employee + FOREIGN KEY (employee_id) REFERENCES employees (id) + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_training_reminder_log_topic + FOREIGN KEY (training_topic_id) REFERENCES training_topics (id) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +# 2. Upload storage folder + +Create the storage directory with the correct permissions for the web server: + +```bash +mkdir -p /var/www/zibo-dashboard/public/userarea/files/employees +chown -R www-data:www-data /var/www/zibo-dashboard/public/userarea/files +chmod -R 775 /var/www/zibo-dashboard/public/userarea/files +``` + +Uploaded files will be organized as: + +``` +files/employees/{employee_id}/documents/ # File Repository (HR) +files/employees/{employee_id}/trainings/ # Training certificates +``` + +# 3. Cron for automated emails + +```cron +0 7 * * * /usr/bin/php /var/www/zibo-dashboard/public/userarea/cron/send_training_reminders.php \ + >> /var/www/zibo-dashboard/storage/logs/training_reminders.log 2>&1 +``` diff --git a/public/userarea/ajax/auth_check.php b/public/userarea/ajax/auth_check.php new file mode 100644 index 0000000..339ced5 --- /dev/null +++ b/public/userarea/ajax/auth_check.php @@ -0,0 +1,18 @@ + false, 'message' => 'Non autorizzato. Effettua il login.']); + exit; +} + +$currentUserId = (int)$_SESSION['iduserlogin']; diff --git a/public/userarea/ajax/employee_profile/delete_document.php b/public/userarea/ajax/employee_profile/delete_document.php new file mode 100644 index 0000000..b5762aa --- /dev/null +++ b/public/userarea/ajax/employee_profile/delete_document.php @@ -0,0 +1,40 @@ + 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 documento non valido.']); + exit; +} + +$stmt = $pdo->prepare("SELECT employee_id, stored_name FROM employee_documents WHERE id = :id LIMIT 1"); +$stmt->execute(['id' => $id]); +$doc = $stmt->fetch(PDO::FETCH_ASSOC); +if (!$doc) { + echo json_encode(['success' => false, 'message' => 'Documento non trovato.']); + exit; +} + +try { + $del = $pdo->prepare("DELETE FROM employee_documents WHERE id = :id"); + $del->execute(['id' => $id]); + + $path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name']; + if (is_file($path)) { + @unlink($path); + } + + echo json_encode(['success' => true]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/ajax/employee_profile/delete_ppe.php b/public/userarea/ajax/employee_profile/delete_ppe.php new file mode 100644 index 0000000..b4d0762 --- /dev/null +++ b/public/userarea/ajax/employee_profile/delete_ppe.php @@ -0,0 +1,26 @@ + 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; +} + +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()]); +} diff --git a/public/userarea/ajax/employee_profile/delete_training.php b/public/userarea/ajax/employee_profile/delete_training.php new file mode 100644 index 0000000..6e23e7e --- /dev/null +++ b/public/userarea/ajax/employee_profile/delete_training.php @@ -0,0 +1,60 @@ + 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 formazione non valido.']); + exit; +} + +try { + $pdo->beginTransaction(); + + $row = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id"); + $row->execute(['id' => $id]); + $tr = $row->fetch(PDO::FETCH_ASSOC); + if (!$tr) { + $pdo->rollBack(); + echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']); + exit; + } + + // Collect attached files BEFORE deletion so we can unlink them after + $files = $pdo->prepare("SELECT stored_name FROM employee_training_attachments WHERE training_id = :id"); + $files->execute(['id' => $id]); + $stored = $files->fetchAll(PDO::FETCH_COLUMN); + + // Log BEFORE delete (FK on log allows SET NULL on training delete but we want a clean record) + $pdo->prepare(" + INSERT INTO employee_training_log + (employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at) + VALUES + (:eid, NULL, 'deleted', NULL, NULL, NULL, :cb, NOW()) + ")->execute(['eid' => $tr['employee_id'], 'cb' => $currentUserId]); + + $pdo->prepare("DELETE FROM employee_trainings WHERE id = :id")->execute(['id' => $id]); + + $pdo->commit(); + + foreach ($stored as $name) { + $path = __DIR__ . '/../../files/employees/' . (int)$tr['employee_id'] . '/trainings/' . $name; + if (is_file($path)) { + @unlink($path); + } + } + + echo json_encode(['success' => true]); +} catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/ajax/employee_profile/delete_training_attachment.php b/public/userarea/ajax/employee_profile/delete_training_attachment.php new file mode 100644 index 0000000..4ef0b39 --- /dev/null +++ b/public/userarea/ajax/employee_profile/delete_training_attachment.php @@ -0,0 +1,59 @@ + 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 allegato non valido.']); + exit; +} + +$row = $pdo->prepare(" + SELECT a.stored_name, a.original_name, a.training_id, t.employee_id + FROM employee_training_attachments a + JOIN employee_trainings t ON t.id = a.training_id + WHERE a.id = :id + LIMIT 1 +"); +$row->execute(['id' => $id]); +$att = $row->fetch(PDO::FETCH_ASSOC); +if (!$att) { + echo json_encode(['success' => false, 'message' => 'Allegato non trovato.']); + exit; +} + +try { + $pdo->beginTransaction(); + $pdo->prepare("DELETE FROM employee_training_attachments WHERE id = :id")->execute(['id' => $id]); + $pdo->prepare(" + INSERT INTO employee_training_log + (employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at) + VALUES + (:eid, :tid, 'attachment_deleted', 'attachment', :name, NULL, :cb, NOW()) + ")->execute([ + 'eid' => $att['employee_id'], + 'tid' => $att['training_id'], + 'name' => $att['original_name'], + 'cb' => $currentUserId, + ]); + $pdo->commit(); + + $path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name']; + if (is_file($path)) { + @unlink($path); + } + + echo json_encode(['success' => true]); +} catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/ajax/employee_profile/download_document.php b/public/userarea/ajax/employee_profile/download_document.php new file mode 100644 index 0000000..6121316 --- /dev/null +++ b/public/userarea/ajax/employee_profile/download_document.php @@ -0,0 +1,57 @@ +getConnection(); + +$stmt = $pdo->prepare(" + SELECT d.*, e.auth_user_id + FROM employee_documents d + JOIN employees e ON e.id = d.employee_id + WHERE d.id = :id + LIMIT 1 +"); +$stmt->execute(['id' => $id]); +$doc = $stmt->fetch(PDO::FETCH_ASSOC); + +if (!$doc) { + http_response_code(404); + exit('Documento non trovato.'); +} + +/* Access check: HR roles can download any; otherwise only own employee */ +$roleStmt = $pdo->prepare(" + SELECT r.name + FROM auth_users u + LEFT JOIN auth_roles r ON r.id = u.role_id + WHERE u.id = :id LIMIT 1 +"); +$roleStmt->execute(['id' => $currentUserId]); +$role = (string)$roleStmt->fetchColumn(); +$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager']; +$isHr = in_array($role, $hrRoles, true); + +if (!$isHr && (int)$doc['auth_user_id'] !== $currentUserId) { + http_response_code(403); + exit('Accesso negato.'); +} + +$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name']; +if (!is_file($path)) { + http_response_code(404); + exit('File non trovato sul server.'); +} + +while (ob_get_level() > 0) { ob_end_clean(); } +header('Content-Type: ' . (!empty($doc['mime_type']) ? $doc['mime_type'] : 'application/octet-stream')); +header('Content-Disposition: attachment; filename="' . rawurlencode($doc['original_name']) . '"'); +header('Content-Length: ' . filesize($path)); +header('Cache-Control: private, max-age=0, must-revalidate'); +readfile($path); +exit; diff --git a/public/userarea/ajax/employee_profile/download_training_attachment.php b/public/userarea/ajax/employee_profile/download_training_attachment.php new file mode 100644 index 0000000..59ffb67 --- /dev/null +++ b/public/userarea/ajax/employee_profile/download_training_attachment.php @@ -0,0 +1,56 @@ +getConnection(); + +$stmt = $pdo->prepare(" + SELECT a.*, t.employee_id, e.auth_user_id + FROM employee_training_attachments a + JOIN employee_trainings t ON t.id = a.training_id + JOIN employees e ON e.id = t.employee_id + WHERE a.id = :id + LIMIT 1 +"); +$stmt->execute(['id' => $id]); +$att = $stmt->fetch(PDO::FETCH_ASSOC); +if (!$att) { + http_response_code(404); + exit('Allegato non trovato.'); +} + +/* Access: HR or owning employee */ +$roleStmt = $pdo->prepare(" + SELECT r.name FROM auth_users u + LEFT JOIN auth_roles r ON r.id = u.role_id + WHERE u.id = :id LIMIT 1 +"); +$roleStmt->execute(['id' => $currentUserId]); +$role = (string)$roleStmt->fetchColumn(); +$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager']; +$isHr = in_array($role, $hrRoles, true); + +if (!$isHr && (int)$att['auth_user_id'] !== $currentUserId) { + http_response_code(403); + exit('Accesso negato.'); +} + +$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name']; +if (!is_file($path)) { + http_response_code(404); + exit('File non trovato sul server.'); +} + +while (ob_get_level() > 0) { ob_end_clean(); } +header('Content-Type: ' . (!empty($att['mime_type']) ? $att['mime_type'] : 'application/octet-stream')); +header('Content-Disposition: attachment; filename="' . rawurlencode($att['original_name']) . '"'); +header('Content-Length: ' . filesize($path)); +header('Cache-Control: private, max-age=0, must-revalidate'); +readfile($path); +exit; diff --git a/public/userarea/ajax/employee_profile/get_training_attachments.php b/public/userarea/ajax/employee_profile/get_training_attachments.php new file mode 100644 index 0000000..fb9040c --- /dev/null +++ b/public/userarea/ajax/employee_profile/get_training_attachments.php @@ -0,0 +1,58 @@ + false, 'message' => 'ID formazione non valido.']); + exit; +} + +$pdo = DBHandlerSelect::getInstance()->getConnection(); + +/* Access: HR or owner */ +$ownerStmt = $pdo->prepare(" + SELECT e.auth_user_id + FROM employee_trainings t + JOIN employees e ON e.id = t.employee_id + WHERE t.id = :id LIMIT 1 +"); +$ownerStmt->execute(['id' => $trainingId]); +$ownerAuthUserId = $ownerStmt->fetchColumn(); +if ($ownerAuthUserId === false) { + echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']); + exit; +} + +$roleStmt = $pdo->prepare(" + SELECT r.name FROM auth_users u + LEFT JOIN auth_roles r ON r.id = u.role_id + WHERE u.id = :id LIMIT 1 +"); +$roleStmt->execute(['id' => $currentUserId]); +$role = (string)$roleStmt->fetchColumn(); +$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager']; +$isHr = in_array($role, $hrRoles, true); + +if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) { + http_response_code(403); + echo json_encode(['success' => false, 'message' => 'Accesso negato.']); + exit; +} + +$stmt = $pdo->prepare(" + SELECT id, original_name, mime_type, size, created_at + FROM employee_training_attachments + WHERE training_id = :tid + ORDER BY created_at DESC +"); +$stmt->execute(['tid' => $trainingId]); +$attachments = $stmt->fetchAll(PDO::FETCH_ASSOC); + +echo json_encode([ + 'success' => true, + 'attachments' => $attachments, + 'can_edit' => $isHr, +]); diff --git a/public/userarea/ajax/employee_profile/get_training_log.php b/public/userarea/ajax/employee_profile/get_training_log.php new file mode 100644 index 0000000..2250b51 --- /dev/null +++ b/public/userarea/ajax/employee_profile/get_training_log.php @@ -0,0 +1,57 @@ + false, 'message' => 'ID formazione non valido.']); + exit; +} + +$pdo = DBHandlerSelect::getInstance()->getConnection(); + +/* Access: HR or owner */ +$ownerStmt = $pdo->prepare(" + SELECT e.auth_user_id + FROM employee_trainings t + JOIN employees e ON e.id = t.employee_id + WHERE t.id = :id LIMIT 1 +"); +$ownerStmt->execute(['id' => $trainingId]); +$ownerAuthUserId = $ownerStmt->fetchColumn(); +if ($ownerAuthUserId === false) { + echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']); + exit; +} + +$roleStmt = $pdo->prepare(" + SELECT r.name FROM auth_users u + LEFT JOIN auth_roles r ON r.id = u.role_id + WHERE u.id = :id LIMIT 1 +"); +$roleStmt->execute(['id' => $currentUserId]); +$role = (string)$roleStmt->fetchColumn(); +$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager']; +$isHr = in_array($role, $hrRoles, true); + +if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) { + http_response_code(403); + echo json_encode(['success' => false, 'message' => 'Accesso negato.']); + exit; +} + +$stmt = $pdo->prepare(" + SELECT l.id, l.action, l.field, l.old_value, l.new_value, l.changed_at, + TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS changed_by_name, + u.email AS changed_by_email + FROM employee_training_log l + LEFT JOIN auth_users u ON u.id = l.changed_by + WHERE l.training_id = :tid + ORDER BY l.changed_at DESC, l.id DESC +"); +$stmt->execute(['tid' => $trainingId]); +$entries = $stmt->fetchAll(PDO::FETCH_ASSOC); + +echo json_encode(['success' => true, 'entries' => $entries]); diff --git a/public/userarea/ajax/employee_profile/save_personal.php b/public/userarea/ajax/employee_profile/save_personal.php new file mode 100644 index 0000000..a71cf04 --- /dev/null +++ b/public/userarea/ajax/employee_profile/save_personal.php @@ -0,0 +1,116 @@ + false, 'message' => 'Metodo non consentito.']); + exit; +} + +$pdo = DBHandlerSelect::getInstance()->getConnection(); + +$employeeId = (int)($_POST['employee_id'] ?? 0); +$firstName = trim($_POST['first_name'] ?? ''); +$lastName = trim($_POST['last_name'] ?? ''); +$employeeCode = trim($_POST['employee_code'] ?? ''); +$address = trim($_POST['address'] ?? ''); +$phone = trim($_POST['phone'] ?? ''); +$email = trim($_POST['email'] ?? ''); +$hireDate = trim($_POST['hire_date'] ?? ''); +$departmentId = $_POST['department_id'] ?? ''; +$jobRoleId = $_POST['job_role_id'] ?? ''; +$status = trim($_POST['status'] ?? ''); +$authUserId = $_POST['auth_user_id'] ?? ''; +$roleId = $_POST['role_id'] ?? ''; + +if ($employeeId <= 0) { + echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']); + exit; +} +if ($firstName === '' || $lastName === '') { + echo json_encode(['success' => false, 'message' => 'Nome e cognome sono obbligatori.']); + exit; +} + +$allowedStatus = ['active', 'inactive', 'suspended']; +if (!in_array($status, $allowedStatus, true)) { + $status = 'active'; +} + +$departmentId = ($departmentId === '' || $departmentId === null) ? null : (int)$departmentId; +$jobRoleId = ($jobRoleId === '' || $jobRoleId === null) ? null : (int)$jobRoleId; +$authUserId = ($authUserId === '' || $authUserId === null) ? null : (int)$authUserId; +$roleId = ($roleId === '' || $roleId === null) ? null : (int)$roleId; +$hireDate = $hireDate === '' ? null : $hireDate; + +if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) { + echo json_encode(['success' => false, 'message' => 'Email non valida.']); + exit; +} + +if ($employeeCode !== '') { + $check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE employee_code = :code AND id <> :id"); + $check->execute(['code' => $employeeCode, 'id' => $employeeId]); + if ((int)$check->fetchColumn() > 0) { + echo json_encode(['success' => false, 'message' => 'Codice dipendente già in uso.']); + exit; + } +} + +if ($authUserId !== null) { + $check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE auth_user_id = :uid AND id <> :id"); + $check->execute(['uid' => $authUserId, 'id' => $employeeId]); + if ((int)$check->fetchColumn() > 0) { + echo json_encode(['success' => false, 'message' => 'Questo utente è già associato ad un altro dipendente.']); + exit; + } +} + +try { + $stmt = $pdo->prepare(" + UPDATE employees + SET first_name = :first_name, + last_name = :last_name, + employee_code = :employee_code, + address = :address, + phone = :phone, + email = :email, + hire_date = :hire_date, + department_id = :department_id, + job_role_id = :job_role_id, + status = :status, + auth_user_id = :auth_user_id, + updated_at = NOW() + WHERE id = :id + "); + $stmt->execute([ + 'first_name' => $firstName, + 'last_name' => $lastName, + 'employee_code' => $employeeCode !== '' ? $employeeCode : null, + 'address' => $address !== '' ? $address : null, + 'phone' => $phone !== '' ? $phone : null, + 'email' => $email !== '' ? $email : null, + 'hire_date' => $hireDate, + 'department_id' => $departmentId, + 'job_role_id' => $jobRoleId, + 'status' => $status, + 'auth_user_id' => $authUserId, + 'id' => $employeeId, + ]); + + // Optionally update Vanguard role for the linked auth_user + if ($authUserId !== null && $roleId !== null) { + $check = $pdo->prepare("SELECT COUNT(*) FROM auth_roles WHERE id = ?"); + $check->execute([$roleId]); + if ((int)$check->fetchColumn() > 0) { + $upd = $pdo->prepare("UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :uid"); + $upd->execute(['role_id' => $roleId, 'uid' => $authUserId]); + } + } + + echo json_encode(['success' => true]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/ajax/employee_profile/save_ppe.php b/public/userarea/ajax/employee_profile/save_ppe.php new file mode 100644 index 0000000..1dfb320 --- /dev/null +++ b/public/userarea/ajax/employee_profile/save_ppe.php @@ -0,0 +1,82 @@ + 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; + +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, + ]); + 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.']); + exit; + } + + $stmt = $pdo->prepare(" + INSERT INTO employee_ppe + (employee_id, item_name, delivery_date, delivered_by, notes, created_by, created_at, updated_at) + VALUES + (:employee_id, :item_name, :delivery_date, :delivered_by, :notes, :created_by, NOW(), NOW()) + "); + $stmt->execute([ + 'employee_id' => $employeeId, + 'item_name' => $itemName, + 'delivery_date' => $deliveryDate, + 'delivered_by' => $deliveredBy, + 'notes' => $notes, + 'created_by' => $currentUserId, + ]); + + echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/ajax/employee_profile/save_training.php b/public/userarea/ajax/employee_profile/save_training.php new file mode 100644 index 0000000..06b7366 --- /dev/null +++ b/public/userarea/ajax/employee_profile/save_training.php @@ -0,0 +1,177 @@ + false, 'message' => 'Metodo non consentito.']); + exit; +} + +$pdo = DBHandlerSelect::getInstance()->getConnection(); + +$id = (int)($_POST['id'] ?? 0); +$employeeId = (int)($_POST['employee_id'] ?? 0); +$topicId = (int)($_POST['training_topic_id'] ?? 0); +$completedDate = trim($_POST['completed_date'] ?? ''); +$deliveredBy = trim($_POST['delivered_by'] ?? ''); +$description = trim($_POST['description'] ?? ''); +$trainingType = trim($_POST['training_type'] ?? 'initial'); +$freqRaw = $_POST['update_frequency_months'] ?? ''; +$remRaw = $_POST['reminder_days'] ?? ''; + +if ($employeeId <= 0) { + echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']); + exit; +} +if ($topicId <= 0) { + echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']); + exit; +} +if ($completedDate === '') { + echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']); + exit; +} +if (!in_array($trainingType, ['initial', 'refresher'], true)) { + $trainingType = 'initial'; +} + +$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id"); +$topicStmt->execute(['id' => $topicId]); +$topic = $topicStmt->fetch(PDO::FETCH_ASSOC); +if (!$topic) { + echo json_encode(['success' => false, 'message' => 'Corso non trovato.']); + exit; +} + +$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw); +$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw); + +/* Effective frequency for next_due_date: explicit override or topic default */ +$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null); + +$nextDue = null; +if ($effFreq !== null && $effFreq > 0) { + $d = DateTime::createFromFormat('Y-m-d', $completedDate); + if ($d) { + $d->modify('+' . (int)$effFreq . ' months'); + $nextDue = $d->format('Y-m-d'); + } +} + +$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null; +$description = $description !== '' ? $description : null; + +try { + $pdo->beginTransaction(); + + if ($id > 0) { + $old = $pdo->prepare("SELECT * FROM employee_trainings WHERE id = :id"); + $old->execute(['id' => $id]); + $oldRow = $old->fetch(PDO::FETCH_ASSOC); + if (!$oldRow) { + $pdo->rollBack(); + echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']); + exit; + } + + $upd = $pdo->prepare(" + UPDATE employee_trainings + SET training_topic_id = :topic_id, + completed_date = :completed_date, + delivered_by = :delivered_by, + description = :description, + training_type = :training_type, + update_frequency_months = :freq, + reminder_days = :rem, + next_due_date = :next_due, + updated_at = NOW() + WHERE id = :id + "); + $upd->execute([ + 'topic_id' => $topicId, + 'completed_date' => $completedDate, + 'delivered_by' => $deliveredBy, + 'description' => $description, + 'training_type' => $trainingType, + 'freq' => $freq, + 'rem' => $rem, + 'next_due' => $nextDue, + 'id' => $id, + ]); + + $fields = [ + 'training_topic_id' => $topicId, + 'completed_date' => $completedDate, + 'delivered_by' => $deliveredBy, + 'description' => $description, + 'training_type' => $trainingType, + 'update_frequency_months' => $freq, + 'reminder_days' => $rem, + 'next_due_date' => $nextDue, + ]; + $logStmt = $pdo->prepare(" + INSERT INTO employee_training_log + (employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at) + VALUES + (:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW()) + "); + foreach ($fields as $f => $newV) { + $oldV = $oldRow[$f] ?? null; + if ((string)$oldV !== (string)$newV) { + $logStmt->execute([ + 'eid' => $employeeId, + 'tid' => $id, + 'field' => $f, + 'old_v' => $oldV, + 'new_v' => $newV, + 'cb' => $currentUserId, + ]); + } + } + + $pdo->commit(); + echo json_encode(['success' => true, 'id' => $id]); + exit; + } + + $ins = $pdo->prepare(" + INSERT INTO employee_trainings + (employee_id, training_topic_id, completed_date, + delivered_by, description, + training_type, update_frequency_months, reminder_days, next_due_date, + created_by, created_at, updated_at) + VALUES + (:eid, :tid, :completed_date, + :delivered_by, :description, + :training_type, :freq, :rem, :next_due, + :cb, NOW(), NOW()) + "); + $ins->execute([ + 'eid' => $employeeId, + 'tid' => $topicId, + 'completed_date' => $completedDate, + 'delivered_by' => $deliveredBy, + 'description' => $description, + 'training_type' => $trainingType, + 'freq' => $freq, + 'rem' => $rem, + 'next_due' => $nextDue, + 'cb' => $currentUserId, + ]); + $newId = (int)$pdo->lastInsertId(); + + $pdo->prepare(" + INSERT INTO employee_training_log + (employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at) + VALUES + (:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW()) + ")->execute(['eid' => $employeeId, 'tid' => $newId, 'cb' => $currentUserId]); + + $pdo->commit(); + echo json_encode(['success' => true, 'id' => $newId]); +} catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/ajax/employee_profile/upload_document.php b/public/userarea/ajax/employee_profile/upload_document.php new file mode 100644 index 0000000..3b0e34f --- /dev/null +++ b/public/userarea/ajax/employee_profile/upload_document.php @@ -0,0 +1,89 @@ + false, 'message' => 'Metodo non consentito.']); + exit; +} + +$pdo = DBHandlerSelect::getInstance()->getConnection(); + +$employeeId = (int)($_POST['employee_id'] ?? 0); +$category = trim($_POST['category'] ?? 'other'); +$notes = trim($_POST['notes'] ?? ''); + +$allowedCategories = ['job_description', 'contract', 'rules', 'other']; +if (!in_array($category, $allowedCategories, true)) { + $category = 'other'; +} + +if ($employeeId <= 0) { + echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']); + 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.']); + exit; +} + +if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { + $errCode = $_FILES['file']['error'] ?? -1; + $msg = 'Errore nel caricamento del file.'; + if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) { + $msg = 'Il file supera la dimensione massima consentita.'; + } + echo json_encode(['success' => false, 'message' => $msg]); + exit; +} + +$originalName = $_FILES['file']['name']; +$tmpPath = $_FILES['file']['tmp_name']; +$size = (int)$_FILES['file']['size']; +$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null); + +$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/documents'; +if (!is_dir($dir)) { + if (!mkdir($dir, 0775, true) && !is_dir($dir)) { + echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']); + exit; + } +} + +$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName); +$storedName = uniqid('doc_') . '_' . $safeOriginal; +$destPath = $dir . '/' . $storedName; + +if (!move_uploaded_file($tmpPath, $destPath)) { + echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']); + exit; +} + +try { + $stmt = $pdo->prepare(" + INSERT INTO employee_documents + (employee_id, category, original_name, stored_name, mime_type, size, notes, uploaded_by, created_at) + VALUES + (:employee_id, :category, :original_name, :stored_name, :mime_type, :size, :notes, :uploaded_by, NOW()) + "); + $stmt->execute([ + 'employee_id' => $employeeId, + 'category' => $category, + 'original_name' => $originalName, + 'stored_name' => $storedName, + 'mime_type' => $mimeType, + 'size' => $size, + 'notes' => $notes !== '' ? $notes : null, + 'uploaded_by' => $currentUserId, + ]); + + echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]); +} catch (Exception $e) { + @unlink($destPath); + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/ajax/employee_profile/upload_training_attachment.php b/public/userarea/ajax/employee_profile/upload_training_attachment.php new file mode 100644 index 0000000..2230064 --- /dev/null +++ b/public/userarea/ajax/employee_profile/upload_training_attachment.php @@ -0,0 +1,98 @@ + false, 'message' => 'Metodo non consentito.']); + exit; +} + +$pdo = DBHandlerSelect::getInstance()->getConnection(); + +$trainingId = (int)($_POST['training_id'] ?? 0); +if ($trainingId <= 0) { + echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']); + exit; +} + +$tr = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id"); +$tr->execute(['id' => $trainingId]); +$trainingRow = $tr->fetch(PDO::FETCH_ASSOC); +if (!$trainingRow) { + echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']); + exit; +} +$employeeId = (int)$trainingRow['employee_id']; + +if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { + $errCode = $_FILES['file']['error'] ?? -1; + $msg = 'Errore nel caricamento del file.'; + if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) { + $msg = 'Il file supera la dimensione massima consentita.'; + } + echo json_encode(['success' => false, 'message' => $msg]); + exit; +} + +$originalName = $_FILES['file']['name']; +$tmpPath = $_FILES['file']['tmp_name']; +$size = (int)$_FILES['file']['size']; +$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null); + +$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/trainings'; +if (!is_dir($dir)) { + if (!mkdir($dir, 0775, true) && !is_dir($dir)) { + echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']); + exit; + } +} + +$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName); +$storedName = uniqid('tr_') . '_' . $safeOriginal; +$destPath = $dir . '/' . $storedName; + +if (!move_uploaded_file($tmpPath, $destPath)) { + echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']); + exit; +} + +try { + $pdo->beginTransaction(); + + $ins = $pdo->prepare(" + INSERT INTO employee_training_attachments + (training_id, original_name, stored_name, mime_type, size, uploaded_by, created_at) + VALUES + (:tid, :original_name, :stored_name, :mime_type, :size, :uploaded_by, NOW()) + "); + $ins->execute([ + 'tid' => $trainingId, + 'original_name' => $originalName, + 'stored_name' => $storedName, + 'mime_type' => $mimeType, + 'size' => $size, + 'uploaded_by' => $currentUserId, + ]); + $attachmentId = (int)$pdo->lastInsertId(); + + $pdo->prepare(" + INSERT INTO employee_training_log + (employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at) + VALUES + (:eid, :tid, 'attachment_added', 'attachment', NULL, :name, :cb, NOW()) + ")->execute([ + 'eid' => $employeeId, + 'tid' => $trainingId, + 'name' => $originalName, + 'cb' => $currentUserId, + ]); + + $pdo->commit(); + echo json_encode(['success' => true, 'id' => $attachmentId]); +} catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + @unlink($destPath); + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/ajax/hr_auth_check.php b/public/userarea/ajax/hr_auth_check.php new file mode 100644 index 0000000..b4b5141 --- /dev/null +++ b/public/userarea/ajax/hr_auth_check.php @@ -0,0 +1,32 @@ +getConnection(); + +$stmt = $pdo->prepare(" + SELECT r.name AS role_name + FROM auth_users u + LEFT JOIN auth_roles r ON r.id = u.role_id + WHERE u.id = :id + LIMIT 1 +"); +$stmt->execute(['id' => $currentUserId]); +$currentUserRole = (string)$stmt->fetchColumn(); + +$allowedHrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager']; + +if (!in_array($currentUserRole, $allowedHrRoles, true)) { + header('Content-Type: application/json'); + http_response_code(403); + echo json_encode([ + 'success' => false, + 'message' => 'Permessi insufficienti per questa operazione.', + ]); + exit; +} diff --git a/public/userarea/ajax/job_roles/delete.php b/public/userarea/ajax/job_roles/delete.php new file mode 100644 index 0000000..1e14588 --- /dev/null +++ b/public/userarea/ajax/job_roles/delete.php @@ -0,0 +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 mansione non valido.']); + exit; +} + +try { + $usage = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE job_role_id = :id"); + $usage->execute(['id' => $id]); + if ((int)$usage->fetchColumn() > 0) { + echo json_encode([ + 'success' => false, + 'message' => 'Questa mansione è associata a uno o più dipendenti e non può essere cancellata.', + ]); + exit; + } + + $stmt = $pdo->prepare("DELETE FROM job_roles WHERE id = :id"); + $stmt->execute(['id' => $id]); + + echo json_encode(['success' => true]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/ajax/job_roles/save.php b/public/userarea/ajax/job_roles/save.php new file mode 100644 index 0000000..dadf798 --- /dev/null +++ b/public/userarea/ajax/job_roles/save.php @@ -0,0 +1,77 @@ + false, 'message' => 'Metodo non consentito.']); + exit; +} + +$pdo = DBHandlerSelect::getInstance()->getConnection(); + +$id = (int)($_POST['id'] ?? 0); +$name = trim($_POST['name'] ?? ''); +$description = trim($_POST['description'] ?? ''); +$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999; +$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1; + +if ($name === '') { + echo json_encode(['success' => false, 'message' => 'Il nome della mansione è obbligatorio.']); + exit; +} + +try { + if ($id > 0) { + $check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name AND id <> :id"); + $check->execute(['name' => $name, 'id' => $id]); + if ((int)$check->fetchColumn() > 0) { + echo json_encode(['success' => false, 'message' => 'Esiste già un\'altra mansione con questo nome.']); + exit; + } + + $stmt = $pdo->prepare(" + UPDATE job_roles + SET name = :name, + description = :description, + sort_order = :sort_order, + is_active = :is_active, + updated_at = NOW() + WHERE id = :id + "); + $stmt->execute([ + 'name' => $name, + 'description' => $description !== '' ? $description : null, + 'sort_order' => $sort_order, + 'is_active' => $is_active, + 'id' => $id, + ]); + + echo json_encode(['success' => true, 'id' => $id]); + exit; + } + + $check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name"); + $check->execute(['name' => $name]); + if ((int)$check->fetchColumn() > 0) { + echo json_encode(['success' => false, 'message' => 'Esiste già una mansione con questo nome.']); + exit; + } + + $stmt = $pdo->prepare(" + INSERT INTO job_roles (name, description, sort_order, is_active, created_at, updated_at) + VALUES (:name, :description, :sort_order, :is_active, NOW(), NOW()) + "); + $stmt->execute([ + 'name' => $name, + 'description' => $description !== '' ? $description : null, + 'sort_order' => $sort_order, + 'is_active' => $is_active, + ]); + + echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/ajax/training_topics/delete.php b/public/userarea/ajax/training_topics/delete.php new file mode 100644 index 0000000..b308b74 --- /dev/null +++ b/public/userarea/ajax/training_topics/delete.php @@ -0,0 +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 corso non valido.']); + exit; +} + +try { + $usage = $pdo->prepare("SELECT COUNT(*) FROM employee_trainings WHERE training_topic_id = :id"); + $usage->execute(['id' => $id]); + if ((int)$usage->fetchColumn() > 0) { + echo json_encode([ + 'success' => false, + 'message' => 'Questo corso ha già delle registrazioni di formazione e non può essere cancellato.', + ]); + exit; + } + + $stmt = $pdo->prepare("DELETE FROM training_topics WHERE id = :id"); + $stmt->execute(['id' => $id]); + + echo json_encode(['success' => true]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/ajax/training_topics/save.php b/public/userarea/ajax/training_topics/save.php new file mode 100644 index 0000000..ce5d7b7 --- /dev/null +++ b/public/userarea/ajax/training_topics/save.php @@ -0,0 +1,94 @@ + false, 'message' => 'Metodo non consentito.']); + exit; +} + +$pdo = DBHandlerSelect::getInstance()->getConnection(); + +$id = (int)($_POST['id'] ?? 0); +$name = trim($_POST['name'] ?? ''); +$description = trim($_POST['description'] ?? ''); +$freqRaw = $_POST['default_frequency_months'] ?? ''; +$remRaw = $_POST['default_reminder_days'] ?? ''; +$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999; +$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1; +$is_mandatory = isset($_POST['is_mandatory']) && (int)$_POST['is_mandatory'] === 1 ? 1 : 0; + +$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw); +$rem = ($remRaw === '' || $remRaw === null) ? 30 : max(0, (int)$remRaw); + +if ($name === '') { + echo json_encode(['success' => false, 'message' => 'Il nome del corso è obbligatorio.']); + exit; +} + +try { + if ($id > 0) { + $check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name AND id <> :id"); + $check->execute(['name' => $name, 'id' => $id]); + if ((int)$check->fetchColumn() > 0) { + echo json_encode(['success' => false, 'message' => 'Esiste già un altro corso con questo nome.']); + exit; + } + + $stmt = $pdo->prepare(" + UPDATE training_topics + SET name = :name, + description = :description, + default_frequency_months = :freq, + default_reminder_days = :rem, + sort_order = :sort_order, + is_active = :is_active, + is_mandatory = :is_mandatory, + updated_at = NOW() + WHERE id = :id + "); + $stmt->execute([ + 'name' => $name, + 'description' => $description !== '' ? $description : null, + 'freq' => $freq, + 'rem' => $rem, + 'sort_order' => $sort_order, + 'is_active' => $is_active, + 'is_mandatory' => $is_mandatory, + 'id' => $id, + ]); + + echo json_encode(['success' => true, 'id' => $id]); + exit; + } + + $check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name"); + $check->execute(['name' => $name]); + if ((int)$check->fetchColumn() > 0) { + echo json_encode(['success' => false, 'message' => 'Esiste già un corso con questo nome.']); + exit; + } + + $stmt = $pdo->prepare(" + INSERT INTO training_topics + (name, description, default_frequency_months, default_reminder_days, sort_order, is_active, is_mandatory, created_at, updated_at) + VALUES + (:name, :description, :freq, :rem, :sort_order, :is_active, :is_mandatory, NOW(), NOW()) + "); + $stmt->execute([ + 'name' => $name, + 'description' => $description !== '' ? $description : null, + 'freq' => $freq, + 'rem' => $rem, + 'sort_order' => $sort_order, + 'is_active' => $is_active, + 'is_mandatory' => $is_mandatory, + ]); + + echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/cron/send_training_reminders.php b/public/userarea/cron/send_training_reminders.php new file mode 100644 index 0000000..8849273 --- /dev/null +++ b/public/userarea/cron/send_training_reminders.php @@ -0,0 +1,347 @@ + topic default > 30 days). + * Sends "expired" emails when next_due_date is in the past. + * Skips rows with next_due_date IS NULL (one-off trainings). + * Skips already-sent notifications (same training + addressee + next_due_date). + * Recipients: the employee (employees.email or auth_users.email) + every HR user + * with role Admin / Superuser / employee-hr / manager. + * + * Optional CLI flags: + * --dry-run — log only, no SMTP, no DB write + * --only-email=foo@bar — restrict to a single addressee (for testing) + */ + +require_once __DIR__ . '/../class/db-functions.php'; +require_once __DIR__ . '/../../../vendor/autoload.php'; + +use Dotenv\Dotenv; +use PHPMailer\PHPMailer\PHPMailer; +use PHPMailer\PHPMailer\Exception; + +$dotenv = Dotenv::createImmutable(__DIR__ . '/../../../'); +$dotenv->load(); + +$db = DBHandlerSelect::getInstance(); +$pdo = $db->getConnection(); + +$today = date('Y-m-d'); +$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/'); + +/* CLI flags */ +$dryRun = false; +$onlyEmail = null; +foreach (array_slice($argv ?? [], 1) as $a) { + if ($a === '--dry-run' || $a === '-n') { + $dryRun = true; + } elseif (strpos($a, '--only-email=') === 0) { + $onlyEmail = substr($a, strlen('--only-email=')); + } +} + +$sent = 0; +$skipped = 0; +$errors = 0; + +/* Candidate trainings (with optional override reminder + topic default) */ +$stmt = $pdo->query(" + SELECT et.id, et.employee_id, et.completed_date, et.next_due_date, + et.reminder_days, et.delivered_by, + tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem, + e.first_name, e.last_name, e.employee_code, + e.email AS employee_email_direct, + au.email AS employee_email_auth + FROM employee_trainings et + JOIN training_topics tt ON tt.id = et.training_topic_id + JOIN employees e ON e.id = et.employee_id + LEFT JOIN auth_users au ON au.id = e.auth_user_id + WHERE et.next_due_date IS NOT NULL +"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + +if (empty($rows)) { + echo date('Y-m-d H:i:s') . " — Nessuna formazione da notificare.\n"; + exit(0); +} + +/* HR addressees (one query, reused per training) */ +$hrUsers = $pdo->query(" + SELECT u.id, u.email, TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS name + FROM auth_users u + JOIN auth_roles r ON r.id = u.role_id + WHERE r.name IN ('Admin','Superuser','employee-hr','manager') + AND u.email IS NOT NULL AND u.email <> '' +")->fetchAll(PDO::FETCH_ASSOC); + +$checkSent = $pdo->prepare(" + SELECT COUNT(*) FROM training_reminder_log + WHERE training_id = ? AND addressee_email = ? AND next_due_date = ? +"); +$insertLog = $pdo->prepare(" + INSERT INTO training_reminder_log + (training_id, addressee_email, next_due_date, status_at_send, sent_at) + VALUES (?, ?, ?, ?, NOW()) +"); + +foreach ($rows as $r) { + $rem = $r['reminder_days'] !== null + ? (int)$r['reminder_days'] + : ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30); + $isOverdue = $r['next_due_date'] < $today; + $daysLeft = (int)((strtotime($r['next_due_date']) - strtotime($today)) / 86400); + + if (!$isOverdue && $daysLeft > $rem) { + continue; // not yet in the reminder window + } + $type = $isOverdue ? 'expired' : 'update_to_be_scheduled'; + + $employeeFullName = trim($r['first_name'] . ' ' . $r['last_name']); + $employeeEmail = !empty($r['employee_email_direct']) + ? $r['employee_email_direct'] + : (!empty($r['employee_email_auth']) ? $r['employee_email_auth'] : null); + + /* Collect addressees (employee + HR), deduplicated by lowercased email */ + $recipients = []; + if ($employeeEmail) { + $key = strtolower(trim($employeeEmail)); + $recipients[$key] = ['email' => $employeeEmail, 'name' => $employeeFullName, 'is_hr' => false]; + } + foreach ($hrUsers as $hr) { + $key = strtolower(trim((string)$hr['email'])); + if ($key === '' || isset($recipients[$key])) continue; + $recipients[$key] = ['email' => $hr['email'], 'name' => trim((string)$hr['name']), 'is_hr' => true]; + } + if (empty($recipients)) { + continue; + } + + foreach ($recipients as $email => $rec) { + if ($onlyEmail !== null && strcasecmp($rec['email'], $onlyEmail) !== 0) continue; + + $checkSent->execute([$r['id'], $rec['email'], $r['next_due_date']]); + if ($checkSent->fetchColumn() > 0) { + $skipped++; + continue; + } + + try { + $mail = new PHPMailer(true); + + // SMTP config from .env + $mailer = $_ENV['MAIL_MAILER'] ?? 'mail'; + if ($mailer === 'smtp') { + $mail->isSMTP(); + $mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost'; + $mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587); + if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') { + $mail->SMTPAuth = true; + $mail->Username = $_ENV['MAIL_USERNAME']; + $mail->Password = $_ENV['MAIL_PASSWORD'] ?? ''; + } + $enc = $_ENV['MAIL_ENCRYPTION'] ?? ''; + if ($enc && $enc !== 'null') { + $mail->SMTPSecure = $enc; + } + } + + $mail->CharSet = 'UTF-8'; + $mail->setFrom( + $_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it', + $_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA' + ); + $mail->addAddress($rec['email'], $rec['name'] ?: $rec['email']); + + $profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training'; + $topicText = $r['topic_name'] . ' — ' . $employeeFullName + . (!empty($r['employee_code']) ? ' (' . $r['employee_code'] . ')' : ''); + + if ($isOverdue) { + $mail->Subject = '⚠️ Formazione scaduta: ' . $r['topic_name']; + $mail->Body = buildHtml( + 'Formazione scaduta', + $topicText, + 'Completata il ' . date('d/m/Y', strtotime($r['completed_date'])) . '. ' + . 'Il prossimo aggiornamento era previsto per ' . date('d/m/Y', strtotime($r['next_due_date'])) . '' + . ' (scaduta da ' . abs($daysLeft) . ' giorni).', + '#dc3545', + $profileUrl, + $rec['is_hr'] + ); + } else { + $mail->Subject = '📚 Formazione in scadenza: ' . $r['topic_name']; + $daysText = $daysLeft === 0 ? 'oggi' : 'tra ' . $daysLeft . ' giorni'; + $mail->Body = buildHtml( + 'Formazione in scadenza', + $topicText, + 'Completata il ' . date('d/m/Y', strtotime($r['completed_date'])) . '. ' + . 'Prossimo aggiornamento previsto per ' . date('d/m/Y', strtotime($r['next_due_date'])) . '' + . ' (' . $daysText . ').', + '#e8930c', + $profileUrl, + $rec['is_hr'] + ); + } + + $mail->isHTML(true); + $mail->AltBody = strip_tags(str_replace('
', "\n", $mail->Body)); + + if ($dryRun) { + echo date('H:i:s') . " ◌ DRY {$type} → {$rec['email']} — {$r['topic_name']}\n"; + $sent++; + continue; + } + + $mail->send(); + $insertLog->execute([$r['id'], $rec['email'], $r['next_due_date'], $type]); + $sent++; + echo date('H:i:s') . " ✓ {$type} → {$rec['email']} — {$r['topic_name']}\n"; + + } catch (Exception $e) { + $errors++; + echo date('H:i:s') . " ✗ Errore {$rec['email']}: {$e->getMessage()}\n"; + } + } +} + +/* ============================================================================ + NOT-PRESENT reminders — mandatory topics with no record for an employee. + Notify HR only. + De-dup by (employee_id, training_topic_id, addressee_email). + ============================================================================ */ +$missingStmt = $pdo->query(" + SELECT e.id AS employee_id, e.first_name, e.last_name, e.employee_code, + tt.id AS topic_id, tt.name AS topic_name + FROM employees e + CROSS JOIN training_topics tt + WHERE tt.is_active = 1 AND tt.is_mandatory = 1 + AND (e.status IS NULL OR e.status = 'active') + AND NOT EXISTS ( + SELECT 1 FROM employee_trainings et + WHERE et.employee_id = e.id AND et.training_topic_id = tt.id + ) + ORDER BY e.last_name, e.first_name, tt.name +"); +$missingRows = $missingStmt->fetchAll(PDO::FETCH_ASSOC); + +$checkMissingSent = $pdo->prepare(" + SELECT COUNT(*) FROM training_reminder_log + WHERE employee_id = ? AND training_topic_id = ? AND addressee_email = ? + AND status_at_send = 'not_present' +"); +$insertMissingLog = $pdo->prepare(" + INSERT INTO training_reminder_log + (training_id, employee_id, training_topic_id, addressee_email, next_due_date, status_at_send, sent_at) + VALUES (NULL, ?, ?, ?, NULL, 'not_present', NOW()) +"); + +foreach ($missingRows as $m) { + $employeeFullName = trim($m['first_name'] . ' ' . $m['last_name']); + + foreach ($hrUsers as $hr) { + $email = trim((string)$hr['email']); + if ($email === '') continue; + if ($onlyEmail !== null && strcasecmp($email, $onlyEmail) !== 0) continue; + + $checkMissingSent->execute([$m['employee_id'], $m['topic_id'], $email]); + if ($checkMissingSent->fetchColumn() > 0) { + $skipped++; + continue; + } + + try { + $mail = new PHPMailer(true); + $mailer = $_ENV['MAIL_MAILER'] ?? 'mail'; + if ($mailer === 'smtp') { + $mail->isSMTP(); + $mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost'; + $mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587); + if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') { + $mail->SMTPAuth = true; + $mail->Username = $_ENV['MAIL_USERNAME']; + $mail->Password = $_ENV['MAIL_PASSWORD'] ?? ''; + } + $enc = $_ENV['MAIL_ENCRYPTION'] ?? ''; + if ($enc && $enc !== 'null') { + $mail->SMTPSecure = $enc; + } + } + + $mail->CharSet = 'UTF-8'; + $mail->setFrom( + $_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it', + $_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA' + ); + $mail->addAddress($email, trim((string)$hr['name']) ?: $email); + + $profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$m['employee_id'] . '#tab-training'; + $topicText = $m['topic_name'] . ' — ' . $employeeFullName + . (!empty($m['employee_code']) ? ' (' . $m['employee_code'] . ')' : ''); + + $mail->Subject = '🔔 Formazione obbligatoria non presente: ' . $m['topic_name']; + $mail->Body = buildHtml( + 'Formazione obbligatoria non presente', + $topicText, + 'Il dipendente ' . htmlspecialchars($employeeFullName) . ' non ha nessuna registrazione per il corso obbligatorio ' . htmlspecialchars($m['topic_name']) . '. Programma la prima erogazione.', + '#6b7280', + $profileUrl, + true + ); + $mail->isHTML(true); + $mail->AltBody = strip_tags(str_replace('
', "\n", $mail->Body)); + + if ($dryRun) { + echo date('H:i:s') . " ◌ DRY not_present → {$email} — {$m['topic_name']} / {$employeeFullName}\n"; + $sent++; + continue; + } + + $mail->send(); + $insertMissingLog->execute([$m['employee_id'], $m['topic_id'], $email]); + $sent++; + echo date('H:i:s') . " ✓ not_present → {$email} — {$m['topic_name']} / {$employeeFullName}\n"; + + } catch (Exception $e) { + $errors++; + echo date('H:i:s') . " ✗ Errore {$email}: {$e->getMessage()}\n"; + } + } +} + +echo "\n" . date('Y-m-d H:i:s') . " — Completato. Inviate: {$sent}, Saltate: {$skipped}, Errori: {$errors}\n"; + +// --- HTML email template --- +function buildHtml(string $title, string $topic, string $message, string $accentColor, string $url, bool $isForHr): string +{ + $greeting = $isForHr + ? 'Una formazione richiede attenzione.' + : 'Una delle tue formazioni richiede attenzione.'; + return ' + + + + + + +
+ + + + +
+

' . htmlspecialchars($title) . '

+
+

' . htmlspecialchars($greeting) . '

+

' . htmlspecialchars($topic) . '

+

' . $message . '

+ Apri profilo +
+

ZIBOGOMMA — Formazione

+
+
+ +'; +} diff --git a/public/userarea/departments.php b/public/userarea/departments.php index e2d725e..aa4316b 100644 --- a/public/userarea/departments.php +++ b/public/userarea/departments.php @@ -256,7 +256,6 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC); - @@ -367,7 +366,7 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC); -
+
diff --git a/public/userarea/employee-profile.php b/public/userarea/employee-profile.php new file mode 100644 index 0000000..1861576 --- /dev/null +++ b/public/userarea/employee-profile.php @@ -0,0 +1,1991 @@ +getConnection(); + +/* ========================================== + PERMISSIONS + ========================================== */ +$isHrManager = Auth::user()->hasRole('Admin') + || Auth::user()->hasRole('Superuser') + || Auth::user()->hasRole('employee-hr') + || Auth::user()->hasRole('manager'); + +/* ========================================== + RESOLVE TARGET EMPLOYEE + ========================================== */ +$requestedId = isset($_GET['id']) ? (int)$_GET['id'] : 0; +$isOwnProfile = ($requestedId === 0); + +if ($isOwnProfile) { + $stmt = $pdo->prepare("SELECT id FROM employees WHERE auth_user_id = :uid LIMIT 1"); + $stmt->execute(['uid' => $iduserlogin]); + $employeeId = (int)$stmt->fetchColumn(); +} else { + if (!$isHrManager) { + // Non-HR users can only view their own profile. + header('Location: employee-profile.php'); + exit; + } + $employeeId = $requestedId; +} + +/* ========================================== + LOAD EMPLOYEE DATA + ========================================== */ +$employee = null; +if ($employeeId > 0) { + $stmt = $pdo->prepare(" + SELECT e.*, + d.name AS department_name, + d.color AS department_color, + jr.name AS job_role_name, + 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 + 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 auth_users au ON au.id = e.auth_user_id + WHERE e.id = :id + LIMIT 1 + "); + $stmt->execute(['id' => $employeeId]); + $employee = $stmt->fetch(PDO::FETCH_ASSOC) ?: null; +} + +/* Authorization: own profile must match auth_user_id (defence in depth) */ +if (!$isHrManager && $employee && (int)$employee['auth_user_id'] !== (int)$iduserlogin) { + header('Location: employee-profile.php'); + exit; +} + +$canEdit = $isHrManager; + +/* ========================================== + DOCUMENTS (File Repository) + ========================================== */ +$documents = []; +if ($employee) { + $stmt = $pdo->prepare(" + SELECT d.*, + TRIM(CONCAT(COALESCE(au.first_name,''),' ',COALESCE(au.last_name,''))) AS uploader_name, + au.email AS uploader_email + FROM employee_documents d + LEFT JOIN auth_users au ON au.id = d.uploaded_by + WHERE d.employee_id = :eid + ORDER BY d.created_at DESC + "); + $stmt->execute(['eid' => $employeeId]); + $documents = $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +/* ========================================== + TRAINING HISTORY + ========================================== */ +$trainings = []; +$trainingTopicsAll = []; +$missingMandatoryTopics = []; +if ($employee) { + $stmt = $pdo->prepare(" + SELECT et.*, + tt.name AS topic_name, + tt.default_frequency_months AS topic_default_freq, + tt.default_reminder_days AS topic_default_rem, + (SELECT COUNT(*) FROM employee_training_attachments a WHERE a.training_id = et.id) AS attachments_count + FROM employee_trainings et + JOIN training_topics tt ON tt.id = et.training_topic_id + WHERE et.employee_id = :eid + ORDER BY et.completed_date DESC, et.id DESC + "); + $stmt->execute(['eid' => $employeeId]); + $trainings = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if ($canEdit) { + $trainingTopicsAll = $pdo->query(" + SELECT id, name, default_frequency_months, default_reminder_days + FROM training_topics + WHERE is_active = 1 + ORDER BY sort_order, name + ")->fetchAll(PDO::FETCH_ASSOC); + } + + $missingStmt = $pdo->prepare(" + SELECT tt.id, tt.name + FROM training_topics tt + WHERE tt.is_active = 1 AND tt.is_mandatory = 1 + AND NOT EXISTS ( + SELECT 1 FROM employee_trainings et + WHERE et.employee_id = :eid AND et.training_topic_id = tt.id + ) + ORDER BY tt.sort_order, tt.name + "); + $missingStmt->execute(['eid' => $employeeId]); + $missingMandatoryTopics = $missingStmt->fetchAll(PDO::FETCH_ASSOC); +} + +/* ========================================== + PPE (Assigned) + ========================================== */ +$ppeList = []; +if ($employee) { + $stmt = $pdo->prepare(" + SELECT * + FROM employee_ppe + WHERE employee_id = :eid + ORDER BY delivery_date DESC, created_at DESC + "); + $stmt->execute(['eid' => $employeeId]); + $ppeList = $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +/* ========================================== + DROPDOWN DATA FOR EDIT MODAL + ========================================== */ +$departments = $isHrManager + ? $pdo->query("SELECT id, name FROM departments WHERE is_active = 1 ORDER BY sort_order, name")->fetchAll(PDO::FETCH_ASSOC) + : []; +$jobRoles = $isHrManager + ? $pdo->query("SELECT id, name FROM job_roles WHERE is_active = 1 ORDER BY sort_order, name")->fetchAll(PDO::FETCH_ASSOC) + : []; +$authUsers = $isHrManager + ? $pdo->query("SELECT id, username, first_name, last_name, email, role_id FROM auth_users ORDER BY first_name, last_name")->fetchAll(PDO::FETCH_ASSOC) + : []; +$rolesList = $isHrManager + ? $pdo->query("SELECT id, name, display_name FROM auth_roles ORDER BY display_name, name")->fetchAll(PDO::FETCH_ASSOC) + : []; + +/* ========================================== + HELPERS + ========================================== */ +function statusBadge(string $status): array { + switch ($status) { + case 'active': return ['label' => 'Attivo', 'class' => 'success']; + case 'inactive': return ['label' => 'Cessato', 'class' => 'secondary']; + case 'suspended': return ['label' => 'Sospeso', 'class' => 'warning']; + default: return ['label' => htmlspecialchars($status), 'class' => 'secondary']; + } +} +function fmtDate(?string $d): string { + if (!$d || $d === '0000-00-00') return '—'; + $ts = strtotime($d); + return $ts ? date('d/m/Y', $ts) : '—'; +} +function valOrDash($v): string { + $v = (string)($v ?? ''); + return $v !== '' ? htmlspecialchars($v) : '—'; +} +function categoryLabel(string $c): string { + switch ($c) { + case 'job_description': return 'Mansionario'; + case 'contract': return 'Contratto'; + case 'rules': return 'Regolamento'; + case 'other': return 'Altro'; + default: return htmlspecialchars($c); + } +} +function trainingStatus(?string $nextDue, ?int $reminderDays, ?int $topicDefaultRem): array { + if (!$nextDue) { + return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success']; + } + $rem = $reminderDays !== null ? $reminderDays : ($topicDefaultRem !== null ? $topicDefaultRem : 30); + $today = new DateTime('today'); + $due = DateTime::createFromFormat('Y-m-d', $nextDue); + if (!$due) return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success']; + $daysLeft = (int)$today->diff($due)->format('%r%a'); + if ($daysLeft < 0) return ['code' => 'expired', 'label' => 'Scaduto', 'class' => 'danger']; + if ($daysLeft <= $rem) return ['code' => 'due_soon', 'label' => 'Da aggiornare', 'class' => 'warning']; + return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success']; +} +function fmtFileSize(?int $bytes): string { + if ($bytes === null || $bytes <= 0) return '—'; + if ($bytes < 1024) return $bytes . ' B'; + if ($bytes < 1024 * 1024) return number_format($bytes / 1024, 1) . ' KB'; + if ($bytes < 1024 * 1024 * 1024) return number_format($bytes / 1024 / 1024, 1) . ' MB'; + return number_format($bytes / 1024 / 1024 / 1024, 1) . ' GB'; +} +?> + + + + + + + + + Profilo Dipendente - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + + + + + + + +
+ + + +
+
+ +
+
+
+ +
+ +
+ +
+ + +
+ +

+ +

+

+ +

+
+ + + + +
+
+ + avatar + + + +
+
+

+ +
+ Codice: +
+ +
+ + 💼 + + + + 🏢 + + + + + +
+
+ + + +
+ + + + + +
+ + +
+ +
+ +
+
+
+
Nome
+
+
+
+
Cognome
+
+
+
+
Codice Dipendente
+
+
+
+
Data di Assunzione
+
+
+
+
Indirizzo
+
+
+
+
Telefono
+
+ + + +
+
+
+
Email
+
+ + + +
+
+
+
Reparto
+
+
+
+
Mansione
+
+
+
+
Stato
+
+
+
+
Utente collegato
+
+ + + + + + + Nessun utente collegato + +
+
+
+
+ + +
+ +
+ +
+ + + +
+ +
Nessun documento
+

+ +

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
CategoriaNome FileDimensioneCaricato daDataAzioni
+ + + + +
+ +
+ ⬇️ Scarica + + + +
+
+ + +
+ + +
+
+ + +
+ + + + +
+ +
+ Dim.: + Da: +
+
+ ⬇️ Scarica + + + +
+
+ +
+ +
+ + +
+ +
+ +
+ + + +
+ +
Nessun DPI assegnato
+

+ +

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
DPIData ConsegnaConsegnato daNoteAzioni
+ + +
+
+ + +
+ + +
+
+ 🦺 + +
+
+ + Consegnato da: + + + Note: + +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ +
+ + ⚠️ + obbligator: + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
Nessuna formazione registrata
+

+ +

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
CorsoTipoCompletatoProssimo agg.StatoAllegatiAzioni
+ + + + + + +
+
+ + +
+ + +
+
+ 📖 + +
+
+ Tipo: + Completato: + + Prossimo: + + 0): ?> + Allegati: + +
+
+ + + + + + +
+
+ +
+ + +
+
+ + +
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/userarea/employees.php b/public/userarea/employees.php index 2b25e30..c80c278 100644 --- a/public/userarea/employees.php +++ b/public/userarea/employees.php @@ -17,16 +17,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj try { if ($action === 'add') { - // Codice originale per add $employee_code = trim($_POST['employee_code'] ?? ''); $first_name = trim($_POST['first_name'] ?? ''); $last_name = trim($_POST['last_name'] ?? ''); + $address = trim($_POST['address'] ?? ''); + $phone = trim($_POST['phone'] ?? ''); + $email = trim($_POST['email'] ?? ''); $department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null; - $position = trim($_POST['position'] ?? ''); + $job_role_id = ($_POST['job_role_id'] ?? '') !== '' ? (int)$_POST['job_role_id'] : null; $hire_date = trim($_POST['hire_date'] ?? ''); $status = trim($_POST['status'] ?? 'active'); $auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null; - $role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null; + $role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null; if ($first_name === '' || $last_name === '') { echo json_encode([ @@ -35,23 +37,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj ]); exit; } - + if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) { + echo json_encode(['success' => false, 'message' => 'Email non valida.']); + exit; + } if (!in_array($status, ['active', 'inactive', 'suspended'], true)) { $status = 'active'; } $sql = "INSERT INTO employees - (auth_user_id, employee_code, first_name, last_name, department_id, position, hire_date, status, created_at, updated_at) + (auth_user_id, employee_code, first_name, last_name, address, phone, email, + department_id, job_role_id, hire_date, status, created_at, updated_at) VALUES - (:auth_user_id, :employee_code, :first_name, :last_name, :department_id, :position, :hire_date, :status, NOW(), NOW())"; + (:auth_user_id, :employee_code, :first_name, :last_name, :address, :phone, :email, + :department_id, :job_role_id, :hire_date, :status, NOW(), NOW())"; $stmt = $pdo->prepare($sql); $stmt->execute([ 'auth_user_id' => $auth_user_id, 'employee_code' => $employee_code !== '' ? $employee_code : null, 'first_name' => $first_name, 'last_name' => $last_name, + 'address' => $address !== '' ? $address : null, + 'phone' => $phone !== '' ? $phone : null, + 'email' => $email !== '' ? $email : null, 'department_id' => $department_id, - 'position' => $position !== '' ? $position : null, + 'job_role_id' => $job_role_id, 'hire_date' => $hire_date !== '' ? $hire_date : null, 'status' => $status ]); @@ -74,17 +84,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj } if ($action === 'edit') { - // Codice originale per edit - $id = (int)($_POST['id'] ?? 0); + $id = (int)($_POST['id'] ?? 0); $employee_code = trim($_POST['employee_code'] ?? ''); $first_name = trim($_POST['first_name'] ?? ''); $last_name = trim($_POST['last_name'] ?? ''); + $address = trim($_POST['address'] ?? ''); + $phone = trim($_POST['phone'] ?? ''); + $email = trim($_POST['email'] ?? ''); $department_id = $_POST['department_id'] !== '' ? (int)$_POST['department_id'] : null; - $position = trim($_POST['position'] ?? ''); + $job_role_id = ($_POST['job_role_id'] ?? '') !== '' ? (int)$_POST['job_role_id'] : null; $hire_date = trim($_POST['hire_date'] ?? ''); $status = trim($_POST['status'] ?? 'active'); $auth_user_id = $_POST['auth_user_id'] !== '' ? (int)$_POST['auth_user_id'] : null; - $role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null; + $role_id = $_POST['role_id'] !== '' ? (int)$_POST['role_id'] : null; if ($id <= 0) { echo json_encode(['success' => false, 'message' => 'Invalid employee ID.']); @@ -98,7 +110,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj ]); exit; } - + if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) { + echo json_encode(['success' => false, 'message' => 'Email non valida.']); + exit; + } if (!in_array($status, ['active', 'inactive', 'suspended'], true)) { $status = 'active'; } @@ -108,8 +123,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj employee_code = :employee_code, first_name = :first_name, last_name = :last_name, + address = :address, + phone = :phone, + email = :email, department_id = :department_id, - position = :position, + job_role_id = :job_role_id, hire_date = :hire_date, status = :status, updated_at = NOW() @@ -120,8 +138,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['aj 'employee_code' => $employee_code !== '' ? $employee_code : null, 'first_name' => $first_name, 'last_name' => $last_name, + 'address' => $address !== '' ? $address : null, + 'phone' => $phone !== '' ? $phone : null, + 'email' => $email !== '' ? $email : null, 'department_id' => $department_id, - 'position' => $position !== '' ? $position : null, + 'job_role_id' => $job_role_id, 'hire_date' => $hire_date !== '' ? $hire_date : null, 'status' => $status, 'id' => $id @@ -223,6 +244,7 @@ $sql = " SELECT e.*, d.name AS department_name, d.color AS department_color, + jr.name AS job_role_name, au.email AS user_email, au.role_id AS user_role_id, ar.display_name AS role_display_name, @@ -230,6 +252,7 @@ $sql = " CONCAT(COALESCE(au.first_name, ''), ' ', COALESCE(au.last_name, '')) AS user_fullname FROM employees e LEFT JOIN departments d ON e.department_id = d.id + LEFT JOIN job_roles jr ON jr.id = e.job_role_id LEFT JOIN auth_users au ON e.auth_user_id = au.id LEFT JOIN auth_roles ar ON ar.id = au.role_id ORDER BY e.id DESC @@ -237,6 +260,11 @@ $sql = " $stmtEmployees = $pdo->query($sql); $employees = $stmtEmployees->fetchAll(PDO::FETCH_ASSOC); +// Job roles for the dropdown +$jobRoles = $pdo->query(" + SELECT id, name FROM job_roles WHERE is_active = 1 ORDER BY sort_order, name +")->fetchAll(PDO::FETCH_ASSOC); + // Users list for select $sqlUsers = " SELECT id, @@ -297,7 +325,6 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC); - @@ -415,7 +442,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC); -
+
@@ -448,14 +475,15 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC); ID - Code - Name - Department - Position - Hire Date - Status - Linked User - Actions + Codice + Nome + Contatti + Reparto + Mansione + Data Assunzione + Stato + Utente collegato + Azioni @@ -484,7 +512,24 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC); - + + + + + + + + + ✉️ +
+ + + + 📞 + + + - + @@ -494,7 +539,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC); - - + @@ -510,7 +555,10 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC); data-first_name="" data-last_name="" data-department_id="" - data-position="" + data-job_role_id="" + data-address="" + data-phone="" + data-email="" data-hire_date="" data-status="" data-auth_user_id="" @@ -560,26 +608,42 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);