id, $pages, true) && strpos($screen->id, 'im-candidates') === false && strpos($screen->id, 'im-candidate-detail') === false) return; ?> 'Insufficient permissions']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => 'Invalid parameters']); $c = IM_Candidate::get($id); if ($c && $c->status === 'screening') { IM_Candidate::update_status($id, 'invited'); } IM_Mailer::send_interview_invite($id) ? wp_send_json_success(['message' => 'Interview invitation email sent successfully!']) : wp_send_json_error(['message' => 'Email sending failed. Please check mail configuration.']); }); add_action('wp_ajax_im_action_reject', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => 'Insufficient permissions']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => 'Invalid parameters']); IM_Candidate::update_status($id, 'rejected'); IM_Mailer::send_reject($id) ? wp_send_json_success(['message' => 'Candidate rejected and email sent!']) : wp_send_json_error(['message' => 'Status updated, but email sending failed.']); }); add_action('wp_ajax_im_action_hire', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => 'Insufficient permissions']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => 'Invalid parameters']); IM_Mailer::send_hire($id) ? wp_send_json_success(['message' => 'Candidate hired and training link email sent!']) : wp_send_json_error(['message' => 'Hiring failed. Please check mail configuration.']); }); add_action('wp_ajax_im_action_resend_joinus', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => 'Insufficient permissions']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => 'Invalid parameters']); IM_Mailer::send_joinus_confirmation($id) ? wp_send_json_success(['message' => 'Form link email resent!']) : wp_send_json_error(['message' => 'Email sending failed.']); }); add_action('wp_ajax_im_action_resend_training', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => 'Insufficient permissions']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => 'Invalid parameters']); $c = IM_Candidate::get($id); if (!$c || $c->status !== 'training') wp_send_json_error(['message' => 'Candidate is not in training status']); IM_Mailer::send_hire($id) ? wp_send_json_success(['message' => 'Training link email resent!']) : wp_send_json_error(['message' => 'Email sending failed.']); }); add_action('wp_ajax_im_action_resend_trained_email', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => 'Insufficient permissions']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => 'Invalid parameters']); $c = IM_Candidate::get($id); if (!$c || $c->status !== 'trained') wp_send_json_error(['message' => 'Candidate is not in training-completed status']); IM_Mailer::send_training_complete($id) ? wp_send_json_success(['message' => 'Account email resent!']) : wp_send_json_error(['message' => 'Email sending failed.']); }); add_action('wp_ajax_im_action_delete', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => 'Insufficient permissions']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => 'Invalid parameters']); IM_Candidate::delete_with_files($id) ? wp_send_json_success(['message' => 'Candidate record and related files deleted!']) : wp_send_json_error(['message' => 'Delete failed, please try again.']); }); /* ============================================================ 候选人列表页 ============================================================ */ function im_admin_list() { $page = max(1, (int) ($_GET['paged'] ?? 1)); $status = sanitize_text_field($_GET['status'] ?? ''); $search = sanitize_text_field($_GET['s'] ?? ''); $per_page = 20; $args = compact('page', 'status', 'search') + ['per_page' => $per_page]; $items = IM_Candidate::get_list($args); $total = IM_Candidate::count($args); $pages = (int) ceil($total / $per_page); $cnt = [ '' => IM_Candidate::count(), 'applied' => IM_Candidate::count(['status' => 'applied']), 'screening' => IM_Candidate::count(['status' => 'screening']), 'invited' => IM_Candidate::count(['status' => 'invited']), 'rejected' => IM_Candidate::count(['status' => 'rejected']), 'completed' => IM_Candidate::count(['status' => 'completed']), 'hired' => IM_Candidate::count(['status' => 'hired']), 'training' => IM_Candidate::count(['status' => 'training']), 'trained' => IM_Candidate::count(['status' => 'trained']), ]; $tabs = [ '' => 'All', 'applied' => 'Applied', 'screening' => 'Pending Review', 'invited' => 'Invited', 'rejected' => 'Rejected', 'completed' => 'Completed', 'hired' => 'Hired', 'training' => 'Training In Progress', 'trained' => 'Training Completed' ]; $status_lbl = [ 'applied' => 'Awaiting Detailed Form', 'screening' => 'Detailed Form Submitted', 'invited' => 'Invited', 'rejected' => 'Rejected', 'completed' => 'Completed', 'hired' => 'Hired', 'training' => 'Training In Progress', 'trained' => 'Training Completed' ]; $colors = [ 'applied' => '#f59e0b', 'screening' => '#8b5cf6', 'invited' => '#3b82f6', 'rejected' => '#ef4444', 'completed' => '#10b981', 'hired' => '#059669', 'training' => '#f97316', 'trained' => '#06b6d4' ]; ?>

Candidate Management


id); $subs = IM_Candidate::get_subjects($c); $color = $colors[$c->status] ?? '#6b7280'; $clabel = $status_lbl[$c->status] ?? $c->status; $name_disp = $c->status === 'applied' ? esc_html($c->last_name . ' ' . $c->first_name) : '' . esc_html($c->last_name . ' ' . $c->first_name) . ''; ?>
Name Email Updated Status Subjects / Details Actions
preferred_name): ?> (preferred_name) ?>) email) ?> updated_at))) ?> status === 'applied' && !empty($c->apply_opened_at)): ?>
✓ Form opened
status === 'applied' && empty($c->apply_opened_at)): ?>
○ Form not opened
status === 'invited'): ?> id); if (!empty($tk_list)): $tk = $tk_list[0]; ?> opened_at)): ?>
✓ Interview questions viewed
○ Interview link not opened
status === 'training'): ?> training_opened_at)): ?>
✓ Training page opened
○ Training page not opened
status === 'trained'): ?> training_completed_at)): ?>
training_completed_at)) ?> Completed
status === 'applied'): ?> Waiting for detailed form 3): ?>+ status !== 'applied'): ?> View Details status === 'applied'): ?> status === 'screening'): ?> status === 'invited'): ?> status === 'completed'): ?> status === 'training'): ?> status === 'trained'): ?>
apply_token_used) && $c->apply_token): ?> status, ['invited', 'completed', 'hired', 'rejected', 'training', 'trained'])): $latest_token = IM_Token::get_by_candidate($c->id); if (!empty($latest_token)): $tk2 = $latest_token[0]; ?>
Debug: Interview Link
token, IM_INTERVIEW_PAGE_URL)) ?>
status, ['training', 'trained']) && !empty($c->training_token)): ?>
No candidate data available
1): $base = admin_url('admin.php?page=im-candidates' . ($status ? '&status=' . $status : '') . ($search ? '&s=' . urlencode($search) : '')); ?>

