MERGE_TEST_BUG_FIXES

This commit is contained in:
Ubuntu 2026-07-21 12:35:29 +05:30
commit e1ef840aee
4 changed files with 1135 additions and 0 deletions

View File

@ -0,0 +1,18 @@
<?php
/**
* HR Login Diagnosis routes (loaded via Config\Routing::$routeFiles).
*
* @var \CodeIgniter\Router\RouteCollection $routes
*/
$routes->group('hrLoginDiagnose', ['filter' => 'authMVC'], static function ($routes) {
$routes->get('/', 'HrLoginDiagnoseController::index');
$routes->get('', 'HrLoginDiagnoseController::index');
$routes->post('check', 'HrLoginDiagnoseController::check');
});
// Fallback when request path still includes index.php (wrong baseURL / form action).
$routes->get('index.php/hrLoginDiagnose', 'HrLoginDiagnoseController::index', ['filter' => 'authMVC']);
$routes->get('index.php/hrLoginDiagnose/', 'HrLoginDiagnoseController::index', ['filter' => 'authMVC']);
$routes->post('index.php/hrLoginDiagnose/check', 'HrLoginDiagnoseController::check', ['filter' => 'authMVC']);

View File

@ -27,6 +27,7 @@ class Routing extends BaseRouting
*/
public array $routeFiles = [
APPPATH . 'Config/Routes.php',
APPPATH . 'Config/HrLoginDiagnoseRoutes.php',
];
/**

View File

@ -0,0 +1,526 @@
<?php
namespace App\Controllers;
use CodeIgniter\API\ResponseTrait;
/**
* Standalone HR login diagnostic tool.
* Does not modify existing auth / HR access controllers.
*/
class HrLoginDiagnoseController extends AdminController
{
use ResponseTrait;
public function index()
{
$email = trim((string) $this->request->getGet('email'));
$mobile = trim((string) $this->request->getGet('mobile'));
$data = [
'title' => 'HR Login Diagnostic',
'email' => $email,
'mobile' => $mobile,
'result' => null,
'has_search' => false,
];
if ($email !== '' || $mobile !== '') {
$data['has_search'] = true;
$data['result'] = $this->diagnose($email, $mobile);
}
return view('hr_login_diagnose', $data);
}
public function check()
{
try {
$payload = $this->request->getJSON(true);
if (!is_array($payload) || empty($payload)) {
$payload = [
'email' => $this->request->getPost('email'),
'mobile' => $this->request->getPost('mobile'),
];
}
$email = trim((string) ($payload['email'] ?? ''));
$mobile = trim((string) ($payload['mobile'] ?? ''));
if ($email === '' && $mobile === '') {
return $this->respond([
'status' => false,
'message' => 'Enter email or mobile.',
], 400);
}
$result = $this->diagnose($email, $mobile);
return $this->respond([
'status' => true,
'data' => $result,
], 200);
} catch (\Throwable $e) {
log_message('error', 'HrLoginDiagnose check failed: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
return $this->respond([
'status' => false,
'message' => 'Diagnosis failed: ' . $e->getMessage(),
], 500);
}
}
private function diagnose(string $email, string $mobile): array
{
$issues = [];
$postDb = \Config\Database::connect();
$contacts = $this->fetchPostContacts($postDb, $email, $mobile);
if (empty($contacts)) {
$issues[] = $this->issue(
'HR_NOT_FOUND',
'high',
'No active HR found in post level_contacts for the given email/mobile (contact_type=client).',
'Create/activate the HR contact under the correct client branch, or verify the email/mobile.'
);
return [
'input' => ['email' => $email, 'mobile' => $mobile],
'overall_status' => 'FAIL',
'summary' => 'HR not found in post DB.',
'issues' => $issues,
'post' => ['contacts' => []],
'hr_access_control'=> [],
'pre' => ['reachable' => null, 'mappings' => []],
'login_simulation' => [
'would_authenticate' => false,
'cards' => [],
],
];
}
$accessRows = [];
$preMaps = [];
$loginCards = [];
$preReachable = null;
try {
$preDb = \Config\Database::connect('preDB');
$preDb->connect();
$preReachable = true;
} catch (\Throwable $e) {
$preReachable = false;
$preDb = null;
$issues[] = $this->issue(
'PRE_DB_UNREACHABLE',
'high',
'Could not connect to preDB: ' . $e->getMessage(),
'Check preDB credentials in Database config / .env.'
);
}
foreach ($contacts as $contact) {
$flags = [
'HR_FOUND' => true,
'BRANCH_ACTIVE' => (int) ($contact['branch_active'] ?? 0) === 1,
'CLIENT_ACTIVE' => (int) ($contact['client_active'] ?? 0) === 1,
];
if (!$flags['BRANCH_ACTIVE']) {
$issues[] = $this->issue(
'BRANCH_INACTIVE',
'high',
"Branch inactive for post_hr_id={$contact['post_hr_id']} (branch #{$contact['post_branch_id']}).",
'Activate client_branch.is_active=1 for this branch.'
);
}
if (!$flags['CLIENT_ACTIVE']) {
$issues[] = $this->issue(
'CLIENT_INACTIVE',
'high',
"Client inactive for post_hr_id={$contact['post_hr_id']} (client #{$contact['post_client_id']}).",
'Activate clients.is_active=1 for this client.'
);
}
$access = $this->fetchAccessRow(
$postDb,
(int) $contact['post_hr_id'],
(int) $contact['post_branch_id'],
(int) $contact['post_client_id']
);
$modules = [];
$hasModules = false;
$tokenReady = false;
$failureReason = null;
if (empty($access)) {
$failureReason = 'NO_HR_ACCESS_CONTROL_ROW';
$issues[] = $this->issue(
'NO_HR_ACCESS_CONTROL_ROW',
'high',
"No hr_access_control row for post_hr_id={$contact['post_hr_id']}, post_branch_id={$contact['post_branch_id']}, post_client_id={$contact['post_client_id']}.",
'Open Client Onboarding → HR Access and save at least one module for this HR/branch.'
);
} else {
$modules = json_decode($access['allowed_modules'] ?? '[]', true);
if (!is_array($modules)) {
$modules = [];
}
$hasModules = $this->hasAnyModule($modules);
$tokenReady = $hasModules;
if (!$hasModules) {
$failureReason = 'EMPTY_ALLOWED_MODULES';
$issues[] = $this->issue(
'EMPTY_ALLOWED_MODULES',
'high',
"hr_access_control id={$access['id']} exists but allowed_modules is empty.",
'Assign pre/post modules in HR Access Control and save.'
);
}
}
$accessRows[] = [
'id' => $access['id'] ?? null,
'post_hr_id' => (int) $contact['post_hr_id'],
'pre_hr_id' => isset($access['pre_hr_id']) ? (int) $access['pre_hr_id'] : null,
'post_branch_id' => (int) $contact['post_branch_id'],
'pre_branch_id' => isset($access['pre_branch_id']) ? (int) $access['pre_branch_id'] : null,
'post_client_id' => (int) $contact['post_client_id'],
'pre_client_id' => isset($access['pre_client_id']) ? (int) $access['pre_client_id'] : null,
'allowed_modules' => $modules,
'allowed_pre_policies' => json_decode($access['allowed_pre_policies'] ?? '[]', true) ?: [],
'allowed_active_policies' => json_decode($access['allowed_active_policies'] ?? '[]', true) ?: [],
'allowed_cd' => json_decode($access['allowed_cd'] ?? '[]', true) ?: [],
'is_active' => isset($access['is_active']) ? (int) $access['is_active'] : null,
'has_token' => $tokenReady,
'token_ready' => $tokenReady,
'failure_reason' => $failureReason,
'note' => $failureReason ?: 'OK',
];
$preMap = $this->buildPreMapping($preDb, $contact, $access);
$preMaps[] = $preMap;
if ($preReachable === true) {
if (empty($preMap['PRE_BRANCH_LINKED'])) {
$issues[] = $this->issue(
'PRE_BRANCH_NOT_LINKED',
'medium',
"Post branch #{$contact['post_branch_id']} has no valid pre_branch_id link.",
'Set client_branch.pre_branch_id to the matching pre branch id.'
);
} elseif (empty($preMap['PRE_HR_MATCHED'])) {
$issues[] = $this->issue(
'PRE_HR_NOT_MATCHED',
'medium',
"No pre level_contacts match for email+mobile under pre client #{$preMap['pre_client_id']}.",
'Align email and mobile between pre and post HR contacts.'
);
} elseif (empty($preMap['PRE_IDS_CONSISTENT']) && !empty($access)) {
$issues[] = $this->issue(
'PRE_IDS_INCONSISTENT',
'medium',
"hr_access_control pre ids do not match live pre data for post_hr_id={$contact['post_hr_id']}.",
'Re-save HR Access Control so pre_hr_id / pre_client_id / pre_branch_id are refreshed.'
);
}
}
$canLoginCard = $flags['BRANCH_ACTIVE'] && $flags['CLIENT_ACTIVE'] && $tokenReady;
$loginCards[] = [
'client' => $contact['client_name'] ?? '',
'branch' => $contact['branch_name'] ?? '',
'post_hr_id' => (int) $contact['post_hr_id'],
'post_client_id' => (int) $contact['post_client_id'],
'post_branch_id' => (int) $contact['post_branch_id'],
'has_token' => $canLoginCard,
'failure_reason' => $canLoginCard
? null
: ($failureReason
?: (!$flags['BRANCH_ACTIVE'] ? 'BRANCH_INACTIVE'
: (!$flags['CLIENT_ACTIVE'] ? 'CLIENT_INACTIVE' : 'UNKNOWN'))),
];
$contact['hr_found'] = true;
$contact['flags'] = $flags;
}
$anyToken = false;
foreach ($loginCards as $card) {
if (!empty($card['has_token'])) {
$anyToken = true;
break;
}
}
$wouldAuth = !empty($contacts);
$overall = ($wouldAuth && $anyToken && empty(array_filter($issues, static fn ($i) => ($i['severity'] ?? '') === 'high')))
? 'PASS'
: (($anyToken && $wouldAuth) ? 'WARN' : 'FAIL');
if ($overall === 'PASS') {
$summary = 'HR identity, access row, and login readiness look OK.';
$verdict = 'OK';
} elseif ($overall === 'WARN') {
$summary = 'Login may work for some cards, but mapping/access issues remain.';
$verdict = 'OK_WITH_WARNINGS';
} else {
$summary = 'HR login will fail or return empty token/modules.';
$verdict = 'WILL_FAIL';
}
// Deduplicate issues by code+message
$unique = [];
$deduped = [];
foreach ($issues as $issue) {
$key = ($issue['code'] ?? '') . '|' . ($issue['message'] ?? '');
if (isset($unique[$key])) {
continue;
}
$unique[$key] = true;
$deduped[] = $issue;
}
$postContacts = array_map(static function ($c) {
return [
'post_hr_id' => (int) $c['post_hr_id'],
'hr_name' => $c['hr_name'] ?? '',
'hr_mail' => $c['hr_mail'] ?? '',
'hr_mobile' => $c['hr_mobile'] ?? '',
'is_active' => (int) ($c['is_active'] ?? 0),
'post_branch_id' => (int) ($c['post_branch_id'] ?? 0),
'branch_name' => $c['branch_name'] ?? '',
'branch_active' => (int) ($c['branch_active'] ?? 0),
'pre_branch_id' => $c['pre_branch_id'] ?? null,
'post_client_id' => (int) ($c['post_client_id'] ?? 0),
'client_name' => $c['client_name'] ?? '',
'client_active' => (int) ($c['client_active'] ?? 0),
'hr_found' => true,
];
}, $contacts);
$firstPost = $postContacts[0] ?? [];
$firstPre = $preMaps[0] ?? [];
$tokenCards = array_filter($loginCards, static fn ($c) => ! empty($c['has_token']));
$preMatched = array_filter($preMaps, static fn ($m) => ! empty($m['PRE_HR_MATCHED']));
return [
'input' => ['email' => $email, 'mobile' => $mobile],
'overall_status' => $overall,
'verdict' => $verdict,
'verdict_message' => $summary,
'summary' => $summary,
'issues' => $deduped,
'problems' => $deduped,
'post' => [
'contacts' => $postContacts,
'raw_found' => count($postContacts) > 0,
'eligible' => $anyToken,
'filters_summary' => [
'hr_active' => ! empty($firstPost) && (int) ($firstPost['is_active'] ?? 0) === 1,
'branch_active' => ! empty($firstPost) && (int) ($firstPost['branch_active'] ?? 0) === 1,
'client_active' => ! empty($firstPost) && (int) ($firstPost['client_active'] ?? 0) === 1,
'has_access' => count($tokenCards) > 0,
'token_ready' => $anyToken,
],
'primary' => $firstPost,
],
'hr_access_control' => $accessRows,
'pre' => [
'reachable' => $preReachable,
'mappings' => $preMaps,
'raw_found' => count($preMatched) > 0,
'eligible' => count($preMatched) > 0,
'filters_summary' => [
'db_reachable' => $preReachable === true,
'PRE_BRANCH_LINKED' => ! empty($firstPre['PRE_BRANCH_LINKED']),
'PRE_HR_MATCHED' => ! empty($firstPre['PRE_HR_MATCHED']),
'PRE_IDS_CONSISTENT' => ! empty($firstPre['PRE_IDS_CONSISTENT']),
'PRE_CLIENT_MAPPED' => ! empty($firstPre['PRE_CLIENT_MAPPED']),
],
'primary' => $firstPre,
'error' => $preReachable === false ? 'preDB connection failed' : null,
],
'login_simulation' => [
'would_authenticate' => $wouldAuth,
'cards' => $loginCards,
],
'merge' => [
'post_eligible' => $anyToken,
'pre_eligible' => count($preMatched) > 0,
'post_contacts' => count($postContacts),
'pre_matched' => count($preMatched),
'token_cards' => count($tokenCards),
'access_rows' => count(array_filter($accessRows, static fn ($a) => ! empty($a['id']))),
'would_return_both' => $anyToken && count($preMatched) > 0,
'winner' => ($anyToken && count($preMatched) > 0) ? 'both' : ($anyToken ? 'post' : (count($preMatched) > 0 ? 'pre' : 'none')),
'reason' => $summary,
],
];
}
private function fetchPostContacts($db, string $email, string $mobile): array
{
$builder = $db->table('level_contacts lc')
->select("
lc.id AS post_hr_id,
lc.name AS hr_name,
lc.email AS hr_mail,
lc.mobile AS hr_mobile,
lc.is_active,
cb.id AS post_branch_id,
cb.branch_name,
cb.is_active AS branch_active,
cb.pre_branch_id,
c.id AS post_client_id,
c.client_name,
c.is_active AS client_active
")
->join('client_branch cb', 'cb.id = lc.ref_id', 'left')
->join('clients c', 'c.id = cb.client_id', 'left')
->where('lc.contact_type', 'client')
->where('lc.is_active', 1);
if ($email !== '' && $mobile !== '') {
$builder->groupStart()
->where('lc.email', $email)
->orWhere('lc.mobile', $mobile)
->groupEnd();
} elseif ($email !== '') {
$builder->where('lc.email', $email);
} else {
$builder->where('lc.mobile', $mobile);
}
return $builder->get()->getResultArray();
}
private function fetchAccessRow($db, int $postHrId, int $postBranchId, int $postClientId): ?array
{
$row = $db->table('hr_access_control')
->where('post_hr_id', $postHrId)
->where('post_branch_id', $postBranchId)
->where('post_client_id', $postClientId)
->where('is_active', 1)
->get()
->getRowArray();
return $row ?: null;
}
private function hasAnyModule(array $modules): bool
{
if (isset($modules['pre']) || isset($modules['post'])) {
$pre = is_array($modules['pre'] ?? null) ? $modules['pre'] : [];
$post = is_array($modules['post'] ?? null) ? $modules['post'] : [];
return count($pre) > 0 || count($post) > 0;
}
return count($modules) > 0;
}
private function buildPreMapping($preDb, array $contact, ?array $access): array
{
$result = [
'post_hr_id' => (int) $contact['post_hr_id'],
'pre_hr_id' => null,
'pre_client_id' => null,
'pre_branch_id' => $contact['pre_branch_id'] ?? null,
'pre_client_name' => null,
'pre_branch_name' => null,
'hr_name' => null,
'hr_mail' => null,
'hr_mobile' => null,
'PRE_BRANCH_LINKED' => false,
'PRE_HR_MATCHED' => false,
'PRE_IDS_CONSISTENT' => false,
'PRE_CLIENT_MAPPED' => false,
'access_pre_hr_id' => isset($access['pre_hr_id']) ? (int) $access['pre_hr_id'] : null,
'access_pre_client_id' => isset($access['pre_client_id']) ? (int) $access['pre_client_id'] : null,
'access_pre_branch_id' => isset($access['pre_branch_id']) ? (int) $access['pre_branch_id'] : null,
];
if ($preDb === null) {
return $result;
}
$preBranchId = $contact['pre_branch_id'] ?? null;
if (empty($preBranchId)) {
return $result;
}
$preBranch = $preDb->table('client_branch cb')
->select('cb.id AS pre_branch_id, cb.client_id AS pre_client_id, cb.branch_name, c.client_name')
->join('clients c', 'c.id = cb.client_id', 'left')
->where('cb.id', $preBranchId)
->where('cb.is_active', 1)
->where('c.is_active', 1)
->get()
->getRowArray();
if (empty($preBranch)) {
return $result;
}
$result['PRE_BRANCH_LINKED'] = true;
$result['pre_branch_id'] = (int) $preBranch['pre_branch_id'];
$result['pre_client_id'] = (int) $preBranch['pre_client_id'];
$result['pre_branch_name'] = $preBranch['branch_name'] ?? null;
$result['pre_client_name'] = $preBranch['client_name'] ?? null;
$result['PRE_CLIENT_MAPPED'] = true;
$email = trim((string) ($contact['hr_mail'] ?? ''));
$mobile = trim((string) ($contact['hr_mobile'] ?? ''));
$preHrBuilder = $preDb->table('level_contacts lc')
->select('lc.id AS pre_hr_id, lc.name, lc.email, lc.mobile')
->join('client_branch cb', 'cb.id = lc.ref_id')
->where('lc.contact_type', 'client')
->where('lc.is_active', 1)
->where('cb.is_active', 1)
->where('cb.client_id', $preBranch['pre_client_id']);
if ($email !== '' && $mobile !== '') {
$preHrBuilder->where('lc.email', $email)->where('lc.mobile', $mobile);
} elseif ($email !== '') {
$preHrBuilder->where('lc.email', $email);
} elseif ($mobile !== '') {
$preHrBuilder->where('lc.mobile', $mobile);
}
$preHr = $preHrBuilder->get()->getRowArray();
if (!empty($preHr)) {
$result['PRE_HR_MATCHED'] = true;
$result['pre_hr_id'] = (int) $preHr['pre_hr_id'];
$result['hr_name'] = $preHr['name'] ?? null;
$result['hr_mail'] = $preHr['email'] ?? null;
$result['hr_mobile'] = $preHr['mobile'] ?? null;
}
if (!empty($access) && $result['PRE_HR_MATCHED']) {
$idsMatch =
(int) ($access['pre_hr_id'] ?? 0) === (int) $result['pre_hr_id']
&& (int) ($access['pre_client_id'] ?? 0) === (int) $result['pre_client_id']
&& (int) ($access['pre_branch_id'] ?? 0) === (int) $result['pre_branch_id'];
$result['PRE_IDS_CONSISTENT'] = $idsMatch;
} elseif (empty($access)) {
$result['PRE_IDS_CONSISTENT'] = false;
}
return $result;
}
private function issue(string $code, string $severity, string $message, string $fix = ''): array
{
return [
'code' => $code,
'severity' => $severity,
'message' => $message,
'fix' => $fix,
];
}
}

View File

@ -0,0 +1,590 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= esc($title ?? 'HR 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;flex-wrap:wrap;}
.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-table{border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;margin-bottom:14px;}
.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;}
.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;}
pre.raw-json{
margin:0;padding:16px 20px;background:#111827;color:#e5e7eb;
font-family:var(--mono);font-size:11.5px;overflow:auto;max-height:480px;
}
@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">
<?php
// Prefer browser path without index.php (avoids "Can't find a route for get: index.php/...").
$diagPath = parse_url($_SERVER['REQUEST_URI'] ?? '/hrLoginDiagnose', PHP_URL_PATH) ?: '/hrLoginDiagnose';
$diagPath = preg_replace('#/index\.php#', '', $diagPath) ?? $diagPath;
$diagPath = preg_replace('#/+#', '/', $diagPath) ?? $diagPath;
$diagPath = rtrim($diagPath, '/');
if ($diagPath === '' || ! str_ends_with($diagPath, 'hrLoginDiagnose')) {
$diagPath = '/hrLoginDiagnose';
}
?>
<div class="page-head">
<h1>HR login diagnostic</h1>
<p><b>PRE</b> (enrollment) + <b>POST</b> (live) &middot; <code>hr_access_control</code> &middot; read-only &middot; does not send OTP</p>
</div>
<form method="get" action="<?= esc($diagPath) ?>" class="card search-card">
<div class="search-grid">
<div class="field">
<label>Email <span>level_contacts.email</span></label>
<input type="text" name="email" value="<?= esc($email ?? '') ?>" placeholder="hr@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="<?= esc($diagPath) ?>">Clear</a>
<p class="search-hint">Provide at least email or mobile. Checks post identity, <b>hr_access_control</b>, and pre mapping.</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'] ?? ($result['issues'] ?? []);
$accessRows = $result['hr_access_control'] ?? [];
$loginSim = $result['login_simulation'] ?? [];
$postPrimary = $post['primary'] ?? [];
$prePrimary = $pre['primary'] ?? [];
$postFs = $post['filters_summary'] ?? [];
$preFs = $pre['filters_summary'] ?? [];
$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'] ?? $result['summary'] ?? '') ?></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:220px;">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>
<?php if (! empty($pre['error'])): ?>
<div class="alert-inline"><?= esc($pre['error']) ?></div>
<?php endif; ?>
<table class="kv-table">
<tr>
<td class="kv-key">Raw found</td>
<td class="kv-val"><?= ! empty($pre['raw_found']) ? $mark(true) . ' Yes' : $mark(false) . ' No' ?></td>
</tr>
<tr>
<td class="kv-key">Eligible (matched)</td>
<td class="kv-val"><?= ! empty($pre['eligible']) ? $mark(true) . ' Yes' : $mark(false) . ' No' ?></td>
</tr>
<tr><td class="kv-key">preDB reachable</td><td class="kv-val"><?= isset($preFs['db_reachable']) ? $mark((bool) $preFs['db_reachable']) : '—' ?></td></tr>
<tr><td class="kv-key">PRE_BRANCH_LINKED</td><td class="kv-val"><?= isset($preFs['PRE_BRANCH_LINKED']) ? $mark((bool) $preFs['PRE_BRANCH_LINKED']) : '—' ?></td></tr>
<tr><td class="kv-key">PRE_HR_MATCHED</td><td class="kv-val"><?= isset($preFs['PRE_HR_MATCHED']) ? $mark((bool) $preFs['PRE_HR_MATCHED']) : '—' ?></td></tr>
<tr><td class="kv-key">PRE_IDS_CONSISTENT</td><td class="kv-val"><?= isset($preFs['PRE_IDS_CONSISTENT']) ? $mark((bool) $preFs['PRE_IDS_CONSISTENT']) : '—' ?></td></tr>
<tr><td class="kv-key">PRE_CLIENT_MAPPED</td><td class="kv-val"><?= isset($preFs['PRE_CLIENT_MAPPED']) ? $mark((bool) $preFs['PRE_CLIENT_MAPPED']) : '—' ?></td></tr>
<tr><td class="kv-key">pre_hr_id</td><td class="kv-val"><?= esc((string) ($prePrimary['pre_hr_id'] ?? '—')) ?></td></tr>
<tr><td class="kv-key">name</td><td class="kv-val"><?= esc((string) ($prePrimary['hr_name'] ?? '—')) ?></td></tr>
<tr><td class="kv-key">email</td><td class="kv-val"><?= esc((string) ($prePrimary['hr_mail'] ?? '—')) ?></td></tr>
<tr><td class="kv-key">mobile</td><td class="kv-val"><?= esc((string) ($prePrimary['hr_mobile'] ?? '—')) ?></td></tr>
<tr><td class="kv-key">client</td><td class="kv-val"><?= esc((string) (($prePrimary['pre_client_name'] ?? '') . ' #' . ($prePrimary['pre_client_id'] ?? ''))) ?></td></tr>
<tr><td class="kv-key">branch</td><td class="kv-val"><?= esc((string) (($prePrimary['pre_branch_name'] ?? '') . ' #' . ($prePrimary['pre_branch_id'] ?? ''))) ?></td></tr>
<tr><td class="kv-key">pre_client_id</td><td class="kv-val"><?= esc((string) ($prePrimary['pre_client_id'] ?? '—')) ?></td></tr>
<tr><td class="kv-key">pre_branch_id</td><td class="kv-val"><?= esc((string) ($prePrimary['pre_branch_id'] ?? '—')) ?></td></tr>
</table>
</div>
<div class="compare-card">
<div class="compare-head">POST <span>(live)</span></div>
<table class="kv-table">
<tr>
<td class="kv-key">Raw found</td>
<td class="kv-val">
<?= ! empty($post['raw_found'])
? $mark(true) . ' Yes (' . count($post['contacts'] ?? []) . ')'
: $mark(false) . ' No' ?>
</td>
</tr>
<tr>
<td class="kv-key">Eligible for login</td>
<td class="kv-val"><?= ! empty($post['eligible']) ? $mark(true) . ' Yes' : $mark(false) . ' No' ?></td>
</tr>
<tr><td class="kv-key">hr active</td><td class="kv-val"><?= isset($postFs['hr_active']) ? $mark((bool) $postFs['hr_active']) : '—' ?></td></tr>
<tr><td class="kv-key">branch active</td><td class="kv-val"><?= isset($postFs['branch_active']) ? $mark((bool) $postFs['branch_active']) : '—' ?></td></tr>
<tr><td class="kv-key">client active</td><td class="kv-val"><?= isset($postFs['client_active']) ? $mark((bool) $postFs['client_active']) : '—' ?></td></tr>
<tr><td class="kv-key">has access row</td><td class="kv-val"><?= isset($postFs['has_access']) ? $mark((bool) $postFs['has_access']) : '—' ?></td></tr>
<tr><td class="kv-key">token ready</td><td class="kv-val"><?= isset($postFs['token_ready']) ? $mark((bool) $postFs['token_ready']) : '—' ?></td></tr>
<tr><td class="kv-key">post_hr_id</td><td class="kv-val"><?= esc((string) ($postPrimary['post_hr_id'] ?? '—')) ?></td></tr>
<tr><td class="kv-key">name</td><td class="kv-val"><?= esc((string) ($postPrimary['hr_name'] ?? '—')) ?></td></tr>
<tr><td class="kv-key">email</td><td class="kv-val"><?= esc((string) ($postPrimary['hr_mail'] ?? '—')) ?></td></tr>
<tr><td class="kv-key">mobile</td><td class="kv-val"><?= esc((string) ($postPrimary['hr_mobile'] ?? '—')) ?></td></tr>
<tr><td class="kv-key">client</td><td class="kv-val"><?= esc((string) (($postPrimary['client_name'] ?? '') . ' #' . ($postPrimary['post_client_id'] ?? ''))) ?></td></tr>
<tr><td class="kv-key">branch</td><td class="kv-val"><?= esc((string) (($postPrimary['branch_name'] ?? '') . ' #' . ($postPrimary['post_branch_id'] ?? ''))) ?></td></tr>
</table>
</div>
</div>
<div class="tabs" role="tablist">
<button type="button" class="tab active" data-tab="merge">Summary</button>
<button type="button" class="tab" data-tab="access">Access</button>
<button type="button" class="tab" data-tab="login">Login sim</button>
<button type="button" class="tab" data-tab="mapping">Pre mapping</button>
<button type="button" class="tab" data-tab="contacts">Post contacts</button>
<button type="button" class="tab" data-tab="raw">Raw JSON</button>
</div>
<div class="tab-panel active" id="panel-merge">
<table class="cred-table">
<tr><td class="cred-key">POST eligible</td><td><?= ! empty($merge['post_eligible']) ? 'Yes' : 'No' ?></td></tr>
<tr><td class="cred-key">PRE eligible</td><td><?= ! empty($merge['pre_eligible']) ? 'Yes' : 'No' ?></td></tr>
<tr><td class="cred-key">POST contacts</td><td class="val-raw"><?= esc((string) ($merge['post_contacts'] ?? 0)) ?></td></tr>
<tr><td class="cred-key">PRE matched</td><td class="val-raw"><?= esc((string) ($merge['pre_matched'] ?? 0)) ?></td></tr>
<tr><td class="cred-key">Access rows with id</td><td class="val-raw"><?= esc((string) ($merge['access_rows'] ?? 0)) ?></td></tr>
<tr><td class="cred-key">Token-ready cards</td><td class="val-raw"><?= esc((string) ($merge['token_cards'] ?? 0)) ?></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">Reason</td><td class="fix-text"><?= esc((string) ($merge['reason'] ?? '')) ?></td></tr>
</table>
</div>
<div class="tab-panel" id="panel-access">
<div class="dup-wrap">
<h6>hr_access_control <span class="fix-text">(<?= count($accessRows) ?>)</span></h6>
<?php if (empty($accessRows)): ?>
<p class="muted" style="padding:0;">No access rows evaluated.</p>
<?php else: ?>
<div class="dup-table">
<table>
<thead>
<tr>
<th>Access ID</th>
<th>post_hr_id</th>
<th>Branch / Client</th>
<th>Modules</th>
<th>Token ready</th>
<th>Note</th>
</tr>
</thead>
<tbody>
<?php foreach ($accessRows as $a): ?>
<tr class="<?= empty($a['token_ready']) ? 'sev-high' : '' ?>">
<td><?= esc((string) ($a['id'] ?? '—')) ?></td>
<td><?= esc((string) ($a['post_hr_id'] ?? '')) ?></td>
<td>b#<?= esc((string) ($a['post_branch_id'] ?? '')) ?> / c#<?= esc((string) ($a['post_client_id'] ?? '')) ?></td>
<td class="val-raw"><?= esc(json_encode($a['allowed_modules'] ?? [])) ?></td>
<td class="<?= ! empty($a['token_ready']) ? 'window-open' : 'window-closed' ?>"><?= ! empty($a['token_ready']) ? 'Yes' : 'No' ?></td>
<td class="fix-text"><?= esc((string) ($a['note'] ?? $a['failure_reason'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<div class="tab-panel" id="panel-login">
<div class="dup-wrap">
<h6>Login simulation <span class="fix-text">(mirrors getVerifiedHrData lookup)</span></h6>
<table class="cred-table" style="margin-bottom:14px;">
<tr>
<td class="cred-key">Would authenticate (OTP identity)</td>
<td><?= ! empty($loginSim['would_authenticate']) ? '<span class="val-yes">Yes</span>' : '<span class="val-no">No</span>' ?></td>
</tr>
</table>
<?php $cards = $loginSim['cards'] ?? []; ?>
<?php if (empty($cards)): ?>
<p class="muted" style="padding:0;">No login cards.</p>
<?php else: ?>
<div class="dup-table">
<table>
<thead>
<tr>
<th>Client</th>
<th>Branch</th>
<th>Has token</th>
<th>Failure</th>
</tr>
</thead>
<tbody>
<?php foreach ($cards as $c): ?>
<tr>
<td><?= esc((string) ($c['client'] ?? '')) ?></td>
<td><?= esc((string) ($c['branch'] ?? '')) ?></td>
<td class="<?= ! empty($c['has_token']) ? 'window-open' : 'window-closed' ?>"><?= ! empty($c['has_token']) ? 'Yes' : 'No' ?></td>
<td class="code"><?= esc((string) ($c['failure_reason'] ?? '—')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<div class="tab-panel" id="panel-mapping">
<div class="dup-wrap">
<h6>Pre mapping rows</h6>
<?php $maps = $pre['mappings'] ?? []; ?>
<?php if (empty($maps)): ?>
<p class="muted" style="padding:0;">No pre mappings.</p>
<?php else: ?>
<div class="dup-table">
<table>
<thead>
<tr>
<th>post_hr_id</th>
<th>pre_hr_id</th>
<th>Name</th>
<th>Email</th>
<th>Mobile</th>
<th>Client</th>
<th>Branch</th>
<th>Flags</th>
</tr>
</thead>
<tbody>
<?php foreach ($maps as $m): ?>
<tr>
<td><?= esc((string) ($m['post_hr_id'] ?? '')) ?></td>
<td class="val-raw"><?= esc((string) ($m['pre_hr_id'] ?? '—')) ?></td>
<td><?= esc((string) ($m['hr_name'] ?? '—')) ?></td>
<td><?= esc((string) ($m['hr_mail'] ?? '—')) ?></td>
<td><?= esc((string) ($m['hr_mobile'] ?? '—')) ?></td>
<td><?= esc((string) (($m['pre_client_name'] ?? '') . ' #' . ($m['pre_client_id'] ?? ''))) ?></td>
<td><?= esc((string) (($m['pre_branch_name'] ?? '') . ' #' . ($m['pre_branch_id'] ?? ''))) ?></td>
<td>
<?= ! empty($m['PRE_BRANCH_LINKED']) ? $mark(true) : $mark(false) ?> branch
&nbsp;<?= ! empty($m['PRE_HR_MATCHED']) ? $mark(true) : $mark(false) ?> hr
&nbsp;<?= ! empty($m['PRE_IDS_CONSISTENT']) ? $mark(true) : $mark(false) ?> ids
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<div class="tab-panel" id="panel-contacts">
<div class="dup-wrap">
<h6>POST level_contacts</h6>
<?php $contacts = $post['contacts'] ?? []; ?>
<?php if (empty($contacts)): ?>
<p class="muted" style="padding:0;">No post contacts.</p>
<?php else: ?>
<div class="dup-table">
<table>
<thead>
<tr>
<th>post_hr_id</th>
<th>Name</th>
<th>Email</th>
<th>Mobile</th>
<th>Branch</th>
<th>Client</th>
<th>Flags</th>
</tr>
</thead>
<tbody>
<?php foreach ($contacts as $c): ?>
<tr>
<td><?= esc((string) ($c['post_hr_id'] ?? '')) ?></td>
<td><?= esc((string) ($c['hr_name'] ?? '')) ?></td>
<td><?= esc((string) ($c['hr_mail'] ?? '')) ?></td>
<td><?= esc((string) ($c['hr_mobile'] ?? '')) ?></td>
<td><?= esc((string) (($c['branch_name'] ?? '') . ' #' . ($c['post_branch_id'] ?? ''))) ?></td>
<td><?= esc((string) (($c['client_name'] ?? '') . ' #' . ($c['post_client_id'] ?? ''))) ?></td>
<td>
<?= (int) ($c['is_active'] ?? 0) === 1 ? $mark(true) : $mark(false) ?> hr
&nbsp;<?= (int) ($c['branch_active'] ?? 0) === 1 ? $mark(true) : $mark(false) ?> branch
&nbsp;<?= (int) ($c['client_active'] ?? 0) === 1 ? $mark(true) : $mark(false) ?> client
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<div class="tab-panel" id="panel-raw">
<pre class="raw-json"><?= esc(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) ?></pre>
</div>
<script>
(function () {
var tabs = document.querySelectorAll('.tab');
var panels = {
merge: document.getElementById('panel-merge'),
access: document.getElementById('panel-access'),
login: document.getElementById('panel-login'),
mapping: document.getElementById('panel-mapping'),
contacts: document.getElementById('panel-contacts'),
raw: document.getElementById('panel-raw')
};
tabs.forEach(function (tab) {
tab.addEventListener('click', function () {
tabs.forEach(function (t) { t.classList.remove('active'); });
Object.keys(panels).forEach(function (k) { if (panels[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('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>