feat: Add ABDM Task Board module with ABHA workflow
- Add ABDM sidebar heading and Task Board nav link - Create migration 000040: abdm_work_tasks table - Add AbdmTaskBoard controller with backfill methods - Add AbdmWorkTaskService for task creation/deduplication - Add AbdmGateway controller for ABDM API integration - Separate task types: patient_abha_create vs patient_abha_update - Add lab_report_publish and radiology_report_publish task types - Patient.php: auto-create ABDM tasks on registration - Diagnosis.php: auto-create tasks on report verify/finalize - Task board UI: 7 filter buttons with default Unupdated ABHA view - Fix SQL crash in backfillLabRadiologyTasks (dynamic column detection) - Improve load_form AJAX error display with HTTP status + response text - Add ABDM routes in Routes.php - Add FHIR R4 builder library updates - Add bridge sync service updates
This commit is contained in:
parent
9d88aaa2a6
commit
062fc7ab32
27
README.md
27
README.md
|
|
@ -168,6 +168,33 @@ Generate encryption key:
|
|||
php spark key:generate
|
||||
```
|
||||
|
||||
### ABDM via Eka.care (existing routes, provider switch)
|
||||
|
||||
This project keeps the same ABDM controller routes and queue events, and can switch
|
||||
the queue dispatcher to Eka for ABDM/NHCX integrations.
|
||||
|
||||
Add these keys in `.env`:
|
||||
|
||||
```dotenv
|
||||
ABDM_SYNC_PROVIDER = eka
|
||||
EKA_BASE_URL = https://api.eka.care
|
||||
EKA_BEARER_TOKEN = your-token
|
||||
# Optional alternatives
|
||||
# EKA_API_KEY = your-api-key
|
||||
# EKA_CLIENT_ID = your-client-id
|
||||
# EKA_EVENT_ENDPOINTS_JSON = {"abdm.abha.validate":"/your/path"}
|
||||
# EKA_WEBHOOK_SECRET = shared-secret-for-callback-signature
|
||||
```
|
||||
|
||||
Then process queue normally:
|
||||
|
||||
```bash
|
||||
php spark bridge:sync
|
||||
```
|
||||
|
||||
If `EKA_WEBHOOK_SECRET` is set, callback endpoints require a valid HMAC-SHA256 signature
|
||||
in `X-Eka-Signature`, `X-Signature`, or `X-Hub-Signature-256`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Database Setup
|
||||
|
|
|
|||
|
|
@ -388,6 +388,7 @@ $routes->group('billing', function($routes) {
|
|||
$routes->post('patient/create', 'Patient::create');
|
||||
$routes->post('patient/search', 'Patient::search');
|
||||
$routes->post('patient/search_adv', 'Patient::search_adv');
|
||||
$routes->post('patient/abha_fetch_profile', 'Patient::abha_fetch_profile');
|
||||
$routes->get('patient/person_record/(:num)', 'Patient::person_record/$1');
|
||||
$routes->get('patient/person_record/(:num)/(:num)', 'Patient::person_record/$1/$2');
|
||||
$routes->get('patient/show_cards/(:num)', 'Patient::show_cards/$1');
|
||||
|
|
@ -501,6 +502,20 @@ $routes->get('Opd_prescription/Prescription/(:num)', 'Opd_prescription::Prescrip
|
|||
$routes->get('Opd_prescription/opd_prescription_print/(:num)/(:num)', 'Opd_prescription::opd_prescription_print/$1/$2');
|
||||
$routes->get('Opd_prescription/opd_prescription_print/(:num)/(:num)/(:num)', 'Opd_prescription::opd_prescription_print/$1/$2/$3');
|
||||
$routes->post('Opd_prescription/opd_prescription_save', 'Opd_prescription::opd_prescription_save');
|
||||
$routes->post('AbdmGateway/abha_validate', 'AbdmGateway::abhaValidate');
|
||||
$routes->post('AbdmGateway/scan_share_lookup', 'AbdmGateway::scanShareLookup');
|
||||
$routes->post('AbdmGateway/consent_request', 'AbdmGateway::consentRequest');
|
||||
$routes->post('AbdmGateway/consent_callback', 'AbdmGateway::consentCallback');
|
||||
$routes->post('AbdmGateway/share_prescription_bundle', 'AbdmGateway::sharePrescriptionBundle');
|
||||
$routes->post('AbdmGateway/share_ipd_discharge_bundle', 'AbdmGateway::shareIpdDischargeBundle');
|
||||
$routes->post('AbdmGateway/share_diagnosis_report_bundle', 'AbdmGateway::shareDiagnosisReportBundle');
|
||||
$routes->post('AbdmGateway/nhcx_claim_create', 'AbdmGateway::nhcxClaimCreate');
|
||||
$routes->post('AbdmGateway/nhcx_claim_status_request', 'AbdmGateway::nhcxClaimStatusRequest');
|
||||
$routes->post('AbdmGateway/nhcx_claim_status_callback', 'AbdmGateway::nhcxClaimStatusCallback');
|
||||
$routes->get('AbdmTaskBoard', 'AbdmTaskBoard::index');
|
||||
$routes->get('AbdmTaskBoard/list', 'AbdmTaskBoard::list');
|
||||
$routes->post('AbdmTaskBoard/perform_action', 'AbdmTaskBoard::performAction');
|
||||
$routes->post('AbdmTaskBoard/mark_status', 'AbdmTaskBoard::markStatus');
|
||||
$routes->get('Opd_prescription/fhir_bundle/(:num)', 'Opd_prescription::fhir_bundle/$1');
|
||||
$routes->get('Opd_prescription/fhir_bundle/(:num)/(:num)', 'Opd_prescription::fhir_bundle/$1/$2');
|
||||
$routes->get('Opd_prescription/fhir_bundle_history/(:num)', 'Opd_prescription::fhir_bundle_history/$1');
|
||||
|
|
|
|||
757
app/Controllers/AbdmGateway.php
Normal file
757
app/Controllers/AbdmGateway.php
Normal file
|
|
@ -0,0 +1,757 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\BridgeSyncService;
|
||||
use App\Libraries\FhirR4Builder;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class AbdmGateway extends BaseController
|
||||
{
|
||||
public function abhaValidate()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
||||
if ($abhaId === '' || preg_match('/^\d{14}$/', $abhaId) !== 1) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA ID must be a 14-digit number.']);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'abha_id' => $abhaId,
|
||||
'requested_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
];
|
||||
|
||||
$queueId = null;
|
||||
try {
|
||||
$bridge = new BridgeSyncService();
|
||||
$queueId = $bridge->enqueue('abdm.abha.validate', $payload, 'abha', $abhaId);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'status' => 'queued',
|
||||
'queue_id' => $queueId,
|
||||
'message' => 'ABHA validation request queued to center server.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function consentRequest()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
||||
$purposeCode = trim((string) $this->request->getPost('purpose_code'));
|
||||
$expiresAt = trim((string) $this->request->getPost('expires_at'));
|
||||
|
||||
if ($patientId <= 0 || $abhaId === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id and abha_id are required']);
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists('abdm_consent_records')) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'abdm_consent_records table not found']);
|
||||
}
|
||||
|
||||
$consentHandle = 'CH-' . date('YmdHis') . '-' . strtoupper(bin2hex(random_bytes(4)));
|
||||
$requestedAt = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
|
||||
$rawPayload = [
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'purpose_code' => $purposeCode,
|
||||
'expires_at' => $expiresAt,
|
||||
'consent_handle' => $consentHandle,
|
||||
'requested_at' => $requestedAt,
|
||||
];
|
||||
|
||||
$this->db->table('abdm_consent_records')->insert([
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'consent_handle' => $consentHandle,
|
||||
'consent_status' => 'requested',
|
||||
'purpose_code' => $purposeCode,
|
||||
'requested_at' => $requestedAt,
|
||||
'expires_at' => $expiresAt !== '' ? $expiresAt : null,
|
||||
'raw_payload_json' => (string) json_encode($rawPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'created_at' => $requestedAt,
|
||||
'updated_at' => $requestedAt,
|
||||
]);
|
||||
|
||||
$queueId = null;
|
||||
try {
|
||||
$bridge = new BridgeSyncService();
|
||||
$queueId = $bridge->enqueue('abdm.consent.requested', $rawPayload, 'consent', $consentHandle);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'consent_handle' => $consentHandle,
|
||||
'queue_id' => $queueId,
|
||||
'status' => 'requested',
|
||||
]);
|
||||
}
|
||||
|
||||
public function consentCallback()
|
||||
{
|
||||
$signatureFailure = $this->validateWebhookSignature();
|
||||
if ($signatureFailure !== null) {
|
||||
return $signatureFailure;
|
||||
}
|
||||
|
||||
$payload = $this->request->getJSON(true);
|
||||
if (! is_array($payload) || empty($payload['consent_handle'])) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'consent_handle is required']);
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists('abdm_consent_records')) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'abdm_consent_records table not found']);
|
||||
}
|
||||
|
||||
$handle = trim((string) $payload['consent_handle']);
|
||||
$status = trim((string) ($payload['consent_status'] ?? 'approved'));
|
||||
$expiresAt = trim((string) ($payload['expires_at'] ?? ''));
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
|
||||
$existing = $this->db->table('abdm_consent_records')
|
||||
->where('consent_handle', $handle)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if (empty($existing)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => 'consent_handle not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$currentStatus = strtolower(trim((string) ($existing['consent_status'] ?? '')));
|
||||
$incomingStatus = strtolower($status);
|
||||
$currentExpires = trim((string) ($existing['expires_at'] ?? ''));
|
||||
$incomingExpires = $expiresAt !== '' ? $expiresAt : '';
|
||||
|
||||
if ($this->isStaleConsentTransition($currentStatus, $incomingStatus)) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'consent_handle' => $handle,
|
||||
'status' => $currentStatus,
|
||||
'ignored' => 1,
|
||||
'reason' => 'stale_transition',
|
||||
]);
|
||||
}
|
||||
|
||||
$rawPayloadJson = (string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$existingPayload = trim((string) ($existing['raw_payload_json'] ?? ''));
|
||||
$isDuplicate = ($currentStatus === $incomingStatus) && ($currentExpires === $incomingExpires) && ($existingPayload === $rawPayloadJson);
|
||||
if ($isDuplicate) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'consent_handle' => $handle,
|
||||
'status' => $incomingStatus,
|
||||
'duplicate' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
$update = [
|
||||
'consent_status' => $incomingStatus,
|
||||
'granted_at' => $incomingStatus === 'approved'
|
||||
? ((string) ($existing['granted_at'] ?? '') !== '' ? (string) $existing['granted_at'] : $now)
|
||||
: (string) ($existing['granted_at'] ?? ''),
|
||||
'expires_at' => $expiresAt !== '' ? $expiresAt : null,
|
||||
'raw_payload_json' => $rawPayloadJson,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
$this->db->table('abdm_consent_records')
|
||||
->where('consent_handle', $handle)
|
||||
->update($update);
|
||||
|
||||
return $this->response->setJSON(['ok' => 1, 'consent_handle' => $handle, 'status' => $incomingStatus]);
|
||||
}
|
||||
|
||||
public function scanShareLookup()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$qrPayload = trim((string) $this->request->getPost('qr_payload'));
|
||||
if ($qrPayload === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'qr_payload is required']);
|
||||
}
|
||||
|
||||
$abhaId = '';
|
||||
if (preg_match('/\b(\d{14})\b/', $qrPayload, $match) === 1) {
|
||||
$abhaId = (string) ($match[1] ?? '');
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'qr_payload' => $qrPayload,
|
||||
'abha_id_hint' => $abhaId,
|
||||
'requested_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
];
|
||||
|
||||
$queueId = null;
|
||||
try {
|
||||
$bridge = new BridgeSyncService();
|
||||
$queueId = $bridge->enqueue('abdm.scan_share.lookup', $payload, 'abha_scan', $abhaId !== '' ? $abhaId : null);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'status' => 'queued',
|
||||
'queue_id' => $queueId,
|
||||
'abha_id_hint' => $abhaId,
|
||||
'message' => 'Scan & Share lookup queued to center server.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function sharePrescriptionBundle()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$opdId = (int) $this->request->getPost('opd_id');
|
||||
$sessionId = (int) $this->request->getPost('opd_session_id');
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
||||
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
||||
|
||||
if ($opdId <= 0 || $patientId <= 0 || $abhaId === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'opd_id, patient_id and abha_id are required']);
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists('opd_fhir_documents')) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'opd_fhir_documents table not found']);
|
||||
}
|
||||
|
||||
$consent = $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle);
|
||||
if ($consent === null) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => 'No active consent found. Share blocked due to expiry/not-approved consent.',
|
||||
]);
|
||||
}
|
||||
|
||||
$builder = $this->db->table('opd_fhir_documents')
|
||||
->where('opd_id', $opdId)
|
||||
->where('bundle_type', 'MedicationRequestBundle');
|
||||
if ($sessionId > 0) {
|
||||
$builder->where('opd_session_id', $sessionId);
|
||||
}
|
||||
$bundleRow = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
|
||||
|
||||
if (empty($bundleRow)) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'No FHIR bundle found for selected OPD/session']);
|
||||
}
|
||||
|
||||
$bundleJson = (string) ($bundleRow['bundle_json'] ?? '{}');
|
||||
$bundle = json_decode($bundleJson, true);
|
||||
if (! is_array($bundle)) {
|
||||
$bundle = ['raw' => $bundleJson];
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'opd_id' => $opdId,
|
||||
'opd_session_id' => (int) ($bundleRow['opd_session_id'] ?? 0),
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
||||
'bundle_type' => (string) ($bundleRow['bundle_type'] ?? 'MedicationRequestBundle'),
|
||||
'bundle' => $bundle,
|
||||
];
|
||||
|
||||
$queueId = null;
|
||||
try {
|
||||
$bridge = new BridgeSyncService();
|
||||
$queueId = $bridge->enqueue('abdm.fhir.share.requested', $payload, 'opd_fhir_document', (string) ($bundleRow['id'] ?? ''));
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'queue_id' => $queueId,
|
||||
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
||||
'status' => 'queued',
|
||||
]);
|
||||
}
|
||||
|
||||
public function shareIpdDischargeBundle()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$ipdId = (int) $this->request->getPost('ipd_id');
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
||||
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
||||
|
||||
if ($ipdId <= 0 || $patientId <= 0 || $abhaId === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ipd_id, patient_id and abha_id are required']);
|
||||
}
|
||||
|
||||
$consent = $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle);
|
||||
if ($consent === null) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => 'No active consent found. Share blocked due to expiry/not-approved consent.',
|
||||
]);
|
||||
}
|
||||
|
||||
$ipdRow = $this->db->tableExists('ipd_master')
|
||||
? ($this->db->table('ipd_master')->where('id', $ipdId)->get(1)->getRowArray() ?? [])
|
||||
: [];
|
||||
|
||||
$summaryHtml = '';
|
||||
if ($this->db->tableExists('ipd_discharge') && $this->db->fieldExists('content', 'ipd_discharge')) {
|
||||
$summaryRow = $this->db->table('ipd_discharge')
|
||||
->select('content')
|
||||
->where('ipd_id', $ipdId)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
$summaryHtml = trim((string) ($summaryRow['content'] ?? ''));
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'ipd_id' => $ipdId,
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
||||
'ipd_code' => trim((string) ($ipdRow['ipd_code'] ?? '')),
|
||||
'register_date' => trim((string) ($ipdRow['register_date'] ?? '')),
|
||||
'discharge_date' => trim((string) ($ipdRow['discharge_date'] ?? '')),
|
||||
'discharge_time' => trim((string) ($ipdRow['discharge_time'] ?? '')),
|
||||
'summary_html' => $summaryHtml,
|
||||
];
|
||||
|
||||
$queueId = null;
|
||||
try {
|
||||
$bridge = new BridgeSyncService();
|
||||
$queueId = $bridge->enqueue('abdm.ipd.discharge.share.requested', $payload, 'ipd_discharge', (string) $ipdId);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'queue_id' => $queueId,
|
||||
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
||||
'status' => 'queued',
|
||||
]);
|
||||
}
|
||||
|
||||
public function shareDiagnosisReportBundle()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(403)->setJSON(['error' => 'AJAX only']);
|
||||
}
|
||||
|
||||
$labReqId = (int) ($this->request->getPost('lab_req_id') ?? 0);
|
||||
$patientId = (int) ($this->request->getPost('patient_id') ?? 0);
|
||||
$abhaId = trim((string) ($this->request->getPost('abha_id') ?? ''));
|
||||
$consentHandle = trim((string) ($this->request->getPost('consent_handle') ?? ''));
|
||||
|
||||
if ($labReqId <= 0 || $patientId <= 0 || $abhaId === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error' => 'lab_req_id, patient_id and abha_id are required']);
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$labReq = $db->table('lab_request')
|
||||
->select('id, patient_name, lab_type, charge_id, Report_Data, report_data_Impression, status, reported_time')
|
||||
->where('id', $labReqId)
|
||||
->get(1)
|
||||
->getRow();
|
||||
|
||||
if (! $labReq) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error' => 'Lab request not found']);
|
||||
}
|
||||
|
||||
$consentRecord = $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle);
|
||||
|
||||
$payload = [
|
||||
'lab_req_id' => $labReqId,
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => (string) ($labReq->patient_name ?? ''),
|
||||
'abha_id' => $abhaId,
|
||||
'lab_type' => (int) ($labReq->lab_type ?? 0),
|
||||
'invoice_id' => (int) ($labReq->charge_id ?? 0),
|
||||
'report_html' => (string) ($labReq->Report_Data ?? ''),
|
||||
'impression' => (string) ($labReq->report_data_Impression ?? ''),
|
||||
'report_status' => (int) ($labReq->status ?? 0),
|
||||
'reported_time' => (string) ($labReq->reported_time ?? ''),
|
||||
'consent_handle' => $consentRecord['consent_handle'] ?? $consentHandle,
|
||||
];
|
||||
|
||||
$bridge = new BridgeSyncService();
|
||||
$queueId = $bridge->enqueue('abdm.diagnosis.report.share.requested', $payload, 'lab_request', (string) $labReqId);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'queue_id' => $queueId,
|
||||
'consent_handle' => $payload['consent_handle'],
|
||||
'status' => 'queued',
|
||||
]);
|
||||
}
|
||||
|
||||
public function nhcxClaimCreate()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists('nhcx_claim_documents')) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'nhcx_claim_documents table not found']);
|
||||
}
|
||||
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$encounterId = (int) $this->request->getPost('encounter_id');
|
||||
$ipdId = (int) $this->request->getPost('ipd_id');
|
||||
$caseId = (int) $this->request->getPost('case_id');
|
||||
$totalAmount = (float) $this->request->getPost('total_amount');
|
||||
$claimType = trim((string) $this->request->getPost('claim_type')) ?: 'institutional';
|
||||
|
||||
if ($patientId <= 0 || $encounterId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id and encounter_id are required']);
|
||||
}
|
||||
|
||||
$patient = [
|
||||
'id' => (string) $patientId,
|
||||
'name' => (string) $this->request->getPost('patient_name'),
|
||||
'gender' => (string) $this->request->getPost('patient_gender'),
|
||||
'birthDate' => (string) $this->request->getPost('patient_birthdate'),
|
||||
'abhaAddress' => (string) $this->request->getPost('abha_address'),
|
||||
];
|
||||
|
||||
$encounter = [
|
||||
'id' => (string) $encounterId,
|
||||
'status' => 'finished',
|
||||
];
|
||||
|
||||
$items = $this->request->getPost('items');
|
||||
if (is_string($items)) {
|
||||
$decoded = json_decode($items, true);
|
||||
$items = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
if (! is_array($items)) {
|
||||
$items = [];
|
||||
}
|
||||
|
||||
$claim = [
|
||||
'id' => 'claim-' . $patientId . '-' . $encounterId . '-' . date('YmdHis'),
|
||||
'status' => 'active',
|
||||
'use' => 'claim',
|
||||
'type' => $claimType,
|
||||
'total' => $totalAmount,
|
||||
'provider' => (string) $this->request->getPost('provider_name'),
|
||||
'insurer' => (string) $this->request->getPost('insurer_name'),
|
||||
'priority' => (string) $this->request->getPost('priority'),
|
||||
'items' => $items,
|
||||
];
|
||||
|
||||
$fhir = new FhirR4Builder();
|
||||
$bundle = $fhir->buildClaimBundle($patient, $encounter, $claim);
|
||||
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
$this->db->table('nhcx_claim_documents')->insert([
|
||||
'ipd_id' => $ipdId > 0 ? $ipdId : null,
|
||||
'case_id' => $caseId > 0 ? $caseId : null,
|
||||
'patient_id' => $patientId,
|
||||
'claim_type' => $claimType,
|
||||
'claim_json' => $bundleJson,
|
||||
'claim_status' => 'draft',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
$documentId = (int) $this->db->insertID();
|
||||
$queueId = null;
|
||||
try {
|
||||
$bridge = new BridgeSyncService();
|
||||
$queueId = $bridge->enqueue(
|
||||
'nhcx.claim.created',
|
||||
[
|
||||
'nhcx_claim_document_id' => $documentId,
|
||||
'patient_id' => $patientId,
|
||||
'encounter_id' => $encounterId,
|
||||
'bundle' => $bundle,
|
||||
],
|
||||
'nhcx_claim_document',
|
||||
(string) $documentId
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'document_id' => $documentId,
|
||||
'queue_id' => $queueId,
|
||||
'claim_status' => 'draft',
|
||||
]);
|
||||
}
|
||||
|
||||
public function nhcxClaimStatusRequest()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists('nhcx_claim_documents')) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'nhcx_claim_documents table not found']);
|
||||
}
|
||||
|
||||
$documentId = (int) $this->request->getPost('document_id');
|
||||
if ($documentId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'document_id is required']);
|
||||
}
|
||||
|
||||
$row = $this->db->table('nhcx_claim_documents')->where('id', $documentId)->get(1)->getRowArray();
|
||||
if (empty($row)) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'claim document not found']);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'nhcx_claim_document_id' => $documentId,
|
||||
'external_ref' => (string) ($row['external_ref'] ?? ''),
|
||||
'claim_status' => (string) ($row['claim_status'] ?? ''),
|
||||
'requested_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
];
|
||||
|
||||
$queueId = null;
|
||||
try {
|
||||
$bridge = new BridgeSyncService();
|
||||
$queueId = $bridge->enqueue('nhcx.claim.status.requested', $payload, 'nhcx_claim_document', (string) $documentId);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'document_id' => $documentId,
|
||||
'queue_id' => $queueId,
|
||||
'status' => 'queued',
|
||||
]);
|
||||
}
|
||||
|
||||
public function nhcxClaimStatusCallback()
|
||||
{
|
||||
$signatureFailure = $this->validateWebhookSignature();
|
||||
if ($signatureFailure !== null) {
|
||||
return $signatureFailure;
|
||||
}
|
||||
|
||||
$payload = $this->request->getJSON(true);
|
||||
if (! is_array($payload)) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid JSON payload']);
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists('nhcx_claim_documents')) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'nhcx_claim_documents table not found']);
|
||||
}
|
||||
|
||||
$documentId = (int) ($payload['document_id'] ?? 0);
|
||||
$externalRef = trim((string) ($payload['external_ref'] ?? ''));
|
||||
$status = trim((string) ($payload['claim_status'] ?? 'unknown'));
|
||||
$errorMessage = trim((string) ($payload['error_message'] ?? ''));
|
||||
|
||||
if ($documentId <= 0 && $externalRef === '') {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'document_id or external_ref is required']);
|
||||
}
|
||||
|
||||
$existingBuilder = $this->db->table('nhcx_claim_documents');
|
||||
if ($documentId > 0) {
|
||||
$existingBuilder->where('id', $documentId);
|
||||
} else {
|
||||
$existingBuilder->where('external_ref', $externalRef);
|
||||
}
|
||||
$existing = $existingBuilder->get(1)->getRowArray();
|
||||
if (empty($existing)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => 'claim document not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$currentStatus = strtolower(trim((string) ($existing['claim_status'] ?? 'unknown')));
|
||||
$incomingStatus = strtolower($status);
|
||||
$currentExternalRef = trim((string) ($existing['external_ref'] ?? ''));
|
||||
$currentError = trim((string) ($existing['error_message'] ?? ''));
|
||||
|
||||
if ($this->isStaleClaimTransition($currentStatus, $incomingStatus)) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'document_id' => (int) ($existing['id'] ?? $documentId),
|
||||
'external_ref' => $currentExternalRef,
|
||||
'claim_status' => $currentStatus,
|
||||
'ignored' => 1,
|
||||
'reason' => 'stale_transition',
|
||||
]);
|
||||
}
|
||||
|
||||
$isDuplicate = ($currentStatus === $incomingStatus)
|
||||
&& ($currentExternalRef === $externalRef)
|
||||
&& ($currentError === $errorMessage);
|
||||
if ($isDuplicate) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'document_id' => (int) ($existing['id'] ?? $documentId),
|
||||
'external_ref' => $currentExternalRef,
|
||||
'claim_status' => $currentStatus,
|
||||
'duplicate' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
$builder = $this->db->table('nhcx_claim_documents')->where('id', (int) $existing['id']);
|
||||
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
$builder->update([
|
||||
'claim_status' => $incomingStatus,
|
||||
'external_ref' => $externalRef !== '' ? $externalRef : null,
|
||||
'error_message' => $errorMessage !== '' ? mb_substr($errorMessage, 0, 1000) : null,
|
||||
'pushed_at' => $incomingStatus === 'submitted'
|
||||
? ((string) ($existing['pushed_at'] ?? '') !== '' ? (string) $existing['pushed_at'] : $now)
|
||||
: (string) ($existing['pushed_at'] ?? ''),
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'document_id' => (int) ($existing['id'] ?? $documentId),
|
||||
'external_ref' => $externalRef,
|
||||
'claim_status' => $incomingStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
private function validateWebhookSignature()
|
||||
{
|
||||
$secret = $this->readRuntimeSetting('EKA_WEBHOOK_SECRET');
|
||||
if ($secret === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$signature = trim((string) ($this->request->getHeaderLine('X-Eka-Signature') ?: ''));
|
||||
if ($signature === '') {
|
||||
$signature = trim((string) ($this->request->getHeaderLine('X-Signature') ?: ''));
|
||||
}
|
||||
if ($signature === '') {
|
||||
$signature = trim((string) ($this->request->getHeaderLine('X-Hub-Signature-256') ?: ''));
|
||||
}
|
||||
if ($signature === '') {
|
||||
return $this->response->setStatusCode(401)->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => 'Missing signature header',
|
||||
]);
|
||||
}
|
||||
|
||||
$signature = strtolower($signature);
|
||||
if (str_starts_with($signature, 'sha256=')) {
|
||||
$signature = substr($signature, 7);
|
||||
}
|
||||
|
||||
$rawBody = (string) $this->request->getBody();
|
||||
$expected = hash_hmac('sha256', $rawBody, $secret);
|
||||
if (! hash_equals($expected, $signature)) {
|
||||
return $this->response->setStatusCode(401)->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => 'Invalid signature',
|
||||
]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function readRuntimeSetting(string $name): string
|
||||
{
|
||||
$envValue = getenv($name);
|
||||
if ($envValue !== false) {
|
||||
$value = trim((string) $envValue);
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->db->tableExists('hospital_setting')) {
|
||||
$row = $this->db->table('hospital_setting')
|
||||
->select('s_value')
|
||||
->where('s_name', $name)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
$value = trim((string) ($row['s_value'] ?? ''));
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function isStaleConsentTransition(string $currentStatus, string $incomingStatus): bool
|
||||
{
|
||||
if ($incomingStatus === '' || $currentStatus === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($incomingStatus === 'requested' && $currentStatus !== 'requested') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isStaleClaimTransition(string $currentStatus, string $incomingStatus): bool
|
||||
{
|
||||
if ($incomingStatus === '' || $currentStatus === '' || $incomingStatus === 'unknown') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$rank = [
|
||||
'draft' => 10,
|
||||
'submitted' => 20,
|
||||
'in_review' => 30,
|
||||
'approved' => 40,
|
||||
'rejected' => 40,
|
||||
'failed' => 40,
|
||||
'settled' => 50,
|
||||
'paid' => 50,
|
||||
];
|
||||
|
||||
$currentRank = (int) ($rank[$currentStatus] ?? 0);
|
||||
$incomingRank = (int) ($rank[$incomingStatus] ?? 0);
|
||||
|
||||
return $currentRank > 0 && $incomingRank > 0 && $incomingRank < $currentRank;
|
||||
}
|
||||
|
||||
private function getActiveConsentRecord(int $patientId, string $abhaId, string $consentHandle = ''): ?array
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_consent_records')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
$builder = $this->db->table('abdm_consent_records')
|
||||
->where('patient_id', $patientId)
|
||||
->where('abha_id', $abhaId)
|
||||
->where('consent_status', 'approved')
|
||||
->groupStart()
|
||||
->where('expires_at IS NULL', null, false)
|
||||
->orWhere('expires_at >', $now)
|
||||
->groupEnd();
|
||||
|
||||
if ($consentHandle !== '') {
|
||||
$builder->where('consent_handle', $consentHandle);
|
||||
}
|
||||
|
||||
$row = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
|
||||
return ! empty($row) ? $row : null;
|
||||
}
|
||||
}
|
||||
308
app/Controllers/AbdmTaskBoard.php
Normal file
308
app/Controllers/AbdmTaskBoard.php
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\AbdmWorkTaskService;
|
||||
use App\Libraries\BridgeSyncService;
|
||||
|
||||
class AbdmTaskBoard extends BaseController
|
||||
{
|
||||
private AbdmWorkTaskService $taskService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->taskService = new AbdmWorkTaskService();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->backfillPatientAbhaTasks();
|
||||
$this->backfillLabRadiologyTasks();
|
||||
$tasks = $this->taskService->getOpenTasks(300);
|
||||
|
||||
return view('abdm/task_board', [
|
||||
'tasks' => $tasks,
|
||||
]);
|
||||
}
|
||||
|
||||
public function list()
|
||||
{
|
||||
$this->backfillPatientAbhaTasks();
|
||||
$this->backfillLabRadiologyTasks();
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'tasks' => $this->taskService->getOpenTasks(300),
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function markStatus()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$taskId = (int) $this->request->getPost('task_id');
|
||||
$status = trim((string) $this->request->getPost('status'));
|
||||
$note = trim((string) $this->request->getPost('note'));
|
||||
|
||||
if ($taskId <= 0 || $status === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'task_id and status are required']);
|
||||
}
|
||||
|
||||
$ok = $this->taskService->markTaskStatus($taskId, $status, $note);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => $ok ? 1 : 0,
|
||||
'task_id' => $taskId,
|
||||
'status' => $status,
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function performAction()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$taskId = (int) $this->request->getPost('task_id');
|
||||
$action = trim((string) $this->request->getPost('action'));
|
||||
|
||||
$task = $this->taskService->getTask($taskId);
|
||||
if ($task === null) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Task not found']);
|
||||
}
|
||||
|
||||
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
||||
if (! $this->isValidAbhaNumber($abhaId)) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA ID must be a 14-digit number']);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'task_id' => $taskId,
|
||||
'task_code' => (string) ($task['task_code'] ?? ''),
|
||||
'task_type' => (string) ($task['task_type'] ?? ''),
|
||||
'patient_id' => (int) ($task['patient_id'] ?? 0),
|
||||
'patient_name' => (string) ($task['patient_name'] ?? ''),
|
||||
'abha_id' => $abhaId,
|
||||
'entity_type' => (string) ($task['entity_type'] ?? ''),
|
||||
'entity_id' => (string) ($task['entity_id'] ?? ''),
|
||||
'opd_session_id' => (int) $this->request->getPost('opd_session_id'),
|
||||
];
|
||||
|
||||
$eventType = $this->resolveActionEventType($action, (string) ($task['task_type'] ?? ''));
|
||||
if ($eventType === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Unsupported action']);
|
||||
}
|
||||
|
||||
$queueId = null;
|
||||
try {
|
||||
$bridge = new BridgeSyncService();
|
||||
$queueId = $bridge->enqueue($eventType, $payload, 'abdm_task', (string) $taskId);
|
||||
} catch (\Throwable $e) {
|
||||
$this->taskService->markTaskStatus($taskId, 'failed', $e->getMessage());
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Queue failure: ' . $e->getMessage()]);
|
||||
}
|
||||
|
||||
$this->taskService->markTaskStatus($taskId, 'in_progress', 'Action queued: ' . $eventType);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'queue_id' => $queueId,
|
||||
'event_type' => $eventType,
|
||||
'task_id' => $taskId,
|
||||
'status' => 'in_progress',
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveActionEventType(string $action, string $taskType): string
|
||||
{
|
||||
$action = strtolower(trim($action));
|
||||
$taskType = strtolower(trim($taskType));
|
||||
|
||||
if ($action === 'create_abha') {
|
||||
return 'abdm.abha.create.requested';
|
||||
}
|
||||
|
||||
if ($action === 'update_abha') {
|
||||
return 'abdm.abha.update.requested';
|
||||
}
|
||||
|
||||
if ($action === 'submit') {
|
||||
if ($taskType === 'opd_prescription_publish') {
|
||||
return 'abdm.opd.prescription.share.requested';
|
||||
}
|
||||
if ($taskType === 'ipd_admission_publish') {
|
||||
return 'abdm.ipd.admission.share.requested';
|
||||
}
|
||||
if ($taskType === 'ipd_discharge_publish') {
|
||||
return 'abdm.ipd.discharge.share.requested';
|
||||
}
|
||||
if ($taskType === 'lab_report_publish' || $taskType === 'radiology_report_publish') {
|
||||
return 'abdm.diagnosis.report.share.requested';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function isValidAbhaNumber(string $abhaId): bool
|
||||
{
|
||||
return preg_match('/^\d{14}$/', $abhaId) === 1;
|
||||
}
|
||||
|
||||
private function backfillPatientAbhaTasks(): void
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_work_tasks') || ! $this->db->tableExists('patient_master')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$abhaField = $this->resolvePatientAbhaField();
|
||||
if ($abhaField === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = $this->db->table('patient_master')
|
||||
->select('id,p_fname,' . $abhaField)
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(500)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$patientId = (int) ($row['id'] ?? 0);
|
||||
if ($patientId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$abha = trim((string) ($row[$abhaField] ?? ''));
|
||||
if (preg_match('/^\d{14}$/', $abha) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = $this->db->table('abdm_work_tasks')
|
||||
->select('id')
|
||||
->whereIn('task_type', ['patient_abha_create', 'patient_abha_link'])
|
||||
->where('entity_type', 'patient')
|
||||
->where('entity_id', (string) $patientId)
|
||||
->whereIn('status', ['pending', 'in_progress'])
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if (! empty($exists)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->taskService->createOrRefreshTask(
|
||||
'patient_abha_create',
|
||||
'patient_registration',
|
||||
'patient',
|
||||
(string) $patientId,
|
||||
$patientId,
|
||||
trim((string) ($row['p_fname'] ?? '')),
|
||||
$abha,
|
||||
'create_abha',
|
||||
['trigger' => 'task_board.backfill']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function resolvePatientAbhaField(): ?string
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fields = $this->db->getFieldNames('patient_master') ?? [];
|
||||
foreach (['abha_id', 'abha_no', 'abha'] as $field) {
|
||||
if (in_array($field, $fields, true)) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('abha_address', $fields, true)) {
|
||||
return 'abha_address';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function backfillLabRadiologyTasks(): void
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_work_tasks') || ! $this->db->tableExists('lab_request') || ! $this->db->tableExists('patient_master')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
|
||||
$abhaSelectParts = [];
|
||||
foreach (['abha_id', 'abha_no', 'abha_address', 'abha'] as $field) {
|
||||
if (in_array($field, $patientFields, true)) {
|
||||
$abhaSelectParts[] = 'p.' . $field;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($abhaSelectParts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$select = 'r.id, r.patient_id, r.patient_name, r.lab_type, r.charge_id, r.status, ' . implode(', ', $abhaSelectParts);
|
||||
|
||||
$rows = $this->db->table('lab_request r')
|
||||
->select($select)
|
||||
->join('patient_master p', 'p.id = r.patient_id', 'left')
|
||||
->where('r.status >=', 2)
|
||||
->orderBy('r.id', 'DESC')
|
||||
->limit(500)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$labReqId = (int) ($row['id'] ?? 0);
|
||||
$patientId = (int) ($row['patient_id'] ?? 0);
|
||||
if ($labReqId <= 0 || $patientId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$abha = trim((string) ($row['abha_id'] ?? $row['abha_no'] ?? $row['abha_address'] ?? $row['abha'] ?? ''));
|
||||
if (preg_match('/^\d{14}$/', $abha) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$labType = (int) ($row['lab_type'] ?? 0);
|
||||
$taskType = in_array($labType, [1, 2, 3, 4, 6], true) ? 'radiology_report_publish' : 'lab_report_publish';
|
||||
|
||||
$exists = $this->db->table('abdm_work_tasks')
|
||||
->select('id')
|
||||
->where('task_type', $taskType)
|
||||
->where('entity_type', 'lab_request')
|
||||
->where('entity_id', (string) $labReqId)
|
||||
->whereIn('status', ['pending', 'in_progress'])
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($exists)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->taskService->createOrRefreshTask(
|
||||
$taskType,
|
||||
'diagnosis',
|
||||
'lab_request',
|
||||
(string) $labReqId,
|
||||
$patientId,
|
||||
trim((string) ($row['patient_name'] ?? '')),
|
||||
$abha,
|
||||
'submit',
|
||||
[
|
||||
'lab_type' => $labType,
|
||||
'invoice_id' => (int) ($row['charge_id'] ?? 0),
|
||||
'trigger' => 'task_board.backfill',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Controllers\Billing;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\AbdmWorkTaskService;
|
||||
use App\Libraries\BridgeSyncService;
|
||||
use App\Models\BedAssignmentHistoryModel;
|
||||
use App\Models\ItemIpdModel;
|
||||
use App\Models\IpdBillingModel;
|
||||
|
|
@ -280,6 +282,8 @@ class Ipd extends BaseController
|
|||
->where('id', $caseId)
|
||||
->update(['ipd_id' => $insertId]);
|
||||
}
|
||||
|
||||
$this->enqueueIpdAdmissionSync((int) $insertId, 'ipd.admission.created');
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
|
|
@ -583,6 +587,8 @@ class Ipd extends BaseController
|
|||
return $this->response->setStatusCode(500)->setJSON(['update' => 0, 'message' => 'Unable to update admission details']);
|
||||
}
|
||||
|
||||
$this->enqueueIpdAdmissionSync($ipdId, 'ipd.admission.updated');
|
||||
|
||||
$freshPanelData = $this->ipdModel->getIpdPanelInfo($ipdId);
|
||||
$admissionData = $this->buildAdmissionTabData($freshPanelData, $ipdId);
|
||||
|
||||
|
|
@ -2654,6 +2660,88 @@ class Ipd extends BaseController
|
|||
return (int) $this->db->insertID();
|
||||
}
|
||||
|
||||
private function enqueueIpdAdmissionSync(int $ipdId, string $eventType): void
|
||||
{
|
||||
if ($ipdId <= 0 || ! $this->db->tableExists('ipd_master')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ipdRow = $this->db->table('ipd_master')->where('id', $ipdId)->get(1)->getRowArray();
|
||||
if (empty($ipdRow)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$patientId = (int) ($ipdRow['p_id'] ?? 0);
|
||||
$patientRow = [];
|
||||
if ($patientId > 0 && $this->db->tableExists('patient_master')) {
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
|
||||
$abhaId = '';
|
||||
foreach (['abha_id', 'abha_no', 'abha_address', 'abha'] as $field) {
|
||||
if (trim((string) ($patientRow[$field] ?? '')) !== '') {
|
||||
$abhaId = trim((string) ($patientRow[$field] ?? ''));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$doctorIds = [];
|
||||
if ($this->db->tableExists('ipd_doctor')) {
|
||||
$doctorRows = $this->db->table('ipd_doctor')
|
||||
->select('doc_id')
|
||||
->where('ipd_id', $ipdId)
|
||||
->orderBy('id', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($doctorRows as $doctorRow) {
|
||||
$docId = (int) ($doctorRow['doc_id'] ?? 0);
|
||||
if ($docId > 0) {
|
||||
$doctorIds[$docId] = $docId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'ipd_id' => $ipdId,
|
||||
'ipd_code' => trim((string) ($ipdRow['ipd_code'] ?? '')),
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => trim((string) ($patientRow['p_fname'] ?? $ipdRow['P_name'] ?? '')),
|
||||
'abha_id' => $abhaId,
|
||||
'register_date' => trim((string) ($ipdRow['register_date'] ?? '')),
|
||||
'reg_time' => trim((string) ($ipdRow['reg_time'] ?? '')),
|
||||
'discharge_date' => trim((string) ($ipdRow['discharge_date'] ?? '')),
|
||||
'discharge_time' => trim((string) ($ipdRow['discharge_time'] ?? '')),
|
||||
'dept_id' => (int) ($ipdRow['dept_id'] ?? 0),
|
||||
'doctor_ids' => array_values($doctorIds),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
try {
|
||||
$bridgeSync = new BridgeSyncService();
|
||||
$bridgeSync->enqueue($eventType, $payload, 'ipd', (string) $ipdId);
|
||||
} catch (\Throwable $e) {
|
||||
// Do not block IPD admission workflows if queue service is unavailable.
|
||||
}
|
||||
|
||||
if ($eventType === 'ipd.admission.created' && preg_match('/^\d{14}$/', $abhaId) === 1) {
|
||||
$taskService = new AbdmWorkTaskService();
|
||||
$taskService->createOrRefreshTask(
|
||||
'ipd_admission_publish',
|
||||
'ipd',
|
||||
'ipd',
|
||||
(string) $ipdId,
|
||||
$patientId,
|
||||
trim((string) ($patientRow['p_fname'] ?? $ipdRow['P_name'] ?? '')),
|
||||
$abhaId,
|
||||
'submit',
|
||||
[
|
||||
'ipd_id' => $ipdId,
|
||||
'trigger' => 'ipd.admission.created',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function syncNursingChargesToInvoice(int $ipdId): void
|
||||
{
|
||||
if (! $this->db->tableExists('nursing_ipd_charges')) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Controllers;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\AbdmWorkTaskService;
|
||||
use App\Libraries\BridgeSyncService;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Exception;
|
||||
use Mpdf\HTMLParserMode;
|
||||
|
|
@ -1082,11 +1084,16 @@ class Diagnosis extends BaseController
|
|||
}
|
||||
|
||||
$patientRow = $this->db->table('patient_master')
|
||||
->select('gender, age, age_in, age_in_month, dob')
|
||||
->select('gender, age, age_in, age_in_month, dob, abha_id, abha_no, abha_address, abha')
|
||||
->where('id', (int) ($reportRow->patient_id ?? 0))
|
||||
->get()
|
||||
->getRow();
|
||||
|
||||
$reportRow->patient_abha = '';
|
||||
if ($patientRow) {
|
||||
$reportRow->patient_abha = trim((string) ($patientRow->abha_id ?? $patientRow->abha_no ?? $patientRow->abha_address ?? $patientRow->abha ?? ''));
|
||||
}
|
||||
|
||||
$genderRaw = (int) ($patientRow->gender ?? 0);
|
||||
$reportRow->gender_text = $genderRaw === 1 ? 'Male' : ($genderRaw === 2 ? 'Female' : '-');
|
||||
|
||||
|
|
@ -1227,6 +1234,8 @@ class Diagnosis extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
$this->enqueueLabReportSync($labReqId, 'radiology.report.updated');
|
||||
|
||||
return $this->response->setJSON(['status' => 'success', 'message' => 'Saved']);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setJSON(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
|
|
@ -1303,6 +1312,8 @@ class Diagnosis extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
$this->enqueueLabReportSync($labReqId, 'lab.report.updated');
|
||||
|
||||
return $this->response->setJSON(['status' => 'success', 'message' => 'Saved']);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setJSON(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
|
|
@ -1327,6 +1338,8 @@ class Diagnosis extends BaseController
|
|||
'status' => 2,
|
||||
]);
|
||||
|
||||
$this->enqueueLabReportSync($labReqId, 'lab.report.confirmed');
|
||||
|
||||
return $this->response->setJSON(['status' => 'success', 'message' => 'Verified and Ready for Print']);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setJSON(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
|
|
@ -3803,4 +3816,90 @@ class Diagnosis extends BaseController
|
|||
{
|
||||
return $this->selectLabInvoicePath((int) $invoiceId, (int) $labType);
|
||||
}
|
||||
|
||||
private function enqueueLabReportSync(int $labReqId, string $eventType): void
|
||||
{
|
||||
try {
|
||||
if (! $this->db->tableExists('bridge_sync_queue')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$labReq = $this->db->table('lab_request')
|
||||
->select('id, patient_id, patient_name, lab_type, charge_id, status, reported_time')
|
||||
->where('id', $labReqId)
|
||||
->get(1)
|
||||
->getRow();
|
||||
|
||||
if (! $labReq) {
|
||||
return;
|
||||
}
|
||||
|
||||
$patientId = (int) ($labReq->patient_id ?? 0);
|
||||
$abhaId = '';
|
||||
|
||||
if ($patientId > 0) {
|
||||
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
|
||||
$abhaColumns = [];
|
||||
foreach (['abha_id', 'abha_no', 'abha_address', 'abha'] as $field) {
|
||||
if (in_array($field, $patientFields, true)) {
|
||||
$abhaColumns[] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
$patient = null;
|
||||
if (! empty($abhaColumns)) {
|
||||
$patient = $this->db->table('patient_master')
|
||||
->select(implode(',', $abhaColumns))
|
||||
->where('id', $patientId)
|
||||
->get(1)
|
||||
->getRow();
|
||||
}
|
||||
|
||||
if ($patient) {
|
||||
$abhaId = trim((string) ($patient->abha_id ?? $patient->abha_no ?? $patient->abha_address ?? $patient->abha ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'lab_req_id' => $labReqId,
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => (string) ($labReq->patient_name ?? ''),
|
||||
'abha_id' => $abhaId,
|
||||
'lab_type' => (int) ($labReq->lab_type ?? 0),
|
||||
'invoice_id' => (int) ($labReq->charge_id ?? 0),
|
||||
'report_status' => (int) ($labReq->status ?? 0),
|
||||
'reported_time' => (string) ($labReq->reported_time ?? ''),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$bridgeSync = new BridgeSyncService();
|
||||
$bridgeSync->enqueue($eventType, $payload, 'lab_request', (string) $labReqId);
|
||||
|
||||
// Create ABDM work tasks for verified/finalized reports so staff can submit them.
|
||||
if ((int) ($labReq->status ?? 0) >= 2 && preg_match('/^\d{14}$/', $abhaId) === 1) {
|
||||
$taskType = $this->isRadiologyType((int) ($labReq->lab_type ?? 0))
|
||||
? 'radiology_report_publish'
|
||||
: 'lab_report_publish';
|
||||
|
||||
$taskService = new AbdmWorkTaskService();
|
||||
$taskService->createOrRefreshTask(
|
||||
$taskType,
|
||||
'diagnosis',
|
||||
'lab_request',
|
||||
(string) $labReqId,
|
||||
$patientId,
|
||||
(string) ($labReq->patient_name ?? ''),
|
||||
$abhaId,
|
||||
'submit',
|
||||
[
|
||||
'lab_type' => (int) ($labReq->lab_type ?? 0),
|
||||
'invoice_id' => (int) ($labReq->charge_id ?? 0),
|
||||
'trigger' => $eventType,
|
||||
]
|
||||
);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', '[ABDM] enqueueLabReportSync failed for lab_req ' . $labReqId . ': ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Controllers;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\AbdmWorkTaskService;
|
||||
use App\Libraries\BridgeSyncService;
|
||||
use App\Models\IpdBillingModel;
|
||||
use Mpdf\Mpdf;
|
||||
use Mpdf\Output\Destination;
|
||||
|
|
@ -3296,6 +3298,10 @@ class Ipd_discharge extends BaseController
|
|||
$savedAny = $this->upsertByIpd('ipd_discharge_drug_food_interaction', $ipdId, $legacyData) || $savedAny;
|
||||
}
|
||||
|
||||
if ($savedAny) {
|
||||
$this->enqueueIpdDischargeSync($ipdId, $patientId, 'dietary_autosave');
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'update' => $savedAny ? 1 : 0,
|
||||
'error_text' => $savedAny ? 'Dietary advice saved.' : 'Unable to save dietary advice.',
|
||||
|
|
@ -4165,6 +4171,10 @@ class Ipd_discharge extends BaseController
|
|||
$noticeType = 'warning';
|
||||
}
|
||||
|
||||
if ($savedAny) {
|
||||
$this->enqueueIpdDischargeSync($ipdId, $patientId, $action);
|
||||
}
|
||||
|
||||
$ajaxMode = strtolower(trim((string) ($this->request->getPost('ajax_mode') ?? '')));
|
||||
if ($ajaxMode === 'json') {
|
||||
return $this->response->setJSON([
|
||||
|
|
@ -4415,6 +4425,8 @@ class Ipd_discharge extends BaseController
|
|||
$renderedHtml = (string) ($templatePack['rendered_html'] ?? $content);
|
||||
$templateName = (string) ($templatePack['selected_template_name'] ?? 'Discharge Template');
|
||||
|
||||
$this->createIpdDischargeWorkTask($ipdId, $panelData);
|
||||
|
||||
try {
|
||||
$patient = $panelData['person_info'] ?? null;
|
||||
$ipd = $panelData['ipd_info'] ?? null;
|
||||
|
|
@ -4478,6 +4490,47 @@ class Ipd_discharge extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
private function createIpdDischargeWorkTask(int $ipdId, array $panelData): void
|
||||
{
|
||||
$person = $panelData['person_info'] ?? null;
|
||||
if (! $person) {
|
||||
return;
|
||||
}
|
||||
|
||||
$patientId = (int) ($person->id ?? $person->p_id ?? 0);
|
||||
if ($patientId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$abhaId = trim((string) (
|
||||
$person->abha_id
|
||||
?? $person->abha_no
|
||||
?? $person->abha_address
|
||||
?? $person->abha
|
||||
?? ''
|
||||
));
|
||||
|
||||
if (preg_match('/^\d{14}$/', $abhaId) !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$taskService = new AbdmWorkTaskService();
|
||||
$taskService->createOrRefreshTask(
|
||||
'ipd_discharge_publish',
|
||||
'ipd_discharge',
|
||||
'ipd',
|
||||
(string) $ipdId,
|
||||
$patientId,
|
||||
trim((string) ($person->p_fname ?? '')),
|
||||
$abhaId,
|
||||
'submit',
|
||||
[
|
||||
'ipd_id' => $ipdId,
|
||||
'trigger' => 'ipd.discharge.printed',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function buildDischargePdfHtml(array $panelData, string $renderedContent, bool $withHeader, string $templateName): string
|
||||
{
|
||||
$ipd = $panelData['ipd_info'] ?? null;
|
||||
|
|
@ -4541,4 +4594,65 @@ class Ipd_discharge extends BaseController
|
|||
{
|
||||
return $this->show_discharge($ipdId, 3);
|
||||
}
|
||||
|
||||
private function enqueueIpdDischargeSync(int $ipdId, int $patientId, string $action): void
|
||||
{
|
||||
if ($ipdId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ipdRow = $this->db->tableExists('ipd_master')
|
||||
? ($this->db->table('ipd_master')->where('id', $ipdId)->get(1)->getRowArray() ?? [])
|
||||
: [];
|
||||
|
||||
if ($patientId <= 0) {
|
||||
$patientId = (int) ($ipdRow['p_id'] ?? 0);
|
||||
}
|
||||
|
||||
$patientRow = [];
|
||||
if ($patientId > 0 && $this->db->tableExists('patient_master')) {
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
|
||||
$abhaId = '';
|
||||
foreach (['abha_id', 'abha_no', 'abha_address', 'abha'] as $field) {
|
||||
$candidate = trim((string) ($patientRow[$field] ?? ''));
|
||||
if ($candidate !== '') {
|
||||
$abhaId = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$dischargeContent = '';
|
||||
if ($this->db->tableExists('ipd_discharge') && $this->db->fieldExists('content', 'ipd_discharge')) {
|
||||
$dischargeRow = $this->db->table('ipd_discharge')
|
||||
->select('content')
|
||||
->where('ipd_id', $ipdId)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
$dischargeContent = trim((string) ($dischargeRow['content'] ?? ''));
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'ipd_id' => $ipdId,
|
||||
'patient_id' => $patientId,
|
||||
'ipd_code' => trim((string) ($ipdRow['ipd_code'] ?? '')),
|
||||
'patient_name' => trim((string) ($patientRow['p_fname'] ?? $ipdRow['P_name'] ?? '')),
|
||||
'abha_id' => $abhaId,
|
||||
'action' => trim($action) !== '' ? trim($action) : 'save_main',
|
||||
'discharge_date' => trim((string) ($ipdRow['discharge_date'] ?? '')),
|
||||
'discharge_time' => trim((string) ($ipdRow['discharge_time'] ?? '')),
|
||||
'discharge_status' => trim((string) ($ipdRow['discarge_patient_status'] ?? '')),
|
||||
'summary_html' => $dischargeContent,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
try {
|
||||
$bridgeSync = new BridgeSyncService();
|
||||
$bridgeSync->enqueue('ipd.discharge.updated', $payload, 'ipd_discharge', (string) $ipdId);
|
||||
} catch (\Throwable $e) {
|
||||
// Do not block discharge workflows if queue service is unavailable.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,13 +86,60 @@ class Opd extends BaseController
|
|||
$content = $this->readOpdTemplateContentByName($name);
|
||||
|
||||
$placeholders = [
|
||||
'hospital_name', 'hospital_address', 'hospital_phone', 'hospital_email', 'doctor_name', 'print_time',
|
||||
// Hospital
|
||||
'hospital_name', 'hospital_address', 'hospital_phone', 'hospital_email',
|
||||
'H_Name', 'H_address_1', 'H_address_2', 'H_phone_No', 'H_Email', 'H_logo',
|
||||
// Doctor
|
||||
'doctor_name', 'doctor_full_name', 'doctor_title',
|
||||
'doctor_reg_no', 'doctor_registration',
|
||||
'doctor_phone', 'doctor_email', 'doctor_address',
|
||||
'doctor_specialization', 'SpecName',
|
||||
'short_description', 'doctor_short_description',
|
||||
'pName', 'pRelative', 'age_sex', 'phoneno', 'p_address', 'uhid_no', 'opd_sr_no', 'opd_no',
|
||||
'opd_date', 'exp_date', 'SpecName', 'opd_fee_desc', 'total_no_visit', 'last_opdvisit_date', 'str_opd_book_date',
|
||||
'Complaint', 'diagnosis', 'Provisional_diagnosis', 'Finding_Examinations', 'medical', 'investigation',
|
||||
'Prescriber_Remarks', 'advice', 'next_visit', 'refer_to', 'vital_content', 'painscale_img',
|
||||
'hospital_section', 'patient_section', 'content_section',
|
||||
'doctor_sign_html',
|
||||
// Print metadata
|
||||
'print_time', 'current_date',
|
||||
// Patient
|
||||
'pName', 'pRelative', 'age_sex', 'phoneno', 'p_address',
|
||||
'uhid_no', 'opd_sr_no', 'opd_no', 'opd_date', 'exp_date',
|
||||
'opd_fee_desc', 'total_no_visit', 'last_opdvisit_date', 'str_opd_book_date',
|
||||
// General Examination (raw values)
|
||||
'bp', 'bp_diastolic', 'pulse', 'temp', 'spo2', 'rr_min',
|
||||
'height', 'weight', 'waist',
|
||||
'pallor', 'icterus', 'cyanosis', 'clubbing', 'edema',
|
||||
// General Examination (formatted blocks)
|
||||
'vital_content', // BP/Pulse/Temp/SpO2/RR/Ht/Wt/Waist formatted
|
||||
'vital_content_extra', // Pallor/Icterus/Cyanosis/Clubbing/Edema
|
||||
'vital_content_full', // All vitals + extra combined
|
||||
'general_examination', // Alias of vital_content_full
|
||||
// Pain scale
|
||||
'pain_value', // 0-4
|
||||
'pain_label', // e.g. "Mild Pain"
|
||||
'pain_scale', // Formatted block: Pain Scale : Mild Pain
|
||||
// Complication & Addiction
|
||||
'Complication', 'complication', // Formatted block of checked flags
|
||||
'Addiction', 'addiction', // Formatted block of checked flags
|
||||
// Drug allergy / NABH
|
||||
'drug_allergy_status', 'drug_allergy_details',
|
||||
'adr_history', 'current_medications',
|
||||
'drug_allergy_block', // Formatted: Drug Allergy : status — details
|
||||
'adr_history_block', // Formatted: ADR History : ...
|
||||
'current_medications_block', // Formatted: Current Medications : ...
|
||||
// Women related
|
||||
'women_lmp', 'women_last_baby', 'women_pregnancy_related', 'women_related_problems',
|
||||
'women_block', // Formatted: Women Related : LMP | Last Baby | ...
|
||||
// Co-morbidities
|
||||
'morbidities', // Raw comma-separated list
|
||||
'morbidities_block', // Formatted: Co-Morbidities : Diabetes, HTn, ...
|
||||
// Clinical content
|
||||
'painscale_img',
|
||||
'Complaint', 'diagnosis', 'Provisional_diagnosis', 'Finding_Examinations',
|
||||
'medical', 'investigation', 'Prescriber_Remarks', 'advice', 'next_visit', 'refer_to',
|
||||
// Composed blocks
|
||||
'Rx', 'RxTable', 'RxFullBlock',
|
||||
'VitalsBlock', 'ComplaintBlock', 'DiagnosisBlock', 'InvestigationBlock',
|
||||
'RemarksBlock', 'AdviceBlock', 'NextVisitBlock',
|
||||
// Legacy layout sections
|
||||
'hospital_section', 'patient_section', 'content_section', 'content',
|
||||
];
|
||||
|
||||
return view('billing/opd_template_builder', [
|
||||
|
|
@ -1402,7 +1449,16 @@ class Opd extends BaseController
|
|||
} else {
|
||||
$customHtml = '';
|
||||
if ($templateKey !== '') {
|
||||
// 1) Try DB-stored template first
|
||||
$customHtml = $this->readStoredOpdTemplateContent($templateKey);
|
||||
|
||||
// 2) Fall back to filesystem HTML file in opd_templates/
|
||||
if ($customHtml === '') {
|
||||
$fsPath = APPPATH . 'Views/billing/opd_templates/' . $templateKey . '.html';
|
||||
if (is_file($fsPath)) {
|
||||
$customHtml = (string) file_get_contents($fsPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($customHtml !== '') {
|
||||
|
|
@ -1869,23 +1925,49 @@ class Opd extends BaseController
|
|||
$generic = trim((string) ($med['genericname'] ?? $med['generic_name'] ?? $med['salt_name'] ?? ''));
|
||||
$dose = trim((string) ($med['dosage_label'] ?? $med['dosage'] ?? $med['drug_dose'] ?? $med['dose'] ?? ''));
|
||||
$freq = trim((string) ($med['dosage_freq_label'] ?? $med['dosage_freq'] ?? $med['drug_freq'] ?? $med['frequency'] ?? ''));
|
||||
$doseLocalRaw = trim((string) ($med['dosage_local_label'] ?? $med['dosage_local'] ?? ''));
|
||||
$freqLocalRaw = trim((string) ($med['dosage_freq_local_label'] ?? $med['dosage_freq_local'] ?? ''));
|
||||
$days = trim((string) ($med['no_of_days'] ?? $med['drug_day'] ?? $med['days'] ?? ''));
|
||||
$when = trim((string) ($med['dosage_when_label'] ?? $med['dosage_when'] ?? ''));
|
||||
$remark = trim((string) ($med['remark'] ?? ''));
|
||||
$inst = trim(($when !== '' ? $when : '') . ($remark !== '' ? ($when !== '' ? ' | ' : '') . $remark : ''));
|
||||
$doseFreqText = trim($dose . ' ' . $freq);
|
||||
|
||||
// Hindi for freq/schedule:
|
||||
// 1) exact match against doseShedHindiMap (DB: opd_dose_shed)
|
||||
// 2) extract "ABBR (SIGN)" part up to closing ')' — handles "OD (ONE TIME A DAY) Daily"
|
||||
// 3) exact match in doseFreqHindiMap (DB: opd_dose_frequency)
|
||||
// 4) translated fallback
|
||||
$freqKey = strtolower(trim($freq));
|
||||
$doseFreqLocal = $doseShedHindiMap[$freqKey] ?? $doseFreqHindiMap[$freqKey] ?? '';
|
||||
if ($doseFreqLocal === '' && preg_match('/^(.+?\))/u', $freq, $_m) === 1) {
|
||||
$signKey = strtolower(trim($_m[1]));
|
||||
$doseFreqLocal = $doseShedHindiMap[$signKey] ?? '';
|
||||
// Hindi for dose + frequency:
|
||||
// Prefer pre-resolved local labels from buildOpdLetterPrintData(), then resolve via maps.
|
||||
$doseLocal = $doseLocalRaw;
|
||||
if ($doseLocal === '' && $dose !== '') {
|
||||
$doseKey = strtolower(trim($dose));
|
||||
$doseLocal = $doseShedHindiMap[$doseKey] ?? '';
|
||||
if ($doseLocal === '' && preg_match('/^(.+?\))/u', $dose, $_m) === 1) {
|
||||
$doseSignKey = strtolower(trim($_m[1]));
|
||||
$doseLocal = $doseShedHindiMap[$doseSignKey] ?? '';
|
||||
}
|
||||
if ($doseLocal === '') {
|
||||
$doseFallback = $this->translateToLocalPatientText($dose);
|
||||
if (strtolower($doseFallback) !== strtolower($dose)) {
|
||||
$doseLocal = $doseFallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$freqLocal = $freqLocalRaw;
|
||||
if ($freqLocal === '' && $freq !== '') {
|
||||
$freqKey = strtolower(trim($freq));
|
||||
$freqLocal = $doseShedHindiMap[$freqKey] ?? $doseFreqHindiMap[$freqKey] ?? '';
|
||||
if ($freqLocal === '' && preg_match('/^(.+?\))/u', $freq, $_m) === 1) {
|
||||
$signKey = strtolower(trim($_m[1]));
|
||||
$freqLocal = $doseShedHindiMap[$signKey] ?? '';
|
||||
}
|
||||
if ($freqLocal === '') {
|
||||
$freqFallback = $this->translateToLocalPatientText($freq);
|
||||
if (strtolower($freqFallback) !== strtolower($freq)) {
|
||||
$freqLocal = $freqFallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$doseFreqLocal = trim($doseLocal . (($doseLocal !== '' && $freqLocal !== '') ? ' ' : '') . $freqLocal);
|
||||
if ($doseFreqLocal === '' && $doseFreqText !== '') {
|
||||
$fallback = $this->translateToLocalPatientText($doseFreqText);
|
||||
if (strtolower($fallback) !== strtolower($doseFreqText)) {
|
||||
|
|
@ -2015,6 +2097,15 @@ class Opd extends BaseController
|
|||
$pulse = trim((string) ($rx['pulse'] ?? ''));
|
||||
$temp = trim((string) ($rx['temp'] ?? ''));
|
||||
$spo2 = trim((string) ($rx['spo2'] ?? ''));
|
||||
$rrMin = trim((string) ($rx['rr_min'] ?? ''));
|
||||
$height = trim((string) ($rx['height'] ?? ''));
|
||||
$weight = trim((string) ($rx['weight'] ?? ''));
|
||||
$waist = trim((string) ($rx['waist'] ?? ''));
|
||||
$pallor = trim((string) ($rx['pallor'] ?? ''));
|
||||
$icterus = trim((string) ($rx['icterus'] ?? $rx['Icterus'] ?? ''));
|
||||
$cyanosis = trim((string) ($rx['cyanosis'] ?? ''));
|
||||
$clubbing = trim((string) ($rx['clubbing'] ?? ''));
|
||||
$edema = trim((string) ($rx['edema'] ?? ''));
|
||||
|
||||
$vitals = [];
|
||||
if ($bp !== '') {
|
||||
|
|
@ -2029,6 +2120,54 @@ class Opd extends BaseController
|
|||
if ($spo2 !== '') {
|
||||
$vitals[] = 'SpO2: ' . $spo2;
|
||||
}
|
||||
if ($rrMin !== '') {
|
||||
$vitals[] = 'RR: ' . $rrMin . '/min';
|
||||
}
|
||||
if ($height !== '') {
|
||||
$vitals[] = 'Ht: ' . $height;
|
||||
}
|
||||
if ($weight !== '') {
|
||||
$vitals[] = 'Wt: ' . $weight;
|
||||
}
|
||||
if ($waist !== '') {
|
||||
$vitals[] = 'Waist: ' . $waist;
|
||||
}
|
||||
|
||||
// ── Pain scale ──────────────────────────────────────────────────────
|
||||
$painValue = trim((string) ($rx['pain_value'] ?? ''));
|
||||
$painLabels = ['0' => 'No Pain', '1' => 'Mild Pain', '2' => 'Moderate', '3' => 'Intense', '4' => 'Worst Pain Possible'];
|
||||
$painLabel = $painValue !== '' ? ($painLabels[$painValue] ?? '') : '';
|
||||
|
||||
// ── Complication flags ───────────────────────────────────────────────
|
||||
$complicationFlags = [];
|
||||
if (!empty($rx['pregnancy'])) { $complicationFlags[] = 'Pregnancy'; }
|
||||
if (!empty($rx['lactation'])) { $complicationFlags[] = 'Lactation'; }
|
||||
if (!empty($rx['liver_insufficiency'])) { $complicationFlags[] = 'Liver Insufficiency'; }
|
||||
if (!empty($rx['renal_insufficiency'])) { $complicationFlags[] = 'Renal Insufficiency'; }
|
||||
if (!empty($rx['pulmonary_insufficiency'])) { $complicationFlags[] = 'Pulmonary Insufficiency'; }
|
||||
if (!empty($rx['corona_suspected'])) { $complicationFlags[] = 'Corona Suspected'; }
|
||||
if (!empty($rx['dengue'])) { $complicationFlags[] = 'Dengue'; }
|
||||
$complicationText = implode(', ', $complicationFlags);
|
||||
|
||||
// ── Addiction flags (from patient_master or rx) ──────────────────────
|
||||
$pRowCopy = $data['patient_master'][0] ?? null;
|
||||
$addictionFlags = [];
|
||||
if (!empty($rx['is_smoking'] ?? $pRowCopy->is_smoking ?? 0)) { $addictionFlags[] = 'Smoking'; }
|
||||
if (!empty($rx['is_alcohol'] ?? $pRowCopy->is_alcohol ?? 0)) { $addictionFlags[] = 'Alcohol'; }
|
||||
if (!empty($rx['is_drug_abuse'] ?? $pRowCopy->is_drug_abuse ?? 0)) { $addictionFlags[] = 'Drug Abuse'; }
|
||||
$addictionText = implode(', ', $addictionFlags);
|
||||
|
||||
// ── Drug allergy / NABH ──────────────────────────────────────────────
|
||||
$drugAllergyStatus = trim((string) ($rx['drug_allergy_status'] ?? ''));
|
||||
$drugAllergyDetails = trim((string) ($rx['drug_allergy_details'] ?? ''));
|
||||
$adrHistory = trim((string) ($rx['adr_history'] ?? ''));
|
||||
$currentMedications = trim((string) ($rx['current_medications'] ?? ''));
|
||||
|
||||
// ── Women related ────────────────────────────────────────────────────
|
||||
$womenLmp = trim((string) ($rx['women_lmp'] ?? ''));
|
||||
$womenLastBaby = trim((string) ($rx['women_last_baby'] ?? ''));
|
||||
$womenPregnancyRelated = trim((string) ($rx['women_pregnancy_related'] ?? ''));
|
||||
$womenRelatedProblems = trim((string) ($rx['women_related_problems'] ?? ''));
|
||||
|
||||
$printContent = '<table width="100%" border="0" style="font-size:10pt;">'
|
||||
. '<tr>'
|
||||
|
|
@ -2098,6 +2237,41 @@ class Opd extends BaseController
|
|||
'doctor' => '<p style="text-align:right;">Dr. ' . esc((string) ($opd->doc_name ?? '')) . '</p>',
|
||||
'top_content' => '',
|
||||
'vital_content' => implode(' | ', $vitals),
|
||||
'vital_content_extra' => implode(' | ', array_filter([
|
||||
$pallor !== '' ? 'Pallor: ' . $pallor : '',
|
||||
$icterus !== '' ? 'Icterus: ' . $icterus : '',
|
||||
$cyanosis !== '' ? 'Cyanosis: ' . $cyanosis : '',
|
||||
$clubbing !== '' ? 'Clubbing: ' . $clubbing : '',
|
||||
$edema !== '' ? 'Edema: ' . $edema : '',
|
||||
], static fn ($v): bool => $v !== '')),
|
||||
'height' => $height,
|
||||
'weight' => $weight,
|
||||
'waist' => $waist,
|
||||
'rr_min' => $rrMin,
|
||||
'pallor' => $pallor,
|
||||
'icterus' => $icterus,
|
||||
'cyanosis' => $cyanosis,
|
||||
'clubbing' => $clubbing,
|
||||
'edema' => $edema,
|
||||
'bp' => $bp,
|
||||
'bp_diastolic' => $diastolic,
|
||||
'pulse' => $pulse,
|
||||
'temp' => $temp,
|
||||
'spo2' => $spo2,
|
||||
'pain_value' => $painValue,
|
||||
'pain_label' => $painLabel,
|
||||
'Complication' => $complicationText,
|
||||
'complication' => $complicationText,
|
||||
'Addiction' => $addictionText,
|
||||
'addiction' => $addictionText,
|
||||
'drug_allergy_status' => $drugAllergyStatus,
|
||||
'drug_allergy_details' => $drugAllergyDetails,
|
||||
'adr_history' => $adrHistory,
|
||||
'current_medications' => $currentMedications,
|
||||
'women_lmp' => $womenLmp,
|
||||
'women_last_baby' => $womenLastBaby,
|
||||
'women_pregnancy_related' => $womenPregnancyRelated,
|
||||
'women_related_problems' => $womenRelatedProblems,
|
||||
'Finding_Examinations' => $findingExaminationsText,
|
||||
'finding_examinations' => $findingExaminationsText,
|
||||
'Prescriber_Remarks' => $prescriberRemarksText,
|
||||
|
|
@ -2105,11 +2279,9 @@ class Opd extends BaseController
|
|||
'advice_local' => $rxAdviceLocal,
|
||||
'next_visit' => $nextVisitText,
|
||||
'refer_to' => (string) ($rx['refer_to'] ?? ''),
|
||||
'painscale' => '',
|
||||
'painscale' => $painLabel,
|
||||
'painscale_img' => '',
|
||||
'morbidities' => implode(', ', is_array($data['selected_morbidities'] ?? null) ? ($data['selected_morbidities'] ?? []) : []),
|
||||
'Addiction' => '',
|
||||
'Complication' => '',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -2611,6 +2783,51 @@ class Opd extends BaseController
|
|||
}
|
||||
$tokens['next_visit'] = $formatBlock('Next Visit', (string) ($tokens['next_visit_raw'] ?? ''));
|
||||
|
||||
// ── General Examination extras (formatted) ────────────────────────────
|
||||
$vitalRaw = (string) ($tokens['vital_content_raw'] ?? '');
|
||||
$vitalExtraRaw = trim((string) ($tokens['vital_content_extra'] ?? ''));
|
||||
$vitalFull = trim($vitalRaw . ($vitalExtraRaw !== '' ? ' | ' . $vitalExtraRaw : ''));
|
||||
$tokens['vital_content_full'] = $formatBlock('Vitals', $vitalFull);
|
||||
$tokens['general_examination'] = $tokens['vital_content_full'];
|
||||
|
||||
// ── Pain scale (formatted) ────────────────────────────────────────────
|
||||
$painLabel = trim((string) ($tokens['pain_label'] ?? ''));
|
||||
$tokens['pain_scale'] = $painLabel !== '' ? $formatBlock('Pain Scale', $painLabel) : '';
|
||||
|
||||
// ── Complication & Addiction (formatted) ──────────────────────────────
|
||||
$tokens['Complication'] = $formatBlock('Complication', (string) ($tokens['complication'] ?? ''));
|
||||
$tokens['complication'] = $tokens['Complication'];
|
||||
$tokens['Addiction'] = $formatBlock('Addiction', (string) ($tokens['addiction'] ?? ''));
|
||||
$tokens['addiction'] = $tokens['Addiction'];
|
||||
|
||||
// ── Drug allergy / NABH (formatted) ──────────────────────────────────
|
||||
$allergyStatus = trim((string) ($tokens['drug_allergy_status'] ?? ''));
|
||||
$allergyDetails = trim((string) ($tokens['drug_allergy_details'] ?? ''));
|
||||
$adrHistory = trim((string) ($tokens['adr_history'] ?? ''));
|
||||
$currentMeds = trim((string) ($tokens['current_medications'] ?? ''));
|
||||
|
||||
$allergyBlock = $allergyStatus !== '' ? esc($allergyStatus) : '';
|
||||
if ($allergyDetails !== '') {
|
||||
$allergyBlock .= ($allergyBlock !== '' ? ' — ' : '') . esc($allergyDetails);
|
||||
}
|
||||
$tokens['drug_allergy_block'] = $allergyBlock !== '' ? $formatBlock('Drug Allergy', $allergyBlock) : '';
|
||||
$tokens['adr_history_block'] = $adrHistory !== '' ? $formatBlock('ADR History', esc($adrHistory)) : '';
|
||||
$tokens['current_medications_block'] = $currentMeds !== '' ? $formatBlock('Current Medications', esc($currentMeds)) : '';
|
||||
|
||||
// ── Women related (formatted) ─────────────────────────────────────────
|
||||
$womenParts = [];
|
||||
$womenLmpRaw = trim((string) ($tokens['women_lmp'] ?? ''));
|
||||
if ($womenLmpRaw !== '') { $womenParts[] = 'LMP: ' . esc($womenLmpRaw) . ' days ago'; }
|
||||
$womenLastBabyRaw = trim((string) ($tokens['women_last_baby'] ?? ''));
|
||||
if ($womenLastBabyRaw !== '') { $womenParts[] = 'Last Baby: ' . esc($womenLastBabyRaw); }
|
||||
$womenPregRaw = trim((string) ($tokens['women_pregnancy_related'] ?? ''));
|
||||
if ($womenPregRaw !== '') { $womenParts[] = esc($womenPregRaw); }
|
||||
$womenBlock = implode(' | ', $womenParts);
|
||||
$tokens['women_block'] = $womenBlock !== '' ? $formatBlock('Women Related', $womenBlock) : '';
|
||||
|
||||
// ── Morbidities (formatted) ───────────────────────────────────────────
|
||||
$tokens['morbidities_block'] = $formatBlock('Co-Morbidities', (string) ($tokens['morbidities'] ?? ''));
|
||||
|
||||
$medicalHtml = trim((string) ($tokens['medical'] ?? ''));
|
||||
$tokens['Rx'] = $medicalHtml !== ''
|
||||
? '<div style="margin:8px 0 6px 0;"><div style="font-weight:700;font-size:24px;line-height:1.2;margin-bottom:8px;">Rx :</div>' . $medicalHtml . '</div>'
|
||||
|
|
@ -2707,6 +2924,49 @@ class Opd extends BaseController
|
|||
? ('<br><strong>सलाह:</strong> ' . $adviceLocal)
|
||||
: '';
|
||||
|
||||
// ── Doctor master tokens ──────────────────────────────────────────────
|
||||
$docRow = $data['doctor_master'][0] ?? null;
|
||||
$docTitle = trim((string) ($docRow->p_title ?? 'Dr.'));
|
||||
$docFname = trim((string) ($docRow->p_fname ?? ''));
|
||||
$docMname = trim((string) ($docRow->p_mname ?? ''));
|
||||
$docLname = trim((string) ($docRow->p_lname ?? ''));
|
||||
$docFullName = trim(trim($docTitle . ' ' . $docFname . ' ' . $docMname) . ' ' . $docLname);
|
||||
$docRegNo = trim((string) ($docRow->doctor_reg_no ?? ''));
|
||||
$docPhone = trim((string) ($docRow->mphone1 ?? ''));
|
||||
$docEmail = trim((string) ($docRow->email1 ?? ''));
|
||||
$docSpec = trim((string) ($docRow->SpecName ?? $tokens['SpecName'] ?? ''));
|
||||
$docAdd1 = trim((string) ($docRow->add1 ?? ''));
|
||||
$docAdd2 = trim((string) ($docRow->add2 ?? ''));
|
||||
$docCity = trim((string) ($docRow->city ?? ''));
|
||||
$docAddress = trim(trim($docAdd1 . ', ' . $docAdd2, ', ') . (($docCity !== '') ? ', ' . $docCity : ''), ', ');
|
||||
|
||||
$tokens['doctor_title'] = esc($docTitle);
|
||||
$tokens['doctor_full_name'] = esc($docFullName);
|
||||
$tokens['doctor_reg_no'] = esc($docRegNo);
|
||||
$tokens['doctor_registration'] = esc($docRegNo);
|
||||
$tokens['doctor_phone'] = esc($docPhone);
|
||||
$tokens['doctor_email'] = esc($docEmail);
|
||||
$tokens['doctor_specialization'] = esc($docSpec);
|
||||
$tokens['doctor_address'] = esc($docAddress);
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// ── Composed section tokens (available in flat/custom templates too) ─
|
||||
// hospital_section : hospital name + address + phone/email block
|
||||
$hospitalSectionParts = array_filter([
|
||||
$hName !== '' ? '<strong>' . esc($hName) . '</strong>' : '',
|
||||
trim($hAddress1 . ', ' . $hAddress2, ', ') !== '' ? esc(trim($hAddress1 . ', ' . $hAddress2, ', ')) : '',
|
||||
$hPhone !== '' ? 'Ph: ' . esc($hPhone) : '',
|
||||
$hEmail !== '' ? 'Email: ' . esc($hEmail) : '',
|
||||
], static fn ($v): bool => $v !== '');
|
||||
$tokens['hospital_section'] = implode('<br>', $hospitalSectionParts);
|
||||
|
||||
// patient_section : reuse the existing patient info table ({{content}})
|
||||
$tokens['patient_section'] = (string) ($tokens['content'] ?? '');
|
||||
|
||||
// content_section : all clinical blocks combined (same as {{RxFullBlock}})
|
||||
$tokens['content_section'] = (string) ($tokens['RxFullBlock'] ?? '');
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Controllers;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\AbdmWorkTaskService;
|
||||
use App\Libraries\BridgeSyncService;
|
||||
use App\Models\OpdMedicineModel;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
|
@ -7712,6 +7713,8 @@ OPD SNAPSHOT JSON: " . $payload;
|
|||
|
||||
public function opd_prescription_print(int $opdId, int $opdSessionId, int $printType = 0)
|
||||
{
|
||||
$this->createOpdPrescriptionWorkTask($opdId, $opdSessionId);
|
||||
|
||||
$printConfig = $this->resolvePrescriptionLayoutByDoctorField($opdId, $printType);
|
||||
$layoutMode = (string) ($printConfig['layout'] ?? 'content_only');
|
||||
$templateKey = (string) ($printConfig['template'] ?? '');
|
||||
|
|
@ -7786,4 +7789,54 @@ OPD SNAPSHOT JSON: " . $payload;
|
|||
|
||||
return ['layout' => $fallback, 'template' => ''];
|
||||
}
|
||||
|
||||
private function createOpdPrescriptionWorkTask(int $opdId, int $opdSessionId): void
|
||||
{
|
||||
if ($opdId <= 0 || ! $this->db->tableExists('opd_master') || ! $this->db->tableExists('patient_master')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$opdRow = $this->db->table('opd_master')->select('opd_id,p_id')->where('opd_id', $opdId)->get(1)->getRowArray();
|
||||
if (empty($opdRow)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$patientId = (int) ($opdRow['p_id'] ?? 0);
|
||||
if ($patientId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$patient = $this->db->table('patient_master')
|
||||
->select('p_fname,abha_id,abha_no,abha_address,abha')
|
||||
->where('id', $patientId)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
$abhaId = trim((string) ($patient['abha_id'] ?? $patient['abha_no'] ?? $patient['abha_address'] ?? $patient['abha'] ?? ''));
|
||||
if (! $this->isValidAbhaNumber($abhaId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$taskService = new AbdmWorkTaskService();
|
||||
$taskService->createOrRefreshTask(
|
||||
'opd_prescription_publish',
|
||||
'opd_prescription',
|
||||
'opd',
|
||||
(string) $opdId,
|
||||
$patientId,
|
||||
trim((string) ($patient['p_fname'] ?? '')),
|
||||
$abhaId,
|
||||
'submit',
|
||||
[
|
||||
'opd_id' => $opdId,
|
||||
'opd_session_id' => $opdSessionId,
|
||||
'trigger' => 'opd.prescription.printed',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function isValidAbhaNumber(string $abhaId): bool
|
||||
{
|
||||
return preg_match('/^\d{14}$/', trim($abhaId)) === 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Controllers;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\AbdmWorkTaskService;
|
||||
use App\Libraries\BridgeSyncService;
|
||||
use App\Models\BloodGroupModel;
|
||||
use App\Models\PatientModel;
|
||||
|
||||
|
|
@ -139,6 +141,9 @@ class Patient extends BaseController
|
|||
(string) $this->request->getPost('input_relative_name'),
|
||||
]);
|
||||
|
||||
$this->enqueuePatientAbhaSync($insertId, $data, 'patient.created');
|
||||
$this->createPatientAbdmWorkTask($insertId, $data, $abhaId);
|
||||
|
||||
// Check Multiple UHID
|
||||
$relativeName = trim(strtoupper((string) $this->request->getPost('input_relative_name')));
|
||||
$patientName = trim(strtoupper((string) $this->request->getPost('input_name')));
|
||||
|
|
@ -220,6 +225,56 @@ class Patient extends BaseController
|
|||
return view('billing/search_adv_data', ['search_result' => $searchResult]);
|
||||
}
|
||||
|
||||
public function abha_fetch_profile()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
||||
if (! $this->isValidAbhaId($abhaId)) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA ID must be a 14-digit number']);
|
||||
}
|
||||
|
||||
$profile = [];
|
||||
$abhaField = $this->resolvePatientAbhaIdField();
|
||||
if ($abhaField !== null) {
|
||||
$row = $this->db->table('patient_master')
|
||||
->select('id,p_fname,gender,dob,mphone1,city,state')
|
||||
->where($abhaField, $abhaId)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if (! empty($row)) {
|
||||
$profile = [
|
||||
'name' => (string) ($row['p_fname'] ?? ''),
|
||||
'gender' => (string) ($row['gender'] ?? ''),
|
||||
'dob' => (string) ($row['dob'] ?? ''),
|
||||
'mobile' => (string) ($row['mphone1'] ?? ''),
|
||||
'city' => (string) ($row['city'] ?? ''),
|
||||
'state' => (string) ($row['state'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$queueId = null;
|
||||
try {
|
||||
$bridge = new BridgeSyncService();
|
||||
$queueId = $bridge->enqueue('abdm.abha.profile.fetch.requested', [
|
||||
'abha_id' => $abhaId,
|
||||
'requested_at' => date('Y-m-d H:i:s'),
|
||||
], 'abha', $abhaId);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'queue_id' => $queueId,
|
||||
'profile' => $profile,
|
||||
'source' => ! empty($profile) ? 'local_cache' : 'abdm_queue',
|
||||
]);
|
||||
}
|
||||
|
||||
public function search()
|
||||
{
|
||||
$sdata = (string) $this->request->getPost('txtsearch');
|
||||
|
|
@ -467,6 +522,8 @@ class Patient extends BaseController
|
|||
(string) $this->request->getPost('input_relative_name'),
|
||||
]);
|
||||
|
||||
$this->enqueuePatientAbhaSync($pid, $data, 'patient.updated');
|
||||
|
||||
return $this->response->setJSON([
|
||||
'update' => 1,
|
||||
'showcontent' => 'Data Saved successfully',
|
||||
|
|
@ -522,6 +579,7 @@ class Patient extends BaseController
|
|||
|
||||
$patientModel = new PatientModel();
|
||||
$patientModel->updatePatient($data, $pid);
|
||||
$this->enqueuePatientAbhaSync($pid, $data, 'patient.abha.updated');
|
||||
|
||||
return $this->response->setJSON([
|
||||
'update' => 1,
|
||||
|
|
@ -1063,5 +1121,70 @@ class Patient extends BaseController
|
|||
return preg_match('/^\d{14}$/', $value) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $patientData
|
||||
*/
|
||||
private function enqueuePatientAbhaSync(int $patientId, array $patientData, string $eventType): void
|
||||
{
|
||||
if ($patientId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$abhaField = $this->resolvePatientAbhaIdField();
|
||||
$abhaId = $abhaField !== null ? trim((string) ($patientData[$abhaField] ?? '')) : '';
|
||||
|
||||
$payload = [
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'name' => trim((string) ($patientData['p_fname'] ?? '')),
|
||||
'mobile' => trim((string) ($patientData['mphone1'] ?? '')),
|
||||
'gender' => trim((string) ($patientData['gender'] ?? '')),
|
||||
'dob' => trim((string) ($patientData['dob'] ?? '')),
|
||||
'city' => trim((string) ($patientData['city'] ?? '')),
|
||||
'state' => trim((string) ($patientData['state'] ?? '')),
|
||||
];
|
||||
|
||||
try {
|
||||
$bridgeSync = new BridgeSyncService();
|
||||
$bridgeSync->enqueue(
|
||||
$eventType,
|
||||
$payload,
|
||||
'patient',
|
||||
(string) $patientId
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
// Do not block patient workflows if queue service is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $patientData
|
||||
*/
|
||||
private function createPatientAbdmWorkTask(int $patientId, array $patientData, string $abhaId): void
|
||||
{
|
||||
if ($patientId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$taskService = new AbdmWorkTaskService();
|
||||
$patientName = trim((string) ($patientData['p_fname'] ?? ''));
|
||||
$actionMode = $abhaId !== '' ? 'update_abha' : 'create_abha';
|
||||
$taskType = $abhaId !== '' ? 'patient_abha_update' : 'patient_abha_create';
|
||||
|
||||
$taskService->createOrRefreshTask(
|
||||
$taskType,
|
||||
'patient_registration',
|
||||
'patient',
|
||||
(string) $patientId,
|
||||
$patientId,
|
||||
$patientName,
|
||||
$abhaId,
|
||||
$actionMode,
|
||||
[
|
||||
'trigger' => 'patient.created',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,361 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateAbdmCoreTables extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->createAbdmApiLogsTable();
|
||||
$this->createAbdmConsentRecordsTable();
|
||||
$this->createAbdmWorkTasksTable();
|
||||
$this->createNhcxClaimDocumentsTable();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('nhcx_claim_documents', true);
|
||||
$this->forge->dropTable('abdm_work_tasks', true);
|
||||
$this->forge->dropTable('abdm_consent_records', true);
|
||||
$this->forge->dropTable('abdm_api_logs', true);
|
||||
}
|
||||
|
||||
private function createAbdmApiLogsTable(): void
|
||||
{
|
||||
if ($this->hasTable('abdm_api_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'channel' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'default' => 'bridge',
|
||||
],
|
||||
'event_type' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => true,
|
||||
],
|
||||
'endpoint' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'http_method' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 16,
|
||||
'null' => true,
|
||||
],
|
||||
'entity_type' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => true,
|
||||
],
|
||||
'entity_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 80,
|
||||
'null' => true,
|
||||
],
|
||||
'request_json' => [
|
||||
'type' => 'LONGTEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'response_code' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
],
|
||||
'response_json' => [
|
||||
'type' => 'LONGTEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 20,
|
||||
'default' => 'pending',
|
||||
],
|
||||
'error_message' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addKey('channel');
|
||||
$this->forge->addKey('status');
|
||||
$this->forge->addKey('event_type');
|
||||
$this->forge->createTable('abdm_api_logs', true);
|
||||
}
|
||||
|
||||
private function createAbdmConsentRecordsTable(): void
|
||||
{
|
||||
if ($this->hasTable('abdm_consent_records')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'patient_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'abha_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 30,
|
||||
'null' => true,
|
||||
],
|
||||
'consent_handle' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => false,
|
||||
],
|
||||
'consent_status' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 30,
|
||||
'default' => 'requested',
|
||||
],
|
||||
'purpose_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 60,
|
||||
'null' => true,
|
||||
],
|
||||
'requested_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'granted_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'expires_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'raw_payload_json' => [
|
||||
'type' => 'LONGTEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addKey('patient_id');
|
||||
$this->forge->addKey('abha_id');
|
||||
$this->forge->addKey('consent_handle');
|
||||
$this->forge->addKey('consent_status');
|
||||
$this->forge->createTable('abdm_consent_records', true);
|
||||
}
|
||||
|
||||
private function createNhcxClaimDocumentsTable(): void
|
||||
{
|
||||
if ($this->hasTable('nhcx_claim_documents')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'ipd_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'case_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'patient_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'claim_type' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 60,
|
||||
'default' => 'institutional',
|
||||
],
|
||||
'claim_json' => [
|
||||
'type' => 'LONGTEXT',
|
||||
'null' => false,
|
||||
],
|
||||
'claim_status' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 30,
|
||||
'default' => 'draft',
|
||||
],
|
||||
'external_ref' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => true,
|
||||
],
|
||||
'error_message' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'pushed_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addKey('ipd_id');
|
||||
$this->forge->addKey('case_id');
|
||||
$this->forge->addKey('patient_id');
|
||||
$this->forge->addKey('claim_status');
|
||||
$this->forge->createTable('nhcx_claim_documents', true);
|
||||
}
|
||||
|
||||
private function createAbdmWorkTasksTable(): void
|
||||
{
|
||||
if ($this->hasTable('abdm_work_tasks')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'task_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 40,
|
||||
'null' => false,
|
||||
],
|
||||
'task_type' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 60,
|
||||
'null' => false,
|
||||
],
|
||||
'source_module' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 40,
|
||||
'null' => true,
|
||||
],
|
||||
'entity_type' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 40,
|
||||
'null' => true,
|
||||
],
|
||||
'entity_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 40,
|
||||
'null' => true,
|
||||
],
|
||||
'patient_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'patient_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => true,
|
||||
],
|
||||
'abha_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 60,
|
||||
'null' => true,
|
||||
],
|
||||
'action_mode' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 40,
|
||||
'null' => true,
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 20,
|
||||
'default' => 'pending',
|
||||
],
|
||||
'priority' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 20,
|
||||
'default' => 'normal',
|
||||
],
|
||||
'payload_json' => [
|
||||
'type' => 'LONGTEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'last_action_result' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'completed_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey('task_code');
|
||||
$this->forge->addKey('task_type');
|
||||
$this->forge->addKey('status');
|
||||
$this->forge->addKey('patient_id');
|
||||
$this->forge->addKey(['entity_type', 'entity_id']);
|
||||
$this->forge->createTable('abdm_work_tasks', true);
|
||||
}
|
||||
|
||||
private function hasTable(string $table): bool
|
||||
{
|
||||
if (! preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = $this->db->query("SHOW TABLES LIKE '" . $table . "'")->getRowArray();
|
||||
return ! empty($row);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddAbdmWorkTasksTable extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if ($this->hasTable('abdm_work_tasks')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'task_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 40,
|
||||
'null' => false,
|
||||
],
|
||||
'task_type' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 60,
|
||||
'null' => false,
|
||||
],
|
||||
'source_module' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 40,
|
||||
'null' => true,
|
||||
],
|
||||
'entity_type' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 40,
|
||||
'null' => true,
|
||||
],
|
||||
'entity_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 40,
|
||||
'null' => true,
|
||||
],
|
||||
'patient_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'patient_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => true,
|
||||
],
|
||||
'abha_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 60,
|
||||
'null' => true,
|
||||
],
|
||||
'action_mode' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 40,
|
||||
'null' => true,
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 20,
|
||||
'default' => 'pending',
|
||||
],
|
||||
'priority' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 20,
|
||||
'default' => 'normal',
|
||||
],
|
||||
'payload_json' => [
|
||||
'type' => 'LONGTEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'last_action_result' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'completed_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey('task_code');
|
||||
$this->forge->addKey('task_type');
|
||||
$this->forge->addKey('status');
|
||||
$this->forge->addKey('patient_id');
|
||||
$this->forge->addKey(['entity_type', 'entity_id']);
|
||||
$this->forge->createTable('abdm_work_tasks', true);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('abdm_work_tasks', true);
|
||||
}
|
||||
|
||||
private function hasTable(string $table): bool
|
||||
{
|
||||
if (! preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = $this->db->query("SHOW TABLES LIKE '" . $table . "'")->getRowArray();
|
||||
return ! empty($row);
|
||||
}
|
||||
}
|
||||
223
app/Libraries/AbdmWorkTaskService.php
Normal file
223
app/Libraries/AbdmWorkTaskService.php
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class AbdmWorkTaskService
|
||||
{
|
||||
private BaseConnection $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = db_connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $meta
|
||||
*/
|
||||
public function createOrRefreshTask(
|
||||
string $taskType,
|
||||
string $sourceModule,
|
||||
string $entityType,
|
||||
string $entityId,
|
||||
int $patientId,
|
||||
string $patientName,
|
||||
string $abhaId,
|
||||
string $actionMode,
|
||||
array $meta = []
|
||||
): ?int {
|
||||
if (! $this->db->tableExists('abdm_work_tasks')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entityId = trim($entityId);
|
||||
if ($taskType === '' || $entityType === '' || $entityId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
$payload = [
|
||||
'task_type' => $taskType,
|
||||
'source_module' => $sourceModule,
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityId,
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => $patientName,
|
||||
'abha_id' => $abhaId,
|
||||
'action_mode' => $actionMode,
|
||||
'meta' => $meta,
|
||||
];
|
||||
|
||||
$existing = $this->db->table('abdm_work_tasks')
|
||||
->where('task_type', $taskType)
|
||||
->where('entity_type', $entityType)
|
||||
->where('entity_id', $entityId)
|
||||
->whereIn('status', ['pending', 'in_progress'])
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
$row = [
|
||||
'source_module' => $sourceModule,
|
||||
'patient_id' => $patientId > 0 ? $patientId : null,
|
||||
'patient_name' => $patientName !== '' ? $patientName : null,
|
||||
'abha_id' => $abhaId !== '' ? $abhaId : null,
|
||||
'action_mode' => $actionMode !== '' ? $actionMode : null,
|
||||
'payload_json' => (string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if (! empty($existing)) {
|
||||
$this->db->table('abdm_work_tasks')->where('id', (int) $existing['id'])->update($row);
|
||||
return (int) $existing['id'];
|
||||
}
|
||||
|
||||
$row['task_code'] = $this->generateTaskCode();
|
||||
$row['task_type'] = $taskType;
|
||||
$row['entity_type'] = $entityType;
|
||||
$row['entity_id'] = $entityId;
|
||||
$row['status'] = 'pending';
|
||||
$row['priority'] = 'normal';
|
||||
$row['created_at'] = $now;
|
||||
|
||||
$this->db->table('abdm_work_tasks')->insert($row);
|
||||
$insertId = (int) $this->db->insertID();
|
||||
|
||||
return $insertId > 0 ? $insertId : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getOpenTasks(int $limit = 200): array
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_work_tasks')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->syncPatientAbhaBacklog(max(200, $limit));
|
||||
|
||||
return $this->db->table('abdm_work_tasks')
|
||||
->whereIn('status', ['pending', 'in_progress'])
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(max(1, $limit))
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
public function markTaskStatus(int $taskId, string $status, string $resultText = ''): bool
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_work_tasks') || $taskId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$status = strtolower(trim($status));
|
||||
if (! in_array($status, ['pending', 'in_progress', 'completed', 'failed', 'cancelled'], true)) {
|
||||
$status = 'pending';
|
||||
}
|
||||
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
$row = [
|
||||
'status' => $status,
|
||||
'last_action_result' => $resultText !== '' ? mb_substr($resultText, 0, 1000) : null,
|
||||
'updated_at' => $now,
|
||||
'completed_at' => in_array($status, ['completed', 'cancelled'], true) ? $now : null,
|
||||
];
|
||||
|
||||
return (bool) $this->db->table('abdm_work_tasks')->where('id', $taskId)->update($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function getTask(int $taskId): ?array
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_work_tasks') || $taskId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $this->db->table('abdm_work_tasks')->where('id', $taskId)->get(1)->getRowArray();
|
||||
return ! empty($row) ? $row : null;
|
||||
}
|
||||
|
||||
private function generateTaskCode(): string
|
||||
{
|
||||
return 'ABDM-' . date('YmdHis') . '-' . strtoupper(bin2hex(random_bytes(3)));
|
||||
}
|
||||
|
||||
private function syncPatientAbhaBacklog(int $limit): void
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$abhaField = $this->resolvePatientAbhaField();
|
||||
if ($abhaField === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$select = 'id, p_fname';
|
||||
if ($abhaField !== '') {
|
||||
$select .= ', ' . $abhaField;
|
||||
}
|
||||
|
||||
$rows = $this->db->table('patient_master')
|
||||
->select($select)
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(max(1, $limit))
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($rows)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$patientId = (int) ($row['id'] ?? 0);
|
||||
if ($patientId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$abhaId = trim((string) ($row[$abhaField] ?? ''));
|
||||
if (preg_match('/^\d{14}$/', $abhaId) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->createOrRefreshTask(
|
||||
'patient_abha_create',
|
||||
'patient_registration',
|
||||
'patient',
|
||||
(string) $patientId,
|
||||
$patientId,
|
||||
trim((string) ($row['p_fname'] ?? '')),
|
||||
$abhaId,
|
||||
'create_abha',
|
||||
[
|
||||
'trigger' => 'patient.backlog.sync',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function resolvePatientAbhaField(): ?string
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fields = $this->db->getFieldNames('patient_master') ?? [];
|
||||
foreach (['abha_id', 'abha_no', 'abha'] as $field) {
|
||||
if (in_array($field, $fields, true)) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('abha_address', $fields, true)) {
|
||||
return 'abha_address';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -66,18 +66,6 @@ class BridgeSyncService
|
|||
return $summary;
|
||||
}
|
||||
|
||||
$endpoint = $this->readSetting('BRIDGE_SYNC_URL');
|
||||
if ($endpoint === '') {
|
||||
$summary['message'] = 'BRIDGE_SYNC_URL is not configured';
|
||||
return $summary;
|
||||
}
|
||||
|
||||
$token = $this->readSetting('BRIDGE_SYNC_TOKEN');
|
||||
$source = $this->readSetting('BRIDGE_SOURCE_CODE');
|
||||
if ($source === '') {
|
||||
$source = (string) ($this->readSetting('HOSPITAL_CODE') ?: 'hms-local');
|
||||
}
|
||||
|
||||
$rows = $this->db->table('bridge_sync_queue')
|
||||
->whereIn('status', ['pending', 'retry'])
|
||||
->groupStart()
|
||||
|
|
@ -134,23 +122,33 @@ class BridgeSyncService
|
|||
$payload = ['raw' => (string) ($row['payload_json'] ?? '')];
|
||||
}
|
||||
|
||||
$postBody = [
|
||||
'queue_id' => $queueId,
|
||||
'source' => $source,
|
||||
'event_type' => (string) ($row['event_type'] ?? ''),
|
||||
'entity_type' => (string) ($row['entity_type'] ?? ''),
|
||||
'entity_id' => (string) ($row['entity_id'] ?? ''),
|
||||
'payload' => $payload,
|
||||
'occurred_at' => (string) ($row['created_at'] ?? Time::now('Asia/Kolkata')->toDateTimeString()),
|
||||
];
|
||||
$dispatch = $this->buildDispatchContext($row, $payload);
|
||||
if (($dispatch['ok'] ?? false) !== true) {
|
||||
$nextAttempts = $attempts + 1;
|
||||
$isFinalFailure = $nextAttempts >= $maxAttempts;
|
||||
$delaySeconds = min(3600, (int) pow(2, min(10, $nextAttempts)) * 30);
|
||||
$reason = trim((string) ($dispatch['error'] ?? 'No dispatch configuration'));
|
||||
|
||||
$headers = ['Content-Type' => 'application/json'];
|
||||
if ($token !== '') {
|
||||
$headers['Authorization'] = 'Bearer ' . $token;
|
||||
$this->db->table('bridge_sync_queue')->where('id', $queueId)->update([
|
||||
'status' => $isFinalFailure ? 'failed' : 'retry',
|
||||
'attempts' => $nextAttempts,
|
||||
'next_attempt_at' => $isFinalFailure ? null : date('Y-m-d H:i:s', time() + $delaySeconds),
|
||||
'last_error' => mb_substr($reason, 0, 500),
|
||||
'locked_at' => null,
|
||||
'locked_by' => null,
|
||||
]);
|
||||
$summary['failed']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$channel = (string) ($dispatch['channel'] ?? 'bridge');
|
||||
$endpoint = (string) ($dispatch['endpoint'] ?? '');
|
||||
$method = strtoupper((string) ($dispatch['method'] ?? 'POST'));
|
||||
$headers = (array) ($dispatch['headers'] ?? ['Content-Type' => 'application/json']);
|
||||
$postBody = (array) ($dispatch['body'] ?? []);
|
||||
|
||||
try {
|
||||
$response = $client->post($endpoint, [
|
||||
$response = $client->request($method, $endpoint, [
|
||||
'headers' => $headers,
|
||||
'json' => $postBody,
|
||||
]);
|
||||
|
|
@ -158,6 +156,20 @@ class BridgeSyncService
|
|||
$status = $response->getStatusCode();
|
||||
$body = (string) $response->getBody();
|
||||
|
||||
$this->logApiCall(
|
||||
$channel,
|
||||
(string) ($row['event_type'] ?? ''),
|
||||
$endpoint,
|
||||
$method,
|
||||
(string) ($row['entity_type'] ?? ''),
|
||||
(string) ($row['entity_id'] ?? ''),
|
||||
$postBody,
|
||||
$status,
|
||||
$body,
|
||||
$status >= 200 && $status < 300 ? 'success' : 'error',
|
||||
$status >= 200 && $status < 300 ? '' : ('HTTP ' . $status)
|
||||
);
|
||||
|
||||
if ($status >= 200 && $status < 300) {
|
||||
$this->db->table('bridge_sync_queue')->where('id', $queueId)->update([
|
||||
'status' => 'sent',
|
||||
|
|
@ -184,6 +196,20 @@ class BridgeSyncService
|
|||
]);
|
||||
$summary['failed']++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logApiCall(
|
||||
$channel,
|
||||
(string) ($row['event_type'] ?? ''),
|
||||
$endpoint,
|
||||
$method,
|
||||
(string) ($row['entity_type'] ?? ''),
|
||||
(string) ($row['entity_id'] ?? ''),
|
||||
$postBody,
|
||||
null,
|
||||
'',
|
||||
'error',
|
||||
$e->getMessage()
|
||||
);
|
||||
|
||||
$nextAttempts = $attempts + 1;
|
||||
$isFinalFailure = $nextAttempts >= $maxAttempts;
|
||||
$delaySeconds = min(3600, (int) pow(2, min(10, $nextAttempts)) * 30);
|
||||
|
|
@ -204,8 +230,58 @@ class BridgeSyncService
|
|||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $requestPayload
|
||||
*/
|
||||
private function logApiCall(
|
||||
string $channel,
|
||||
string $eventType,
|
||||
string $endpoint,
|
||||
string $method,
|
||||
string $entityType,
|
||||
string $entityId,
|
||||
array $requestPayload,
|
||||
?int $responseCode,
|
||||
string $responseBody,
|
||||
string $status,
|
||||
string $errorMessage
|
||||
): void {
|
||||
if (! $this->db->tableExists('abdm_api_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$requestJson = (string) json_encode($requestPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$decodedBody = json_decode($responseBody, true);
|
||||
$responseJson = is_array($decodedBody)
|
||||
? (string) json_encode($decodedBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
: trim($responseBody);
|
||||
|
||||
$this->db->table('abdm_api_logs')->insert([
|
||||
'channel' => $channel !== '' ? $channel : 'bridge',
|
||||
'event_type' => $eventType,
|
||||
'endpoint' => $endpoint,
|
||||
'http_method' => strtoupper($method),
|
||||
'entity_type' => $entityType !== '' ? $entityType : null,
|
||||
'entity_id' => $entityId !== '' ? $entityId : null,
|
||||
'request_json' => $requestJson,
|
||||
'response_code' => $responseCode,
|
||||
'response_json' => $responseJson !== '' ? $responseJson : null,
|
||||
'status' => $status,
|
||||
'error_message' => $errorMessage !== '' ? mb_substr($errorMessage, 0, 1000) : null,
|
||||
'created_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function readSetting(string $name): string
|
||||
{
|
||||
$envValue = getenv($name);
|
||||
if ($envValue !== false) {
|
||||
$value = trim((string) $envValue);
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (defined($name)) {
|
||||
$value = trim((string) constant($name));
|
||||
if ($value !== '') {
|
||||
|
|
@ -225,4 +301,163 @@ class BridgeSyncService
|
|||
|
||||
return trim((string) ($row['s_value'] ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildDispatchContext(array $row, array $payload): array
|
||||
{
|
||||
$eventType = (string) ($row['event_type'] ?? '');
|
||||
if ($eventType === '') {
|
||||
return ['ok' => false, 'error' => 'Missing event_type'];
|
||||
}
|
||||
|
||||
$isAbdmOrNhcx = str_starts_with($eventType, 'abdm.') || str_starts_with($eventType, 'nhcx.');
|
||||
$provider = strtolower($this->readSetting('ABDM_SYNC_PROVIDER'));
|
||||
if ($provider === '' || ! $isAbdmOrNhcx) {
|
||||
$provider = strtolower($this->readSetting('BRIDGE_SYNC_PROVIDER'));
|
||||
}
|
||||
if ($provider === '') {
|
||||
$provider = 'bridge';
|
||||
}
|
||||
|
||||
if ($provider === 'eka' && $isAbdmOrNhcx) {
|
||||
return $this->buildEkaDispatchContext($row, $payload);
|
||||
}
|
||||
|
||||
return $this->buildBridgeDispatchContext($row, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildBridgeDispatchContext(array $row, array $payload): array
|
||||
{
|
||||
$endpoint = $this->readSetting('BRIDGE_SYNC_URL');
|
||||
if ($endpoint === '') {
|
||||
return ['ok' => false, 'error' => 'BRIDGE_SYNC_URL is not configured'];
|
||||
}
|
||||
|
||||
$token = $this->readSetting('BRIDGE_SYNC_TOKEN');
|
||||
$source = $this->readSetting('BRIDGE_SOURCE_CODE');
|
||||
if ($source === '') {
|
||||
$source = (string) ($this->readSetting('HOSPITAL_CODE') ?: 'hms-local');
|
||||
}
|
||||
|
||||
$headers = ['Content-Type' => 'application/json'];
|
||||
if ($token !== '') {
|
||||
$headers['Authorization'] = 'Bearer ' . $token;
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'channel' => 'bridge',
|
||||
'endpoint' => $endpoint,
|
||||
'method' => 'POST',
|
||||
'headers' => $headers,
|
||||
'body' => [
|
||||
'queue_id' => (int) ($row['id'] ?? 0),
|
||||
'source' => $source,
|
||||
'event_type' => (string) ($row['event_type'] ?? ''),
|
||||
'entity_type' => (string) ($row['entity_type'] ?? ''),
|
||||
'entity_id' => (string) ($row['entity_id'] ?? ''),
|
||||
'payload' => $payload,
|
||||
'occurred_at' => (string) ($row['created_at'] ?? Time::now('Asia/Kolkata')->toDateTimeString()),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildEkaDispatchContext(array $row, array $payload): array
|
||||
{
|
||||
$baseUrl = rtrim($this->readSetting('EKA_BASE_URL'), '/');
|
||||
if ($baseUrl === '') {
|
||||
return ['ok' => false, 'error' => 'EKA_BASE_URL is not configured'];
|
||||
}
|
||||
|
||||
$eventType = (string) ($row['event_type'] ?? '');
|
||||
$eventPath = $this->resolveEkaEventPath($eventType);
|
||||
if ($eventPath === '') {
|
||||
return ['ok' => false, 'error' => 'No Eka endpoint mapping found for event ' . $eventType];
|
||||
}
|
||||
|
||||
$token = $this->readSetting('EKA_BEARER_TOKEN');
|
||||
$apiKey = $this->readSetting('EKA_API_KEY');
|
||||
$clientId = $this->readSetting('EKA_CLIENT_ID');
|
||||
|
||||
$headers = ['Content-Type' => 'application/json'];
|
||||
if ($token !== '') {
|
||||
$headers['Authorization'] = 'Bearer ' . $token;
|
||||
} elseif ($apiKey !== '') {
|
||||
$headers['x-api-key'] = $apiKey;
|
||||
}
|
||||
if ($clientId !== '') {
|
||||
$headers['x-client-id'] = $clientId;
|
||||
}
|
||||
|
||||
$source = (string) ($this->readSetting('HOSPITAL_CODE') ?: 'hms-local');
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'channel' => 'eka',
|
||||
'endpoint' => $baseUrl . $eventPath,
|
||||
'method' => 'POST',
|
||||
'headers' => $headers,
|
||||
'body' => [
|
||||
'queue_id' => (int) ($row['id'] ?? 0),
|
||||
'source' => $source,
|
||||
'event_type' => $eventType,
|
||||
'entity_type' => (string) ($row['entity_type'] ?? ''),
|
||||
'entity_id' => (string) ($row['entity_id'] ?? ''),
|
||||
'occurred_at' => (string) ($row['created_at'] ?? Time::now('Asia/Kolkata')->toDateTimeString()),
|
||||
'payload' => $payload,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveEkaEventPath(string $eventType): string
|
||||
{
|
||||
$mapping = [
|
||||
'abdm.abha.validate' => '/api/v1/abdm/abha/validate',
|
||||
'abdm.abha.profile.fetch.requested' => '/api/v1/abdm/abha/profile',
|
||||
'abdm.abha.create.requested' => '/api/v1/abdm/abha/create',
|
||||
'abdm.abha.update.requested' => '/api/v1/abdm/abha/update',
|
||||
'abdm.scan_share.lookup' => '/api/v1/abdm/scan-share/lookup',
|
||||
'abdm.consent.requested' => '/api/v1/abdm/consents/request',
|
||||
'abdm.fhir.share.requested' => '/api/v1/abdm/health-information/share',
|
||||
'abdm.opd.prescription.share.requested' => '/api/v1/abdm/health-information/share',
|
||||
'abdm.ipd.admission.share.requested' => '/api/v1/abdm/health-information/ipd-admission/share',
|
||||
'abdm.ipd.discharge.share.requested' => '/api/v1/abdm/health-information/ipd-discharge/share',
|
||||
'abdm.diagnosis.report.share.requested' => '/api/v1/abdm/health-information/diagnosis-report/share',
|
||||
'nhcx.claim.created' => '/api/v1/nhcx/claims/create',
|
||||
'nhcx.claim.status.requested' => '/api/v1/nhcx/claims/status',
|
||||
];
|
||||
|
||||
$jsonMap = trim($this->readSetting('EKA_EVENT_ENDPOINTS_JSON'));
|
||||
if ($jsonMap !== '') {
|
||||
$decoded = json_decode($jsonMap, true);
|
||||
if (is_array($decoded)) {
|
||||
foreach ($decoded as $key => $value) {
|
||||
if (is_string($key) && is_string($value) && $key !== '' && $value !== '') {
|
||||
$mapping[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$path = trim((string) ($mapping[$eventType] ?? ''));
|
||||
if ($path === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,95 @@ class FhirR4Builder
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $patient
|
||||
* @param array<string, mixed> $encounter
|
||||
* @param array<string, mixed> $claim
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildClaimBundle(array $patient, array $encounter, array $claim): array
|
||||
{
|
||||
$issuedAt = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
$patientRef = 'Patient/' . (string) ($patient['id'] ?? 'unknown');
|
||||
$encounterRef = 'Encounter/' . (string) ($encounter['id'] ?? 'unknown');
|
||||
|
||||
$claimId = (string) ($claim['id'] ?? ('claim-' . time()));
|
||||
$items = is_array($claim['items'] ?? null) ? ($claim['items'] ?? []) : [];
|
||||
|
||||
$claimItems = [];
|
||||
foreach ($items as $index => $item) {
|
||||
if (! is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$claimItems[] = [
|
||||
'sequence' => $index + 1,
|
||||
'productOrService' => [
|
||||
'text' => (string) ($item['name'] ?? $item['description'] ?? ''),
|
||||
],
|
||||
'quantity' => [
|
||||
'value' => (float) ($item['qty'] ?? 1),
|
||||
],
|
||||
'unitPrice' => [
|
||||
'value' => (float) ($item['unit_price'] ?? 0),
|
||||
'currency' => (string) ($item['currency'] ?? 'INR'),
|
||||
],
|
||||
'net' => [
|
||||
'value' => (float) ($item['amount'] ?? 0),
|
||||
'currency' => (string) ($item['currency'] ?? 'INR'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$claimResource = [
|
||||
'resourceType' => 'Claim',
|
||||
'id' => $claimId,
|
||||
'status' => (string) ($claim['status'] ?? 'active'),
|
||||
'use' => (string) ($claim['use'] ?? 'claim'),
|
||||
'type' => [
|
||||
'text' => (string) ($claim['type'] ?? 'institutional'),
|
||||
],
|
||||
'patient' => ['reference' => $patientRef],
|
||||
'created' => $issuedAt,
|
||||
'provider' => [
|
||||
'display' => (string) ($claim['provider'] ?? ''),
|
||||
],
|
||||
'insurer' => [
|
||||
'display' => (string) ($claim['insurer'] ?? ''),
|
||||
],
|
||||
'priority' => [
|
||||
'text' => (string) ($claim['priority'] ?? 'normal'),
|
||||
],
|
||||
'item' => $claimItems,
|
||||
'total' => [
|
||||
'value' => (float) ($claim['total'] ?? 0),
|
||||
'currency' => (string) ($claim['currency'] ?? 'INR'),
|
||||
],
|
||||
'encounter' => [[
|
||||
'reference' => $encounterRef,
|
||||
]],
|
||||
];
|
||||
|
||||
return [
|
||||
'resourceType' => 'Bundle',
|
||||
'type' => 'collection',
|
||||
'timestamp' => $issuedAt,
|
||||
'entry' => [[
|
||||
'resource' => $this->buildPatientResource($patient),
|
||||
], [
|
||||
'resource' => [
|
||||
'resourceType' => 'Encounter',
|
||||
'id' => (string) ($encounter['id'] ?? 'unknown'),
|
||||
'status' => (string) ($encounter['status'] ?? 'finished'),
|
||||
'subject' => ['reference' => $patientRef],
|
||||
],
|
||||
], [
|
||||
'resource' => $claimResource,
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $patient
|
||||
*
|
||||
|
|
|
|||
269
app/Views/abdm/task_board.php
Normal file
269
app/Views/abdm/task_board.php
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ABDM Work Task Board</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background: #f5f7fb; }
|
||||
.container-wrap { max-width: 1280px; margin: 24px auto; }
|
||||
.table td, .table th { vertical-align: middle; }
|
||||
.status-pill { text-transform: uppercase; font-size: 11px; letter-spacing: .4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-wrap px-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-0">ABDM Work Task Board</h4>
|
||||
<div class="small text-muted">Unupdated ABHA Patient List + Separate ABDM Task Queues</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="btnRefresh">Refresh</button>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-3" id="taskFilters">
|
||||
<button class="btn btn-sm btn-primary filter-btn" data-filter="all">All</button>
|
||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="patient_abha_create">Unupdated ABHA</button>
|
||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="patient_abha_update">ABHA Update</button>
|
||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="opd_prescription_publish">OPD Publish</button>
|
||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="lab_report_publish">Lab Reports</button>
|
||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="radiology_report_publish">Radiology Reports</button>
|
||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="ipd_admission_publish">IPD Admission</button>
|
||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="ipd_discharge_publish">IPD Discharge</button>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped mb-0" id="taskTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Task</th>
|
||||
<th>Patient</th>
|
||||
<th>ABHA</th>
|
||||
<th>Entity</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach (($tasks ?? []) as $t): ?>
|
||||
<tr data-task-id="<?= (int) ($t['id'] ?? 0) ?>" data-task-type="<?= esc((string) ($t['task_type'] ?? '')) ?>" data-entity-id="<?= esc((string) ($t['entity_id'] ?? '')) ?>">
|
||||
<td><?= (int) ($t['id'] ?? 0) ?></td>
|
||||
<td>
|
||||
<div><strong><?= esc((string) ($t['task_code'] ?? '')) ?></strong></div>
|
||||
<div class="text-muted small"><?= esc((string) ($t['task_type'] ?? '')) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><?= esc((string) ($t['patient_name'] ?? '')) ?></div>
|
||||
<div class="text-muted small">#<?= (int) ($t['patient_id'] ?? 0) ?></div>
|
||||
</td>
|
||||
<td><input type="text" class="form-control form-control-sm abha-input" value="<?= esc((string) ($t['abha_id'] ?? '')) ?>" placeholder="14-digit ABHA"></td>
|
||||
<td>
|
||||
<div><?= esc((string) ($t['entity_type'] ?? '')) ?></div>
|
||||
<div class="text-muted small"><?= esc((string) ($t['entity_id'] ?? '')) ?></div>
|
||||
</td>
|
||||
<td><span class="badge bg-secondary status-pill"><?= esc((string) ($t['status'] ?? 'pending')) ?></span></td>
|
||||
<td>
|
||||
<div class="d-flex gap-1 flex-wrap">
|
||||
<?php $type = strtolower((string) ($t['task_type'] ?? '')); ?>
|
||||
<?php if ($type === 'patient_abha_create' || $type === 'patient_abha_link'): ?>
|
||||
<button class="btn btn-sm btn-outline-success action-btn" data-action="create_abha">Create ABHA</button>
|
||||
<?php elseif ($type === 'patient_abha_update'): ?>
|
||||
<button class="btn btn-sm btn-outline-primary action-btn" data-action="update_abha">Update ABHA</button>
|
||||
<?php else: ?>
|
||||
<button class="btn btn-sm btn-outline-warning action-btn" data-action="submit">Submit</button>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-sm btn-outline-dark close-btn">Close</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="small text-muted mt-2" id="statusBox">Ready</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="taskActionModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">ABDM Guided Action</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2 small text-muted" id="modalTaskSummary"></div>
|
||||
<label class="form-label">ABHA Number</label>
|
||||
<input type="text" class="form-control" id="modalAbhaInput" maxlength="14" placeholder="Enter 14-digit ABHA">
|
||||
<div class="form-text">ABHA validation is required before queueing the ABDM API action.</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="modalConfirmBtn">Queue Action</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var csrfName = '<?= csrf_token() ?>';
|
||||
var csrfHash = '<?= csrf_hash() ?>';
|
||||
var statusBox = document.getElementById('statusBox');
|
||||
var modalEl = document.getElementById('taskActionModal');
|
||||
var actionModal = new bootstrap.Modal(modalEl);
|
||||
var modalAbhaInput = document.getElementById('modalAbhaInput');
|
||||
var modalTaskSummary = document.getElementById('modalTaskSummary');
|
||||
var modalConfirmBtn = document.getElementById('modalConfirmBtn');
|
||||
var selectedAction = '';
|
||||
var selectedRow = null;
|
||||
|
||||
function applyFilter(filter) {
|
||||
var rows = document.querySelectorAll('#taskTable tbody tr');
|
||||
rows.forEach(function (row) {
|
||||
var type = (row.getAttribute('data-task-type') || '').toLowerCase();
|
||||
row.style.display = (filter === 'all' || type === filter.toLowerCase()) ? '' : 'none';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.filter-btn').forEach(function (btn) {
|
||||
btn.classList.remove('btn-primary');
|
||||
btn.classList.add('btn-outline-primary');
|
||||
});
|
||||
var active = document.querySelector('.filter-btn[data-filter="' + filter + '"]');
|
||||
if (active) {
|
||||
active.classList.remove('btn-outline-primary');
|
||||
active.classList.add('btn-primary');
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(msg, isError) {
|
||||
statusBox.textContent = msg;
|
||||
statusBox.style.color = isError ? '#dc3545' : '#6c757d';
|
||||
}
|
||||
|
||||
function post(url, payload, cb) {
|
||||
payload[csrfName] = csrfHash;
|
||||
var body = Object.keys(payload).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(payload[key]);
|
||||
}).join('&');
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: body
|
||||
}).then(function (r) {
|
||||
return r.json();
|
||||
}).then(function (json) {
|
||||
if (json && json.csrfHash) {
|
||||
csrfHash = json.csrfHash;
|
||||
}
|
||||
cb(json || {});
|
||||
}).catch(function (e) {
|
||||
setStatus('Request failed: ' + e.message, true);
|
||||
});
|
||||
}
|
||||
|
||||
function runAction(row, action) {
|
||||
var taskId = row.getAttribute('data-task-id');
|
||||
var abha = modalAbhaInput.value.trim();
|
||||
|
||||
setStatus('Submitting action...');
|
||||
post('<?= base_url('AbdmTaskBoard/perform_action') ?>', {
|
||||
task_id: taskId,
|
||||
action: action,
|
||||
abha_id: abha,
|
||||
opd_session_id: ''
|
||||
}, function (res) {
|
||||
if (res.ok !== 1) {
|
||||
setStatus(res.error_text || 'Action failed', true);
|
||||
return;
|
||||
}
|
||||
|
||||
var badge = row.querySelector('.status-pill');
|
||||
if (badge) {
|
||||
badge.textContent = 'IN_PROGRESS';
|
||||
badge.className = 'badge bg-info status-pill';
|
||||
}
|
||||
setStatus('Action queued: ' + (res.event_type || '-'));
|
||||
actionModal.hide();
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.action-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
selectedRow = btn.closest('tr');
|
||||
if (!selectedRow) {
|
||||
return;
|
||||
}
|
||||
selectedAction = btn.getAttribute('data-action') || 'submit';
|
||||
|
||||
var abhaInput = selectedRow.querySelector('.abha-input');
|
||||
var taskType = selectedRow.getAttribute('data-task-type') || '';
|
||||
modalAbhaInput.value = abhaInput ? abhaInput.value.trim() : '';
|
||||
modalTaskSummary.textContent = 'Task: ' + taskType + ' | Action: ' + selectedAction.toUpperCase();
|
||||
actionModal.show();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.filter-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
applyFilter(btn.getAttribute('data-filter') || 'all');
|
||||
});
|
||||
});
|
||||
|
||||
applyFilter('patient_abha_create');
|
||||
|
||||
modalConfirmBtn.addEventListener('click', function () {
|
||||
if (!selectedRow || !selectedAction) {
|
||||
return;
|
||||
}
|
||||
var abha = modalAbhaInput.value.trim();
|
||||
if (!/^\d{14}$/.test(abha)) {
|
||||
setStatus('ABHA must be a 14-digit number.', true);
|
||||
return;
|
||||
}
|
||||
var rowInput = selectedRow.querySelector('.abha-input');
|
||||
if (rowInput) {
|
||||
rowInput.value = abha;
|
||||
}
|
||||
runAction(selectedRow, selectedAction);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.close-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var row = btn.closest('tr');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
var taskId = row.getAttribute('data-task-id');
|
||||
post('<?= base_url('AbdmTaskBoard/mark_status') ?>', {
|
||||
task_id: taskId,
|
||||
status: 'completed',
|
||||
note: 'Closed from task board'
|
||||
}, function (res) {
|
||||
if (res.ok !== 1) {
|
||||
setStatus(res.error_text || 'Close failed', true);
|
||||
return;
|
||||
}
|
||||
row.remove();
|
||||
setStatus('Task closed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('btnRefresh').addEventListener('click', function () {
|
||||
window.location.reload();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -280,6 +280,51 @@
|
|||
});
|
||||
});
|
||||
|
||||
$('#input_abha_id').on('change blur', function() {
|
||||
var abhaId = ($(this).val() || '').toString().trim();
|
||||
if (!/^\d{14}$/.test(abhaId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.post('<?= base_url('patient/abha_fetch_profile') ?>', {
|
||||
abha_id: abhaId,
|
||||
'<?= csrf_token() ?>': $('input[name="<?= csrf_token() ?>"]').val()
|
||||
}, function(resp) {
|
||||
if (!resp || resp.ok != 1 || !resp.profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
var p = resp.profile || {};
|
||||
if (p.name && !$('#input_name').val()) {
|
||||
$('#input_name').val(p.name);
|
||||
}
|
||||
if (p.mobile && !$('#input_mphone1').val()) {
|
||||
$('#input_mphone1').val(p.mobile);
|
||||
}
|
||||
if (p.city && !$('#input_city').val()) {
|
||||
$('#input_city').val(p.city);
|
||||
}
|
||||
if (p.state && !$('#input_state').val()) {
|
||||
$('#input_state').val(p.state);
|
||||
}
|
||||
if (p.dob && !$('#datepicker_dob').val()) {
|
||||
$('#datepicker_dob').val(p.dob);
|
||||
if ($('#chk_age').is(':checked')) {
|
||||
$('#chk_age').prop('checked', false).trigger('click');
|
||||
}
|
||||
}
|
||||
|
||||
if (p.gender) {
|
||||
var g = (p.gender + '').toLowerCase();
|
||||
if (g === '1' || g === 'male' || g === 'm') {
|
||||
$('#options_gender1').prop('checked', true);
|
||||
} else if (g === '2' || g === 'female' || g === 'f') {
|
||||
$('#options_gender2').prop('checked', true);
|
||||
}
|
||||
}
|
||||
}, 'json');
|
||||
});
|
||||
|
||||
$("#input_city").autocomplete({
|
||||
source: "<?= base_url('billing/patient/city') ?>",
|
||||
minLength: 3,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,18 @@ if ($person) {
|
|||
));
|
||||
}
|
||||
|
||||
$patientId = (int) ($person->id ?? $person->p_id ?? 0);
|
||||
$patientAbha = '';
|
||||
if ($person) {
|
||||
$patientAbha = trim((string) (
|
||||
$person->abha_id
|
||||
?? $person->abha_no
|
||||
?? $person->abha_address
|
||||
?? $person->abha
|
||||
?? ''
|
||||
));
|
||||
}
|
||||
|
||||
$age = '';
|
||||
if ($person) {
|
||||
$age = get_age_1($person->dob ?? null, $person->age ?? '', $person->age_in_month ?? '', $person->estimate_dob ?? '');
|
||||
|
|
@ -615,6 +627,41 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
|
|||
<div class="discharge-side-actions">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="btn_preview_side" onclick="openDischargePreview('<?= site_url('Ipd_discharge/preview_discharge_report/' . $ipdId . '?regen=1') ?>', 'Discharge Preview');">Preview</button>
|
||||
</div>
|
||||
|
||||
<div class="card mt-2 border-primary-subtle" id="ipd_abdm_panel">
|
||||
<div class="card-header py-1"><strong>ABDM Sandbox Actions</strong></div>
|
||||
<div class="card-body p-2">
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1 small">ABHA Address/ID</label>
|
||||
<input type="text" class="form-control form-control-sm" id="ipd_abha_address" value="<?= esc($patientAbha) ?>" maxlength="32" placeholder="Enter ABHA Address">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1 small">QR Payload (Scan & Share)</label>
|
||||
<input type="text" class="form-control form-control-sm" id="ipd_abdm_qr_payload" placeholder="Paste scanned QR payload">
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1 mb-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="btn_ipd_abdm_scan_lookup">Scan Lookup</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_ipd_abdm_validate_abha">Validate ABHA</button>
|
||||
</div>
|
||||
<div class="row g-1 mb-2">
|
||||
<div class="col-6">
|
||||
<input type="text" class="form-control form-control-sm" id="ipd_abdm_purpose_code" placeholder="Purpose (CAREMGT)">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="ipd_abdm_consent_expires_at">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1 mb-2">
|
||||
<button type="button" class="btn btn-outline-success btn-sm" id="btn_ipd_abdm_consent_request">Request Consent</button>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm" id="btn_ipd_abdm_share_discharge">Share Discharge</button>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1 small">Consent Handle</label>
|
||||
<input type="text" class="form-control form-control-sm" id="ipd_abdm_consent_handle" placeholder="Auto-filled after consent request">
|
||||
</div>
|
||||
<div class="small text-muted" id="ipd_abdm_action_status">Ready</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -3858,6 +3905,131 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
|
|||
}
|
||||
}, { passive: true, capture: true });
|
||||
syncNavOnScroll();
|
||||
|
||||
// ─── ABDM Sandbox Actions (IPD Discharge) ─────────────────────────────
|
||||
(function initIpdAbdmActions() {
|
||||
var csrfName = '<?= csrf_token() ?>';
|
||||
var csrfHash = '<?= csrf_hash() ?>';
|
||||
var ipdId = <?= (int) $ipdId ?>;
|
||||
var patientId = <?= (int) $patientId ?>;
|
||||
|
||||
function setStatus(msg, isError) {
|
||||
var el = document.getElementById('ipd_abdm_action_status');
|
||||
if (!el) { return; }
|
||||
el.textContent = msg;
|
||||
el.style.color = isError ? '#dc3545' : '#6c757d';
|
||||
}
|
||||
|
||||
function abdmPost(url, payload, onSuccess) {
|
||||
setStatus('Sending…');
|
||||
var data = Object.assign({}, payload);
|
||||
data[csrfName] = csrfHash;
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
data: data,
|
||||
dataType: 'json'
|
||||
}).done(function(resp) {
|
||||
// refresh CSRF token if returned
|
||||
if (resp && resp.csrf_hash) { csrfHash = resp.csrf_hash; }
|
||||
if (resp && resp.csrf_token) { csrfName = resp.csrf_token; }
|
||||
onSuccess(resp);
|
||||
}).fail(function(xhr) {
|
||||
setStatus('Request failed: ' + xhr.status + ' ' + xhr.statusText, true);
|
||||
});
|
||||
}
|
||||
|
||||
// Scan & Share Lookup
|
||||
var btnScan = document.getElementById('btn_ipd_abdm_scan_lookup');
|
||||
if (btnScan) {
|
||||
btnScan.addEventListener('click', function() {
|
||||
var qrPayload = (document.getElementById('ipd_abdm_qr_payload') || {}).value || '';
|
||||
if (!qrPayload.trim()) { setStatus('Enter QR payload first.', true); return; }
|
||||
abdmPost('<?= base_url('AbdmGateway/scan_share_lookup') ?>', {
|
||||
qr_payload: qrPayload,
|
||||
patient_id: patientId
|
||||
}, function(resp) {
|
||||
if (resp && resp.abha_address) {
|
||||
var addr = document.getElementById('ipd_abha_address');
|
||||
if (addr) { addr.value = resp.abha_address; }
|
||||
}
|
||||
setStatus(resp && resp.message ? resp.message : 'Lookup done.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Validate ABHA
|
||||
var btnVal = document.getElementById('btn_ipd_abdm_validate_abha');
|
||||
if (btnVal) {
|
||||
btnVal.addEventListener('click', function() {
|
||||
var abhaAddr = (document.getElementById('ipd_abha_address') || {}).value || '';
|
||||
if (!abhaAddr.trim()) { setStatus('Enter ABHA Address first.', true); return; }
|
||||
abdmPost('<?= base_url('AbdmGateway/abha_validate') ?>', {
|
||||
abha_address: abhaAddr,
|
||||
patient_id: patientId
|
||||
}, function(resp) {
|
||||
setStatus(resp && resp.message ? resp.message : (resp && resp.ok ? 'ABHA valid.' : 'Validation failed.'), !(resp && resp.ok));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Request Consent
|
||||
var btnConsent = document.getElementById('btn_ipd_abdm_consent_request');
|
||||
if (btnConsent) {
|
||||
btnConsent.addEventListener('click', function() {
|
||||
var abhaAddr = (document.getElementById('ipd_abha_address') || {}).value || '';
|
||||
var purpose = (document.getElementById('ipd_abdm_purpose_code') || {}).value || 'CAREMGT';
|
||||
var expiresAt = (document.getElementById('ipd_abdm_consent_expires_at') || {}).value || '';
|
||||
if (!abhaAddr.trim()) { setStatus('Enter ABHA Address first.', true); return; }
|
||||
abdmPost('<?= base_url('AbdmGateway/consent_request') ?>', {
|
||||
abha_address: abhaAddr,
|
||||
patient_id: patientId,
|
||||
purpose_code: purpose,
|
||||
expires_at: expiresAt
|
||||
}, function(resp) {
|
||||
if (resp && resp.consent_handle) {
|
||||
var ch = document.getElementById('ipd_abdm_consent_handle');
|
||||
if (ch) { ch.value = resp.consent_handle; }
|
||||
}
|
||||
setStatus(resp && resp.message ? resp.message : (resp && resp.ok ? 'Consent requested.' : 'Consent request failed.'), !(resp && resp.ok));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Share Discharge FHIR Bundle
|
||||
var btnShare = document.getElementById('btn_ipd_abdm_share_discharge');
|
||||
if (btnShare) {
|
||||
btnShare.addEventListener('click', function() {
|
||||
var abhaAddr = (document.getElementById('ipd_abha_address') || {}).value || '';
|
||||
var consentHandle = (document.getElementById('ipd_abdm_consent_handle') || {}).value || '';
|
||||
if (!abhaAddr.trim()) { setStatus('Enter ABHA Address first.', true); return; }
|
||||
abdmPost('<?= base_url('AbdmGateway/share_ipd_discharge_bundle') ?>', {
|
||||
ipd_id: ipdId,
|
||||
patient_id: patientId,
|
||||
abha_id: abhaAddr,
|
||||
consent_handle: consentHandle
|
||||
}, function(resp) {
|
||||
if (resp && resp.consent_handle) {
|
||||
var ch = document.getElementById('ipd_abdm_consent_handle');
|
||||
if (ch && !ch.value) { ch.value = resp.consent_handle; }
|
||||
}
|
||||
setStatus(resp && resp.status ? ('Queued: ' + resp.status) : (resp && resp.message ? resp.message : 'Share request sent.'), !(resp && (resp.ok || resp.queue_id)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Set default consent expiry to +30 days
|
||||
(function setDefaultExpiry() {
|
||||
var el = document.getElementById('ipd_abdm_consent_expires_at');
|
||||
if (!el || el.value) { return; }
|
||||
var d = new Date();
|
||||
d.setDate(d.getDate() + 30);
|
||||
var iso = d.toISOString().slice(0, 16);
|
||||
el.value = iso;
|
||||
})();
|
||||
})();
|
||||
// ─── end ABDM Sandbox Actions ──────────────────────────────────────────
|
||||
|
||||
})();
|
||||
</script>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -308,6 +308,48 @@
|
|||
value="<?= esc($patient_master[0]->abha_address ?? $patient_master[0]->abha ?? '') ?>"
|
||||
placeholder="Enter ABHA Address">
|
||||
</div>
|
||||
<div class="card mt-2 border-primary-subtle">
|
||||
<div class="card-header py-1"><strong>ABDM Sandbox Actions</strong></div>
|
||||
<div class="card-body p-2">
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1 small">QR Payload (Scan & Share)</label>
|
||||
<input type="text" class="form-control form-control-sm" id="abdm_qr_payload" placeholder="Paste scanned QR payload">
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1 mb-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="btn_abdm_scan_lookup">Scan Lookup</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_abdm_validate_abha">Validate ABHA</button>
|
||||
</div>
|
||||
|
||||
<div class="row g-1 mb-2">
|
||||
<div class="col-6">
|
||||
<input type="text" class="form-control form-control-sm" id="abdm_purpose_code" placeholder="Purpose (ex: CAREMGT)">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="abdm_consent_expires_at">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1 mb-2">
|
||||
<button type="button" class="btn btn-outline-success btn-sm" id="btn_abdm_consent_request">Request Consent</button>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm" id="btn_abdm_share_fhir">Share FHIR</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1 small">Consent Handle</label>
|
||||
<input type="text" class="form-control form-control-sm" id="abdm_consent_handle" placeholder="Auto-filled after consent request">
|
||||
</div>
|
||||
|
||||
<div class="row g-1 mb-2">
|
||||
<div class="col-7">
|
||||
<input type="number" min="1" class="form-control form-control-sm" id="abdm_claim_document_id" placeholder="Claim Document ID">
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<button type="button" class="btn btn-outline-dark btn-sm w-100" id="btn_abdm_claim_status">Claim Status</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="small text-muted" id="abdm_action_status">Ready</div>
|
||||
</div>
|
||||
</div>
|
||||
<div><strong>OPD:</strong> <?= esc($opd_master[0]->opd_code ?? '') ?></div>
|
||||
<div><strong>Date:</strong> <?= esc($opd_master[0]->apointment_date ?? '') ?></div>
|
||||
<div><strong>Doctor:</strong> <?= esc($opd_master[0]->doc_name ?? '') ?></div>
|
||||
|
|
@ -1105,6 +1147,30 @@
|
|||
cardiac: ['ECG', 'Lipid Profile', 'CBC'],
|
||||
viral_infection: ['CBC', 'LFT', 'KFT']
|
||||
};
|
||||
var patientId = <?= (int) ($patient_master[0]->id ?? 0) ?>;
|
||||
|
||||
function setAbdmStatus(msg, level) {
|
||||
var $box = $('#abdm_action_status');
|
||||
if (!$box.length) {
|
||||
return;
|
||||
}
|
||||
level = (level || 'muted').toString();
|
||||
$box.removeClass('text-muted text-success text-danger text-warning');
|
||||
if (level === 'success') {
|
||||
$box.addClass('text-success');
|
||||
} else if (level === 'danger') {
|
||||
$box.addClass('text-danger');
|
||||
} else if (level === 'warning') {
|
||||
$box.addClass('text-warning');
|
||||
} else {
|
||||
$box.addClass('text-muted');
|
||||
}
|
||||
$box.text(msg || 'Ready');
|
||||
}
|
||||
|
||||
function abdmCurrentAbha() {
|
||||
return ($('#abha_address').val() || '').toString().trim();
|
||||
}
|
||||
|
||||
function alignPanelOrderToExample() {
|
||||
var $medicine = $('#panel_medicine');
|
||||
|
|
@ -3390,6 +3456,109 @@
|
|||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
$('#btn_abdm_scan_lookup').on('click', function() {
|
||||
var qrPayload = ($('#abdm_qr_payload').val() || '').toString().trim();
|
||||
if (!qrPayload) {
|
||||
setAbdmStatus('Paste QR payload first.', 'warning');
|
||||
return;
|
||||
}
|
||||
setAbdmStatus('Queuing Scan & Share lookup...', 'muted');
|
||||
apiPost('<?= base_url('AbdmGateway/scan_share_lookup') ?>', {
|
||||
qr_payload: qrPayload
|
||||
}, function(data) {
|
||||
if (data.ok != 1) {
|
||||
setAbdmStatus(data.error_text || 'Scan lookup failed.', 'danger');
|
||||
return;
|
||||
}
|
||||
if (data.abha_id_hint) {
|
||||
$('#abha_address').val(data.abha_id_hint).trigger('input');
|
||||
}
|
||||
setAbdmStatus('Scan lookup queued. Queue ID: ' + (data.queue_id || '-'), 'success');
|
||||
});
|
||||
});
|
||||
|
||||
$('#btn_abdm_validate_abha').on('click', function() {
|
||||
var abha = abdmCurrentAbha();
|
||||
if (!abha) {
|
||||
setAbdmStatus('Enter ABHA first.', 'warning');
|
||||
return;
|
||||
}
|
||||
setAbdmStatus('Queuing ABHA validation...', 'muted');
|
||||
apiPost('<?= base_url('AbdmGateway/abha_validate') ?>', {
|
||||
abha_id: abha
|
||||
}, function(data) {
|
||||
if (data.ok != 1) {
|
||||
setAbdmStatus(data.error_text || 'ABHA validation queue failed.', 'danger');
|
||||
return;
|
||||
}
|
||||
setAbdmStatus('ABHA validation queued. Queue ID: ' + (data.queue_id || '-'), 'success');
|
||||
});
|
||||
});
|
||||
|
||||
$('#btn_abdm_consent_request').on('click', function() {
|
||||
var abha = abdmCurrentAbha();
|
||||
if (!abha) {
|
||||
setAbdmStatus('Enter ABHA first.', 'warning');
|
||||
return;
|
||||
}
|
||||
setAbdmStatus('Requesting consent...', 'muted');
|
||||
apiPost('<?= base_url('AbdmGateway/consent_request') ?>', {
|
||||
patient_id: patientId,
|
||||
abha_id: abha,
|
||||
purpose_code: ($('#abdm_purpose_code').val() || '').toString().trim(),
|
||||
expires_at: ($('#abdm_consent_expires_at').val() || '').toString().trim().replace('T', ' ')
|
||||
}, function(data) {
|
||||
if (data.ok != 1) {
|
||||
setAbdmStatus(data.error_text || 'Consent request failed.', 'danger');
|
||||
return;
|
||||
}
|
||||
if (data.consent_handle) {
|
||||
$('#abdm_consent_handle').val(data.consent_handle);
|
||||
}
|
||||
setAbdmStatus('Consent requested. Handle: ' + (data.consent_handle || '-'), 'success');
|
||||
});
|
||||
});
|
||||
|
||||
$('#btn_abdm_share_fhir').on('click', function() {
|
||||
var abha = abdmCurrentAbha();
|
||||
if (!abha) {
|
||||
setAbdmStatus('Enter ABHA first.', 'warning');
|
||||
return;
|
||||
}
|
||||
setAbdmStatus('Checking consent and queuing FHIR share...', 'muted');
|
||||
apiPost('<?= base_url('AbdmGateway/share_prescription_bundle') ?>', {
|
||||
opd_id: ($('#opd_id').val() || '0').toString(),
|
||||
opd_session_id: ($('#opd_session_id').val() || '0').toString(),
|
||||
patient_id: patientId,
|
||||
abha_id: abha,
|
||||
consent_handle: ($('#abdm_consent_handle').val() || '').toString().trim()
|
||||
}, function(data) {
|
||||
if (data.ok != 1) {
|
||||
setAbdmStatus(data.error_text || 'FHIR share queue failed.', 'danger');
|
||||
return;
|
||||
}
|
||||
setAbdmStatus('FHIR share queued. Queue ID: ' + (data.queue_id || '-'), 'success');
|
||||
});
|
||||
});
|
||||
|
||||
$('#btn_abdm_claim_status').on('click', function() {
|
||||
var docId = parseInt(($('#abdm_claim_document_id').val() || '0').toString(), 10) || 0;
|
||||
if (docId <= 0) {
|
||||
setAbdmStatus('Enter Claim Document ID first.', 'warning');
|
||||
return;
|
||||
}
|
||||
setAbdmStatus('Queuing claim status request...', 'muted');
|
||||
apiPost('<?= base_url('AbdmGateway/nhcx_claim_status_request') ?>', {
|
||||
document_id: docId
|
||||
}, function(data) {
|
||||
if (data.ok != 1) {
|
||||
setAbdmStatus(data.error_text || 'Claim status queue failed.', 'danger');
|
||||
return;
|
||||
}
|
||||
setAbdmStatus('Claim status queued. Queue ID: ' + (data.queue_id || '-'), 'success');
|
||||
});
|
||||
});
|
||||
|
||||
$('.rx-instant, #pregnancy, #lactation, #liver_insufficiency, #renal_insufficiency, #pulmonary_insufficiency, #corona_suspected, #dengue, #is_smoking, #is_alcohol, #is_drug_abuse, input[name="morbidities"], input[name="options-pain"]').on('input change', function() {
|
||||
markDirty('Clinical details updated');
|
||||
scheduleAutoSave();
|
||||
|
|
|
|||
|
|
@ -130,6 +130,36 @@ $editReason = trim((string) ($edit_reason ?? ''));
|
|||
<?php endforeach; ?>
|
||||
<div id="no_templates_msg" style="display:none; color:#888; padding:6px;">No templates found</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-3">
|
||||
|
||||
<div class="card border-primary-subtle">
|
||||
<div class="card-header py-1"><strong>ABDM – Share Report</strong></div>
|
||||
<div class="card-body p-2">
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1 small">ABHA Address/ID</label>
|
||||
<input type="text" class="form-control form-control-sm" id="diag_abha_address" value="<?= esc($report->patient_abha ?? '') ?>" placeholder="Patient ABHA">
|
||||
</div>
|
||||
<div class="row g-1 mb-2">
|
||||
<div class="col-7">
|
||||
<input type="text" class="form-control form-control-sm" id="diag_abdm_purpose_code" value="CAREMGT" placeholder="Purpose (CAREMGT)">
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="diag_abdm_consent_expires_at">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1 mb-2">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_diag_abdm_validate_abha">Validate ABHA</button>
|
||||
<button type="button" class="btn btn-outline-success btn-sm" id="btn_diag_abdm_consent_request">Request Consent</button>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1 small">Consent Handle</label>
|
||||
<input type="text" class="form-control form-control-sm" id="diag_abdm_consent_handle" placeholder="Auto-filled after consent">
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm w-100 mb-2" id="btn_diag_abdm_share_report">Share Report (FHIR)</button>
|
||||
<div class="small text-muted" id="diag_abdm_action_status">Ready</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -413,6 +443,97 @@ $editReason = trim((string) ($edit_reason ?? ''));
|
|||
if (inst) inst.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ABDM Lab/Radiology Report Share ──────────────────────────────
|
||||
(function initDiagAbdmActions() {
|
||||
var csrfName = '<?= csrf_token() ?>';
|
||||
var csrfHash = '<?= csrf_hash() ?>';
|
||||
var labReqId = <?= (int) ($report->id ?? 0) ?>;
|
||||
var patientId = <?= (int) ($report->patient_id ?? 0) ?>;
|
||||
|
||||
function setStatus(msg, isError) {
|
||||
var el = document.getElementById('diag_abdm_action_status');
|
||||
if (!el) { return; }
|
||||
el.textContent = msg;
|
||||
el.style.color = isError ? '#dc3545' : '#6c757d';
|
||||
}
|
||||
|
||||
function abdmPost(url, payload, onSuccess) {
|
||||
setStatus('Sending…');
|
||||
var data = Object.assign({}, payload);
|
||||
data[csrfName] = csrfHash;
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(data).toString()
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(resp) {
|
||||
if (resp && resp.csrf_hash) { csrfHash = resp.csrf_hash; }
|
||||
if (resp && resp.csrf_token) { csrfName = resp.csrf_token; }
|
||||
onSuccess(resp);
|
||||
})
|
||||
.catch(function(e) { setStatus('Request failed: ' + e.message, true); });
|
||||
}
|
||||
|
||||
var btnValidate = document.getElementById('btn_diag_abdm_validate_abha');
|
||||
if (btnValidate) {
|
||||
btnValidate.addEventListener('click', function() {
|
||||
var abha = (document.getElementById('diag_abha_address') || {}).value || '';
|
||||
if (!abha.trim()) { setStatus('Enter ABHA Address first.', true); return; }
|
||||
abdmPost(baseUrl + 'AbdmGateway/abha_validate', { abha_address: abha, patient_id: patientId }, function(resp) {
|
||||
setStatus(resp && resp.message ? resp.message : (resp && resp.ok ? 'ABHA valid.' : 'Validation failed.'), !(resp && resp.ok));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var btnConsent = document.getElementById('btn_diag_abdm_consent_request');
|
||||
if (btnConsent) {
|
||||
btnConsent.addEventListener('click', function() {
|
||||
var abha = (document.getElementById('diag_abha_address') || {}).value || '';
|
||||
var purpose = (document.getElementById('diag_abdm_purpose_code') || {}).value || 'CAREMGT';
|
||||
var expires = (document.getElementById('diag_abdm_consent_expires_at') || {}).value || '';
|
||||
if (!abha.trim()) { setStatus('Enter ABHA Address first.', true); return; }
|
||||
abdmPost(baseUrl + 'AbdmGateway/consent_request', {
|
||||
abha_address: abha, patient_id: patientId, purpose_code: purpose, expires_at: expires
|
||||
}, function(resp) {
|
||||
if (resp && resp.consent_handle) {
|
||||
var ch = document.getElementById('diag_abdm_consent_handle');
|
||||
if (ch) { ch.value = resp.consent_handle; }
|
||||
}
|
||||
setStatus(resp && resp.message ? resp.message : (resp && resp.ok ? 'Consent requested.' : 'Consent request failed.'), !(resp && resp.ok));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var btnShare = document.getElementById('btn_diag_abdm_share_report');
|
||||
if (btnShare) {
|
||||
btnShare.addEventListener('click', function() {
|
||||
var abha = (document.getElementById('diag_abha_address') || {}).value || '';
|
||||
var ch = (document.getElementById('diag_abdm_consent_handle') || {}).value || '';
|
||||
if (!abha.trim()) { setStatus('Enter ABHA Address first.', true); return; }
|
||||
abdmPost(baseUrl + 'AbdmGateway/share_diagnosis_report_bundle', {
|
||||
lab_req_id: labReqId, patient_id: patientId, abha_id: abha, consent_handle: ch
|
||||
}, function(resp) {
|
||||
if (resp && resp.consent_handle) {
|
||||
var chEl = document.getElementById('diag_abdm_consent_handle');
|
||||
if (chEl && !chEl.value) { chEl.value = resp.consent_handle; }
|
||||
}
|
||||
setStatus(resp && resp.status ? ('Queued: ' + resp.status) : (resp && resp.message ? resp.message : 'Share request sent.'), !(resp && (resp.ok || resp.queue_id)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Default consent expiry +30 days
|
||||
(function() {
|
||||
var el = document.getElementById('diag_abdm_consent_expires_at');
|
||||
if (!el || el.value) { return; }
|
||||
var d = new Date(); d.setDate(d.getDate() + 30);
|
||||
el.value = d.toISOString().slice(0, 16);
|
||||
})();
|
||||
})();
|
||||
// ─── end ABDM ─────────────────────────────────────────────────────
|
||||
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -286,6 +286,14 @@ if (! $canDiagnosis && $user && method_exists($user, 'inGroup')) {
|
|||
</li>
|
||||
<?php } ?>
|
||||
|
||||
<li class="nav-heading">ABDM</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('AbdmTaskBoard') ?>','ABDM Task Board')">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
<span>ABDM Task Board</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-heading">Reports</li>
|
||||
|
||||
<li class="nav-item">
|
||||
|
|
|
|||
|
|
@ -172,7 +172,18 @@
|
|||
$('#main').html('Request timed out. Please try again.');
|
||||
return;
|
||||
}
|
||||
$('#main').html('Error loading content.');
|
||||
var details = 'Error loading content.';
|
||||
if (jqXHR && jqXHR.status) {
|
||||
details += ' [HTTP ' + jqXHR.status + ']';
|
||||
}
|
||||
var raw = (jqXHR && jqXHR.responseText) ? String(jqXHR.responseText) : '';
|
||||
if (raw !== '') {
|
||||
raw = raw.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
if (raw !== '') {
|
||||
details += '<br><small>' + raw.substring(0, 220) + '</small>';
|
||||
}
|
||||
}
|
||||
$('#main').html(details);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -204,7 +215,18 @@
|
|||
$("#" + xdiv).html('Request timed out. Please try again.');
|
||||
return;
|
||||
}
|
||||
$("#" + xdiv).html('Error loading content.');
|
||||
var details = 'Error loading content.';
|
||||
if (jqXHR && jqXHR.status) {
|
||||
details += ' [HTTP ' + jqXHR.status + ']';
|
||||
}
|
||||
var raw = (jqXHR && jqXHR.responseText) ? String(jqXHR.responseText) : '';
|
||||
if (raw !== '') {
|
||||
raw = raw.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
if (raw !== '') {
|
||||
details += '<br><small>' + raw.substring(0, 220) + '</small>';
|
||||
}
|
||||
}
|
||||
$("#" + xdiv).html(details);
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
29
env
29
env
|
|
@ -67,3 +67,32 @@
|
|||
#--------------------------------------------------------------------
|
||||
|
||||
# logger.threshold = 4
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# ABDM / EKA CONNECTOR
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# Use "eka" to route ABDM/NHCX queue events to Eka, "bridge" to keep legacy bridge route.
|
||||
# ABDM_SYNC_PROVIDER = eka
|
||||
|
||||
# Optional global override for all queue events.
|
||||
# BRIDGE_SYNC_PROVIDER = bridge
|
||||
|
||||
# Legacy bridge endpoint settings.
|
||||
# BRIDGE_SYNC_URL =
|
||||
# BRIDGE_SYNC_TOKEN =
|
||||
# BRIDGE_SOURCE_CODE =
|
||||
|
||||
# Eka dispatch settings.
|
||||
# EKA_BASE_URL = https://api.eka.care
|
||||
# EKA_BEARER_TOKEN =
|
||||
# EKA_API_KEY =
|
||||
# EKA_CLIENT_ID =
|
||||
|
||||
# Optional JSON map to override default event->path mapping.
|
||||
# Example: {"abdm.abha.validate":"/user-app/abdm-connect/abha/verify"}
|
||||
# EKA_EVENT_ENDPOINTS_JSON =
|
||||
|
||||
# Webhook signature verification secret for callback endpoints.
|
||||
# If empty, signature validation is skipped.
|
||||
# EKA_WEBHOOK_SECRET =
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user