Candidate does not exist.

'; return; } if ($c->status === 'applied') { echo '

This candidate has not submitted the detailed application form yet, so details are unavailable.

Back to List
'; return; } $subjects = IM_Candidate::get_subjects($c); $tokens = IM_Token::get_by_candidate($id); $attachments = IM_Attachment::get_by_candidate($id); $upload_dir = wp_upload_dir(); $status_lbl = [ 'applied' => 'Awaiting Detailed Form', 'screening' => 'Detailed Form Submitted', 'invited' => 'Invited', 'rejected' => 'Rejected', 'completed' => 'Completed', 'hired' => 'Hired', 'training' => 'Training In Progress', 'trained' => 'Training Completed' ]; $status_colors = [ 'applied' => ['bg' => '#fffbeb', 'text' => '#b45309', 'border' => '#fde68a'], 'screening' => ['bg' => '#f3e8ff', 'text' => '#6d28d9', 'border' => '#e9d5ff'], 'invited' => ['bg' => '#eff6ff', 'text' => '#1d4ed8', 'border' => '#bfdbfe'], 'rejected' => ['bg' => '#fef2f2', 'text' => '#b91c1c', 'border' => '#fecaca'], 'completed' => ['bg' => '#ecfdf5', 'text' => '#047857', 'border' => '#a7f3d0'], 'hired' => ['bg' => '#ecfccb', 'text' => '#166534', 'border' => '#d9f99d'], 'training' => ['bg' => '#fff7ed', 'text' => '#c2410c', 'border' => '#fed7aa'], 'trained' => ['bg' => '#ecfeff', 'text' => '#0e7490', 'border' => '#a5f3fc'] ]; $full_name = esc_html($c->first_name . ' ' . $c->last_name); $p_name = esc_html($c->preferred_name); $initials = mb_substr(trim($c->first_name), 0, 1) . mb_substr(trim($c->last_name), 0, 1); if (!$initials && $full_name) $initials = mb_substr($full_name, 0, 1); $s_col = $status_colors[$c->status]; ?>
Back to Candidate List

(' . $p_name . ')' : '' ?>

email) ?> phone): ?> phone) ?> country) || !empty($c->city)): ?> city ?? '') . (!empty($c->city) && !empty($c->country) ? ', ' : '') . ($c->country ?? ''))) ?>
status] ?>

Education

'Bachelor', "Master's" => 'Master', 'PhD' => 'PhD']; ?>
grad_year ?: 'Unknown year') ?> Graduated (degree_level] ?? ($c->degree_level ?: 'Highest Degree')) ?>)

university ?: '—') ?>

Major: major ?: '—') ?>  |  GPA:gpa ?: '—') ?> deans_list ? ' |  🏅 Dean\'s List' : '' ?>
ug_university): ?>
ug_grad_year ?: 'Unknown year') ?> Graduated (Bachelor Degree)

ug_university) ?>

Major: ug_major ?: '—') ?>
ms_university)): ?>
ms_grad_year ?: 'Unknown year') ?> Graduated (Master Degree)

ms_university) ?>

Major: ms_major ?: '—') ?>
ca_highschool): ?>
Canadian High School

ca_highschool_name ?: '—') ?>

e.g. OSSD or BC curriculum

Skills & Experience

Subjects ()
No subjects selected
English Fluency
'Native', 'Fluent' => 'Fluent', 'Basic' => 'Basic']; echo esc_html($fluency_map[$c->languages] ?? ($c->languages ?: '—')); ?>
Teaching Experience
teaching_exp ?: '—')) ?>
has_achievement): ?>
Personal Achievement (achievement_type) ?>)
achievement_desc ?: '—')) ?>
extra_notes): ?>
Additional Notes
extra_notes)) ?>
status === 'screening'): ?>

Review Actions

Decide whether to invite this candidate for an interview

status === 'invited'): ?>

Awaiting Interview

If the candidate did not receive the email, you can resend it

status === 'completed'): ?>

Interview Review

Video has been submitted. Review it and hire if approved.

status === 'training'): ?>

Training In Progress

Candidate is completing training modules

training_opened_at)): ?>
✓ Opened training page at training_opened_at)) ?>
○ Training page not opened yet
status === 'trained'): ?>

Training Completed

Candidate has passed all training modules

training_completed_at)): ?>
✓ Completed training at training_completed_at)) ?>

Attachments

file_type][] = $att; $att_groups = ['transcript_files' => 'Transcripts', 'achievement_files' => 'Achievement Proofs', 'extra_files' => 'Additional Materials']; foreach ($att_groups as $type => $label): if (empty($grouped[$type])) continue; ?>
id . '/' . basename($att->file_path); ?>
file_name) ?>
status, ['invited', 'completed', 'hired', 'rejected', 'training', 'trained'])): ?>

Interview Records

is_used; $expired = strtotime($tk->expires_at) < time(); if ($used) { $bs = '#dcfce7'; $bc = '#86efac'; $bt = '#166534'; $bl = 'Video Submitted'; } elseif ($expired) { $bs = '#fee2e2'; $bc = '#fca5a5'; $bt = '#991b1b'; $bl = 'Link Expired'; } else { $bs = '#dbeafe'; $bc = '#93c5fd'; $bt = '#1e40af'; $bl = 'Active'; } ?>
created_at)) ?> Sent
Valid until expires_at)) ?>
opened_at)): ?>
✓ Viewed questions at opened_at)) ?>
○ Candidate has not opened the link
video_path && file_exists($tk->video_path)): $vurl = $upload_dir['baseurl'] . '/interviews/' . $c->id . '/' . $tk->video_filename; $fext = strtolower(pathinfo($tk->video_filename, PATHINFO_EXTENSION)); $archive_exts = ['zip', 'rar', '7z', 'gz', 'tar']; $fsize = filesize($tk->video_path); $fsize_str = $fsize > 1048576 ? round($fsize / 1048576, 1) . ' MB' : round($fsize / 1024, 1) . ' KB'; ?>
video_filename) ?>
archive ·
Download
submitted_at)) ?> submitted

Video is offline or missing

Interview invitation has not been sent yet
= 400 * 1024 * 1024, 'Suggested: 500M'], ['post_max_size', ini_get('post_max_size'), wp_convert_hr_to_bytes(ini_get('post_max_size')) >= 400 * 1024 * 1024, 'Suggested: 512M'], ['max_execution_time', ini_get('max_execution_time') . 's', (int) ini_get('max_execution_time') === 0 || (int) ini_get('max_execution_time') >= 180, 'Suggested: 300'], ['memory_limit', ini_get('memory_limit'), true, '—'], ]; ?>

Interview Manager — Server Setup Wizard

1) Nginx Configuration (manual required)

Add the following config inside the server { } block, then run sudo nginx -t && sudo nginx -s reload:

# Video upload limits
            client_max_body_size 512M;
            client_body_timeout  300s;
            send_timeout         300s;

            # WordPress permalinks (skip if already set)
            location / {
                try_files $uri $uri/ /index.php?$args;
            }

            # Upload directory security
            location ~* ^/wp-content/uploads/(interviews|im-applications)/ {
                add_header X-Content-Type-Options nosniff;
            }

2) PHP Configuration (php.ini or .user.ini)

upload_max_filesize = 500M
            post_max_size       = 512M
            max_execution_time  = 300
            max_input_time      = 300
            memory_limit        = 256M

Restart PHP-FPM after changes: sudo systemctl restart php8.x-fpm

3) Refresh WordPress Permalinks

Go to Dashboard → Settings → Permalinks, then click "Save Changes" directly (no settings change needed).

Go to Permalink Settings →

4) WordPress Page Setup

Create the following pages in WordPress admin:

Page Shortcode Suggested URL slug
Join Us Registration Page [im_joinus_form] /join-us/
Detailed Application Form Page [im_apply_form] /apply/
Interview Page [im_interview] /interview/
Training Page [im_training] /training/

After creating pages, update the constants at the top of snippet 1 with actual URLs:

define('IM_APPLY_PAGE_URL', home_url('/apply/'));
define('IM_INTERVIEW_PAGE_URL', home_url('/interview/'));
define('IM_TRAINING_PAGE_URL', home_url('/training/'));
define('IM_TRAINING_ACCOUNT', 'your_account@example.com');
define('IM_TRAINING_PASSWORD', 'your_password');

5) Current Environment Check

Configuration Current Value Status
Video Storage Directory
Application Files Directory
Interview Page URL
Training Page URL
Training Temporary Account