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'); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = $isOwnProfile
+ ? 'Il tuo profilo dipendente non è ancora stato creato'
+ : 'Dipendente non trovato' ?>
+
+
+ = $isOwnProfile
+ ? 'Contatta il responsabile HR per la creazione del tuo profilo.'
+ : 'Verifica l\'ID o torna alla lista dipendenti.' ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nome
+
= valOrDash($employee['first_name']) ?>
+
+
+
Cognome
+
= valOrDash($employee['last_name']) ?>
+
+
+
Codice Dipendente
+
= valOrDash($employee['employee_code']) ?>
+
+
+
Data di Assunzione
+
= fmtDate($employee['hire_date']) ?>
+
+
+
Indirizzo
+
= valOrDash($employee['address']) ?>
+
+
+
+
+
Reparto
+
= valOrDash($deptName) ?>
+
+
+
Mansione
+
= valOrDash($jobName) ?>
+
+
+
Stato
+
= htmlspecialchars($status['label']) ?>
+
+
+
Utente collegato
+
+
+ = htmlspecialchars($employee['auth_username']) ?>
+
+ = htmlspecialchars($employee['auth_email']) ?>
+
+
+ Nessun utente collegato
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nessun documento
+
+ = $canEdit
+ ? 'Carica il primo documento (mansionario, contratto, regolamento ecc.).'
+ : 'Nessun documento disponibile al momento.' ?>
+
+
+
+
+
+
+
+
+ | Categoria |
+ Nome File |
+ Dimensione |
+ Caricato da |
+ Data |
+ Azioni |
+
+
+
+
+
+
+ | = categoryLabel((string)$d['category']) ?> |
+
+
+ = htmlspecialchars($d['original_name']) ?>
+
+
+ = htmlspecialchars($d['notes']) ?>
+
+ |
+ = fmtFileSize($d['size'] !== null ? (int)$d['size'] : null) ?> |
+ = htmlspecialchars($upBy) ?> |
+ = fmtDate(substr((string)$d['created_at'], 0, 10)) ?> |
+
+ ⬇️ Scarica
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ = categoryLabel((string)$d['category']) ?>
+ = fmtDate(substr((string)$d['created_at'], 0, 10)) ?>
+
+
+ = htmlspecialchars($d['original_name']) ?>
+
+
+
= htmlspecialchars($d['notes']) ?>
+
+
+ Dim.: = fmtFileSize($d['size'] !== null ? (int)$d['size'] : null) ?>
+ Da: = htmlspecialchars($upBy) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nessun DPI assegnato
+
+ = $canEdit
+ ? 'Aggiungi il primo dispositivo di protezione individuale.'
+ : 'Nessun DPI consegnato al momento.' ?>
+
+
+
+
+
+
+
+
+ | DPI |
+ Data Consegna |
+ Consegnato da |
+ Note |
+ Azioni |
+
+
+
+
+
+
+ | = htmlspecialchars($p['item_name']) ?> |
+ = fmtDate($p['delivery_date']) ?> |
+ = valOrDash($p['delivered_by']) ?> |
+ = valOrDash($p['notes']) ?> |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🦺 = htmlspecialchars($p['item_name']) ?>
+ = fmtDate($p['delivery_date']) ?>
+
+
+
+ Consegnato da: = htmlspecialchars($p['delivered_by']) ?>
+
+
+ Note: = htmlspecialchars($p['notes']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ⚠️ = count($missingMandatoryTopics) ?>
+ obbligator= count($missingMandatoryTopics) === 1 ? 'ia non presente' : 'ie non presenti' ?>:
+
+
+
+
+
+ = htmlspecialchars($mt['name']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nessuna formazione registrata
+
+ = $canEdit
+ ? 'Aggiungi la prima registrazione di formazione (iniziale o aggiornamento).'
+ : 'Nessuna formazione registrata al momento.' ?>
+
+
+
+
+
+
+
+
+ | Corso |
+ Tipo |
+ Completato |
+ Prossimo agg. |
+ Stato |
+ Allegati |
+ Azioni |
+
+
+
+
+
+
+ | = htmlspecialchars($t['topic_name']) ?> |
+ = $typeLabel ?> |
+ = fmtDate($t['completed_date']) ?> |
+ = fmtDate($t['next_due_date']) ?> |
+ = $s['label'] ?> |
+ = (int)$t['attachments_count'] ?> |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ 📖 = htmlspecialchars($t['topic_name']) ?>
+ = $s['label'] ?>
+
+
+ Tipo: = $typeLabel ?>
+ Completato: = fmtDate($t['completed_date']) ?>
+
+ Prossimo: = fmtDate($t['next_due_date']) ?>
+
+ 0): ?>
+ Allegati: = (int)$t['attachments_count'] ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Carica nuovo allegato
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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);
| = (int)$row['id'] ?> |
= htmlspecialchars($row['employee_code'] ?? '') ?> |
- = htmlspecialchars($fullName) ?> |
+
+
+ = htmlspecialchars($fullName) ?>
+
+ |
+
+
+
+ ✉️ = htmlspecialchars($row['email']) ?>
+
+
+
+
+ 📞 = htmlspecialchars($row['phone']) ?>
+
+
+ -
+ |
@@ -494,7 +539,7 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
-
|
- = htmlspecialchars($row['position'] ?? '') ?> |
+ = !empty($row['job_role_name']) ? htmlspecialchars($row['job_role_name']) : '-' ?> |
= $hireDate ?> |
@@ -510,7 +555,10 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
data-first_name="= htmlspecialchars($row['first_name'] ?? '', ENT_QUOTES) ?>"
data-last_name="= htmlspecialchars($row['last_name'] ?? '', ENT_QUOTES) ?>"
data-department_id="= $row['department_id'] !== null ? (int)$row['department_id'] : '' ?>"
- data-position="= htmlspecialchars($row['position'] ?? '', ENT_QUOTES) ?>"
+ data-job_role_id="= $row['job_role_id'] !== null ? (int)$row['job_role_id'] : '' ?>"
+ data-address="= htmlspecialchars($row['address'] ?? '', ENT_QUOTES) ?>"
+ data-phone="= htmlspecialchars($row['phone'] ?? '', ENT_QUOTES) ?>"
+ data-email="= htmlspecialchars($row['email'] ?? '', ENT_QUOTES) ?>"
data-hire_date="= htmlspecialchars($row['hire_date'] ?? '', ENT_QUOTES) ?>"
data-status="= htmlspecialchars($status, ENT_QUOTES) ?>"
data-auth_user_id="= $row['auth_user_id'] !== null ? (int)$row['auth_user_id'] : '' ?>"
@@ -560,26 +608,42 @@ $allSkills = $stmtSkills->fetchAll(PDO::FETCH_ASSOC);
|