# 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 ```