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\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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = '<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>';
|
||||
} else {
|
||||
$hmsLink = '<span class="text-muted small">Not linked</span>';
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user