Fix ABDM OPD HMS patient profile link route to person_record
This commit is contained in:
parent
34ff92f685
commit
e15d876a3f
|
|
@ -4,6 +4,7 @@ namespace App\Controllers;
|
||||||
|
|
||||||
use App\Libraries\Abdm\AbdmConnectorFactory;
|
use App\Libraries\Abdm\AbdmConnectorFactory;
|
||||||
use App\Libraries\Abdm\EAtriaBridgeConnector;
|
use App\Libraries\Abdm\EAtriaBridgeConnector;
|
||||||
|
use App\Models\ClinicalAuditTrailModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AbdmOpdQueue
|
* AbdmOpdQueue
|
||||||
|
|
@ -23,6 +24,19 @@ class AbdmOpdQueue extends BaseController
|
||||||
{
|
{
|
||||||
private EAtriaBridgeConnector $gw;
|
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(
|
public function initController(
|
||||||
\CodeIgniter\HTTP\RequestInterface $request,
|
\CodeIgniter\HTTP\RequestInterface $request,
|
||||||
\CodeIgniter\HTTP\ResponseInterface $response,
|
\CodeIgniter\HTTP\ResponseInterface $response,
|
||||||
|
|
@ -51,6 +65,10 @@ class AbdmOpdQueue extends BaseController
|
||||||
$date = date('Y-m-d');
|
$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 for summary badges
|
||||||
$counts = $db->query(
|
$counts = $db->query(
|
||||||
"SELECT
|
"SELECT
|
||||||
|
|
@ -110,6 +128,8 @@ class AbdmOpdQueue extends BaseController
|
||||||
$tokens = $result['data'] ?? $result['tokens'] ?? [];
|
$tokens = $result['data'] ?? $result['tokens'] ?? [];
|
||||||
if (is_array($tokens) && count($tokens) > 0) {
|
if (is_array($tokens) && count($tokens) > 0) {
|
||||||
$this->syncTokensToDb($tokens, $date);
|
$this->syncTokensToDb($tokens, $date);
|
||||||
|
$this->linkTokensToAppointments($date);
|
||||||
|
$this->syncCompletedTokensFromOpd($date);
|
||||||
// Enrich tokens with HMS patient / OPD data from local DB
|
// Enrich tokens with HMS patient / OPD data from local DB
|
||||||
$result['data'] = $this->enrichTokensFromDb($tokens, $date);
|
$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)]);
|
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 {
|
try {
|
||||||
$result = $this->gw->opdTokenUpdateStatus($tokenId, $status);
|
$result = $this->gw->opdTokenUpdateStatus($tokenId, $status);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
@ -176,10 +222,23 @@ class AbdmOpdQueue extends BaseController
|
||||||
|
|
||||||
// Mirror status to local DB so list() stays consistent
|
// Mirror status to local DB so list() stays consistent
|
||||||
try {
|
try {
|
||||||
\Config\Database::connect()
|
$db->table('abdm_opd_tokens')
|
||||||
->table('abdm_opd_tokens')
|
|
||||||
->where('gateway_token_id', $tokenId)
|
->where('gateway_token_id', $tokenId)
|
||||||
->update(['status' => $status, 'updated_at' => date('Y-m-d H:i:s')]);
|
->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) {
|
} catch (\Throwable $e) {
|
||||||
// non-critical — gateway update already succeeded
|
// non-critical — gateway update already succeeded
|
||||||
}
|
}
|
||||||
|
|
@ -187,6 +246,20 @@ class AbdmOpdQueue extends BaseController
|
||||||
return $this->response->setJSON($result);
|
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,
|
// POST AbdmOpdQueue/process_token/:id — Find or create HMS patient from token,
|
||||||
// return OPD registration URL so reception can open the OPD form.
|
// 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']);
|
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);
|
$this->attachPatientToToken($db, $tokenId, $patientId);
|
||||||
|
|
||||||
// Mark token as CALLED on gateway (non-blocking)
|
// Mark token as CALLED on gateway (non-blocking)
|
||||||
|
|
@ -368,6 +448,23 @@ class AbdmOpdQueue extends BaseController
|
||||||
// non-critical
|
// 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([
|
return $this->response->setJSON([
|
||||||
'ok' => 1,
|
'ok' => 1,
|
||||||
'patient_id' => $patientId,
|
'patient_id' => $patientId,
|
||||||
|
|
@ -571,10 +668,12 @@ class AbdmOpdQueue extends BaseController
|
||||||
}
|
}
|
||||||
|
|
||||||
$rows = $db->query(
|
$rows = $db->query(
|
||||||
"SELECT t.gateway_token_id, t.patient_id, t.opd_id, t.processed_at,
|
"SELECT t.gateway_token_id, t.patient_id, t.opd_id, t.processed_at,
|
||||||
p.p_code, p.p_fname
|
p.p_code, p.p_fname,
|
||||||
|
o.opd_code, o.opd_status
|
||||||
FROM abdm_opd_tokens t
|
FROM abdm_opd_tokens t
|
||||||
LEFT JOIN patient_master p ON p.id = t.patient_id
|
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) . "'
|
WHERE t.queue_date = '" . $db->escapeString($date) . "'
|
||||||
AND t.gateway_token_id IN (" . implode(',', $gids) . ")"
|
AND t.gateway_token_id IN (" . implode(',', $gids) . ")"
|
||||||
)->getResultArray();
|
)->getResultArray();
|
||||||
|
|
@ -590,6 +689,8 @@ class AbdmOpdQueue extends BaseController
|
||||||
$local = $map[$gid];
|
$local = $map[$gid];
|
||||||
$t['hms_patient_id'] = $local['patient_id'];
|
$t['hms_patient_id'] = $local['patient_id'];
|
||||||
$t['hms_opd_id'] = $local['opd_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_processed_at'] = $local['processed_at'];
|
||||||
$t['hms_p_code'] = $local['p_code'];
|
$t['hms_p_code'] = $local['p_code'];
|
||||||
$t['hms_p_name'] = $local['p_fname'];
|
$t['hms_p_name'] = $local['p_fname'];
|
||||||
|
|
@ -597,7 +698,7 @@ class AbdmOpdQueue extends BaseController
|
||||||
? base_url('Opd/addopd/' . (int) $local['patient_id'])
|
? base_url('Opd/addopd/' . (int) $local['patient_id'])
|
||||||
: null;
|
: null;
|
||||||
$t['hms_profile_url'] = $local['patient_id']
|
$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;
|
: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ function renderTokenTable(array $rows, string $ctx): string
|
||||||
if ($pid > 0) {
|
if ($pid > 0) {
|
||||||
$pCode = esc((string) ($t['p_code'] ?? ''));
|
$pCode = esc((string) ($t['p_code'] ?? ''));
|
||||||
$pName = esc((string) ($t['p_fname'] ?? $t['patient_name'] ?? '—'));
|
$pName = esc((string) ($t['p_fname'] ?? $t['patient_name'] ?? '—'));
|
||||||
$hmsLink = '<a href="javascript:load_form(\'' . base_url('Patient/person_profile/' . $pid) . '\',\'Patient Profile\')">'
|
$hmsLink = '<a href="javascript:load_form(\'' . base_url('Patient/person_record/' . $pid) . '\',\'Patient Profile\')">'
|
||||||
. '<strong>' . $pCode . '</strong> ' . $pName . '</a>';
|
. '<strong>' . $pCode . '</strong> ' . $pName . '</a>';
|
||||||
} else {
|
} else {
|
||||||
$hmsLink = '<span class="text-muted small">Not linked</span>';
|
$hmsLink = '<span class="text-muted small">Not linked</span>';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user