| | |
| | | 注åèå |
| | | ============================================================ */ |
| | | add_action('admin_menu', function () { |
| | | add_menu_page('åé人管ç', 'åé人管ç', 'edit_others_posts', 'im-candidates', 'im_admin_list', 'dashicons-groups', 30); |
| | | add_submenu_page('im-candidates', 'åé人å表', 'åé人å表', 'edit_others_posts', 'im-candidates', 'im_admin_list'); |
| | | add_submenu_page('im-candidates', 'åé人详æ
', '', 'edit_others_posts', 'im-candidate-detail', 'im_admin_detail'); |
| | | add_submenu_page('tools.php', 'IM æå¡å¨é
ç½®', 'IM æå¡å¨é
ç½®', 'manage_options', 'im-server-setup', 'im_server_setup'); |
| | | add_menu_page('Candidate Management', 'Candidate Management', 'edit_others_posts', 'im-candidates', 'im_admin_list', 'dashicons-groups', 30); |
| | | add_submenu_page('im-candidates', 'Candidate List', 'Candidate List', 'edit_others_posts', 'im-candidates', 'im_admin_list'); |
| | | add_submenu_page('im-candidates', 'Candidate Details', '', 'edit_others_posts', 'im-candidate-detail', 'im_admin_detail'); |
| | | add_submenu_page('tools.php', 'IM Server Setup', 'IM Server Setup', 'manage_options', 'im-server-setup', 'im_server_setup'); |
| | | }); |
| | | |
| | | /* ============================================================ |
| | |
| | | var title = '', body = '', showTextarea = false; |
| | | |
| | | if (actionType === 'invite') { |
| | | title = '确认åéé¢è¯é请'; |
| | | body = 'å°å <strong>' + name + '</strong> åéé¢è¯é请ï¼<br>é®ä»¶å
å« <strong>24å°æ¶ææ</strong> çä¸å±é¢è¯é¾æ¥ï¼ç¡®è®¤åéï¼'; |
| | | title = 'Confirm Interview Invitation'; |
| | | body = 'An interview invitation will be sent to <strong>' + name + '</strong>.<br>The email includes a dedicated interview link valid for <strong>24 hours</strong>. Proceed?'; |
| | | } else if (actionType === 'reject') { |
| | | title = 'æç»åé人'; |
| | | body = '确认æç» <strong>' + name + '</strong> çç³è¯·ï¼<br>ç³»ç»å°åå
¶åéæç»éç¥é®ä»¶ã'; |
| | | title = 'Reject Candidate'; |
| | | body = 'Are you sure you want to reject <strong>' + name + '</strong>\'s application?<br>A rejection email will be sent.'; |
| | | } else if (actionType === 'hire') { |
| | | title = 'å½ååé人'; |
| | | body = '确认æ£å¼å½å <strong>' + name + '</strong>ï¼<br>ç³»ç»å°åå
¶åéå½åéç¥é®ä»¶åå¹è®é¾æ¥ã'; |
| | | title = 'Hire Candidate'; |
| | | body = 'Are you sure you want to hire <strong>' + name + '</strong>?<br>An offer email and training link will be sent.'; |
| | | } else if (actionType === 'resend_joinus') { |
| | | title = 'éæ°åéé®ä»¶'; |
| | | body = 'å°éæ°å <strong>' + name + '</strong> åéâå®å详ç»è¡¨åâ龿¥é®ä»¶ã'; |
| | | title = 'Resend Email'; |
| | | body = 'A follow-up email will be sent again to <strong>' + name + '</strong> with the "Complete Detailed Form" link.'; |
| | | } else if (actionType === 'resend_training') { |
| | | title = 'éæ°åéå¹è®é¾æ¥'; |
| | | body = 'å°éæ°å <strong>' + name + '</strong> åéå¹è®é¾æ¥é®ä»¶ã'; |
| | | title = 'Resend Training Link'; |
| | | body = 'A training link email will be resent to <strong>' + name + '</strong>.'; |
| | | } else if (actionType === 'resend_trained_email') { |
| | | title = 'éæ°åéè´¦å·é®ä»¶'; |
| | | body = 'å°éæ°å <strong>' + name + '</strong> åé Microsoft Teams 临æ¶è´¦å·é®ä»¶ã'; |
| | | title = 'Resend Account Email'; |
| | | body = 'A Microsoft Teams temporary account email will be resent to <strong>' + name + '</strong>.'; |
| | | } else if (actionType === 'delete') { |
| | | title = 'å é¤åé人'; |
| | | body = '确认永ä¹
å é¤ <strong>' + name + '</strong> çææè®°å½ï¼<br><span style=âcolor:#ef4444â>æ¤æä½ä¸å¯æ¢å¤ï¼å°åæ¶å é¤è¯¥åé人ä¸ä¼ çæææä»¶ï¼éä»¶ãé¢è¯è§é¢çï¼ã</span>'; |
| | | title = 'Delete Candidate'; |
| | | body = 'Permanently delete all records of <strong>' + name + '</strong>?<br><span style="color:#ef4444">This action is irreversible and will also delete all uploaded files (attachments, interview videos, etc.).</span>'; |
| | | } |
| | | |
| | | $('#im-modal h3').text(title); |
| | |
| | | if (!cid) return; |
| | | var reason = $('#im-modal-textarea').is(':visible') ? $.trim($('#im-modal-textarea').val()) : ''; |
| | | |
| | | var $b = $(this); $b.prop('disabled', true).text('å¤çä¸...'); |
| | | var $b = $(this); $b.prop('disabled', true).text('Processing...'); |
| | | |
| | | var $modal = $('#im-modal'); |
| | | var ajaxActUrl = $modal.data('ajaxurl') || '/wp-admin/admin-ajax.php'; |
| | |
| | | dataType: 'json', |
| | | data: { action: ajaxAction, nonce: ajaxNonce, candidate_id: cid, reason: reason }, |
| | | success: function (r) { |
| | | $('#im-modal').fadeOut(150); $b.prop('disabled', false).text('确认'); |
| | | $('#im-modal').fadeOut(150); $b.prop('disabled', false).text('Confirm'); |
| | | var isSuccess = r && r.success; |
| | | var msg = r && r.data && r.data.message ? ((isSuccess ? 'â
' : 'â ') + r.data.message) : (isSuccess ? 'â
æä½æå' : 'â æä½å¤±è´¥ï¼è¯·éè¯'); |
| | | var msg = r && r.data && r.data.message ? ((isSuccess ? 'â
' : 'â ') + r.data.message) : (isSuccess ? 'â
Action completed successfully' : 'â Action failed, please try again'); |
| | | var $n = $('<div class="notice ' + (isSuccess ? 'notice-success' : 'notice-error') + ' is-dismissible" style="margin-top:16px"><p>' + msg + '</p></div>'); |
| | | if ($('.wp-heading-inline').length) { |
| | | $('.wp-heading-inline').after($n); |
| | |
| | | else setTimeout(function () { $n.fadeOut(400, function () { $(this).remove(); }); }, 5000); |
| | | }, |
| | | error: function (xhr, status, error) { |
| | | $('#im-modal').fadeOut(150); $b.prop('disabled', false).text('确认'); |
| | | var errMsg = 'ç½ç»ç¹å¿ææå¡å¨æ¥éï¼è¯·éè¯ã'; |
| | | if (xhr.status === 403) errMsg = '请æ±è¢«æç»(403 Forbidden)ãè¿é常æ¯å 为å®å
¨æä»¶(å¦Wordfence/å®å¡WAF)æ¦æªäºæºå¸¦åæ°ç请æ±ï¼è¯·å°è¯å°åå°å å
¥ç½ååæè系管çåéæ¾æ¦æªã'; |
| | | $('#im-modal').fadeOut(150); $b.prop('disabled', false).text('Confirm'); |
| | | var errMsg = 'Network is busy or server error occurred. Please try again.'; |
| | | if (xhr.status === 403) errMsg = 'Request denied (403 Forbidden). This is usually caused by a security plugin (e.g., Wordfence/BtPanel WAF) blocking parameterized requests. Please whitelist the admin path or contact your administrator.'; |
| | | alert(errMsg); |
| | | } |
| | | }); |
| | |
| | | add_action('wp_ajax_im_action_invite', function () { |
| | | check_ajax_referer('im_admin_nonce', 'nonce'); |
| | | if (!current_user_can('edit_others_posts')) |
| | | wp_send_json_error(['message' => 'æéä¸è¶³']); |
| | | wp_send_json_error(['message' => 'Insufficient permissions']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | 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' => 'é¢è¯é请é®ä»¶å·²æååéï¼']) |
| | | : wp_send_json_error(['message' => 'é®ä»¶åé失败ï¼è¯·æ£æ¥é®ä»¶é
ç½®ã']); |
| | | ? 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' => 'æéä¸è¶³']); |
| | | wp_send_json_error(['message' => 'Insufficient permissions']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | wp_send_json_error(['message' => 'Invalid parameters']); |
| | | IM_Candidate::update_status($id, 'rejected'); |
| | | IM_Mailer::send_reject($id) |
| | | ? wp_send_json_success(['message' => 'å·²æç»è¯¥åé人并åéé®ä»¶ï¼']) |
| | | : wp_send_json_error(['message' => 'å·²æ´æ°ç¶æï¼ä½é®ä»¶åé失败ã']); |
| | | ? 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' => 'æéä¸è¶³']); |
| | | wp_send_json_error(['message' => 'Insufficient permissions']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | wp_send_json_error(['message' => 'Invalid parameters']); |
| | | IM_Mailer::send_hire($id) |
| | | ? wp_send_json_success(['message' => 'å·²å½å该åé人并åéå¹è®é¾æ¥é®ä»¶ï¼']) |
| | | : wp_send_json_error(['message' => 'å½åæä½å¤±è´¥ï¼è¯·æ£æ¥é®ä»¶é
ç½®ã']); |
| | | ? 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' => 'æéä¸è¶³']); |
| | | wp_send_json_error(['message' => 'Insufficient permissions']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | wp_send_json_error(['message' => 'Invalid parameters']); |
| | | IM_Mailer::send_joinus_confirmation($id) |
| | | ? wp_send_json_success(['message' => '表å龿¥é®ä»¶å·²éæ°åéï¼']) |
| | | : wp_send_json_error(['message' => 'é®ä»¶åé失败ã']); |
| | | ? 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' => 'æéä¸è¶³']); |
| | | wp_send_json_error(['message' => 'Insufficient permissions']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | wp_send_json_error(['message' => 'Invalid parameters']); |
| | | $c = IM_Candidate::get($id); |
| | | if (!$c || $c->status !== 'training') |
| | | wp_send_json_error(['message' => '该åé人ä¸å¨å¹è®ç¶æ']); |
| | | wp_send_json_error(['message' => 'Candidate is not in training status']); |
| | | IM_Mailer::send_hire($id) |
| | | ? wp_send_json_success(['message' => 'å¹è®é¾æ¥é®ä»¶å·²éæ°åéï¼']) |
| | | : wp_send_json_error(['message' => 'é®ä»¶åé失败ã']); |
| | | ? 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' => 'æéä¸è¶³']); |
| | | wp_send_json_error(['message' => 'Insufficient permissions']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | wp_send_json_error(['message' => 'Invalid parameters']); |
| | | $c = IM_Candidate::get($id); |
| | | if (!$c || $c->status !== 'trained') |
| | | wp_send_json_error(['message' => '该åé人ä¸å¨å·²å®æå¹è®ç¶æ']); |
| | | wp_send_json_error(['message' => 'Candidate is not in training-completed status']); |
| | | IM_Mailer::send_training_complete($id) |
| | | ? wp_send_json_success(['message' => 'è´¦å·é®ä»¶å·²éæ°åéï¼']) |
| | | : wp_send_json_error(['message' => 'é®ä»¶åé失败ã']); |
| | | ? 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' => 'æéä¸è¶³']); |
| | | wp_send_json_error(['message' => 'Insufficient permissions']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | wp_send_json_error(['message' => 'Invalid parameters']); |
| | | IM_Candidate::delete_with_files($id) |
| | | ? wp_send_json_success(['message' => 'åé人记å½åå
³èæä»¶å·²å é¤ï¼']) |
| | | : wp_send_json_error(['message' => 'å é¤å¤±è´¥ï¼è¯·éè¯ã']); |
| | | ? wp_send_json_success(['message' => 'Candidate record and related files deleted!']) |
| | | : wp_send_json_error(['message' => 'Delete failed, please try again.']); |
| | | }); |
| | | |
| | | /* ============================================================ |
| | |
| | | 'trained' => IM_Candidate::count(['status' => 'trained']), |
| | | ]; |
| | | $tabs = [ |
| | | '' => 'å
¨é¨', |
| | | 'applied' => 'å·²ç³è¯·', |
| | | 'screening' => 'å¾
çé', |
| | | 'invited' => 'å·²é请', |
| | | 'rejected' => 'å·²æç»', |
| | | 'completed' => '已宿', |
| | | 'hired' => 'å·²å½å', |
| | | 'training' => 'æªå®æå¹è®', |
| | | 'trained' => '已宿å¹è®' |
| | | '' => 'All', |
| | | 'applied' => 'Applied', |
| | | 'screening' => 'Pending Review', |
| | | 'invited' => 'Invited', |
| | | 'rejected' => 'Rejected', |
| | | 'completed' => 'Completed', |
| | | 'hired' => 'Hired', |
| | | 'training' => 'Training In Progress', |
| | | 'trained' => 'Training Completed' |
| | | ]; |
| | | |
| | | $status_lbl = [ |
| | | 'applied' => 'å¾
å®åä¿¡æ¯', |
| | | 'screening' => 'å·²æäº¤è¯¦ç»ä¿¡æ¯', |
| | | 'invited' => 'å·²é请', |
| | | 'rejected' => 'å·²æç»', |
| | | 'completed' => '已宿', |
| | | 'hired' => 'å·²å½å', |
| | | 'training' => 'æªå®æå¹è®', |
| | | 'trained' => '已宿å¹è®' |
| | | 'applied' => 'Awaiting Detailed Form', |
| | | 'screening' => 'Detailed Form Submitted', |
| | | 'invited' => 'Invited', |
| | | 'rejected' => 'Rejected', |
| | | 'completed' => 'Completed', |
| | | 'hired' => 'Hired', |
| | | 'training' => 'Training In Progress', |
| | | 'trained' => 'Training Completed' |
| | | ]; |
| | | |
| | | $colors = [ |
| | |
| | | ]; |
| | | ?> |
| | | <div class="wrap im-a"> |
| | | <h1 class="wp-heading-inline">åé人管ç</h1> |
| | | <h1 class="wp-heading-inline">Candidate Management</h1> |
| | | <hr class="wp-header-end"> |
| | | |
| | | <ul class="subsubsub"> |
| | |
| | | <input type="hidden" name="page" value="im-candidates"> |
| | | <?php if ($status): ?><input type="hidden" name="status" value="<?= esc_attr($status) ?>"><?php endif; ?> |
| | | <p class="search-box" style="margin:0"> |
| | | <input type="search" name="s" value="<?= esc_attr($search) ?>" placeholder="æç´¢å§åæé®ç®±..." style="width:240px"> |
| | | <button type="submit" class="button">æç´¢</button> |
| | | <input type="search" name="s" value="<?= esc_attr($search) ?>" placeholder="Search by name or email..." style="width:240px"> |
| | | <button type="submit" class="button">Search</button> |
| | | </p> |
| | | </form> |
| | | |
| | | <table class="wp-list-table widefat fixed striped"> |
| | | <thead> |
| | | <tr> |
| | | <th width="160">å§å</th> |
| | | <th>é®ç®±</th> |
| | | <th width="140">æ´æ°æ¶é´</th> |
| | | <th width="120">ç¶æ</th> |
| | | <th>æè¯¾ç§ç® / 详æ
</th> |
| | | <th width="240">æä½</th> |
| | | <th width="160">Name</th> |
| | | <th>Email</th> |
| | | <th width="140">Updated</th> |
| | | <th width="120">Status</th> |
| | | <th>Subjects / Details</th> |
| | | <th width="240">Actions</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | |
| | | <span class="im-badge" |
| | | style="background:<?= $color ?>18;color:<?= $color ?>;border-color:<?= $color ?>44"><?= $clabel ?></span> |
| | | <?php if ($c->status === 'applied' && !empty($c->apply_opened_at)): ?> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â å·²æå¼è¡¨å</div> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â Form opened</div> |
| | | <?php elseif ($c->status === 'applied' && empty($c->apply_opened_at)): ?> |
| | | <div style="font-size:12px;color:#9ca3af;margin-top:4px">â æªæå¼è¡¨å</div> |
| | | <div style="font-size:12px;color:#9ca3af;margin-top:4px">â Form not opened</div> |
| | | <?php elseif ($c->status === 'invited'): ?> |
| | | <?php $tk_list = IM_Token::get_by_candidate($c->id); |
| | | if (!empty($tk_list)): |
| | | $tk = $tk_list[0]; ?> |
| | | <?php if (!empty($tk->opened_at)): ?> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â å·²æ¥çé¢è¯é¢</div> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â Interview questions viewed</div> |
| | | <?php else: ?> |
| | | <div style="font-size:12px;color:#9ca3af;margin-top:4px">â æªç¹å¼é¢è¯é¾æ¥</div> |
| | | <div style="font-size:12px;color:#9ca3af;margin-top:4px">â Interview link not opened</div> |
| | | <?php endif; ?> |
| | | <?php endif; ?> |
| | | <?php elseif ($c->status === 'training'): ?> |
| | | <?php if (!empty($c->training_opened_at)): ?> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â å·²æå¼å¹è®é¡µé¢</div> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â Training page opened</div> |
| | | <?php else: ?> |
| | | <div style="font-size:12px;color:#9ca3af;margin-top:4px">â æªæå¼å¹è®é¡µé¢</div> |
| | | <div style="font-size:12px;color:#9ca3af;margin-top:4px">â Training page not opened</div> |
| | | <?php endif; ?> |
| | | <?php elseif ($c->status === 'trained'): ?> |
| | | <?php if (!empty($c->training_completed_at)): ?> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â <?= date('m/d H:i', strtotime($c->training_completed_at)) ?> 宿</div> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â <?= date('m/d H:i', strtotime($c->training_completed_at)) ?> Completed</div> |
| | | <?php endif; ?> |
| | | <?php endif; ?> |
| | | </td> |
| | | <td> |
| | | <?php if ($c->status === 'applied'): ?> |
| | | <span class="im-muted">çå¾
å®åä¿¡æ¯</span> |
| | | <span class="im-muted">Waiting for detailed form</span> |
| | | <?php elseif ($subs): ?> |
| | | <?php foreach (array_slice($subs, 0, 3) as $s): ?> |
| | | <span class="im-tag"><?= esc_html($s) ?></span> |
| | |
| | | </td> |
| | | <td> |
| | | <?php if ($c->status !== 'applied'): ?> |
| | | <a href="<?= esc_url($detail) ?>" class="button button-small">æ¥ç详æ
</a> |
| | | <a href="<?= esc_url($detail) ?>" class="button button-small">View Details</a> |
| | | <?php endif; ?> |
| | | |
| | | <?php if ($c->status === 'applied'): ?> |
| | | <button class="button button-small im-action-btn" data-action="resend_joinus" |
| | | data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->last_name . ' ' . $c->first_name) ?>">éæ°åéé®ä»¶</button> |
| | | data-name="<?= esc_attr($c->last_name . ' ' . $c->first_name) ?>">Resend Email</button> |
| | | <?php elseif ($c->status === 'screening'): ?> |
| | | <button class="button button-small button-primary im-action-btn" data-action="invite" |
| | | data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">åéé¢è¯é请</button> |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">Send Interview Invitation</button> |
| | | <button class="button button-small im-action-btn" data-action="reject" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">æç»</button> |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">Reject</button> |
| | | <?php elseif ($c->status === 'invited'): ?> |
| | | <button class="button button-small im-action-btn" data-action="invite" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">éæ°åéé请</button> |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">Resend Invitation</button> |
| | | <button class="button button-small im-action-btn" data-action="reject" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">æç»</button> |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">Reject</button> |
| | | <?php elseif ($c->status === 'completed'): ?> |
| | | <button class="button button-small im-action-btn" style="border-color:#10b981;color:#10b981" |
| | | data-action="hire" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">å½å</button> |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">Hire</button> |
| | | <button class="button button-small im-action-btn" data-action="reject" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">æç»</button> |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">Reject</button> |
| | | <?php elseif ($c->status === 'training'): ?> |
| | | <button class="button button-small im-action-btn" data-action="resend_training" |
| | | data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">éæ°åéå¹è®é¾æ¥</button> |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">Resend Training Link</button> |
| | | <?php elseif ($c->status === 'trained'): ?> |
| | | <button class="button button-small im-action-btn" data-action="resend_trained_email" |
| | | data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">éæ°åéè´¦å·é®ä»¶</button> |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">Resend Account Email</button> |
| | | <?php endif; ?> |
| | | |
| | | <button class="button button-small im-action-btn" style="color:#ef4444;border-color:#fca5a5" |
| | | data-action="delete" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->last_name . ' ' . $c->first_name) ?>">å é¤</button> |
| | | data-name="<?= esc_attr($c->last_name . ' ' . $c->first_name) ?>">Delete</button> |
| | | |
| | | <?php if (defined('IM_DEBUG_LINKS') && IM_DEBUG_LINKS): ?> |
| | | <div |
| | | style="margin-top:12px;padding-top:8px;border-top:1px dashed #e5e7eb;font-size:12px;word-break:break-all;line-height:1.5"> |
| | | <?php if (empty($c->apply_token_used) && $c->apply_token): ?> |
| | | <div style="color:#6b7280;margin-bottom:4px">è°è¯: 表å龿¥ <br><a |
| | | <div style="color:#6b7280;margin-bottom:4px">Debug: Form Link <br><a |
| | | href="<?= esc_url(add_query_arg('im_apply_token', $c->apply_token, IM_APPLY_PAGE_URL)) ?>" |
| | | target="_blank" |
| | | style="color:#8b5cf6"><?= esc_html(add_query_arg('im_apply_token', $c->apply_token, IM_APPLY_PAGE_URL)) ?></a> |
| | |
| | | $latest_token = IM_Token::get_by_candidate($c->id); |
| | | if (!empty($latest_token)): |
| | | $tk2 = $latest_token[0]; ?> |
| | | <div style="color:#6b7280">è°è¯: é¢è¯é¾æ¥ <br><a |
| | | <div style="color:#6b7280">Debug: Interview Link <br><a |
| | | href="<?= esc_url(add_query_arg('im_token', $tk2->token, IM_INTERVIEW_PAGE_URL)) ?>" target="_blank" |
| | | style="color:#3b82f6"><?= esc_html(add_query_arg('im_token', $tk2->token, IM_INTERVIEW_PAGE_URL)) ?></a> |
| | | </div> |
| | | <?php endif; endif; ?> |
| | | <?php if (in_array($c->status, ['training', 'trained']) && !empty($c->training_token)): ?> |
| | | <div style="color:#6b7280">è°è¯: å¹è®é¾æ¥ <br><a |
| | | <div style="color:#6b7280">Debug: Training Link <br><a |
| | | href="<?= esc_url(add_query_arg('im_training_token', $c->training_token, IM_TRAINING_PAGE_URL)) ?>" target="_blank" |
| | | style="color:#f97316"><?= esc_html(add_query_arg('im_training_token', $c->training_token, IM_TRAINING_PAGE_URL)) ?></a> |
| | | </div> |
| | |
| | | </tr> |
| | | <?php endforeach; else: ?> |
| | | <tr> |
| | | <td colspan="6" class="im-empty">ææ åéäººæ°æ®</td> |
| | | <td colspan="6" class="im-empty">No candidate data available</td> |
| | | </tr> |
| | | <?php endif; ?> |
| | | </tbody> |
| | |
| | | $id = (int) ($_GET['id'] ?? 0); |
| | | $c = $id ? IM_Candidate::get($id) : null; |
| | | if (!$c) { |
| | | echo '<div class="wrap"><div class="notice notice-error"><p>åé人ä¸åå¨ã</p></div></div>'; |
| | | echo '<div class="wrap"><div class="notice notice-error"><p>Candidate does not exist.</p></div></div>'; |
| | | return; |
| | | } |
| | | if ($c->status === 'applied') { |
| | | echo '<div class="wrap"><div class="notice notice-warning"><p>该åéäººå°æªæäº¤è¯¦ç»ç³è¯·è¡¨ï¼æ æ³æ¥ç详æ
ã</p></div><a href="' . admin_url('admin.php?page=im-candidates') . '" class="button">è¿åå表</a></div>'; |
| | | echo '<div class="wrap"><div class="notice notice-warning"><p>This candidate has not submitted the detailed application form yet, so details are unavailable.</p></div><a href="' . admin_url('admin.php?page=im-candidates') . '" class="button">Back to List</a></div>'; |
| | | return; |
| | | } |
| | | |
| | |
| | | $attachments = IM_Attachment::get_by_candidate($id); |
| | | $upload_dir = wp_upload_dir(); |
| | | $status_lbl = [ |
| | | 'applied' => 'å¾
å®åä¿¡æ¯', |
| | | 'screening' => 'å·²æäº¤è¯¦ç»ä¿¡æ¯', |
| | | 'invited' => 'å·²é请', |
| | | 'rejected' => 'å·²æç»', |
| | | 'completed' => '已宿', |
| | | 'hired' => 'å·²å½å', |
| | | 'training' => 'æªå®æå¹è®', |
| | | 'trained' => '已宿å¹è®' |
| | | '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'], |
| | |
| | | stroke-linecap="round" stroke-linejoin="round"> |
| | | <path d="M19 12H5m7 7l-7-7 7-7" /> |
| | | </svg> |
| | | è¿ååé人å表 |
| | | Back to Candidate List |
| | | </a> |
| | | |
| | | <!-- Hero Card --> |
| | |
| | | <path d="M22 10v6M2 10l10-5 10 5-10 5z" /> |
| | | <path d="M6 12v5c3 3 9 3 12 0v-5" /> |
| | | </svg> |
| | | æè²èæ¯ |
| | | Education |
| | | </h2> |
| | | <div class="im-timeline"> |
| | | <div class="im-timeline-item"> |
| | | <div class="im-timeline-icon"></div> |
| | | <?php $degree_map = ["Bachelor's" => 'æ¬ç§', "Master's" => 'ç¡å£«', 'PhD' => 'å士']; ?> |
| | | <div class="im-timeline-date"><?= esc_html($c->grad_year ?: 'æªç¥å¹´ä»½') ?> æ¯ä¸ |
| | | (<?= esc_html($degree_map[$c->degree_level] ?? ($c->degree_level ?: 'æé«å¦å')) ?>)</div> |
| | | <?php $degree_map = ["Bachelor's" => 'Bachelor', "Master's" => 'Master', 'PhD' => 'PhD']; ?> |
| | | <div class="im-timeline-date"><?= esc_html($c->grad_year ?: 'Unknown year') ?> Graduated |
| | | (<?= esc_html($degree_map[$c->degree_level] ?? ($c->degree_level ?: 'Highest Degree')) ?>)</div> |
| | | <h3 class="im-timeline-title"><?= esc_html($c->university ?: 'â') ?></h3> |
| | | <div class="im-timeline-sub">ä¸ä¸ï¼<?= esc_html($c->major ?: 'â') ?> | |
| | | <div class="im-timeline-sub">Major: <?= esc_html($c->major ?: 'â') ?> | |
| | | GPAï¼<?= esc_html($c->gpa ?: 'â') ?> |
| | | <?= $c->deans_list ? ' | ð
Dean\'s List' : '' ?> |
| | | </div> |
| | |
| | | <?php if ($c->ug_university): ?> |
| | | <div class="im-timeline-item"> |
| | | <div class="im-timeline-icon" style="border-color:#d1d5db"></div> |
| | | <div class="im-timeline-date" style="color:#6b7280"><?= esc_html($c->ug_grad_year ?: 'æªç¥å¹´ä»½') ?> |
| | | æ¯ä¸ (æ¬ç§å¦å)</div> |
| | | <div class="im-timeline-date" style="color:#6b7280"><?= esc_html($c->ug_grad_year ?: 'Unknown year') ?> |
| | | Graduated (Bachelor Degree)</div> |
| | | <h3 class="im-timeline-title"><?= esc_html($c->ug_university) ?></h3> |
| | | <div class="im-timeline-sub">ä¸ä¸ï¼<?= esc_html($c->ug_major ?: 'â') ?></div> |
| | | <div class="im-timeline-sub">Major: <?= esc_html($c->ug_major ?: 'â') ?></div> |
| | | </div> |
| | | <?php endif; ?> |
| | | <?php if (!empty($c->ms_university)): ?> |
| | | <div class="im-timeline-item"> |
| | | <div class="im-timeline-icon" style="border-color:#8b5cf6"></div> |
| | | <div class="im-timeline-date" style="color:#8b5cf6"><?= esc_html($c->ms_grad_year ?: 'æªç¥å¹´ä»½') ?> |
| | | æ¯ä¸ (ç¡å£«å¦å)</div> |
| | | <div class="im-timeline-date" style="color:#8b5cf6"><?= esc_html($c->ms_grad_year ?: 'Unknown year') ?> |
| | | Graduated (Master Degree)</div> |
| | | <h3 class="im-timeline-title"><?= esc_html($c->ms_university) ?></h3> |
| | | <div class="im-timeline-sub">ä¸ä¸ï¼<?= esc_html($c->ms_major ?: 'â') ?></div> |
| | | <div class="im-timeline-sub">Major: <?= esc_html($c->ms_major ?: 'â') ?></div> |
| | | </div> |
| | | <?php endif; ?> |
| | | <?php if ($c->ca_highschool): ?> |
| | |
| | | <path |
| | | d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" /> |
| | | </svg> |
| | | æè½ä¸ç»éª |
| | | Skills & Experience |
| | | </h2> |
| | | <div class="im-info-grid"> |
| | | <div class="im-info-item full-width"> |
| | | <div class="im-info-label">æè¯¾ç§ç® (<?= count($subjects) ?>个)</div> |
| | | <div class="im-info-label">Subjects (<?= count($subjects) ?>)</div> |
| | | <div class="im-info-val"> |
| | | <?php if ($subjects): |
| | | foreach ($subjects as $s): ?> |
| | | <span class="im-tag"><?= esc_html($s) ?></span> |
| | | <?php endforeach; else: ?> |
| | | <span class="im-muted">æªéæ©ä»»ä½ç§ç®</span> |
| | | <span class="im-muted">No subjects selected</span> |
| | | <?php endif; ?> |
| | | </div> |
| | | </div> |
| | | <div class="im-info-item full-width"> |
| | | <div class="im-info-label">è±è¯æµå©åº¦</div> |
| | | <div class="im-info-label">English Fluency</div> |
| | | <div class="im-info-val"><?php |
| | | $fluency_map = ['Native' => 'Native (æ¯è¯)', 'Fluent' => 'Fluent (æµå©)', 'Basic' => 'Basic (åºç¡)']; |
| | | $fluency_map = ['Native' => 'Native', 'Fluent' => 'Fluent', 'Basic' => 'Basic']; |
| | | echo esc_html($fluency_map[$c->languages] ?? ($c->languages ?: 'â')); |
| | | ?></div> |
| | | </div> |
| | | <div class="im-info-item full-width"> |
| | | <div class="im-info-label">æå¦ç»éª</div> |
| | | <div class="im-info-label">Teaching Experience</div> |
| | | <div class="im-info-val"><?= nl2br(esc_html($c->teaching_exp ?: 'â')) ?></div> |
| | | </div> |
| | | <?php if ($c->has_achievement): ?> |
| | | <div class="im-info-item full-width"> |
| | | <div class="im-info-label">个人æå°± (<?= esc_html($c->achievement_type) ?>)</div> |
| | | <div class="im-info-label">Personal Achievement (<?= esc_html($c->achievement_type) ?>)</div> |
| | | <div class="im-info-val" |
| | | style="background:#f9fafb;padding:12px 16px;border-radius:8px;border:1px solid #f3f4f6"> |
| | | <?= nl2br(esc_html($c->achievement_desc ?: 'â')) ?> |
| | |
| | | <?php endif; ?> |
| | | <?php if ($c->extra_notes): ?> |
| | | <div class="im-info-item full-width"> |
| | | <div class="im-info-label">è¡¥å
说æ</div> |
| | | <div class="im-info-label">Additional Notes</div> |
| | | <div class="im-info-val" |
| | | style="background:#f9fafb;padding:12px 16px;border-radius:8px;border:1px solid #f3f4f6"> |
| | | <?= nl2br(esc_html($c->extra_notes)) ?> |
| | |
| | | <!-- ç¶ææä½åº --> |
| | | <?php if ($c->status === 'screening'): ?> |
| | | <div class="im-action-box"> |
| | | <h3>å®¡æ ¸æä½</h3> |
| | | <p>请å³å®æ¯å¦é请该åé人è¿è¡é¢è¯</p> |
| | | <h3>Review Actions</h3> |
| | | <p>Decide whether to invite this candidate for an interview</p> |
| | | <button class="im-btn-primary im-action-btn" data-action="invite" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>"> |
| | | åéé¢è¯é请 |
| | | Send Interview Invitation |
| | | </button> |
| | | <button class="im-btn-danger im-action-btn" data-action="reject" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>"> |
| | | æç»è¯¥åé人 |
| | | Reject This Candidate |
| | | </button> |
| | | <div id="im-res-<?= $c->id ?>" style="margin-top:12px;font-size:14px;font-weight:600"></div> |
| | | </div> |
| | | <?php elseif ($c->status === 'invited'): ?> |
| | | <div class="im-action-box"> |
| | | <h3>çå¾
é¢è¯ä¸</h3> |
| | | <p>妿åéäººæªæ¶å°é®ä»¶å¯éæ°åé</p> |
| | | <h3>Awaiting Interview</h3> |
| | | <p>If the candidate did not receive the email, you can resend it</p> |
| | | <button class="im-btn-primary im-action-btn" data-action="invite" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>"> |
| | | éæ°åéé请 |
| | | Resend Invitation |
| | | </button> |
| | | <div id="im-res-<?= $c->id ?>" style="margin-top:12px;font-size:14px;font-weight:600"></div> |
| | | </div> |
| | | <?php elseif ($c->status === 'completed'): ?> |
| | | <div class="im-action-box" style="background:linear-gradient(135deg,#ecfdf5,#d1fae5);border-color:#a7f3d0"> |
| | | <h3 style="color:#047857">é¢è¯å®¡æ¥</h3> |
| | | <p style="color:#059669">è§é¢å·²æäº¤ï¼è¯·æ¥çå¹¶å¨éè¿åå½ç¨</p> |
| | | <h3 style="color:#047857">Interview Review</h3> |
| | | <p style="color:#059669">Video has been submitted. Review it and hire if approved.</p> |
| | | <div style="display:flex;gap:12px;flex-wrap:wrap"> |
| | | <button class="im-btn-success im-action-btn" data-action="hire" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>" style="flex:1"> |
| | | æ£å¼å½ååé人 |
| | | Formally Hire Candidate |
| | | </button> |
| | | <button class="im-btn-danger im-action-btn" data-action="reject" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>" style="flex:1"> |
| | | æç»è¯¥åé人 |
| | | Reject This Candidate |
| | | </button> |
| | | </div> |
| | | <div id="im-res-<?= $c->id ?>" style="margin-top:12px;font-size:14px;font-weight:600"></div> |
| | | </div> |
| | | <?php elseif ($c->status === 'training'): ?> |
| | | <div class="im-action-box" style="background:linear-gradient(135deg,#fff7ed,#ffedd5);border-color:#fed7aa"> |
| | | <h3 style="color:#c2410c">å¹è®è¿è¡ä¸</h3> |
| | | <p style="color:#ea580c">åé人æ£å¨å®æå¹è®è¯¾ç¨</p> |
| | | <h3 style="color:#c2410c">Training In Progress</h3> |
| | | <p style="color:#ea580c">Candidate is completing training modules</p> |
| | | <?php if (!empty($c->training_opened_at)): ?> |
| | | <div style="font-size:13px;color:#059669;margin-bottom:12px;font-weight:600">â å·²äº <?= date('Y/m/d H:i', strtotime($c->training_opened_at)) ?> æå¼å¹è®é¡µé¢</div> |
| | | <div style="font-size:13px;color:#059669;margin-bottom:12px;font-weight:600">â Opened training page at <?= date('Y/m/d H:i', strtotime($c->training_opened_at)) ?></div> |
| | | <?php else: ?> |
| | | <div style="font-size:13px;color:#9ca3af;margin-bottom:12px">â å°æªæå¼å¹è®é¡µé¢</div> |
| | | <div style="font-size:13px;color:#9ca3af;margin-bottom:12px">â Training page not opened yet</div> |
| | | <?php endif; ?> |
| | | <button class="im-btn-primary im-action-btn" data-action="resend_training" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>"> |
| | | éæ°åéå¹è®é¾æ¥ |
| | | Resend Training Link |
| | | </button> |
| | | <div id="im-res-<?= $c->id ?>" style="margin-top:12px;font-size:14px;font-weight:600"></div> |
| | | </div> |
| | | <?php elseif ($c->status === 'trained'): ?> |
| | | <div class="im-action-box" style="background:linear-gradient(135deg,#ecfeff,#cffafe);border-color:#a5f3fc"> |
| | | <h3 style="color:#0e7490">å¹è®å·²å®æ</h3> |
| | | <p style="color:#0891b2">åé人已éè¿ææå¹è®æ¨¡å</p> |
| | | <h3 style="color:#0e7490">Training Completed</h3> |
| | | <p style="color:#0891b2">Candidate has passed all training modules</p> |
| | | <?php if (!empty($c->training_completed_at)): ?> |
| | | <div style="font-size:13px;color:#059669;font-weight:600;margin-bottom:12px">â äº <?= date('Y/m/d H:i', strtotime($c->training_completed_at)) ?> 宿å¹è®</div> |
| | | <div style="font-size:13px;color:#059669;font-weight:600;margin-bottom:12px">â Completed training at <?= date('Y/m/d H:i', strtotime($c->training_completed_at)) ?></div> |
| | | <?php endif; ?> |
| | | <button class="im-btn-primary im-action-btn" data-action="resend_trained_email" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>"> |
| | | éæ°åéè´¦å·é®ä»¶ |
| | | Resend Account Email |
| | | </button> |
| | | <div id="im-res-<?= $c->id ?>" style="margin-top:12px;font-size:14px;font-weight:600"></div> |
| | | </div> |
| | |
| | | <path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z" /> |
| | | <path d="M13 2v7h7" /> |
| | | </svg> |
| | | éä»¶ææ |
| | | Attachments |
| | | </h2> |
| | | <?php |
| | | $grouped = []; |
| | | foreach ($attachments as $att) |
| | | $grouped[$att->file_type][] = $att; |
| | | $att_groups = ['transcript_files' => 'æç»©å', 'achievement_files' => 'æå°±è¯æ', 'extra_files' => 'è¡¥å
ææ']; |
| | | $att_groups = ['transcript_files' => 'Transcripts', 'achievement_files' => 'Achievement Proofs', 'extra_files' => 'Additional Materials']; |
| | | foreach ($att_groups as $type => $label): |
| | | if (empty($grouped[$type])) |
| | | continue; |
| | |
| | | </div> |
| | | </div> |
| | | <div class="im-doc-actions"> |
| | | <a href="<?= esc_url($att_url) ?>" target="_blank" class="im-doc-btn" title="卿°æ ç¾é¡µé¢è§"> |
| | | <a href="<?= esc_url($att_url) ?>" target="_blank" class="im-doc-btn" title="Preview in new tab"> |
| | | <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /> |
| | | <circle cx="12" cy="12" r="3" /> |
| | | </svg> |
| | | </a> |
| | | <a href="<?= esc_url($att_url) ?>" download class="im-doc-btn" title="ä¸è½½æä»¶"> |
| | | <a href="<?= esc_url($att_url) ?>" download class="im-doc-btn" title="Download file"> |
| | | <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" /> |
| | |
| | | <path d="M23 7l-7 5 7 5V7z" /> |
| | | <rect x="1" y="5" width="15" height="14" rx="2" ry="2" /> |
| | | </svg> |
| | | é¢è¯è®°å½ |
| | | Interview Records |
| | | </h2> |
| | | <?php if ($tokens): |
| | | foreach ($tokens as $tk): |
| | |
| | | $bs = '#dcfce7'; |
| | | $bc = '#86efac'; |
| | | $bt = '#166534'; |
| | | $bl = 'å·²æäº¤è§é¢'; |
| | | $bl = 'Video Submitted'; |
| | | } elseif ($expired) { |
| | | $bs = '#fee2e2'; |
| | | $bc = '#fca5a5'; |
| | | $bt = '#991b1b'; |
| | | $bl = '龿¥å·²è¿æ'; |
| | | $bl = 'Link Expired'; |
| | | } else { |
| | | $bs = '#dbeafe'; |
| | | $bc = '#93c5fd'; |
| | | $bt = '#1e40af'; |
| | | $bl = 'ææä¸'; |
| | | $bl = 'Active'; |
| | | } |
| | | ?> |
| | | <div class="im-token-box"> |
| | |
| | | <span class="im-badge" |
| | | style="background:<?= $bs ?>;color:<?= $bt ?>;border-color:<?= $bc ?>"><?= $bl ?></span> |
| | | </div> |
| | | <div class="im-token-date"><?= date('Y/m/d H:i', strtotime($tk->created_at)) ?> åé</div> |
| | | <div class="im-token-exp" style="margin-bottom:8px">è³ |
| | | <?= date('m/d H:i', strtotime($tk->expires_at)) ?> 失æ |
| | | <div class="im-token-date"><?= date('Y/m/d H:i', strtotime($tk->created_at)) ?> Sent</div> |
| | | <div class="im-token-exp" style="margin-bottom:8px">Valid until |
| | | <?= date('m/d H:i', strtotime($tk->expires_at)) ?> |
| | | </div> |
| | | |
| | | <?php if (!empty($tk->opened_at)): ?> |
| | | <div class="im-token-exp" style="color:#059669;font-weight:600"> |
| | | â äº <?= date('Y/m/d H:i', strtotime($tk->opened_at)) ?> æ¥çé¢ç® <?= $used ? '' : '(å计æ¶è¿è¡ä¸)' ?> |
| | | â Viewed questions at <?= date('Y/m/d H:i', strtotime($tk->opened_at)) ?> <?= $used ? '' : '(countdown active)' ?> |
| | | </div> |
| | | <?php else: ?> |
| | | <div class="im-token-exp" style="color:#f59e0b">â åéäººæªæå¼é¾æ¥</div> |
| | | <div class="im-token-exp" style="color:#f59e0b">â Candidate has not opened the link</div> |
| | | <?php endif; ?> |
| | | |
| | | <?php if ($used && $tk->video_path && file_exists($tk->video_path)): |
| | |
| | | </div> |
| | | <div style="flex:1;min-width:0"> |
| | | <div style="font-weight:600;color:#1e293b;font-size:14px;word-break:break-all"><?= esc_html($tk->video_filename) ?></div> |
| | | <div style="color:#64748b;font-size:13px;margin-top:2px"><?= strtoupper($fext) ?> å缩å
· <?= $fsize_str ?></div> |
| | | <div style="color:#64748b;font-size:13px;margin-top:2px"><?= strtoupper($fext) ?> archive · <?= $fsize_str ?></div> |
| | | </div> |
| | | <a href="<?= esc_url($vurl) ?>" download style="display:inline-flex;align-items:center;gap:6px;background:#6366f1;color:#fff;padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;text-decoration:none;white-space:nowrap"> |
| | | <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>ä¸è½½ |
| | | <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>Download |
| | | </a> |
| | | </div> |
| | | <div style="margin-top:6px;font-size:12px;color:#94a3b8"><?= date('m/d H:i', strtotime($tk->submitted_at)) ?> æäº¤</div> |
| | | <div style="margin-top:6px;font-size:12px;color:#94a3b8"><?= date('m/d H:i', strtotime($tk->submitted_at)) ?> submitted</div> |
| | | <?php else: ?> |
| | | <video controls class="im-video-player" preload="metadata"> |
| | | <source src="<?= esc_url($vurl) ?>"> |
| | | </video> |
| | | <div class="im-video-footer"> |
| | | <span><?= date('m/d H:i', strtotime($tk->submitted_at)) ?> æäº¤</span> |
| | | <span><?= date('m/d H:i', strtotime($tk->submitted_at)) ?> submitted</span> |
| | | <a href="<?= esc_url($vurl) ?>" download class="im-video-dl"> |
| | | <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" /> |
| | | </svg>ä¸è½½ |
| | | </svg>Download |
| | | </a> |
| | | </div> |
| | | <?php endif; ?> |
| | | <?php elseif ($used): ?> |
| | | <p class="im-muted" style="margin:0;font-size:13px">è§é¢å·²è±æºæä¸åå¨</p> |
| | | <p class="im-muted" style="margin:0;font-size:13px">Video is offline or missing</p> |
| | | <?php endif; ?> |
| | | </div> |
| | | <?php endforeach; else: ?> |
| | | <div class="im-empty" |
| | | style="padding:24px!important;background:#f9fafb;border-radius:12px;border:1px dashed #e5e7eb"> |
| | | å°æªåéé¢è¯é请</div> |
| | | Interview invitation has not been sent yet</div> |
| | | <?php endif; ?> |
| | | </div> |
| | | <?php endif; ?> |
| | |
| | | data-nonce="<?php echo esc_attr(wp_create_nonce('im_admin_nonce')); ?>" |
| | | data-ajaxurl="<?php echo esc_url(admin_url('admin-ajax.php')); ?>"> |
| | | <div class="im-modal"> |
| | | <h3>æä½ç¡®è®¤</h3> |
| | | <h3>Action Confirmation</h3> |
| | | <p id="im-modal-body"></p> |
| | | <textarea id="im-modal-textarea" style="display:none" placeholder="è¾å
¥åå ..."></textarea> |
| | | <textarea id="im-modal-textarea" style="display:none" placeholder="Enter reason..."></textarea> |
| | | <div class="im-modal-btns"> |
| | | <button class="button im-modal-cancel">åæ¶</button> |
| | | <button class="button button-primary" id="im-modal-ok">确认</button> |
| | | <button class="button im-modal-cancel">Cancel</button> |
| | | <button class="button button-primary" id="im-modal-ok">Confirm</button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | $home = rtrim(home_url('/'), '/'); |
| | | |
| | | $checks = [ |
| | | ['upload_max_filesize', ini_get('upload_max_filesize'), wp_convert_hr_to_bytes(ini_get('upload_max_filesize')) >= 400 * 1024 * 1024, '建议设为 500M'], |
| | | ['post_max_size', ini_get('post_max_size'), wp_convert_hr_to_bytes(ini_get('post_max_size')) >= 400 * 1024 * 1024, '建议设为 512M'], |
| | | ['max_execution_time', ini_get('max_execution_time') . 's', (int) ini_get('max_execution_time') === 0 || (int) ini_get('max_execution_time') >= 180, '建议设为 300'], |
| | | ['upload_max_filesize', ini_get('upload_max_filesize'), wp_convert_hr_to_bytes(ini_get('upload_max_filesize')) >= 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, 'â'], |
| | | ]; |
| | | ?> |
| | | <div class="wrap" style="max-width:860px"> |
| | | <h1>Interview Manager â æå¡å¨é
ç½®å导</h1> |
| | | <h1>Interview Manager â Server Setup Wizard</h1> |
| | | |
| | | <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:24px"> |
| | | <h2 style="margin-top:0">â Nginx é
ç½®ï¼å¿
é¡»æå¨å®æï¼</h2> |
| | | <p>å¨ <code>server { }</code> åå
æ·»å 以ä¸é
ç½®ï¼ç¶åæ§è¡ <code>sudo nginx -t && sudo nginx -s reload</code>ï¼</p> |
| | | <h2 style="margin-top:0">1) Nginx Configuration (manual required)</h2> |
| | | <p>Add the following config inside the <code>server { }</code> block, then run <code>sudo nginx -t && sudo nginx -s reload</code>:</p> |
| | | <pre |
| | | style="background:#1e293b;color:#e2e8f0;padding:20px;border-radius:8px;font-size:13px;line-height:1.7;overflow-x:auto"># è§é¢ä¸ä¼ éå¶ |
| | | style="background:#1e293b;color:#e2e8f0;padding:20px;border-radius:8px;font-size:13px;line-height:1.7;overflow-x:auto"># Video upload limits |
| | | client_max_body_size 512M; |
| | | client_body_timeout 300s; |
| | | send_timeout 300s; |
| | | |
| | | # WordPress åºå®é¾æ¥ï¼å·²æåè·³è¿ï¼ |
| | | # 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; |
| | | }</pre> |
| | | </div> |
| | | |
| | | <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:24px"> |
| | | <h2 style="margin-top:0">â¡ PHP é
ç½®ï¼php.ini æ .user.iniï¼</h2> |
| | | <h2 style="margin-top:0">2) PHP Configuration (php.ini or .user.ini)</h2> |
| | | <pre style="background:#1e293b;color:#e2e8f0;padding:20px;border-radius:8px;font-size:13px;line-height:1.7">upload_max_filesize = 500M |
| | | post_max_size = 512M |
| | | max_execution_time = 300 |
| | | max_input_time = 300 |
| | | memory_limit = 256M</pre> |
| | | <p>ä¿®æ¹åéå¯ PHP-FPMï¼<code>sudo systemctl restart php8.x-fpm</code></p> |
| | | <p>Restart PHP-FPM after changes: <code>sudo systemctl restart php8.x-fpm</code></p> |
| | | </div> |
| | | |
| | | <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:24px"> |
| | | <h2 style="margin-top:0">â¢ å·æ° WordPress åºå®é¾æ¥</h2> |
| | | <p>è¿å
¥ <strong>åå° â 设置 â åºå®é¾æ¥</strong>ï¼ç´æ¥ç¹å»ãä¿åæ´æ¹ãå³å¯ï¼æ 鿹任ä½è®¾ç½®ï¼ã</p> |
| | | <p><a href="<?= admin_url('options-permalink.php') ?>" class="button button-primary" target="_blank">åå¾åºå®é¾æ¥è®¾ç½® |
| | | <h2 style="margin-top:0">3) Refresh WordPress Permalinks</h2> |
| | | <p>Go to <strong>Dashboard â Settings â Permalinks</strong>, then click "Save Changes" directly (no settings change needed).</p> |
| | | <p><a href="<?= admin_url('options-permalink.php') ?>" class="button button-primary" target="_blank">Go to Permalink Settings |
| | | â</a></p> |
| | | </div> |
| | | |
| | | <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:24px"> |
| | | <h2 style="margin-top:0">⣠WordPress 页é¢é
ç½®</h2> |
| | | <p>éå¨ WordPress åå°å建以ä¸ä¸¤ä¸ªé¡µé¢ï¼</p> |
| | | <h2 style="margin-top:0">4) WordPress Page Setup</h2> |
| | | <p>Create the following pages in WordPress admin:</p> |
| | | <table class="widefat" style="max-width:600px"> |
| | | <tr> |
| | | <th>页é¢</th> |
| | | <th>Page</th> |
| | | <th>Shortcode</th> |
| | | <th>建议 URL slug</th> |
| | | <th>Suggested URL slug</th> |
| | | </tr> |
| | | <tr> |
| | | <td>Join Us æ¥å页</td> |
| | | <td>Join Us Registration Page</td> |
| | | <td><code>[im_joinus_form]</code></td> |
| | | <td><code>/join-us/</code></td> |
| | | </tr> |
| | | <tr> |
| | | <td>详ç»ç³è¯·è¡¨å页</td> |
| | | <td>Detailed Application Form Page</td> |
| | | <td><code>[im_apply_form]</code></td> |
| | | <td><code>/apply/</code></td> |
| | | </tr> |
| | | <tr> |
| | | <td>é¢è¯é¡µ</td> |
| | | <td>Interview Page</td> |
| | | <td><code>[im_interview]</code></td> |
| | | <td><code>/interview/</code></td> |
| | | </tr> |
| | | <tr> |
| | | <td>å¹è®é¡µ</td> |
| | | <td>Training Page</td> |
| | | <td><code>[im_training]</code></td> |
| | | <td><code>/training/</code></td> |
| | | </tr> |
| | | </table> |
| | | <p style="margin-top:12px">å建好页é¢åï¼å¨ç段 1 é¡¶é¨å°å¯¹åºå¸¸éæ¹ä¸ºå®é
URLï¼</p> |
| | | <p style="margin-top:12px">After creating pages, update the constants at the top of snippet 1 with actual URLs:</p> |
| | | <code>define('IM_APPLY_PAGE_URL', home_url('/apply/'));</code><br> |
| | | <code>define('IM_INTERVIEW_PAGE_URL', home_url('/interview/'));</code><br> |
| | | <code>define('IM_TRAINING_PAGE_URL', home_url('/training/'));</code><br> |
| | |
| | | </div> |
| | | |
| | | <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px"> |
| | | <h2 style="margin-top:0">⤠å½åç¯å¢æ£æµ</h2> |
| | | <h2 style="margin-top:0">5) Current Environment Check</h2> |
| | | <table class="widefat" style="max-width:640px"> |
| | | <thead> |
| | | <tr> |
| | | <th>é
置项</th> |
| | | <th>å½åå¼</th> |
| | | <th>ç¶æ</th> |
| | | <th>Configuration</th> |
| | | <th>Current Value</th> |
| | | <th>Status</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | |
| | | </tr> |
| | | <?php endforeach; ?> |
| | | <tr> |
| | | <td>è§é¢åå¨ç®å½</td> |
| | | <td>Video Storage Directory</td> |
| | | <td style="font-size:12px"><?= esc_html($upload_dir['basedir'] . '/interviews/') ?></td> |
| | | <td><?= is_dir($upload_dir['basedir'] . '/interviews') ? (is_writable($upload_dir['basedir'] . '/interviews') ? 'â
å¯å' : 'â ä¸å¯å') : 'ï¼é¦æ¬¡ä¸ä¼ æ¶èªå¨å建ï¼' ?> |
| | | <td><?= is_dir($upload_dir['basedir'] . '/interviews') ? (is_writable($upload_dir['basedir'] . '/interviews') ? 'â
Writable' : 'â Not writable') : '(Auto-created on first upload)' ?> |
| | | </td> |
| | | </tr> |
| | | <tr> |
| | | <td>ç³è¯·æä»¶ç®å½</td> |
| | | <td>Application Files Directory</td> |
| | | <td style="font-size:12px"><?= esc_html($upload_dir['basedir'] . '/im-applications/') ?></td> |
| | | <td><?= is_dir($upload_dir['basedir'] . '/im-applications') ? (is_writable($upload_dir['basedir'] . '/im-applications') ? 'â
å¯å' : 'â ä¸å¯å') : 'ï¼é¦æ¬¡ä¸ä¼ æ¶èªå¨å建ï¼' ?> |
| | | <td><?= is_dir($upload_dir['basedir'] . '/im-applications') ? (is_writable($upload_dir['basedir'] . '/im-applications') ? 'â
Writable' : 'â Not writable') : '(Auto-created on first upload)' ?> |
| | | </td> |
| | | </tr> |
| | | <tr> |
| | | <td>é¢è¯é¡µé¢ URL</td> |
| | | <td>Interview Page URL</td> |
| | | <td><a href="<?= esc_url(IM_INTERVIEW_PAGE_URL) ?>" target="_blank" |
| | | style="font-size:12px"><?= esc_html(IM_INTERVIEW_PAGE_URL) ?></a></td> |
| | | <td>â</td> |
| | | </tr> |
| | | <tr> |
| | | <td>å¹è®é¡µé¢ URL</td> |
| | | <td>Training Page URL</td> |
| | | <td><a href="<?= esc_url(IM_TRAINING_PAGE_URL) ?>" target="_blank" |
| | | style="font-size:12px"><?= esc_html(IM_TRAINING_PAGE_URL) ?></a></td> |
| | | <td>â</td> |
| | | </tr> |
| | | <tr> |
| | | <td>å¹è®ä¸´æ¶è´¦å·</td> |
| | | <td>Training Temporary Account</td> |
| | | <td style="font-size:12px"><?= esc_html(IM_TRAINING_ACCOUNT) ?></td> |
| | | <td><?= IM_TRAINING_ACCOUNT !== 'your_account@example.com' ? 'â
' : 'â ï¸ è¯·é
ç½®å®é
è´¦å·' ?></td> |
| | | <td><?= IM_TRAINING_ACCOUNT !== 'your_account@example.com' ? 'â
' : 'â ï¸ Configure real account' ?></td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |