From e15d876a3f560bd7aa0bc0c8a3ebefa202379efd Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Mon, 25 May 2026 12:14:23 +0530 Subject: [PATCH] Fix ABDM OPD HMS patient profile link route to person_record --- app/Controllers/AbdmOpdQueue.php | 282 +++++++++++++++++++++++++++++- app/Views/abdm/opd_queue_list.php | 2 +- 2 files changed, 278 insertions(+), 6 deletions(-) diff --git a/app/Controllers/AbdmOpdQueue.php b/app/Controllers/AbdmOpdQueue.php index 62cdb61..4404efd 100644 --- a/app/Controllers/AbdmOpdQueue.php +++ b/app/Controllers/AbdmOpdQueue.php @@ -4,6 +4,7 @@ namespace App\Controllers; use App\Libraries\Abdm\AbdmConnectorFactory; use App\Libraries\Abdm\EAtriaBridgeConnector; +use App\Models\ClinicalAuditTrailModel; /** * AbdmOpdQueue @@ -23,6 +24,19 @@ class AbdmOpdQueue extends BaseController { private EAtriaBridgeConnector $gw; + /** + * Allowed status transitions for OPD token lifecycle. + * PENDING -> CALLED/CANCELLED + * CALLED -> COMPLETED/CANCELLED/PENDING (revert) + * COMPLETED/CANCELLED -> PENDING (reopen) + */ + private const STATUS_TRANSITIONS = [ + 'PENDING' => ['CALLED', 'CANCELLED'], + 'CALLED' => ['COMPLETED', 'CANCELLED', 'PENDING'], + 'COMPLETED' => ['PENDING'], + 'CANCELLED' => ['PENDING'], + ]; + public function initController( \CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, @@ -51,6 +65,10 @@ class AbdmOpdQueue extends BaseController $date = date('Y-m-d'); } + // Keep token->appointment linkage up to date for history display. + $this->linkTokensToAppointments($date); + $this->syncCompletedTokensFromOpd($date); + // Counts for summary badges $counts = $db->query( "SELECT @@ -110,6 +128,8 @@ class AbdmOpdQueue extends BaseController $tokens = $result['data'] ?? $result['tokens'] ?? []; if (is_array($tokens) && count($tokens) > 0) { $this->syncTokensToDb($tokens, $date); + $this->linkTokensToAppointments($date); + $this->syncCompletedTokensFromOpd($date); // Enrich tokens with HMS patient / OPD data from local DB $result['data'] = $this->enrichTokensFromDb($tokens, $date); } @@ -168,6 +188,32 @@ class AbdmOpdQueue extends BaseController return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invalid status. Allowed: ' . implode(', ', $allowed)]); } + $db = \Config\Database::connect(); + $current = $db->query( + "SELECT status + FROM abdm_opd_tokens + WHERE gateway_token_id = " . (int) $tokenId . " + ORDER BY id DESC + LIMIT 1" + )->getRowArray(); + $currentStatus = strtoupper(trim((string) ($current['status'] ?? ''))); + + if ($currentStatus !== '' && $currentStatus === $status) { + return $this->response->setJSON([ + 'ok' => 1, + 'token_id' => $tokenId, + 'status' => $status, + 'message' => 'Token is already in requested status', + ]); + } + + if ($currentStatus !== '' && ! $this->canTransitionStatus($currentStatus, $status)) { + return $this->response->setStatusCode(409)->setJSON([ + 'ok' => 0, + 'error_text' => 'Invalid transition: ' . $currentStatus . ' -> ' . $status, + ]); + } + try { $result = $this->gw->opdTokenUpdateStatus($tokenId, $status); } catch (\Throwable $e) { @@ -176,10 +222,23 @@ class AbdmOpdQueue extends BaseController // Mirror status to local DB so list() stays consistent try { - \Config\Database::connect() - ->table('abdm_opd_tokens') + $db->table('abdm_opd_tokens') ->where('gateway_token_id', $tokenId) ->update(['status' => $status, 'updated_at' => date('Y-m-d H:i:s')]); + + if ($currentStatus !== '' && $currentStatus !== $status) { + $this->logTokenAudit( + 'abdm_opd_queue', + (string) $tokenId, + 'status', + $currentStatus, + $status, + [ + 'source' => 'queue_action', + 'route' => 'AbdmOpdQueue/token_status', + ] + ); + } } catch (\Throwable $e) { // non-critical — gateway update already succeeded } @@ -187,6 +246,20 @@ class AbdmOpdQueue extends BaseController return $this->response->setJSON($result); } + private function canTransitionStatus(string $fromStatus, string $toStatus): bool + { + $from = strtoupper(trim($fromStatus)); + $to = strtoupper(trim($toStatus)); + if ($from === '' || $to === '') { + return false; + } + if ($from === $to) { + return true; + } + + return in_array($to, self::STATUS_TRANSITIONS[$from] ?? [], true); + } + // ------------------------------------------------------------------------- // POST AbdmOpdQueue/process_token/:id — Find or create HMS patient from token, // return OPD registration URL so reception can open the OPD form. @@ -359,6 +432,13 @@ class AbdmOpdQueue extends BaseController return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => 'Failed to resolve patient record']); } + $tokenBefore = $db->table('abdm_opd_tokens') + ->select('id,status,patient_id') + ->where('gateway_token_id', $tokenId) + ->orderBy('id', 'DESC') + ->get() + ->getRowArray(); + $this->attachPatientToToken($db, $tokenId, $patientId); // Mark token as CALLED on gateway (non-blocking) @@ -368,6 +448,23 @@ class AbdmOpdQueue extends BaseController // non-critical } + $oldStatus = strtoupper(trim((string) ($tokenBefore['status'] ?? ''))); + if ($oldStatus !== 'CALLED') { + $this->logTokenAudit( + 'abdm_opd_queue', + (string) $tokenId, + 'status', + $oldStatus, + 'CALLED', + [ + 'source' => 'register_opd', + 'action' => $action, + 'patient_id' => $patientId, + 'is_new_patient' => $isNew ? 1 : 0, + ] + ); + } + return $this->response->setJSON([ 'ok' => 1, 'patient_id' => $patientId, @@ -571,10 +668,12 @@ class AbdmOpdQueue extends BaseController } $rows = $db->query( - "SELECT t.gateway_token_id, t.patient_id, t.opd_id, t.processed_at, - p.p_code, p.p_fname + "SELECT t.gateway_token_id, t.patient_id, t.opd_id, t.processed_at, + p.p_code, p.p_fname, + o.opd_code, o.opd_status FROM abdm_opd_tokens t LEFT JOIN patient_master p ON p.id = t.patient_id + LEFT JOIN opd_master o ON o.opd_id = t.opd_id WHERE t.queue_date = '" . $db->escapeString($date) . "' AND t.gateway_token_id IN (" . implode(',', $gids) . ")" )->getResultArray(); @@ -590,6 +689,8 @@ class AbdmOpdQueue extends BaseController $local = $map[$gid]; $t['hms_patient_id'] = $local['patient_id']; $t['hms_opd_id'] = $local['opd_id']; + $t['hms_opd_code'] = $local['opd_code'] ?? null; + $t['hms_opd_status'] = $local['opd_status'] ?? null; $t['hms_processed_at'] = $local['processed_at']; $t['hms_p_code'] = $local['p_code']; $t['hms_p_name'] = $local['p_fname']; @@ -597,7 +698,7 @@ class AbdmOpdQueue extends BaseController ? base_url('Opd/addopd/' . (int) $local['patient_id']) : null; $t['hms_profile_url'] = $local['patient_id'] - ? base_url('Patient/person_profile/' . (int) $local['patient_id']) + ? base_url('Patient/person_record/' . (int) $local['patient_id']) : null; } } @@ -662,4 +763,175 @@ class AbdmOpdQueue extends BaseController } } } + + // ------------------------------------------------------------------------- + // Private: link processed ABDM tokens with created OPD appointments + // ------------------------------------------------------------------------- + private function linkTokensToAppointments(string $date): void + { + $db = \Config\Database::connect(); + + if (! $db->tableExists('abdm_opd_tokens') || ! $db->tableExists('opd_master')) { + return; + } + + $rows = $db->query( + "SELECT id, patient_id, processed_at + FROM abdm_opd_tokens + WHERE queue_date = '" . $db->escapeString($date) . "' + AND patient_id IS NOT NULL + AND (opd_id IS NULL OR opd_id = 0) + ORDER BY processed_at ASC, id ASC" + )->getResultArray(); + + if (empty($rows)) { + return; + } + + foreach ($rows as $row) { + $tokenRowId = (int) ($row['id'] ?? 0); + $patientId = (int) ($row['patient_id'] ?? 0); + if ($tokenRowId <= 0 || $patientId <= 0) { + continue; + } + + $apdWhere = "p_id = " . $patientId . " AND DATE(apointment_date) = '" . $db->escapeString($date) . "'"; + + $opd = $db->query( + "SELECT opd_id + FROM opd_master + WHERE " . $apdWhere . " + ORDER BY opd_id DESC + LIMIT 1" + )->getRowArray(); + + $opdId = (int) ($opd['opd_id'] ?? 0); + if ($opdId <= 0) { + continue; + } + + $db->table('abdm_opd_tokens') + ->where('id', $tokenRowId) + ->update([ + 'opd_id' => $opdId, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + $this->logTokenAudit( + 'abdm_opd_queue', + (string) $tokenRowId, + 'opd_id', + '', + (string) $opdId, + [ + 'source' => 'link_tokens_to_appointments', + 'queue_date' => $date, + ] + ); + } + } + + private function syncCompletedTokensFromOpd(string $date): void + { + $db = \Config\Database::connect(); + if (! $db->tableExists('abdm_opd_tokens') || ! $db->tableExists('opd_master')) { + return; + } + + $rows = $db->query( + "SELECT t.id, t.gateway_token_id, t.status, t.opd_id + FROM abdm_opd_tokens t + JOIN opd_master o ON o.opd_id = t.opd_id + WHERE t.queue_date = '" . $db->escapeString($date) . "' + AND t.opd_id IS NOT NULL + AND t.status <> 'COMPLETED' + AND o.opd_status = 2" + )->getResultArray(); + + foreach ($rows as $row) { + $tokenId = (int) ($row['gateway_token_id'] ?? 0); + $localId = (int) ($row['id'] ?? 0); + $oldStatus = strtoupper(trim((string) ($row['status'] ?? ''))); + if ($tokenId <= 0 || $localId <= 0 || $oldStatus === 'COMPLETED') { + continue; + } + + $gatewayError = ''; + try { + $this->gw->opdTokenUpdateStatus($tokenId, 'COMPLETED'); + } catch (\Throwable $e) { + $gatewayError = $e->getMessage(); + } + + $db->table('abdm_opd_tokens') + ->where('id', $localId) + ->update([ + 'status' => 'COMPLETED', + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + $this->logTokenAudit( + 'abdm_opd_queue', + (string) $tokenId, + 'status', + $oldStatus, + 'COMPLETED', + [ + 'source' => 'opd_visit_done_auto', + 'queue_date' => $date, + 'opd_id' => (int) ($row['opd_id'] ?? 0), + 'gateway_sync' => $gatewayError === '' ? 'ok' : 'failed', + 'gateway_error' => $gatewayError, + ] + ); + } + } + + private function logTokenAudit( + string $module, + string $recordId, + string $fieldName, + ?string $oldValue, + ?string $newValue, + array $actionMeta = [] + ): void { + try { + $db = \Config\Database::connect(); + if (! $db->tableExists('clinical_audit_trail')) { + return; + } + + $user = auth()->user(); + $userId = (string) ($user->id ?? 0); + $payload = [ + 'module' => $module, + 'record_id' => $recordId, + 'field_name' => $fieldName, + 'old_value' => $oldValue, + 'new_value' => $newValue, + 'user_id' => $userId, + 'action_at' => date('Y-m-d H:i:s'), + ]; + + if (! empty($actionMeta)) { + $payload['new_value'] = trim((string) $newValue) . ' | ' . json_encode($actionMeta, JSON_UNESCAPED_SLASHES); + } + + $payload['hash'] = hash('sha256', implode('|', [ + $payload['module'], + $payload['record_id'], + $payload['field_name'], + (string) $payload['old_value'], + (string) $payload['new_value'], + $payload['user_id'], + $payload['action_at'], + microtime(true), + random_int(1000, 999999), + ])); + + (new ClinicalAuditTrailModel())->insert($payload); + } catch (\Throwable $e) { + // fail-open: audit should not break workflow + } + } } diff --git a/app/Views/abdm/opd_queue_list.php b/app/Views/abdm/opd_queue_list.php index 823e790..0e9093e 100644 --- a/app/Views/abdm/opd_queue_list.php +++ b/app/Views/abdm/opd_queue_list.php @@ -140,7 +140,7 @@ function renderTokenTable(array $rows, string $ctx): string if ($pid > 0) { $pCode = esc((string) ($t['p_code'] ?? '')); $pName = esc((string) ($t['p_fname'] ?? $t['patient_name'] ?? '—')); - $hmsLink = '' + $hmsLink = '' . '' . $pCode . ' ' . $pName . ''; } else { $hmsLink = 'Not linked';