Merge branch 'dev' of bitbucket.org:jubilian/nhance-enrollment into dev

This commit is contained in:
Gowtham M 2026-07-20 17:13:54 +05:30
commit 40a58e148f
14 changed files with 2851 additions and 4 deletions

View File

@ -0,0 +1,117 @@
<?php
/**
* Manual integration script for employeesUplodWithEvents.
*
* Usage:
* php spark test:employee-upload-dates
*
* Inserts a real files row using sample enrollment excel and prints stored enrollment dates.
*/
namespace App\Commands;
use App\Controllers\EmployeeController;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Database;
use Tests\Support\TestUploadedFile;
class TestEmployeeUploadDates extends BaseCommand
{
protected $group = 'Testing';
protected $name = 'test:employee-upload-dates';
protected $description = 'Test employeesUplodWithEvents with d-m-Y and d/m/Y enrollment dates and DB insert';
protected $usage = 'test:employee-upload-dates [--open=01-07-2026] [--close=31/07/2026]';
protected $options = [
'--open' => 'Enrollment open date (d-m-Y or d/m/Y)',
'--close' => 'Enrollment close date (d-m-Y or d/m/Y)',
];
public function run(array $params)
{
helper('utility');
$openDate = CLI::getOption('open') ?? '01-07-2026';
$closeDate = CLI::getOption('close') ?? '31/07/2026';
$sample = ROOTPATH . 'public/sample_excel/enrollment.xlsx';
if (!is_file($sample)) {
CLI::error('Sample file not found: ' . $sample);
return;
}
require_once ROOTPATH . 'tests/_support/TestUploadedFile.php';
$db = Database::connect('default');
$row = $db->query(
'SELECT client_id, policy_id, client_branch_id, created_by
FROM files
WHERE client_id IS NOT NULL
AND policy_id IS NOT NULL
AND client_branch_id IS NOT NULL
AND created_by IS NOT NULL
ORDER BY id DESC
LIMIT 1'
)->getRowArray();
if (empty($row)) {
CLI::error('No fixture row found in files table.');
return;
}
CLI::write('Input dates:', 'yellow');
CLI::write(" open : {$openDate}");
CLI::write(" close : {$closeDate}");
CLI::write('Converted:', 'yellow');
CLI::write(' open : ' . change_date_format($openDate, null, 'Y-m-d'));
CLI::write(' close : ' . change_date_format($closeDate, null, 'Y-m-d'));
$tmpPath = tempnam(sys_get_temp_dir(), 'enroll_manual_');
copy($sample, $tmpPath);
$uploadedFile = new TestUploadedFile(
$tmpPath,
basename($sample),
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
filesize($tmpPath),
UPLOAD_ERR_OK
);
$postData = [
'client_id' => (int) $row['client_id'],
'policy_id' => (int) $row['policy_id'],
'client_branch_id' => (int) $row['client_branch_id'],
'created_by' => (int) $row['created_by'],
'enrollment_open_date' => $openDate,
'enrollment_close_date' => $closeDate,
'emplist' => $uploadedFile,
];
$controller = new EmployeeController();
$result = $controller->employeesUplodWithEvents($postData);
CLI::newLine();
CLI::write('Upload result:', 'green');
CLI::write(json_encode($result, JSON_PRETTY_PRINT));
if (empty($result['file_id'])) {
return;
}
$file = $db->table('files')->where('id', $result['file_id'])->get()->getRowArray();
CLI::newLine();
CLI::write('DB record:', 'green');
CLI::write(json_encode([
'id' => $file['id'] ?? null,
'file_name' => $file['file_name'] ?? null,
'action' => $file['action'] ?? null,
'status' => $file['status'] ?? null,
'enrollment_open_date' => $file['enrollment_open_date'] ?? null,
'enrollment_close_date' => $file['enrollment_close_date'] ?? null,
], JSON_PRETTY_PRINT));
}
}

View File

