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' ]; ?>
| Name | Updated | Status | Subjects / Details | Actions | |
|---|---|---|---|---|---|
| = $name_disp ?> preferred_name): ?> (= esc_html($c->preferred_name) ?>) | = esc_html($c->email) ?> | = esc_html(date('Y-m-d H:i', strtotime($c->updated_at))) ?> |
= $clabel ?>
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)): ?>
✓ = date('m/d H:i', strtotime($c->training_completed_at)) ?> Completed
|
status === 'applied'): ?> Waiting for detailed form = esc_html($s) ?> 3): ?>+= count($subs) - 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
status, ['training', 'trained']) && !empty($c->training_token)): ?>
= esc_html(add_query_arg('im_token', $tk2->token, IM_INTERVIEW_PAGE_URL)) ?> |
| No candidate data available | |||||
Candidate does not exist.
This candidate has not submitted the detailed application form yet, so details are unavailable.
Decide whether to invite this candidate for an interview
If the candidate did not receive the email, you can resend it
Video has been submitted. Review it and hire if approved.
Candidate is completing training modules
training_opened_at)): ?>Candidate has passed all training modules
training_completed_at)): ?>Video is offline or missing
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;
}
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
Go to Dashboard → Settings → Permalinks, then click "Save Changes" directly (no settings change needed).
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');
| Configuration | Current Value | Status |
|---|---|---|
= $name ?> |
= esc_html($val) ?> | = $ok ? '✅' : '⚠️ ' . $tip ?> |
| Video Storage Directory | = esc_html($upload_dir['basedir'] . '/interviews/') ?> | = is_dir($upload_dir['basedir'] . '/interviews') ? (is_writable($upload_dir['basedir'] . '/interviews') ? '✅ Writable' : '❌ Not writable') : '(Auto-created on first upload)' ?> |
| Application Files Directory | = esc_html($upload_dir['basedir'] . '/im-applications/') ?> | = is_dir($upload_dir['basedir'] . '/im-applications') ? (is_writable($upload_dir['basedir'] . '/im-applications') ? '✅ Writable' : '❌ Not writable') : '(Auto-created on first upload)' ?> |
| Interview Page URL | = esc_html(IM_INTERVIEW_PAGE_URL) ?> | — |
| Training Page URL | = esc_html(IM_TRAINING_PAGE_URL) ?> | — |
| Training Temporary Account | = esc_html(IM_TRAINING_ACCOUNT) ?> | = IM_TRAINING_ACCOUNT !== 'your_account@example.com' ? '✅' : '⚠️ Configure real account' ?> |