@ -73,6 +73,12 @@ class Acl
'teams' => []
],
// ===================== LOGIN DIAGNOSTIC =====================
'#^/security/login-diagnostic#' => [
'roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID],
'teams' => []
],
// ===================== INTERNAL TEST =====================
'#^/test#' => [
'roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID],

View File

@ -594,6 +594,11 @@ $routes->group('security/rate-limits', ['filter' => 'authMVC'], function ($route
$routes->post('unblock-user', 'RateLimitAdminController::unblockUser');
});
$routes->group('security/login-diagnostic', ['filter' => 'authMVC'], function ($routes) {
$routes->get('/', 'LoginDiagnosticController::index');
$routes->post('diagnose', 'LoginDiagnosticController::diagnose');
});
//saml - routes
// $routes->group("employeeRest", ['filter' => ['appSignature','AuthApiRateLimitFilter'] ], function ($routes) {

View File

@ -397,8 +397,8 @@ class EmployeeController extends AdminController
$status = 'inprogress';
$hr_id = $post_data['created_by'] ?? null;
$enrollment_open_date = change_date_format($enrollment_open_date, 'd/m/Y', 'Y-m-d');
$enrollment_close_date = change_date_format($enrollment_close_date, 'd/m/Y', 'Y-m-d');
$enrollment_open_date = change_date_format($enrollment_open_date, null, 'Y-m-d');
$enrollment_close_date = change_date_format($enrollment_close_date, null, 'Y-m-d');
$file_id = $this->fileModel->insert(['file_name' => $filename, 'client_id' => $client_id, 'policy_id' => $policy_id, 'created_by' => $loggedInUserID, 'status' => $status, 'action' => $action, 'client_branch_id' => $branch_id, 'uploaded_by' => 1, 'enrollment_open_date' => $enrollment_open_date, 'enrollment_close_date' => $enrollment_close_date, 'hr_id' => $hr_id]); //here field policy_id have client_policy_id and not policy id from policy master
$this->myLogger->logme("error", '{file_id} uploaded success', ['file_id' => $file_id]);

View File

@ -3251,11 +3251,16 @@ class EmployeeRestController extends AdminController
$value['insurer_name'] = $insurerData->name ?? null;
$value['insurer_short_name'] = $insurerData->short_name ?? null;
$employeeDetails = $this->employeePolicyModel->getEmployeePolicy( client_id:$value['client_id'],policy_id: $value['client_policy_id'],status:0,branch_id:$client_branch_id);
$totalCount = 0;
$enrolledCount = 0;
$draftCount = 0;
if(count($employeeDetails))
{
foreach ($employeeDetails as $item) {
if ($item['relationship'] !== 'Self') {
continue;
}
$totalCount++;
if ($item['emp_status'] === 'enrolled') {
$enrolledCount++;
} elseif ($item['emp_status'] === 'draft') {
@ -3264,7 +3269,7 @@ class EmployeeRestController extends AdminController
}
}
$value['totalMembersCount'] = count($employeeDetails);
$value['totalMembersCount'] = $totalCount;
$value['membersCountOfEnrolled'] = $enrolledCount;
$value['membersCountOfDraft'] = $draftCount;
$value['membersCountOfLoggedIn'] = $loggedInCounts[$value['client_policy_id']] ?? 0;

View File

@ -0,0 +1,68 @@
<?php
namespace App\Controllers;
use App\Helpers\LoginDiagnosticHelper;
/**
* Admin-only employee login diagnostic (PRE + POST).
* Does not modify existing auth controllers/models/views.
*/
class LoginDiagnosticController extends BaseController
{
public function index(): void
{
$email = trim((string) $this->request->getGet('email'));
$mobile = trim((string) $this->request->getGet('mobile'));
$data = [
'title' => 'Employee Login Diagnostic',
'email' => $email,
'mobile' => $mobile,
'result' => null,
'has_search' => false,
];
if ($email !== '' || $mobile !== '') {
$data['has_search'] = true;
$data['result'] = LoginDiagnosticHelper::diagnose(
$email !== '' ? $email : null,
$mobile !== '' ? $mobile : null
);
}
echo view('admin/login_diagnostic', $data);
}
public function diagnose()
{
$email = trim((string) $this->request->getPost('email'));
$mobile = trim((string) $this->request->getPost('mobile'));
if ($email === '' && $mobile === '') {
$json = $this->request->getJSON(true);
if (is_array($json)) {
$email = trim((string) ($json['email'] ?? ''));
$mobile = trim((string) ($json['mobile'] ?? ''));
}
}
if ($this->request->isAJAX() || str_contains((string) $this->request->getHeaderLine('Accept'), 'application/json')) {
$result = LoginDiagnosticHelper::diagnose(
$email !== '' ? $email : null,
$mobile !== '' ? $mobile : null
);
return $this->response->setJSON($result);
}
$query = [];
if ($email !== '') {
$query['email'] = $email;
}
if ($mobile !== '') {
$query['mobile'] = $mobile;
}
return redirect()->to(base_url('security/login-diagnostic?' . http_build_query($query)));
}
}

View File

@ -853,10 +853,74 @@ class RestAuthenticationController extends AdminController
if (!empty($HRAccessData) && isset($HRAccessData['allowed_modules'])) {
$decoded = json_decode($HRAccessData['allowed_modules'], true);
// $decoded = json_decode($HRAccessData['allowed_modules'], true);
// $getAllhrData[$key]['allowed_modules'] = $decoded;
// $HRAccessData['pre_client_id'] = md5($HRAccessData['pre_client_id']);
// $HRAccessData['post_client_id'] = md5($HRAccessData['post_client_id']);
$decoded = json_decode($HRAccessData['allowed_modules'], true) ?? [];
$allowedActivePolicies = json_decode($HRAccessData['allowed_active_policies'] ?? '[]', true) ?? [];
if (!is_array($allowedActivePolicies)) {
$allowedActivePolicies = [];
}
if (empty($allowedActivePolicies) && is_array($decoded)) {
if (isset($decoded['post']) && is_array($decoded['post'])) {
$decoded['post'] = array_values(array_filter(
$decoded['post'],
static fn($module) => (int) $module !== 4
));
} elseif (!isset($decoded['pre']) && !isset($decoded['post'])) {
$decoded = array_values(array_filter(
$decoded,
static fn($module) => (int) $module !== 4
));
}
$HRAccessData['allowed_modules'] = json_encode($decoded);
}
$claimsSubMenu = [];
$hasClaimsModule = false;
if (is_array($decoded)) {
if (isset($decoded['post']) && is_array($decoded['post'])) {
$hasClaimsModule = in_array(4, array_map('intval', $decoded['post']), true);
} elseif (!isset($decoded['pre']) && !isset($decoded['post'])) {
$hasClaimsModule = in_array(4, array_map('intval', $decoded), true);
}
}
if ($hasClaimsModule) {
$claimsSubMenu[] = 'EB';
$hasNonEbPolicy = false;
if (!empty($allowedActivePolicies)) {
$policyAllocgRows = $db2->table('client_policy')
->select('policy_type.allocg')
->join('policy_type', 'policy_type.id = client_policy.policy_type_id', 'left')
->whereIn('client_policy.id', $allowedActivePolicies)
->get()
->getResultArray();
foreach ($policyAllocgRows as $policyRow) {
if (($policyRow['allocg'] ?? '') !== 'EB') {
$hasNonEbPolicy = true;
break;
}
}
}
if ($hasNonEbPolicy) {
$claimsSubMenu[] = 'Non-EB';
}
}
$getAllhrData[$key]['allowed_modules'] = $decoded;
$getAllhrData[$key]['claims_sub_menu'] = $claimsSubMenu;
$HRAccessData['pre_client_id'] = md5($HRAccessData['pre_client_id']);
$HRAccessData['post_client_id'] = md5($HRAccessData['post_client_id']);
$HRAccessData['claims_sub_menu'] = $claimsSubMenu;
$token = JWTToken::encode($HRAccessData);
$getAllhrData[$key]['token'] = $token;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,632 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= esc($title ?? 'Employee Login Diagnostic') ?></title>
<style>
:root{
--bg: #F6F8F9;
--surface: #FFFFFF;
--border: #E3E7EA;
--border-strong: #CBD2D8;
--text-primary: #16212B;
--text-secondary: #5B6770;
--text-muted: #8B959C;
--teal: #0E8F9C;
--teal-dark: #0A6B75;
--teal-tint: #E7F5F6;
--success-bg: #E4F5EC;
--success-border: #B9E3CC;
--success-text: #1B7A4C;
--warn-bg: #FFF6E0;
--warn-border: #F0D9A0;
--warn-text: #8A5B00;
--danger-bg: #FDECEE;
--danger-border: #F0C4CB;
--danger-text: #9B1C3A;
--danger-code: #C43D6E;
--low-badge-bg: #EDEFF1;
--low-badge-text: #4A545C;
--med-badge-bg: #FFF3CD;
--med-badge-text: #664D03;
--high-badge-bg: #F8D7DA;
--high-badge-text: #842029;
--mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace;
--sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--radius-sm: 6px;
--radius-md: 10px;
}
*{box-sizing:border-box;}
body{
margin:0;
background:var(--bg);
font-family:var(--sans);
color:var(--text-primary);
padding:32px 40px 64px;
}
.wrap{max-width:1320px;margin:0 auto;}
.page-head{margin-bottom:24px;}
.page-head h1{font-size:22px;font-weight:600;margin:0 0 4px;letter-spacing:-0.01em;}
.page-head p{margin:0;font-size:13.5px;color:var(--text-secondary);}
.page-head p b{color:var(--text-primary);font-weight:600;}
.card{
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius-md);
}
.search-card{padding:20px 24px;margin-bottom:16px;}
.search-grid{display:grid;grid-template-columns:1fr 1fr auto auto;gap:16px;align-items:end;}
.field label{display:block;font-size:12.5px;font-weight:600;color:var(--text-primary);margin-bottom:6px;}
.field label span{font-weight:400;color:var(--text-muted);font-family:var(--mono);font-size:11.5px;margin-left:2px;}
.field input{
width:100%;height:38px;border:1px solid var(--border-strong);border-radius:var(--radius-sm);
padding:0 12px;font-size:13.5px;font-family:var(--sans);color:var(--text-primary);
background:#FCFDFD;
}
.field input::placeholder{color:var(--text-muted);}
.field input:focus{outline:none;border-color:var(--teal);box-shadow:0 0 0 3px rgba(14,143,156,0.12);}
.btn{
height:38px;padding:0 20px;border-radius:var(--radius-sm);font-size:13.5px;font-weight:600;
border:1px solid transparent;cursor:pointer;white-space:nowrap;text-decoration:none;
display:inline-flex;align-items:center;justify-content:center;
}
.btn-primary{background:var(--teal);color:#fff;}
.btn-primary:hover{background:var(--teal-dark);}
.btn-ghost{background:#fff;border-color:var(--border-strong);color:var(--text-primary);}
.btn-ghost:hover{background:#F5F6F7;}
.search-hint{grid-column:1/-1;font-size:12px;color:var(--text-muted);margin:2px 0 0;}
.search-hint b{color:var(--text-secondary);font-weight:600;}
.status-banner{
display:flex;align-items:center;justify-content:space-between;
border-radius:var(--radius-md);padding:16px 22px;margin-bottom:16px;
}
.status-ok{background:var(--success-bg);border:1px solid var(--success-border);}
.status-ok .txt strong{color:var(--success-text);}
.status-ok .txt span{color:#2C5F45;}
.status-ok .copy-btn{border-color:var(--success-border);color:var(--success-text);}
.status-ok .copy-btn:hover{background:#F1FBF6;}
.status-warn,.status-partial{background:var(--warn-bg);border:1px solid var(--warn-border);}
.status-warn .txt strong,.status-partial .txt strong{color:var(--warn-text);}
.status-warn .txt span,.status-partial .txt span{color:#6B4A10;}
.status-warn .copy-btn,.status-partial .copy-btn{border-color:var(--warn-border);color:var(--warn-text);}
.status-fail{background:var(--danger-bg);border:1px solid var(--danger-border);}
.status-fail .txt strong{color:var(--danger-text);}
.status-fail .txt span{color:#7A2A3A;}
.status-fail .copy-btn{border-color:var(--danger-border);color:var(--danger-text);}
.status-banner .txt strong{display:block;font-size:14.5px;font-weight:700;margin-bottom:2px;}
.status-banner .txt span{font-size:13px;}
.status-banner .copy-btn{
background:#fff;font-size:12.5px;font-weight:600;padding:8px 14px;border-radius:var(--radius-sm);
cursor:pointer;border:1px solid;
}
.section-title{
font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;
color:var(--text-secondary);padding:14px 20px;background:#F8F9FA;
border:1px solid var(--border);border-bottom:none;border-radius:var(--radius-md) var(--radius-md) 0 0;
}
.section-title span{text-transform:none;font-weight:400;letter-spacing:0;color:var(--text-muted);}
table{width:100%;border-collapse:collapse;}
.problems-table{border:1px solid var(--border);border-radius:0 0 var(--radius-md) var(--radius-md);overflow:hidden;margin-bottom:20px;background:#fff;}
.problems-table th{
text-align:left;font-size:12px;font-weight:700;color:var(--text-secondary);
padding:10px 20px;border-bottom:1px solid var(--border);background:#fff;
}
.problems-table td{
padding:12px 20px;font-size:13.5px;border-bottom:1px solid var(--border);
border-left:3px solid var(--teal);vertical-align:middle;
}
.problems-table tr:last-child td{border-bottom:none;}
.problems-table tr:nth-child(even) td{background:#FAFBFC;}
.problems-table .empty{border-left-color:transparent;color:var(--text-muted);text-align:center;}
.badge{
display:inline-block;font-size:11px;font-weight:700;padding:3px 9px;border-radius:4px;letter-spacing:0.03em;
}
.badge-low{background:var(--low-badge-bg);color:var(--low-badge-text);}
.badge-medium{background:var(--med-badge-bg);color:var(--med-badge-text);}
.badge-high{background:var(--high-badge-bg);color:var(--high-badge-text);}
.code{font-family:var(--mono);font-size:12.5px;color:var(--danger-code);font-weight:600;}
.fix-text{color:var(--text-secondary);}
.compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;}
.compare-card{border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden;background:#fff;}
.compare-head{
padding:14px 20px;background:#F8F9FA;border-bottom:1px solid var(--border);
font-size:13px;font-weight:700;color:var(--text-primary);
}
.compare-head span{font-weight:400;color:var(--text-muted);}
.kv-table td{padding:10px 20px;font-size:13px;border-bottom:1px solid var(--border);}
.kv-table tr:last-child td{border-bottom:none;}
.kv-table tr:nth-child(even) td{background:#FAFBFC;}
.kv-key{color:var(--text-secondary);font-weight:600;width:44%;}
.kv-val{color:var(--text-primary);font-family:var(--mono);font-size:12.5px;}
.check{color:#1B9C56;font-weight:700;}
.cross{color:var(--danger-code);font-weight:700;}
.val-flag{color:var(--danger-code);font-weight:700;}
.alert-inline{margin:12px 20px;padding:10px 12px;border-radius:var(--radius-sm);background:var(--warn-bg);border:1px solid var(--warn-border);color:var(--warn-text);font-size:12.5px;}
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--border);margin-bottom:0;padding:0 4px;}
.tab{
padding:11px 18px;font-size:13px;font-weight:600;color:var(--text-secondary);
cursor:pointer;border-bottom:2px solid transparent;background:none;border-top:none;border-left:none;border-right:none;
font-family:var(--sans);
}
.tab.active{color:var(--teal-dark);border-bottom-color:var(--teal);}
.tab:hover:not(.active){color:var(--text-primary);}
.tab-panel{border:1px solid var(--border);border-top:none;border-radius:0 0 var(--radius-md) var(--radius-md);overflow:hidden;background:#fff;display:none;}
.tab-panel.active{display:block;}
.cred-table th{
text-align:left;font-size:12px;font-weight:700;color:var(--text-secondary);
padding:12px 20px;background:#F8F9FA;border-bottom:1px solid var(--border);
}
.cred-table th.center, .cred-table td.center{text-align:left;width:22%;}
.cred-table td{padding:11px 20px;font-size:13.5px;border-bottom:1px solid var(--border);}
.cred-table tr:last-child td{border-bottom:none;}
.cred-table tr:nth-child(even) td{background:#FAFBFC;}
.cred-key{font-weight:600;color:var(--text-primary);}
.val-yes{color:#1B9C56;font-weight:600;}
.val-no{color:var(--text-muted);}
.val-raw{font-family:var(--mono);color:var(--danger-code);font-weight:600;}
.muted{color:var(--text-muted);font-size:13px;padding:16px 20px;}
.dup-wrap{padding:16px 20px;}
.dup-wrap h6{margin:0 0 10px;font-size:13px;font-weight:700;}
.dup-wrap h6 + .dup-table, .dup-wrap .dup-block{margin-bottom:18px;}
.dup-subhead{font-size:12px;font-weight:700;color:var(--text-secondary);margin:14px 0 8px;text-transform:uppercase;letter-spacing:0.03em;}
.dup-subhead .count{font-weight:600;color:var(--text-muted);text-transform:none;letter-spacing:0;}
.dup-table{border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;}
.dup-table th,.dup-table td{padding:8px 12px;font-size:12px;border-bottom:1px solid var(--border);text-align:left;}
.dup-table th{background:#F8F9FA;font-weight:700;color:var(--text-secondary);}
.dup-table tr:last-child td{border-bottom:none;}
.winner{display:inline-block;background:var(--success-bg);color:var(--success-text);font-size:10px;font-weight:700;padding:2px 6px;border-radius:4px;margin-right:4px;}
.dup-badge{display:inline-block;font-size:10px;font-weight:700;padding:2px 6px;border-radius:4px;}
.dup-self{background:var(--danger-bg);color:var(--danger-text);}
.dup-family{background:var(--warn-bg);color:var(--warn-text);}
.row-self-dup td{background:#FDECEE !important;}
.row-family-dup td{background:#FFF8E8 !important;}
.problems-table tr.sev-high td{border-left-color:var(--danger-text);background:#FDF2F4;}
.problems-table tr.sev-medium td{border-left-color:#C9A227;}
.problems-table tr.sev-low td{border-left-color:var(--text-muted);}
.window-open{color:#1B9C56;font-weight:700;}
.window-closed{color:var(--danger-text);font-weight:700;}
.window-pending{color:var(--warn-text);font-weight:700;}
.window-missing{color:var(--text-muted);font-weight:600;}
.policy-wrap{padding:16px 20px;}
.policy-wrap h6{margin:0 0 10px;font-size:13px;font-weight:700;}
.policy-note{font-size:12px;color:var(--text-muted);margin:0 0 12px;}
@media (max-width:900px){
body{padding:20px 16px 40px;}
.search-grid{grid-template-columns:1fr;}
.compare-grid{grid-template-columns:1fr;}
.status-banner{flex-direction:column;align-items:flex-start;gap:12px;}
}
</style>
</head>
<body>
<div class="wrap">
<div class="page-head">
<h1>Employee login diagnostic</h1>
<p><b>PRE</b> (enrollment) + <b>POST</b> (live) &middot; read-only &middot; does not send OTP</p>
</div>
<form method="get" action="<?= base_url('security/login-diagnostic') ?>" class="card search-card">
<div class="search-grid">
<div class="field">
<label>Email <span>email_corporate</span></label>
<input type="text" name="email" value="<?= esc($email ?? '') ?>" placeholder="user@company.com">
</div>
<div class="field">
<label>Mobile</label>
<input type="text" name="mobile" value="<?= esc($mobile ?? '') ?>" placeholder="10-digit mobile">
</div>
<button type="submit" class="btn btn-primary">Diagnose</button>
<a class="btn btn-ghost" href="<?= base_url('security/login-diagnostic') ?>">Clear</a>
<p class="search-hint">If both are filled, login logic uses <b>mobile first</b> (same as auth APIs).</p>
</div>
</form>
<?php if (! empty($has_search) && is_array($result ?? null)): ?>
<?php
$v = $result['verdict'] ?? 'WILL_FAIL';
$bannerClass = match ($v) {
'OK' => 'status-ok',
'OK_WITH_WARNINGS' => 'status-warn',
'PARTIAL' => 'status-partial',
default => 'status-fail',
};
$pre = $result['pre'] ?? [];
$post = $result['post'] ?? [];
$merge = $result['merge'] ?? [];
$problems = $result['problems'] ?? [];
$connectivity = $result['connectivity'] ?? [];
$pc = $pre['credentials'] ?? [];
$oc = $post['credentials'] ?? [];
$pt = $pre['token'] ?? [];
$ot = $post['token'] ?? [];
$yn = static fn ($ok) => $ok ? '<span class="val-yes">Yes</span>' : '<span class="val-no">No</span>';
$rawFlag = static function ($val): string {
if ($val === null || $val === '') {
return 'null';
}
return (string) $val;
};
$mark = static fn (bool $ok) => $ok ? '<span class="check">&#10003;</span>' : '<span class="cross">&#10007;</span>';
?>
<div class="status-banner <?= esc($bannerClass) ?>">
<div class="txt">
<strong><?= esc($v) ?></strong>
<span><?= esc($result['verdict_message'] ?? '') ?></span>
</div>
<button type="button" class="copy-btn" id="copyDiagSummary">Copy summary</button>
</div>
<div class="section-title">Problems <span>(failed checks only)</span></div>
<table class="problems-table">
<tr>
<th style="width:80px;">Severity</th>
<th style="width:190px;">Code</th>
<th>Message</th>
<th style="width:340px;">Fix</th>
</tr>
<?php if (empty($problems)): ?>
<tr><td class="empty" colspan="4">No problems detected.</td></tr>
<?php else: ?>
<?php foreach ($problems as $p): ?>
<?php $sev = strtolower((string) ($p['severity'] ?? 'low')); ?>
<tr class="sev-<?= esc($sev) ?>">
<td><span class="badge badge-<?= esc($sev) ?>"><?= esc(strtoupper($sev)) ?></span></td>
<td><span class="code"><?= esc($p['code'] ?? '') ?></span></td>
<td><?= esc($p['message'] ?? '') ?></td>
<td class="fix-text"><?= esc($p['fix'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</table>
<div class="compare-grid">
<div class="compare-card">
<div class="compare-head">PRE <span>(enrollment)</span></div>
<?= view('admin/login_diagnostic_side', ['side' => $pre, 'mark' => $mark]) ?>
</div>
<div class="compare-card">
<div class="compare-head">POST <span>(live)</span></div>
<?= view('admin/login_diagnostic_side', ['side' => $post, 'mark' => $mark]) ?>
</div>
</div>
<div class="tabs" role="tablist">
<button type="button" class="tab active" data-tab="merge">Merge</button>
<button type="button" class="tab" data-tab="dup">Duplicates</button>
<button type="button" class="tab" data-tab="policies">Policies</button>
<button type="button" class="tab" data-tab="wellness">Wellness</button>
<button type="button" class="tab" data-tab="cred">Credentials</button>
<button type="button" class="tab" data-tab="token">Token</button>
<button type="button" class="tab" data-tab="conn">Connectivity</button>
</div>
<div class="tab-panel active" id="panel-merge">
<table class="cred-table">
<tr><td class="cred-key">PRE eligible</td><td><?= ! empty($merge['pre_eligible']) ? 'Yes' : 'No' ?></td></tr>
<tr><td class="cred-key">POST eligible</td><td><?= ! empty($merge['post_eligible']) ? 'Yes' : 'No' ?></td></tr>
<tr><td class="cred-key">PRE short_name</td><td class="val-raw"><?= esc((string) ($merge['pre_short_name'] ?? '')) ?></td></tr>
<tr><td class="cred-key">POST short_name</td><td class="val-raw"><?= esc((string) ($merge['post_short_name'] ?? '')) ?></td></tr>
<tr><td class="cred-key">short_name match</td><td><?= isset($merge['short_name_match']) ? ($merge['short_name_match'] ? 'Yes' : 'No') : '—' ?></td></tr>
<tr><td class="cred-key">PRE created_at</td><td class="val-raw"><?= esc((string) ($merge['pre_created_at'] ?? '—')) ?></td></tr>
<tr><td class="cred-key">POST created_at</td><td class="val-raw"><?= esc((string) ($merge['post_created_at'] ?? '—')) ?></td></tr>
<tr><td class="cred-key">Would return both</td><td><?= ! empty($merge['would_return_both']) ? 'Yes' : 'No' ?></td></tr>
<tr><td class="cred-key">Winner</td><td><b><?= esc((string) ($merge['winner'] ?? '—')) ?></b></td></tr>
<tr><td class="cred-key">OTP sync possible</td><td><?= ! empty($merge['otp_sync_possible']) ? 'Yes' : 'No' ?></td></tr>
<tr><td class="cred-key">Reason</td><td class="fix-text"><?= esc((string) ($merge['reason'] ?? '')) ?></td></tr>
</table>
</div>
<div class="tab-panel" id="panel-dup">
<div class="dup-wrap">
<?php
$preDup = $pre['duplicates'] ?? [];
$postDup = $post['duplicates'] ?? [];
$renderSideDups = static function (string $title, array $side, array $dup): void {
$selfRows = $dup['self_rows'] ?? [];
$familyRows = $dup['family_rows'] ?? [];
// Fallback if older payload
if ($selfRows === [] && $familyRows === [] && ! empty($side['raw_matches'])) {
foreach ($side['raw_matches'] as $r) {
if (strtolower(trim((string) ($r['relationship'] ?? ''))) === 'self') {
$selfRows[] = $r;
} else {
$familyRows[] = $r;
}
}
}
?>
<div class="dup-block">
<h6><?= esc($title) ?></h6>
<div class="dup-subhead">Self <span class="count">(<?= count($selfRows) ?>)</span></div>
<?= view('admin/login_diagnostic_matches', ['rows' => $selfRows]) ?>
<div class="dup-subhead">Family <span class="count">(<?= count($familyRows) ?>)</span></div>
<?= view('admin/login_diagnostic_matches', ['rows' => $familyRows]) ?>
</div>
<?php
};
?>
<?php $renderSideDups('PRE raw matches', $pre, $preDup); ?>
<?php if (! empty($pre['raw_by_personal'])): ?>
<div class="dup-block">
<h6>PRE matches on email_personal only</h6>
<?= view('admin/login_diagnostic_matches', ['rows' => $pre['raw_by_personal']]) ?>
</div>
<?php endif; ?>
<?php $renderSideDups('POST raw matches', $post, $postDup); ?>
</div>
</div>
<div class="tab-panel" id="panel-policies">
<div class="policy-wrap">
<p class="policy-note">PRE: enrollment open/close on <code>employee_polices</code>. POST: live policy mapping. Wellness details are on the <b>Wellness</b> tab (POST only).</p>
<?php if (! empty($pre['policies_error'])): ?>
<div class="alert-inline">PRE policies error: <?= esc($pre['policies_error']) ?></div>
<?php endif; ?>
<?php if (! empty($post['policies_error'])): ?>
<div class="alert-inline">POST policies error: <?= esc($post['policies_error']) ?></div>
<?php endif; ?>
<?php
$windowClass = static function (string $w): string {
return match ($w) {
'OPEN' => 'window-open',
'CLOSED' => 'window-closed',
'NOT_STARTED' => 'window-pending',
default => 'window-missing',
};
};
$renderPolicies = static function (string $title, array $policies, bool $showEnrollment) use ($windowClass): void {
?>
<div class="dup-block">
<h6><?= esc($title) ?> <span class="count">(<?= count($policies) ?>)</span></h6>
<?php if (empty($policies)): ?>
<p class="muted" style="padding:0;">None</p>
<?php else: ?>
<div class="dup-table">
<table>
<thead>
<tr>
<th>Emp policy</th>
<th>Client policy</th>
<th>Policy no</th>
<th>EP status</th>
<th>EP active</th>
<th>Policy period</th>
<?php if ($showEnrollment): ?>
<th>Enrollment open</th>
<th>Enrollment close</th>
<th>Window</th>
<?php else: ?>
<th>CP open_for_enrollment</th>
<th>CP open/close</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($policies as $pol): ?>
<?php
$w = (string) ($pol['enrollment_window'] ?? '');
$rowClass = ($showEnrollment && in_array($w, ['CLOSED', 'NOT_STARTED', 'MISSING'], true)) ? 'row-family-dup' : '';
if ($showEnrollment && $w === 'CLOSED') {
$rowClass = 'row-self-dup';
}
?>
<tr class="<?= esc($rowClass) ?>">
<td><?= esc((string) ($pol['emp_policy_id'] ?? '')) ?></td>
<td><?= esc((string) ($pol['client_policy_id'] ?? '')) ?></td>
<td><?= esc((string) ($pol['policy_no'] ?? '—')) ?></td>
<td><?= esc((string) ($pol['emp_policy_status'] ?? '')) ?></td>
<td><?= esc((string) ($pol['emp_policy_is_active'] ?? '')) ?></td>
<td><?= esc((string) (($pol['policy_start_date'] ?? '—') . ' → ' . ($pol['policy_end_date'] ?? '—'))) ?></td>
<?php if ($showEnrollment): ?>
<td class="val-raw"><?= esc((string) ($pol['enrollment_open_date'] ?? 'null')) ?></td>
<td class="val-raw"><?= esc((string) ($pol['enrollment_close_date'] ?? 'null')) ?></td>
<td class="<?= esc($windowClass($w)) ?>"><?= esc($w) ?></td>
<?php else: ?>
<td><?= esc((string) ($pol['open_for_enrollment'] ?? '—')) ?></td>
<td><?= esc((string) (($pol['client_open_date'] ?? '—') . ' → ' . ($pol['client_close_date'] ?? '—'))) ?></td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php
};
$renderPolicies('PRE employee policies (enrollment dates)', $pre['policies'] ?? [], true);
$renderPolicies('POST employee policies (live mapping)', $post['policies'] ?? [], false);
?>
</div>
</div>
<div class="tab-panel" id="panel-wellness">
<div class="policy-wrap">
<p class="policy-note"><b>POST only.</b> Wellness SSO needs <code>wellness_onboard</code> not null and not <code>0</code> (same as <code>getWellnessUrl</code>). PRE has no wellness fields.</p>
<?php
$postPolicies = $post['policies'] ?? [];
$wellnessReadyCount = 0;
$wellnessPlanCount = 0;
foreach ($postPolicies as $wp) {
if (! empty($wp['wellness_ready'])) {
$wellnessReadyCount++;
}
$pid = $wp['wellness_plan_id'] ?? null;
if ($pid !== null && $pid !== '' && (string) $pid !== '0') {
$wellnessPlanCount++;
}
}
$overallReady = $wellnessReadyCount > 0;
?>
<table class="cred-table" style="margin-bottom:16px;">
<tr>
<td class="cred-key">Overall wellness ready</td>
<td class="<?= $overallReady ? 'window-open' : 'window-closed' ?>">
<?= $overallReady ? 'Yes' : 'No' ?>
<span class="fix-text">(<?= (int) $wellnessReadyCount ?> of <?= count($postPolicies) ?> policies onboarded)</span>
</td>
</tr>
<tr>
<td class="cred-key">Policies with wellness_plan_id</td>
<td><?= (int) $wellnessPlanCount ?></td>
</tr>
<tr>
<td class="cred-key">Rule</td>
<td class="fix-text">Ready = <code>wellness_onboard</code> is not null and not 0</td>
</tr>
</table>
<div class="dup-block">
<h6>POST wellness by policy <span class="count">(<?= count($postPolicies) ?>)</span></h6>
<?php if (empty($postPolicies)): ?>
<p class="muted" style="padding:0;">No POST employee policies to check.</p>
<?php else: ?>
<div class="dup-table">
<table>
<thead>
<tr>
<th>Emp policy</th>
<th>Client policy</th>
<th>Policy no</th>
<th>EP status</th>
<th>EP active</th>
<th>wellness_onboard</th>
<th>Wellness ready</th>
<th>wellness_plan_id</th>
<th>wellness_vendor_id</th>
</tr>
</thead>
<tbody>
<?php foreach ($postPolicies as $pol): ?>
<?php $ready = ! empty($pol['wellness_ready']); ?>
<tr class="<?= $ready ? '' : 'row-self-dup' ?>">
<td><?= esc((string) ($pol['emp_policy_id'] ?? '')) ?></td>
<td><?= esc((string) ($pol['client_policy_id'] ?? '')) ?></td>
<td><?= esc((string) ($pol['policy_no'] ?? '—')) ?></td>
<td><?= esc((string) ($pol['emp_policy_status'] ?? '')) ?></td>
<td><?= esc((string) ($pol['emp_policy_is_active'] ?? '')) ?></td>
<td class="val-raw"><?= esc((string) ($pol['wellness_onboard_raw'] ?? 'null')) ?></td>
<td class="<?= $ready ? 'window-open' : 'window-closed' ?>"><?= $ready ? 'Yes' : 'No' ?></td>
<td class="val-raw"><?= esc((string) (($pol['wellness_plan_id'] ?? '') !== '' && ($pol['wellness_plan_id'] ?? null) !== null ? $pol['wellness_plan_id'] : 'null')) ?></td>
<td class="val-raw"><?= esc((string) (($pol['wellness_vendor_id'] ?? '') !== '' && ($pol['wellness_vendor_id'] ?? null) !== null ? $pol['wellness_vendor_id'] : 'null')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="tab-panel" id="panel-cred">
<table class="cred-table">
<tr>
<th></th>
<th class="center">PRE</th>
<th class="center">POST</th>
</tr>
<tr><td class="cred-key">OTP set</td><td class="center"><?= $yn(! empty($pc['otp_set'])) ?></td><td class="center"><?= $yn(! empty($oc['otp_set'])) ?></td></tr>
<tr><td class="cred-key">MPIN set</td><td class="center"><?= $yn(! empty($pc['mpin_set'])) ?></td><td class="center"><?= $yn(! empty($oc['mpin_set'])) ?></td></tr>
<tr><td class="cred-key">MPIN skipped</td><td class="center"><?= $yn(! empty($pc['mpin_skipped'])) ?></td><td class="center"><?= $yn(! empty($oc['mpin_skipped'])) ?></td></tr>
<tr><td class="cred-key">Biometric set</td><td class="center"><?= $yn(! empty($pc['biometric_set'])) ?></td><td class="center"><?= $yn(! empty($oc['biometric_set'])) ?></td></tr>
<tr>
<td class="cred-key">is_biometric_enabled <span style="font-weight:400;color:var(--text-muted);">(raw)</span></td>
<td class="center val-raw"><?= esc($rawFlag($pc['is_biometric_enabled'] ?? null)) ?></td>
<td class="center val-raw"><?= esc($rawFlag($oc['is_biometric_enabled'] ?? null)) ?></td>
</tr>
<tr><td class="cred-key">Password set</td><td class="center"><?= $yn(! empty($pc['password_set'])) ?></td><td class="center"><?= $yn(! empty($oc['password_set'])) ?></td></tr>
</table>
</div>
<div class="tab-panel" id="panel-token">
<table class="cred-table">
<tr>
<th></th>
<th class="center">PRE</th>
<th class="center">POST</th>
</tr>
<tr><td class="cred-key">Status</td><td class="center"><?= esc((string) ($pt['status'] ?? '—')) ?></td><td class="center"><?= esc((string) ($ot['status'] ?? '—')) ?></td></tr>
<tr><td class="cred-key">Details</td><td class="center fix-text"><?= esc((string) ($pt['human'] ?? '—')) ?></td><td class="center fix-text"><?= esc((string) ($ot['human'] ?? '—')) ?></td></tr>
<tr><td class="cred-key">TOKENTIMEOUT env</td><td colspan="2"><?= esc((string) ($pt['tokentimeout_env'] ?? $ot['tokentimeout_env'] ?? '—')) ?> seconds</td></tr>
</table>
</div>
<div class="tab-panel" id="panel-conn">
<table class="cred-table">
<tr><td class="cred-key">POST_ENROLLMENT_BASEURL</td><td class="val-raw"><?= esc((string) ($connectivity['base_url'] ?? '')) ?></td></tr>
<tr><td class="cred-key">App-Signature configured</td><td><?= ! empty($connectivity['app_signature']) ? 'Yes' : 'No' ?></td></tr>
<tr><td class="cred-key">POST API reachable</td><td><?= ! empty($connectivity['api_reachable']) ? 'Yes' : 'No' ?> (HTTP <?= esc((string) ($connectivity['api_http_code'] ?? '—')) ?>)</td></tr>
<tr><td class="cred-key">API message</td><td class="fix-text"><?= esc((string) ($connectivity['api_message'] ?? '')) ?></td></tr>
<tr><td class="cred-key">postDB connected</td><td><?= ! empty($connectivity['post_db']) ? 'Yes' : 'No' ?></td></tr>
<?php if (! empty($connectivity['post_db_message'])): ?>
<tr><td class="cred-key">postDB error</td><td class="fix-text"><?= esc((string) $connectivity['post_db_message']) ?></td></tr>
<?php endif; ?>
</table>
</div>
<script>
(function () {
var tabs = document.querySelectorAll('.tab');
var panels = {
merge: document.getElementById('panel-merge'),
dup: document.getElementById('panel-dup'),
policies: document.getElementById('panel-policies'),
wellness: document.getElementById('panel-wellness'),
cred: document.getElementById('panel-cred'),
token: document.getElementById('panel-token'),
conn: document.getElementById('panel-conn')
};
tabs.forEach(function (tab) {
tab.addEventListener('click', function () {
tabs.forEach(function (t) { t.classList.remove('active'); });
Object.keys(panels).forEach(function (k) { panels[k].classList.remove('active'); });
tab.classList.add('active');
var key = tab.getAttribute('data-tab');
if (panels[key]) panels[key].classList.add('active');
});
});
var btn = document.getElementById('copyDiagSummary');
if (btn) {
btn.addEventListener('click', function () {
var lines = [];
lines.push('Verdict: <?= esc($v, 'js') ?>');
lines.push('<?= esc($result['verdict_message'] ?? '', 'js') ?>');
lines.push('Input email=<?= esc($result['input']['email'] ?? '', 'js') ?> mobile=<?= esc($result['input']['mobile'] ?? '', 'js') ?>');
lines.push('Merge winner: <?= esc($merge['winner'] ?? '', 'js') ?>');
<?php foreach ($problems as $p): ?>
lines.push('[<?= esc(strtoupper((string) ($p['severity'] ?? '')), 'js') ?>] <?= esc($p['code'] ?? '', 'js') ?>: <?= esc($p['message'] ?? '', 'js') ?>');
<?php endforeach; ?>
navigator.clipboard.writeText(lines.join('\n')).then(function () {
btn.textContent = 'Copied';
setTimeout(function () { btn.textContent = 'Copy summary'; }, 1500);
});
});
}
})();
</script>
<?php elseif (! empty($has_search)): ?>
<div class="status-banner status-fail">
<div class="txt">
<strong>WILL_FAIL</strong>
<span>No diagnostic result returned.</span>
</div>
</div>
<?php endif; ?>
</div>
</body>
</html>

View File

@ -0,0 +1,62 @@
<?php /** @var array $rows */ ?>
<?php if (empty($rows)): ?>
<p class="muted" style="padding:0;">None</p>
<?php else: ?>
<div class="dup-table">
<table>
<thead>
<tr>
<th></th>
<th>ID</th>
<th>Client</th>
<th>Emp code</th>
<th>Name</th>
<th>Rel</th>
<th>Status</th>
<th>Active</th>
<th>Mobile</th>
<th>Email corp</th>
<th>Policies</th>
<th>Created</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<?php
$rowClass = '';
if (! empty($row['is_self_duplicate']) || ! empty($row['is_cross_self_dup'])) {
$rowClass = 'row-self-dup';
} elseif (! empty($row['is_family_duplicate'])) {
$rowClass = 'row-family-dup';
}
?>
<tr class="<?= esc($rowClass) ?>">
<td>
<?php if (! empty($row['is_login_winner'])): ?>
<span class="winner">WINNER</span>
<?php endif; ?>
<?php if (! empty($row['dup_badge'])): ?>
<span class="dup-badge <?= ! empty($row['is_self_duplicate']) || ! empty($row['is_cross_self_dup']) ? 'dup-self' : 'dup-family' ?>">
<?= esc((string) $row['dup_badge']) ?>
</span>
<?php elseif (! empty($row['is_login_pick_if_filters_passed'])): ?>
<span class="winner" style="background:#EDEFF1;color:#4A545C;">highest id</span>
<?php endif; ?>
</td>
<td><?= esc((string) ($row['id'] ?? '')) ?></td>
<td><?= esc((string) ($row['client_id'] ?? '')) ?></td>
<td><?= esc((string) ($row['emp_code'] ?? '')) ?></td>
<td><?= esc((string) ($row['name'] ?? '')) ?></td>
<td><?= esc((string) ($row['relationship'] ?? '')) ?></td>
<td><?= esc((string) ($row['emp_status'] ?? '')) ?></td>
<td><?= esc((string) ($row['is_active'] ?? '')) ?></td>
<td><?= esc((string) ($row['mobile'] ?? '')) ?></td>
<td><?= esc((string) ($row['email_corporate'] ?? '')) ?></td>
<td><?= esc((string) ($row['policies'] ?? '—')) ?></td>
<td><?= esc((string) ($row['created_at'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>

View File

@ -0,0 +1,48 @@
<?php
/** @var array $side */
/** @var callable $mark */
$fs = $side['filters_summary'] ?? [];
$eligible = $side['eligible'] ?? null;
$error = $side['error'] ?? null;
?>
<?php if (! empty($error)): ?>
<div class="alert-inline"><?= esc($error) ?></div>
<?php endif; ?>
<table class="kv-table">
<tr>
<td class="kv-key">Raw found</td>
<td class="kv-val">
<?= ! empty($side['raw_matches'])
? $mark(true) . ' Yes (' . count($side['raw_matches']) . ')'
: $mark(false) . ' No' ?>
</td>
</tr>
<tr>
<td class="kv-key">Eligible for login</td>
<td class="kv-val"><?= ! empty($eligible) ? $mark(true) . ' Yes' : $mark(false) . ' No' ?></td>
</tr>
<tr><td class="kv-key">self</td><td class="kv-val"><?= isset($fs['self']) ? $mark((bool) $fs['self']) : '—' ?></td></tr>
<tr><td class="kv-key">is_active</td><td class="kv-val"><?= isset($fs['is_active']) ? $mark((bool) $fs['is_active']) : '—' ?></td></tr>
<tr><td class="kv-key">emp_status OK</td><td class="kv-val"><?= isset($fs['emp_status_ok']) ? $mark((bool) $fs['emp_status_ok']) : '—' ?></td></tr>
<tr><td class="kv-key">has policy</td><td class="kv-val"><?= isset($fs['has_policy']) ? $mark((bool) $fs['has_policy']) : '—' ?></td></tr>
<tr><td class="kv-key">policy status OK</td><td class="kv-val"><?= isset($fs['policy_status_ok']) ? $mark((bool) $fs['policy_status_ok']) : '—' ?></td></tr>
<tr><td class="kv-key">client_id</td><td class="kv-val"><?= esc((string) ($side['client_id'] ?? ($eligible['client_id'] ?? '—'))) ?></td></tr>
<tr><td class="kv-key">short_name</td><td class="kv-val val-flag"><?= esc((string) ($side['client_short_name'] ?? '')) ?></td></tr>
<?php if (! empty($eligible)): ?>
<tr><td class="kv-key">employee id</td><td class="kv-val"><?= esc((string) ($eligible['id'] ?? '')) ?></td></tr>
<tr><td class="kv-key">emp_code</td><td class="kv-val"><?= esc((string) ($eligible['emp_code'] ?? '')) ?></td></tr>
<tr><td class="kv-key">name</td><td class="kv-val"><?= esc((string) ($eligible['name'] ?? '')) ?></td></tr>
<tr><td class="kv-key">emp_status</td><td class="kv-val"><?= esc((string) ($eligible['emp_status'] ?? '')) ?></td></tr>
<tr><td class="kv-key">mobile</td><td class="kv-val"><?= esc((string) ($eligible['mobile'] ?? '')) ?></td></tr>
<tr><td class="kv-key">email_corporate</td><td class="kv-val"><?= esc((string) ($eligible['email_corporate'] ?? '')) ?></td></tr>
<?php endif; ?>
<?php if (! empty($fs['notes'])): ?>
<tr><td class="kv-key">Notes</td><td class="kv-val" style="font-family:var(--sans);color:var(--text-secondary);"><?= esc(implode(' ', $fs['notes'])) ?></td></tr>
<?php endif; ?>
<?php if (! empty($side['eligible_via_api']) && empty($eligible)): ?>
<tr>
<td class="kv-key">API eligible</td>
<td class="kv-val"><span class="check">&#10003;</span> via getPostEmployeeDataForAuth (id <?= esc((string) ($side['eligible_via_api']['id'] ?? '')) ?>)</td>
</tr>
<?php endif; ?>
</table>

View File

@ -0,0 +1,40 @@
<?php
namespace Tests\Support;
use CodeIgniter\HTTP\Files\UploadedFile;
/**
* UploadedFile stub for CLI tests bypasses is_uploaded_file() / move_uploaded_file().
*/
class TestUploadedFile extends UploadedFile
{
public function isValid(): bool
{
return $this->getError() === UPLOAD_ERR_OK && is_file($this->getTempName());
}
public function move(string $targetPath, ?string $name = null, bool $overwrite = false)
{
$targetPath = rtrim($targetPath, '/') . '/';
if (!is_dir($targetPath)) {
mkdir($targetPath, 0777, true);
}
$name ??= $this->getName();
$destination = $targetPath . $name;
if (!$overwrite && is_file($destination)) {
$destination = $targetPath . uniqid('test_', true) . '_' . $name;
}
if (!copy($this->getTempName(), $destination)) {
return false;
}
@unlink($this->getTempName());
return true;
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace Tests\Unit;
use App\Models\FileModel;
use CodeIgniter\Test\CIUnitTestCase;
use Tests\Support\TestUploadedFile;
/**
* Tests employeesUplodWithEvents enrollment date handling (d-m-Y / d/m/Y) and file upload validation.
*
* @internal
*/
class EmployeesUploadWithEventsTest extends CIUnitTestCase
{
private const SAMPLE_EXCEL = ROOTPATH . 'public/sample_excel/enrollment.xlsx';
protected function setUp(): void
{
parent::setUp();
helper('utility');
}
/**
* @dataProvider enrollmentDateFormatProvider
*/
public function testChangeDateFormatAcceptsDashAndSlashFormats(
string $openDate,
string $closeDate,
string $expectedOpen,
string $expectedClose
): void {
$this->assertSame($expectedOpen, change_date_format($openDate, null, 'Y-m-d'));
$this->assertSame($expectedClose, change_date_format($closeDate, null, 'Y-m-d'));
}
/**
* Mirrors employeesUplodWithEvents date + files insert payload (EmployeeController.php ~395-403).
*
* @dataProvider enrollmentDateFormatProvider
*/
public function testEnrollmentDatesAreConvertedBeforeFilesTableInsert(
string $openDate,
string $closeDate,
string $expectedOpen,
string $expectedClose
): void {
$postData = [
'client_id' => 12,
'policy_id' => 34,
'client_branch_id' => 56,
'created_by' => 78,
'enrollment_open_date' => $openDate,
'enrollment_close_date' => $closeDate,
];
$insertPayload = $this->buildFilesInsertPayload($postData, 'enrollment_test.xlsx', 78);
$this->assertSame($expectedOpen, $insertPayload['enrollment_open_date']);
$this->assertSame($expectedClose, $insertPayload['enrollment_close_date']);
$this->assertSame('enrollment', $insertPayload['action']);
$this->assertSame('inprogress', $insertPayload['status']);
$this->assertSame(12, $insertPayload['client_id']);
$this->assertSame(34, $insertPayload['policy_id']);
$this->assertSame(56, $insertPayload['client_branch_id']);
$this->assertSame(78, $insertPayload['created_by']);
$this->assertSame(78, $insertPayload['hr_id']);
}
public function testValidateExcelFileAcceptsSampleEnrollmentUpload(): void
{
if (!is_file(self::SAMPLE_EXCEL)) {
$this->markTestSkipped('Sample enrollment excel not found.');
}
$uploadedFile = $this->createTestUploadedFile(self::SAMPLE_EXCEL);
$this->assertTrue(validateExcelFile($uploadedFile));
$this->assertTrue($uploadedFile->move(WRITEPATH . 'uploads/excel/'));
$movedPath = WRITEPATH . 'uploads/excel/' . $uploadedFile->getName();
$this->assertFileExists($movedPath);
@unlink($movedPath);
}
public function testFileModelAllowedFieldsIncludeEnrollmentDates(): void
{
$reflection = new \ReflectionClass(FileModel::class);
$property = $reflection->getProperty('allowedFields');
$property->setAccessible(true);
$allowedFields = $property->getValue(new FileModel());
$this->assertContains('enrollment_open_date', $allowedFields);
$this->assertContains('enrollment_close_date', $allowedFields);
}
public static function enrollmentDateFormatProvider(): array
{
return [
'd-m-Y format' => ['01-07-2026', '31-07-2026', '2026-07-01', '2026-07-31'],
'd/m/Y format' => ['01/07/2026', '31/07/2026', '2026-07-01', '2026-07-31'],
'mixed formats' => ['15-08-2026', '30/08/2026', '2026-08-15', '2026-08-30'],
];
}
/**
* @return array<string, mixed>
*/
private function buildFilesInsertPayload(array $postData, string $filename, int $loggedInUserId): array
{
return [
'file_name' => $filename,
'client_id' => $postData['client_id'] ?? null,
'policy_id' => $postData['policy_id'] ?? null,
'created_by' => $loggedInUserId,
'status' => 'inprogress',
'action' => 'enrollment',
'client_branch_id' => $postData['client_branch_id'] ?? null,
'uploaded_by' => 1,
'enrollment_open_date' => change_date_format($postData['enrollment_open_date'] ?? null, null, 'Y-m-d'),
'enrollment_close_date' => change_date_format($postData['enrollment_close_date'] ?? null, null, 'Y-m-d'),
'hr_id' => $postData['created_by'] ?? null,
];
}
private function createTestUploadedFile(string $sourcePath): TestUploadedFile
{
$tmpPath = tempnam(sys_get_temp_dir(), 'enroll_test_') . '.xlsx';
copy($sourcePath, $tmpPath);
return new TestUploadedFile(
$tmpPath,
basename($sourcePath),
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
filesize($tmpPath),
UPLOAD_ERR_OK
);
}
}

View File

@ -0,0 +1,450 @@
<?php
namespace Tests\Unit;
use App\Controllers\RestAuthenticationController;
use App\Models\AuthHistoryModel;
use App\Models\LevelContactModel;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\HTTP\URI;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Database as DatabaseConfig;
use Config\Services;
use ReflectionClass;
use ReflectionProperty;
class GetVerifiedHrDataTest extends CIUnitTestCase
{
private string $jwtSecret = 'test-secret-key-for-get-verified-hr-data';
protected function setUp(): void
{
parent::setUp();
putenv("JWT_SECRET={$this->jwtSecret}");
$_ENV['JWT_SECRET'] = $this->jwtSecret;
$_SERVER['JWT_SECRET'] = $this->jwtSecret;
putenv('POST_ENROLLMENT_BASEURL=http://example.com/');
$_ENV['POST_ENROLLMENT_BASEURL'] = 'http://example.com/';
$this->mockCurlRequest(['ok' => true, 'source' => 'post_enrollment']);
$this->mockRateLimiter();
}
protected function tearDown(): void
{
$this->clearInjectedDbConnections();
Services::reset(true);
putenv('JWT_SECRET');
putenv('POST_ENROLLMENT_BASEURL');
unset(
$_ENV['JWT_SECRET'],
$_SERVER['JWT_SECRET'],
$_ENV['POST_ENROLLMENT_BASEURL']
);
parent::tearDown();
}
public function testInvalidOtpReturnsFailed404()
{
$hrModel = $this->createHrModelStub([
'first' => null,
'findAll' => [],
]);
$controller = $this->buildController(
['email' => 'hr@example.com', 'otp' => '000000'],
$hrModel
);
$body = $this->decodeResponse($controller->getVerifiedHrData());
$this->assertSame('failed', $body['status']);
$this->assertSame(404, $body['code']);
$this->assertSame('', $body['data']);
$this->assertSame(['ok' => true, 'source' => 'post_enrollment'], $body['post_enrollment']);
}
public function testValidOtpWithNoActiveBranchesReturnsFailed404()
{
$hrModel = $this->createHrModelStub([
'first' => ['id' => 5, 'email' => 'hr@example.com', 'mobile' => '9876543210'],
'findAll' => [],
'update' => true,
]);
$authHistoryModel = $this->createMock(AuthHistoryModel::class);
$authHistoryModel->expects($this->once())->method('insert');
$controller = $this->buildController(
['email' => 'hr@example.com', 'otp' => '123456'],
$hrModel,
$authHistoryModel
);
$body = $this->decodeResponse($controller->getVerifiedHrData());
$this->assertSame('failed', $body['status']);
$this->assertSame(404, $body['code']);
$this->assertSame('', $body['data']);
}
public function testValidMobileOtpReturnsSuccessWithClaimsSubMenu()
{
$branchRow = [
'id' => 5,
'mobile' => '9876543210',
'email' => 'hr@example.com',
'client_id' => 42,
'client_name' => 'Acme Corp',
'short_name' => 'ACME',
'client_branch_id' => 7,
'branch_name' => 'HQ',
'post_branch_id' => 70,
];
$hrModel = $this->createHrModelStub([
'first' => ['id' => 5, 'email' => 'hr@example.com', 'mobile' => '9876543210'],
'findAll' => [$branchRow],
'update' => true,
]);
$authHistoryModel = $this->createMock(AuthHistoryModel::class);
$authHistoryModel->expects($this->once())->method('insert');
$this->injectDbConnection('postDB', $this->createQueryResultStub([
'pre_client_id' => 42,
'post_client_id' => 420,
'pre_branch_id' => 7,
'pre_hr_id' => 5,
'allowed_modules' => json_encode(['post' => [1, 4, 5]]),
'allowed_active_policies' => json_encode([101, 102]),
]));
$this->injectDbConnection('tests', $this->createQueryResultStub(
null,
[
['allocg' => 'EB'],
['allocg' => 'GPA'],
]
));
$controller = $this->buildController(
['mobile_no' => '9876543210', 'otp' => '123456'],
$hrModel,
$authHistoryModel
);
$body = $this->decodeResponse($controller->getVerifiedHrData());
$this->assertSame('success', $body['status']);
$this->assertSame(200, $body['code']);
$this->assertCount(1, $body['data']);
$this->assertSame(['post' => [1, 4, 5]], $body['data'][0]['allowed_modules']);
$this->assertSame(['EB', 'Non-EB'], $body['data'][0]['claims_sub_menu']);
$this->assertNotEmpty($body['data'][0]['token']);
$this->assertSame(['ok' => true, 'source' => 'post_enrollment'], $body['post_enrollment']);
}
public function testClaimsModuleWithoutActivePoliciesStripsModuleId4()
{
$branchRow = [
'id' => 5,
'mobile' => null,
'email' => 'hr@example.com',
'client_id' => 42,
'client_name' => 'Acme Corp',
'short_name' => 'ACME',
'client_branch_id' => 7,
'branch_name' => 'HQ',
'post_branch_id' => 70,
];
$hrModel = $this->createHrModelStub([
'first' => ['id' => 5, 'email' => 'hr@example.com', 'mobile' => null],
'findAll' => [$branchRow],
'update' => true,
]);
$authHistoryModel = $this->createMock(AuthHistoryModel::class);
$authHistoryModel->method('insert')->willReturn(1);
$this->injectDbConnection('postDB', $this->createQueryResultStub([
'pre_client_id' => 42,
'post_client_id' => 420,
'pre_branch_id' => 7,
'pre_hr_id' => 5,
'allowed_modules' => json_encode(['post' => [1, 4, 5]]),
'allowed_active_policies' => json_encode([]),
]));
$controller = $this->buildController(
['email' => 'hr@example.com', 'otp' => '123456'],
$hrModel,
$authHistoryModel
);
$body = $this->decodeResponse($controller->getVerifiedHrData());
$this->assertSame('success', $body['status']);
$this->assertSame(['post' => [1, 5]], $body['data'][0]['allowed_modules']);
$this->assertSame([], $body['data'][0]['claims_sub_menu']);
$this->assertNotEmpty($body['data'][0]['token']);
}
public function testMissingHrAccessReturnsEmptyModulesAndEmptyToken()
{
$branchRow = [
'id' => 5,
'mobile' => '9876543210',
'email' => 'hr@example.com',
'client_id' => 42,
'client_name' => 'Acme Corp',
'short_name' => 'ACME',
'client_branch_id' => 7,
'branch_name' => 'HQ',
'post_branch_id' => 70,
];
$hrModel = $this->createHrModelStub([
'first' => ['id' => 5, 'email' => 'hr@example.com', 'mobile' => '9876543210'],
'findAll' => [$branchRow],
'update' => true,
]);
$authHistoryModel = $this->createMock(AuthHistoryModel::class);
$authHistoryModel->method('insert')->willReturn(1);
$this->injectDbConnection('postDB', $this->createQueryResultStub(null));
$controller = $this->buildController(
['mobile_no' => '9876543210', 'otp' => '123456'],
$hrModel,
$authHistoryModel
);
$body = $this->decodeResponse($controller->getVerifiedHrData());
$this->assertSame('success', $body['status']);
$this->assertSame(200, $body['code']);
$this->assertSame([], $body['data'][0]['allowed_modules']);
$this->assertSame('', $body['data'][0]['token']);
}
private function buildController(
array $payload,
object $hrModel,
?AuthHistoryModel $authHistoryModel = null
): RestAuthenticationController {
$json = (object) $payload;
$uri = $this->createMock(URI::class);
$uri->method('getPath')->willReturn('/employeeRest/getVerifiedHrData');
$uri->method('getSegments')->willReturn(['employeeRest', 'getVerifiedHrData']);
$request = $this->getMockBuilder(IncomingRequest::class)
->disableOriginalConstructor()
->onlyMethods([
'getJSON',
'getHeaderLine',
'getIPAddress',
'getMethod',
'getPost',
'getVar',
])
->getMock();
$request->uri = $uri;
$request->method('getJSON')->willReturn($json);
$request->method('getHeaderLine')->with('User-Agent')->willReturn('PHPUnit');
$request->method('getIPAddress')->willReturn('127.0.0.1');
$request->method('getMethod')->willReturn('post');
$request->method('getPost')->willReturn([]);
$request->method('getVar')->willReturnCallback(static function ($key) {
return match ($key) {
'rateLimitFingerprint' => 'test-fingerprint',
'rateLimitIdentity' => 'hr@example.com',
default => null,
};
});
Services::injectMock('request', $request);
$controller = new RestAuthenticationController();
$controller->initController($request, Services::response(), Services::logger());
$reflection = new ReflectionClass($controller);
$hrProp = $reflection->getProperty('hrModel');
$hrProp->setAccessible(true);
$hrProp->setValue($controller, $hrModel);
if ($authHistoryModel !== null) {
$authProp = $reflection->getProperty('authHistoryModel');
$authProp->setAccessible(true);
$authProp->setValue($controller, $authHistoryModel);
}
return $controller;
}
private function createHrModelStub(array $results): object
{
return new class ($results) {
private array $results;
private $setPayload = null;
public function __construct(array $results)
{
$this->results = $results;
}
public function select(...$args)
{
return $this;
}
public function join(...$args)
{
return $this;
}
public function where(...$args)
{
return $this;
}
public function set($data)
{
$this->setPayload = $data;
return $this;
}
public function first()
{
return $this->results['first'] ?? null;
}
public function findAll()
{
return $this->results['findAll'] ?? [];
}
public function update(...$args)
{
return $this->results['update'] ?? true;
}
};
}
/**
* Fluent DB stub returning either a single row (getRowArray) or many rows (getResultArray).
*/
private function createQueryResultStub(?array $row, array $rows = []): object
{
return new class ($row, $rows) {
private ?array $row;
private array $rows;
public function __construct(?array $row, array $rows)
{
$this->row = $row;
$this->rows = $rows;
}
public function table(...$args)
{
return $this;
}
public function select(...$args)
{
return $this;
}
public function join(...$args)
{
return $this;
}
public function where(...$args)
{
return $this;
}
public function whereIn(...$args)
{
return $this;
}
public function get()
{
return $this;
}
public function getRowArray(): ?array
{
return $this->row;
}
public function getResultArray(): array
{
return $this->rows;
}
};
}
private function injectDbConnection(string $group, object $connection): void
{
$property = new ReflectionProperty(DatabaseConfig::class, 'instances');
$property->setAccessible(true);
$instances = $property->getValue() ?? [];
$instances[$group] = $connection;
$property->setValue(null, $instances);
}
private function clearInjectedDbConnections(): void
{
$property = new ReflectionProperty(DatabaseConfig::class, 'instances');
$property->setAccessible(true);
$property->setValue(null, []);
}
private function mockCurlRequest(array $payload): void
{
$response = $this->createMock(ResponseInterface::class);
$response->method('getBody')->willReturn(json_encode($payload));
$curl = $this->getMockBuilder(\CodeIgniter\HTTP\CURLRequest::class)
->disableOriginalConstructor()
->onlyMethods(['post', 'get'])
->getMock();
$curl->method('post')->willReturn($response);
$curl->method('get')->willReturn($response);
Services::injectMock('curlrequest', $curl);
}
private function mockRateLimiter(): void
{
$limiter = $this->getMockBuilder(\App\Libraries\RateLimiterService::class)
->disableOriginalConstructor()
->onlyMethods(['recordIpFailure', 'recordUserFailure'])
->getMock();
$limiter->method('recordIpFailure')->willReturn(null);
$limiter->method('recordUserFailure')->willReturn(null);
Services::injectMock('limiter', $limiter);
}
private function decodeResponse($response): array
{
return json_decode($response->getBody(), true);
}
}