feat: commit all pending page and ABDM gateway updates
This commit is contained in:
parent
c97db078a9
commit
4ed366ccb2
|
|
@ -69,12 +69,38 @@ $routes->get('Medical/SupplierEdit/(:num)', 'Medical::supplier_edit/$1');
|
||||||
$routes->post('Medical/SupplierUpdate', 'Medical::supplier_update');
|
$routes->post('Medical/SupplierUpdate', 'Medical::supplier_update');
|
||||||
$routes->get('product_master/company_master_list', 'Medical::company_master_list');
|
$routes->get('product_master/company_master_list', 'Medical::company_master_list');
|
||||||
$routes->get('product_master/CompanyListSub', 'Medical::company_list_sub');
|
$routes->get('product_master/CompanyListSub', 'Medical::company_list_sub');
|
||||||
|
$routes->get('product_master/CompanyListData', 'Medical::company_list_data');
|
||||||
$routes->get('product_master/CompanyEdit/(:num)', 'Medical::company_edit/$1');
|
$routes->get('product_master/CompanyEdit/(:num)', 'Medical::company_edit/$1');
|
||||||
$routes->post('product_master/CompanyUpdate', 'Medical::company_update');
|
$routes->post('product_master/CompanyUpdate', 'Medical::company_update');
|
||||||
|
$routes->get('product_master/formulation_master_list', 'Medical::formulation_master_list');
|
||||||
|
$routes->get('product_master/FormulationListSub', 'Medical::formulation_list_sub');
|
||||||
|
$routes->get('product_master/FormulationListData', 'Medical::formulation_list_data');
|
||||||
|
$routes->get('product_master/FormulationEdit/(:num)', 'Medical::formulation_edit/$1');
|
||||||
|
$routes->post('product_master/FormulationUpdate', 'Medical::formulation_update');
|
||||||
|
$routes->get('product_master/short_formulation_master_list', 'Medical::short_formulation_master_list');
|
||||||
|
$routes->get('product_master/ShortFormulationListSub', 'Medical::short_formulation_list_sub');
|
||||||
|
$routes->get('product_master/ShortFormulationListData', 'Medical::short_formulation_list_data');
|
||||||
|
$routes->get('product_master/ShortFormulationEdit/(:num)', 'Medical::short_formulation_edit/$1');
|
||||||
|
$routes->post('product_master/ShortFormulationUpdate', 'Medical::short_formulation_update');
|
||||||
|
$routes->post('product_master/SyncDrugMasters', 'Medical::sync_drug_masters');
|
||||||
|
$routes->post('product_master/RebuildDrugMasters', 'Medical::rebuild_drug_masters');
|
||||||
$routes->get('Product_master/company_master_list', 'Medical::company_master_list');
|
$routes->get('Product_master/company_master_list', 'Medical::company_master_list');
|
||||||
$routes->get('Product_master/CompanyListSub', 'Medical::company_list_sub');
|
$routes->get('Product_master/CompanyListSub', 'Medical::company_list_sub');
|
||||||
|
$routes->get('Product_master/CompanyListData', 'Medical::company_list_data');
|
||||||
$routes->get('Product_master/CompanyEdit/(:num)', 'Medical::company_edit/$1');
|
$routes->get('Product_master/CompanyEdit/(:num)', 'Medical::company_edit/$1');
|
||||||
$routes->post('Product_master/CompanyUpdate', 'Medical::company_update');
|
$routes->post('Product_master/CompanyUpdate', 'Medical::company_update');
|
||||||
|
$routes->get('Product_master/formulation_master_list', 'Medical::formulation_master_list');
|
||||||
|
$routes->get('Product_master/FormulationListSub', 'Medical::formulation_list_sub');
|
||||||
|
$routes->get('Product_master/FormulationListData', 'Medical::formulation_list_data');
|
||||||
|
$routes->get('Product_master/FormulationEdit/(:num)', 'Medical::formulation_edit/$1');
|
||||||
|
$routes->post('Product_master/FormulationUpdate', 'Medical::formulation_update');
|
||||||
|
$routes->get('Product_master/short_formulation_master_list', 'Medical::short_formulation_master_list');
|
||||||
|
$routes->get('Product_master/ShortFormulationListSub', 'Medical::short_formulation_list_sub');
|
||||||
|
$routes->get('Product_master/ShortFormulationListData', 'Medical::short_formulation_list_data');
|
||||||
|
$routes->get('Product_master/ShortFormulationEdit/(:num)', 'Medical::short_formulation_edit/$1');
|
||||||
|
$routes->post('Product_master/ShortFormulationUpdate', 'Medical::short_formulation_update');
|
||||||
|
$routes->post('Product_master/SyncDrugMasters', 'Medical::sync_drug_masters');
|
||||||
|
$routes->post('Product_master/RebuildDrugMasters', 'Medical::rebuild_drug_masters');
|
||||||
$routes->get('product_master/medicine_category', 'Medical::medicine_category');
|
$routes->get('product_master/medicine_category', 'Medical::medicine_category');
|
||||||
$routes->get('product_master/medicine_category_edit/(:num)', 'Medical::medicine_category_edit/$1');
|
$routes->get('product_master/medicine_category_edit/(:num)', 'Medical::medicine_category_edit/$1');
|
||||||
$routes->get('product_master/medicine_category_Sub', 'Medical::medicine_category_sub');
|
$routes->get('product_master/medicine_category_Sub', 'Medical::medicine_category_sub');
|
||||||
|
|
@ -544,6 +570,10 @@ $routes->post('Opd_prescription/opd_prescription_save', 'Opd_prescription::opd_p
|
||||||
$abdmPermFilter = 'permission:abdm.access,abdm.taskboard.access,abdm.gateway.use';
|
$abdmPermFilter = 'permission:abdm.access,abdm.taskboard.access,abdm.gateway.use';
|
||||||
$routes->post('AbdmGateway/abha_validate', 'AbdmGateway::abhaValidate', ['filter' => $abdmPermFilter]);
|
$routes->post('AbdmGateway/abha_validate', 'AbdmGateway::abhaValidate', ['filter' => $abdmPermFilter]);
|
||||||
$routes->post('AbdmGateway/scan_share_lookup', 'AbdmGateway::scanShareLookup', ['filter' => $abdmPermFilter]);
|
$routes->post('AbdmGateway/scan_share_lookup', 'AbdmGateway::scanShareLookup', ['filter' => $abdmPermFilter]);
|
||||||
|
$routes->get('AbdmGateway/scan_share_lookup_status/(:num)', 'AbdmGateway::scanShareLookupStatus/$1', ['filter' => $abdmPermFilter]);
|
||||||
|
$routes->get('AbdmGateway/scan_share_resolve_patient/(:num)', 'AbdmGateway::scanShareResolvePatient/$1', ['filter' => $abdmPermFilter]);
|
||||||
|
$routes->post('AbdmGateway/scan_share_link_patient/(:num)', 'AbdmGateway::scanShareLinkPatient/$1', ['filter' => $abdmPermFilter]);
|
||||||
|
$routes->post('AbdmGateway/scan_share_lookup_result_callback', 'AbdmGateway::scanShareLookupResultCallback');
|
||||||
$routes->post('AbdmGateway/consent_request', 'AbdmGateway::consentRequest', ['filter' => $abdmPermFilter]);
|
$routes->post('AbdmGateway/consent_request', 'AbdmGateway::consentRequest', ['filter' => $abdmPermFilter]);
|
||||||
$routes->post('AbdmGateway/consent_callback', 'AbdmGateway::consentCallback');
|
$routes->post('AbdmGateway/consent_callback', 'AbdmGateway::consentCallback');
|
||||||
$routes->post('AbdmGateway/consent_revoked_callback', 'AbdmGateway::consentRevokedCallback');
|
$routes->post('AbdmGateway/consent_revoked_callback', 'AbdmGateway::consentRevokedCallback');
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,10 @@ class AbdmGateway extends BaseController
|
||||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'qr_payload is required']);
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'qr_payload is required']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strlen($qrPayload) < 8) {
|
||||||
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invalid QR payload']);
|
||||||
|
}
|
||||||
|
|
||||||
$abhaId = '';
|
$abhaId = '';
|
||||||
if (preg_match('/\b(\d{14})\b/', $qrPayload, $match) === 1) {
|
if (preg_match('/\b(\d{14})\b/', $qrPayload, $match) === 1) {
|
||||||
$abhaId = (string) ($match[1] ?? '');
|
$abhaId = (string) ($match[1] ?? '');
|
||||||
|
|
@ -259,18 +263,277 @@ class AbdmGateway extends BaseController
|
||||||
];
|
];
|
||||||
|
|
||||||
$queueId = null;
|
$queueId = null;
|
||||||
|
$result = [];
|
||||||
try {
|
try {
|
||||||
$result = $this->connector->scanShareLookup($qrPayload, $abhaId, $payload);
|
$result = $this->connector->scanShareLookup($qrPayload, $abhaId, $payload);
|
||||||
$queueId = $result['queue_id'] ?? null;
|
$queueId = (int) ($result['queue_id'] ?? $result['id'] ?? (($result['data']['queue_id'] ?? 0)));
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
return $this->response->setStatusCode(500)->setJSON([
|
||||||
|
'ok' => 0,
|
||||||
|
'error_text' => $e->getMessage(),
|
||||||
|
'abha_id_hint' => $abhaId,
|
||||||
|
'abha_address' => $abhaId,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scanStatus = [
|
||||||
|
'ok' => 1,
|
||||||
|
'status' => 'queued',
|
||||||
|
'queue_id' => $queueId > 0 ? $queueId : null,
|
||||||
|
'abha_id_hint' => $abhaId,
|
||||||
|
'abha_address' => $abhaId,
|
||||||
|
'status_url' => $queueId > 0 ? base_url('AbdmGateway/scan_share_lookup_status/' . $queueId) : null,
|
||||||
|
'message' => 'Scan & Share lookup queued to center server.',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (is_array($result) && ! empty($result)) {
|
||||||
|
$scanStatus['bridge_result'] = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response->setJSON($scanStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scanShareLookupStatus(int $queueId)
|
||||||
|
{
|
||||||
|
if ($queueId <= 0) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid queue id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->db->tableExists('bridge_sync_queue')) {
|
||||||
|
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'bridge_sync_queue table not found']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $this->db->table('bridge_sync_queue')
|
||||||
|
->select('id, event_type, status, attempts, max_attempts, last_error, created_at, updated_at, sent_at, entity_type, entity_id')
|
||||||
|
->where('id', $queueId)
|
||||||
|
->get(1)
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
if (! $row) {
|
||||||
|
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Queue record not found']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string) ($row['event_type'] ?? '') !== 'abdm.scan_share.lookup') {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Queue record is not a scan-share lookup event']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = strtolower((string) ($row['status'] ?? 'pending'));
|
||||||
|
$isDone = in_array($status, ['sent', 'failed'], true) ? 1 : 0;
|
||||||
|
|
||||||
|
$responseLog = null;
|
||||||
|
if ($this->db->tableExists('abdm_api_logs')) {
|
||||||
|
$likeToken = '"queue_id":' . $queueId;
|
||||||
|
$responseLog = $this->db->table('abdm_api_logs')
|
||||||
|
->select('id, response_code, response_json, status, error_message, created_at')
|
||||||
|
->where('event_type', 'abdm.scan_share.lookup')
|
||||||
|
->like('request_json', $likeToken)
|
||||||
|
->orderBy('id', 'DESC')
|
||||||
|
->get(1)
|
||||||
|
->getRowArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
$resultPayload = $this->getScanShareResultPayload($queueId);
|
||||||
|
$resolved = is_array($resultPayload) ? $this->extractScanShareIdentity($resultPayload) : [];
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'ok' => 1,
|
||||||
|
'queue_id' => (int) ($row['id'] ?? 0),
|
||||||
|
'event_type' => (string) ($row['event_type'] ?? ''),
|
||||||
|
'status' => $status,
|
||||||
|
'done' => $isDone,
|
||||||
|
'attempts' => (int) ($row['attempts'] ?? 0),
|
||||||
|
'max_attempts' => (int) ($row['max_attempts'] ?? 0),
|
||||||
|
'last_error' => (string) ($row['last_error'] ?? ''),
|
||||||
|
'entity_type' => (string) ($row['entity_type'] ?? ''),
|
||||||
|
'entity_id' => (string) ($row['entity_id'] ?? ''),
|
||||||
|
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||||
|
'updated_at' => (string) ($row['updated_at'] ?? ''),
|
||||||
|
'sent_at' => (string) ($row['sent_at'] ?? ''),
|
||||||
|
'bridge_log' => $responseLog,
|
||||||
|
'resolved_data' => $resolved,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scanShareLookupResultCallback()
|
||||||
|
{
|
||||||
|
$signatureFailure = $this->validateWebhookSignature();
|
||||||
|
if ($signatureFailure !== null) {
|
||||||
|
return $signatureFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $this->request->getJSON(true);
|
||||||
|
if (! is_array($payload) || empty($payload)) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid callback payload']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queueId = (int) ($payload['queue_id'] ?? $payload['queueId'] ?? $payload['meta']['queue_id'] ?? 0);
|
||||||
|
if ($queueId <= 0) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'queue_id is required']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = (int) ($payload['ok'] ?? 1) === 1;
|
||||||
|
$status = strtolower(trim((string) ($payload['status'] ?? ($ok ? 'sent' : 'failed'))));
|
||||||
|
$errorText = trim((string) ($payload['error_text'] ?? $payload['error'] ?? ''));
|
||||||
|
|
||||||
|
if ($this->db->tableExists('bridge_sync_queue')) {
|
||||||
|
$this->db->table('bridge_sync_queue')
|
||||||
|
->where('id', $queueId)
|
||||||
|
->where('event_type', 'abdm.scan_share.lookup')
|
||||||
|
->update([
|
||||||
|
'status' => $status === 'failed' ? 'failed' : 'sent',
|
||||||
|
'sent_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||||
|
'last_error' => $errorText !== '' ? mb_substr($errorText, 0, 500) : null,
|
||||||
|
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->db->tableExists('abdm_api_logs')) {
|
||||||
|
$this->db->table('abdm_api_logs')->insert([
|
||||||
|
'channel' => 'bridge',
|
||||||
|
'event_type' => 'abdm.scan_share.lookup.result',
|
||||||
|
'endpoint' => '/AbdmGateway/scan_share_lookup_result_callback',
|
||||||
|
'http_method' => 'POST',
|
||||||
|
'entity_type' => 'abha_scan',
|
||||||
|
'entity_id' => (string) $queueId,
|
||||||
|
'request_json' => (string) json_encode(['queue_id' => $queueId], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
|
'response_code' => $ok ? 200 : 500,
|
||||||
|
'response_json' => (string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
|
'status' => $ok ? 'success' : 'error',
|
||||||
|
'error_message' => $errorText !== '' ? mb_substr($errorText, 0, 1000) : null,
|
||||||
|
'created_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response->setJSON(['ok' => 1, 'queue_id' => $queueId, 'status' => $status !== '' ? $status : 'sent']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scanShareResolvePatient(int $queueId)
|
||||||
|
{
|
||||||
|
if (! $this->request->isAJAX()) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$resultPayload = $this->getScanShareResultPayload($queueId);
|
||||||
|
if (! is_array($resultPayload)) {
|
||||||
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'No scan-share result found for queue']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$identity = $this->extractScanShareIdentity($resultPayload);
|
||||||
|
$matches = $this->findScanSharePatientMatches($identity);
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'ok' => 1,
|
'ok' => 1,
|
||||||
'status' => 'queued',
|
|
||||||
'queue_id' => $queueId,
|
'queue_id' => $queueId,
|
||||||
'abha_id_hint' => $abhaId,
|
'identity' => $identity,
|
||||||
'message' => 'Scan & Share lookup queued to center server.',
|
'matches' => $matches,
|
||||||
|
'requires_confirmation' => count($matches) > 0 ? 1 : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scanShareLinkPatient(int $queueId)
|
||||||
|
{
|
||||||
|
if (! $this->request->isAJAX()) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = strtolower(trim((string) ($this->request->getPost('action') ?? 'check')));
|
||||||
|
if (! in_array($action, ['link_existing', 'create_new'], true)) {
|
||||||
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invalid action']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$identity = [
|
||||||
|
'abha_number' => preg_replace('/\D/', '', (string) ($this->request->getPost('abha_number') ?? '')),
|
||||||
|
'abha_address' => trim((string) ($this->request->getPost('abha_address') ?? '')),
|
||||||
|
'patient_name' => trim((string) ($this->request->getPost('patient_name') ?? '')),
|
||||||
|
'phone' => trim((string) ($this->request->getPost('phone') ?? '')),
|
||||||
|
'gender' => strtoupper(trim((string) ($this->request->getPost('gender') ?? ''))),
|
||||||
|
'dob' => trim((string) ($this->request->getPost('dob') ?? '')),
|
||||||
|
];
|
||||||
|
|
||||||
|
$db = $this->db;
|
||||||
|
$fields = $db->getFieldNames('patient_master') ?? [];
|
||||||
|
$abhaField = $this->resolveFirstExistingColumn($fields, ['abha_id', 'abha_no', 'abha', 'abha_address']);
|
||||||
|
|
||||||
|
$patientId = 0;
|
||||||
|
$pCode = '';
|
||||||
|
$isNew = false;
|
||||||
|
|
||||||
|
if ($action === 'link_existing') {
|
||||||
|
$existingPatientId = (int) ($this->request->getPost('existing_patient_id') ?? 0);
|
||||||
|
if ($existingPatientId <= 0) {
|
||||||
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'existing_patient_id is required']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $db->table('patient_master')
|
||||||
|
->select('id,p_code,mphone1' . ($abhaField ? ',' . $abhaField . ' AS patient_abha' : ''))
|
||||||
|
->where('id', $existingPatientId)
|
||||||
|
->get(1)
|
||||||
|
->getRowArray();
|
||||||
|
if (! $existing) {
|
||||||
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Selected patient not found']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$patientId = (int) ($existing['id'] ?? 0);
|
||||||
|
$pCode = (string) ($existing['p_code'] ?? '');
|
||||||
|
|
||||||
|
$backfill = [];
|
||||||
|
if ($abhaField && trim((string) ($existing['patient_abha'] ?? '')) === '' && ($identity['abha_number'] !== '' || $identity['abha_address'] !== '')) {
|
||||||
|
$backfill[$abhaField] = $identity['abha_number'] !== '' ? $identity['abha_number'] : $identity['abha_address'];
|
||||||
|
}
|
||||||
|
if (trim((string) ($existing['mphone1'] ?? '')) === '' && $identity['phone'] !== '') {
|
||||||
|
$backfill['mphone1'] = $identity['phone'];
|
||||||
|
}
|
||||||
|
if (! empty($backfill)) {
|
||||||
|
$db->table('patient_master')->where('id', $patientId)->update($backfill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'create_new') {
|
||||||
|
if ($identity['patient_name'] === '') {
|
||||||
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_name is required to create patient']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$created = $this->createPatientFromScanIdentity($identity, $abhaField);
|
||||||
|
$patientId = (int) ($created['patient_id'] ?? 0);
|
||||||
|
$pCode = (string) ($created['p_code'] ?? '');
|
||||||
|
$isNew = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($patientId <= 0) {
|
||||||
|
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => 'Unable to resolve patient']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($db->tableExists('bridge_sync_queue')) {
|
||||||
|
$db->table('bridge_sync_queue')
|
||||||
|
->where('id', $queueId)
|
||||||
|
->where('event_type', 'abdm.scan_share.lookup')
|
||||||
|
->update([
|
||||||
|
'entity_type' => 'patient',
|
||||||
|
'entity_id' => (string) $patientId,
|
||||||
|
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getAuditService()->log([
|
||||||
|
'action' => 'scan_share_patient_link',
|
||||||
|
'entity_type' => 'abha_scan',
|
||||||
|
'entity_id' => (string) $queueId,
|
||||||
|
'patient_id' => $patientId,
|
||||||
|
'abha_id' => (string) ($identity['abha_address'] !== '' ? $identity['abha_address'] : $identity['abha_number']),
|
||||||
|
'request' => ['action' => $action, 'identity' => $identity],
|
||||||
|
'response' => ['patient_id' => $patientId, 'p_code' => $pCode],
|
||||||
|
'outcome' => 'success',
|
||||||
|
'error_message' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'ok' => 1,
|
||||||
|
'queue_id' => $queueId,
|
||||||
|
'patient_id' => $patientId,
|
||||||
|
'p_code' => $pCode,
|
||||||
|
'is_new' => $isNew ? 1 : 0,
|
||||||
|
'profile_url' => base_url('Patient/person_record/' . $patientId),
|
||||||
|
'edit_url' => base_url('Patient/person_record/' . $patientId . '/1'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1290,6 +1553,195 @@ class AbdmGateway extends BaseController
|
||||||
return $this->response->setJSON($result);
|
return $this->response->setJSON($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getScanShareResultPayload(int $queueId): ?array
|
||||||
|
{
|
||||||
|
if (! $this->db->tableExists('abdm_api_logs')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$likeToken = '"queue_id":' . $queueId;
|
||||||
|
|
||||||
|
$resultLog = $this->db->table('abdm_api_logs')
|
||||||
|
->select('response_json, request_json')
|
||||||
|
->whereIn('event_type', ['abdm.scan_share.lookup.result', 'abdm.scan_share.lookup'])
|
||||||
|
->groupStart()
|
||||||
|
->like('request_json', $likeToken)
|
||||||
|
->orLike('response_json', $likeToken)
|
||||||
|
->groupEnd()
|
||||||
|
->orderBy('id', 'DESC')
|
||||||
|
->get(1)
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
if (! $resultLog) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$responsePayload = json_decode((string) ($resultLog['response_json'] ?? ''), true);
|
||||||
|
if (is_array($responsePayload) && ! empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestPayload = json_decode((string) ($resultLog['request_json'] ?? ''), true);
|
||||||
|
return is_array($requestPayload) && ! empty($requestPayload) ? $requestPayload : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractScanShareIdentity(array $payload): array
|
||||||
|
{
|
||||||
|
$identity = [
|
||||||
|
'abha_number' => '',
|
||||||
|
'abha_address' => '',
|
||||||
|
'patient_name' => '',
|
||||||
|
'phone' => '',
|
||||||
|
'gender' => '',
|
||||||
|
'dob' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$abhaNumber = (string) ($this->findFirstByKeys($payload, ['abha_number', 'abhaNumber', 'abha_id', 'abhaId', 'healthIdNumber']) ?? '');
|
||||||
|
$abhaDigits = preg_replace('/\D/', '', $abhaNumber);
|
||||||
|
if (strlen($abhaDigits) === 14) {
|
||||||
|
$identity['abha_number'] = $abhaDigits;
|
||||||
|
}
|
||||||
|
|
||||||
|
$abhaAddress = trim((string) ($this->findFirstByKeys($payload, ['abha_address', 'abhaAddress', 'phrAddress', 'healthId']) ?? ''));
|
||||||
|
if ($abhaAddress !== '') {
|
||||||
|
$identity['abha_address'] = $abhaAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
$patientName = trim((string) ($this->findFirstByKeys($payload, ['patient_name', 'patientName', 'name', 'fullName']) ?? ''));
|
||||||
|
if ($patientName !== '') {
|
||||||
|
$identity['patient_name'] = strtoupper($patientName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$phone = preg_replace('/\D/', '', (string) ($this->findFirstByKeys($payload, ['phone', 'mobile', 'mobileNumber']) ?? ''));
|
||||||
|
if ($phone !== '') {
|
||||||
|
$identity['phone'] = $phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gender = strtoupper(substr(trim((string) ($this->findFirstByKeys($payload, ['gender', 'sex']) ?? '')), 0, 1));
|
||||||
|
if (in_array($gender, ['M', 'F', 'O'], true)) {
|
||||||
|
$identity['gender'] = $gender;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dob = trim((string) ($this->findFirstByKeys($payload, ['dob', 'dateOfBirth', 'birthDate']) ?? ''));
|
||||||
|
if ($dob !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dob)) {
|
||||||
|
$identity['dob'] = $dob;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findFirstByKeys(array $data, array $keys): ?string
|
||||||
|
{
|
||||||
|
$normalized = array_map(static fn ($k) => strtolower((string) $k), $keys);
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
$found = $this->findFirstByKeys($value, $keys);
|
||||||
|
if ($found !== null && trim($found) !== '') {
|
||||||
|
return $found;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$k = strtolower((string) $key);
|
||||||
|
if (in_array($k, $normalized, true)) {
|
||||||
|
$v = trim((string) $value);
|
||||||
|
if ($v !== '') {
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findScanSharePatientMatches(array $identity): array
|
||||||
|
{
|
||||||
|
$fields = $this->db->getFieldNames('patient_master') ?? [];
|
||||||
|
$abhaField = $this->resolveFirstExistingColumn($fields, ['abha_id', 'abha_no', 'abha', 'abha_address']);
|
||||||
|
|
||||||
|
$select = 'id,p_code,p_fname,mphone1';
|
||||||
|
if ($abhaField) {
|
||||||
|
$select .= ',' . $abhaField . ' AS patient_abha';
|
||||||
|
}
|
||||||
|
|
||||||
|
$bucket = [];
|
||||||
|
$append = static function (array $rows, string $reason) use (&$bucket): void {
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$id = (int) ($row['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (! isset($bucket[$id])) {
|
||||||
|
$row['match_reasons'] = [];
|
||||||
|
$bucket[$id] = $row;
|
||||||
|
}
|
||||||
|
if (! in_array($reason, $bucket[$id]['match_reasons'], true)) {
|
||||||
|
$bucket[$id]['match_reasons'][] = $reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($abhaField && ($identity['abha_number'] !== '' || $identity['abha_address'] !== '')) {
|
||||||
|
$abha = $identity['abha_number'] !== '' ? $identity['abha_number'] : $identity['abha_address'];
|
||||||
|
$rows = $this->db->table('patient_master')->select($select)->where($abhaField, $abha)->limit(10)->get()->getResultArray();
|
||||||
|
$append($rows, 'ABHA');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($identity['phone'] !== '') {
|
||||||
|
$rows = $this->db->table('patient_master')->select($select)->where('mphone1', $identity['phone'])->limit(10)->get()->getResultArray();
|
||||||
|
$append($rows, 'Phone');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveFirstExistingColumn(array $fields, array $candidates): ?string
|
||||||
|
{
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
if (in_array($candidate, $fields, true)) {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createPatientFromScanIdentity(array $identity, ?string $abhaField): array
|
||||||
|
{
|
||||||
|
$fields = $this->db->getFieldNames('patient_master') ?? [];
|
||||||
|
$genderDb = ($identity['gender'] ?? '') === 'F' ? 2 : 1;
|
||||||
|
$dob = (string) ($identity['dob'] ?? '');
|
||||||
|
|
||||||
|
$insertData = [
|
||||||
|
'p_fname' => strtoupper((string) ($identity['patient_name'] ?? 'ABHA PATIENT')),
|
||||||
|
'mphone1' => (string) ($identity['phone'] ?? ''),
|
||||||
|
'gender' => $genderDb,
|
||||||
|
'blood_group' => 'Not Define',
|
||||||
|
'estimate_dob' => $dob !== '' ? 0 : 1,
|
||||||
|
];
|
||||||
|
if ($dob !== '') {
|
||||||
|
$insertData['dob'] = $dob;
|
||||||
|
} else {
|
||||||
|
$insertData['age'] = 0;
|
||||||
|
$insertData['age_in_month'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($abhaField && (($identity['abha_number'] ?? '') !== '' || ($identity['abha_address'] ?? '') !== '')) {
|
||||||
|
$insertData[$abhaField] = (string) (($identity['abha_number'] ?? '') !== '' ? $identity['abha_number'] : $identity['abha_address']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$today = date('y') . date('m');
|
||||||
|
$countRow = $this->db->query("SELECT COUNT(*) as cnt FROM patient_master WHERE p_code LIKE 'P{$today}%'")->getRow();
|
||||||
|
$seq = str_pad(((int) ($countRow->cnt ?? 0)) + 1, 4, '0', STR_PAD_LEFT);
|
||||||
|
$insertData['p_code'] = 'P' . $today . $seq;
|
||||||
|
|
||||||
|
$this->db->table('patient_master')->insert($insertData);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'patient_id' => (int) $this->db->insertID(),
|
||||||
|
'p_code' => (string) $insertData['p_code'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function validateWebhookSignature()
|
private function validateWebhookSignature()
|
||||||
{
|
{
|
||||||
$secret = $this->readRuntimeSetting('EKA_WEBHOOK_SECRET');
|
$secret = $this->readRuntimeSetting('EKA_WEBHOOK_SECRET');
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use App\Controllers\BaseController;
|
use App\Controllers\BaseController;
|
||||||
|
use App\Libraries\Abdm\AbdmConnectorFactory;
|
||||||
use App\Libraries\HealthplixService;
|
use App\Libraries\HealthplixService;
|
||||||
|
use App\Models\ClinicalAuditTrailModel;
|
||||||
use App\Models\OpdModel;
|
use App\Models\OpdModel;
|
||||||
use App\Models\PaymentModel;
|
use App\Models\PaymentModel;
|
||||||
use CodeIgniter\I18n\Time;
|
use CodeIgniter\I18n\Time;
|
||||||
|
|
@ -883,6 +885,10 @@ class Opd extends BaseController
|
||||||
|
|
||||||
$docId = (int) $docId;
|
$docId = (int) $docId;
|
||||||
$opdDateSql = $this->db->escape($opdDate);
|
$opdDateSql = $this->db->escape($opdDate);
|
||||||
|
$abdmSelect = '0 as is_abdm_source';
|
||||||
|
if ($this->db->tableExists('abdm_opd_tokens')) {
|
||||||
|
$abdmSelect = "(case when exists(select 1 from abdm_opd_tokens t where t.opd_id = o.opd_id) then 1 else 0 end) as is_abdm_source";
|
||||||
|
}
|
||||||
|
|
||||||
$sql = "select d.id as doc_id, d.p_fname,
|
$sql = "select d.id as doc_id, d.p_fname,
|
||||||
group_concat(DISTINCT m.SpecName) as Spec
|
group_concat(DISTINCT m.SpecName) as Spec
|
||||||
|
|
@ -926,6 +932,7 @@ class Opd extends BaseController
|
||||||
1,0
|
1,0
|
||||||
) as has_vitals,
|
) as has_vitals,
|
||||||
coalesce(pr.queue_no,0) as queue_no
|
coalesce(pr.queue_no,0) as queue_no
|
||||||
|
," . $abdmSelect . "
|
||||||
from opd_master o
|
from opd_master o
|
||||||
join patient_master p on o.p_id=p.id
|
join patient_master p on o.p_id=p.id
|
||||||
left join opd_prescription pr on o.opd_id=pr.opd_id
|
left join opd_prescription pr on o.opd_id=pr.opd_id
|
||||||
|
|
@ -1316,6 +1323,8 @@ class Opd extends BaseController
|
||||||
'opd_status_remark' => $statusRemark,
|
'opd_status_remark' => $statusRemark,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->syncAbdmTokenAfterOpdStatus((int) $opdId, (int) $opdStatus, (int) $userId, (string) $userLabel);
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'update' => 1,
|
'update' => 1,
|
||||||
'opd_id' => (int) $opdId,
|
'opd_id' => (int) $opdId,
|
||||||
|
|
@ -1324,6 +1333,119 @@ class Opd extends BaseController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function syncAbdmTokenAfterOpdStatus(int $opdId, int $opdStatus, int $actorId, string $actorLabel): void
|
||||||
|
{
|
||||||
|
if ($opdStatus !== 2 || ! $this->db->tableExists('abdm_opd_tokens')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $this->db->query(
|
||||||
|
"SELECT id, gateway_token_id, status, queue_date
|
||||||
|
FROM abdm_opd_tokens
|
||||||
|
WHERE opd_id = " . (int) $opdId . "
|
||||||
|
AND status <> 'COMPLETED'"
|
||||||
|
)->getResultArray();
|
||||||
|
|
||||||
|
if (empty($rows)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$connector = null;
|
||||||
|
try {
|
||||||
|
$connector = AbdmConnectorFactory::make();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$connector = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$localId = (int) ($row['id'] ?? 0);
|
||||||
|
$gatewayTokenId = (int) ($row['gateway_token_id'] ?? 0);
|
||||||
|
$oldStatus = strtoupper(trim((string) ($row['status'] ?? '')));
|
||||||
|
if ($localId <= 0 || $oldStatus === 'COMPLETED') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gatewaySync = 'skipped';
|
||||||
|
$gatewayError = '';
|
||||||
|
if ($connector && $gatewayTokenId > 0) {
|
||||||
|
try {
|
||||||
|
$connector->opdTokenUpdateStatus($gatewayTokenId, 'COMPLETED');
|
||||||
|
$gatewaySync = 'ok';
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$gatewaySync = 'failed';
|
||||||
|
$gatewayError = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->table('abdm_opd_tokens')
|
||||||
|
->where('id', $localId)
|
||||||
|
->update([
|
||||||
|
'status' => 'COMPLETED',
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$meta = [
|
||||||
|
'source' => 'opd_status_visit_done',
|
||||||
|
'opd_id' => $opdId,
|
||||||
|
'queue_date' => (string) ($row['queue_date'] ?? ''),
|
||||||
|
'gateway_token_id' => $gatewayTokenId,
|
||||||
|
'gateway_sync' => $gatewaySync,
|
||||||
|
'gateway_error' => $gatewayError,
|
||||||
|
'actor' => $actorLabel,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->writeClinicalAuditTrail(
|
||||||
|
'abdm_opd_queue',
|
||||||
|
$gatewayTokenId > 0 ? (string) $gatewayTokenId : (string) $localId,
|
||||||
|
'status',
|
||||||
|
$oldStatus,
|
||||||
|
'COMPLETED | ' . json_encode($meta, JSON_UNESCAPED_SLASHES),
|
||||||
|
(string) $actorId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeClinicalAuditTrail(
|
||||||
|
string $module,
|
||||||
|
string $recordId,
|
||||||
|
string $fieldName,
|
||||||
|
?string $oldValue,
|
||||||
|
?string $newValue,
|
||||||
|
string $userId
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
if (! $this->db->tableExists('clinical_audit_trail')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actionAt = date('Y-m-d H:i:s');
|
||||||
|
$hash = hash('sha256', implode('|', [
|
||||||
|
$module,
|
||||||
|
$recordId,
|
||||||
|
$fieldName,
|
||||||
|
(string) $oldValue,
|
||||||
|
(string) $newValue,
|
||||||
|
$userId,
|
||||||
|
$actionAt,
|
||||||
|
microtime(true),
|
||||||
|
random_int(1000, 999999),
|
||||||
|
]));
|
||||||
|
|
||||||
|
(new ClinicalAuditTrailModel())->insert([
|
||||||
|
'module' => $module,
|
||||||
|
'record_id' => $recordId,
|
||||||
|
'field_name' => $fieldName,
|
||||||
|
'old_value' => $oldValue,
|
||||||
|
'new_value' => $newValue,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'action_at' => $actionAt,
|
||||||
|
'hash' => $hash,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// fail-open: audit write errors must not block OPD flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function addopd(int $pno, int $orgCaseId = 0)
|
public function addopd(int $pno, int $orgCaseId = 0)
|
||||||
{
|
{
|
||||||
$sql = "select *,if(gender=1,'Male','Female') as xgender
|
$sql = "select *,if(gender=1,'Male','Female') as xgender
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class CreateHmsGatewayCompanyMasterTable extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if ($this->db->tableExists('hms_gateway_company_master')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->query(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `hms_gateway_company_master` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`gateway_company_identifier` VARCHAR(100) NOT NULL,
|
||||||
|
`company_name` VARCHAR(255) NOT NULL,
|
||||||
|
`gateway_updated_at` DATETIME NULL DEFAULT NULL,
|
||||||
|
`first_synced_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`last_synced_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`sync_status` ENUM('active','deleted','error') NOT NULL DEFAULT 'active',
|
||||||
|
`name_normalized` VARCHAR(255) NULL DEFAULT NULL,
|
||||||
|
`payload_hash` CHAR(64) NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_gateway_company_identifier` (`gateway_company_identifier`),
|
||||||
|
KEY `idx_company_name` (`company_name`),
|
||||||
|
KEY `idx_name_normalized` (`name_normalized`),
|
||||||
|
KEY `idx_gateway_updated_at` (`gateway_updated_at`),
|
||||||
|
KEY `idx_last_synced_at` (`last_synced_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if ($this->db->tableExists('hms_gateway_company_master')) {
|
||||||
|
$this->forge->dropTable('hms_gateway_company_master', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class CreateMedShortFormulationTable extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if ($this->db->tableExists('med_short_formulation')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->query(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `med_short_formulation` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`short_formulation` VARCHAR(100) NOT NULL,
|
||||||
|
`gateway_identifier` VARCHAR(100) NULL DEFAULT NULL,
|
||||||
|
`last_synced_at` DATETIME NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_short_formulation` (`short_formulation`),
|
||||||
|
KEY `idx_gateway_identifier` (`gateway_identifier`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if ($this->db->tableExists('med_short_formulation')) {
|
||||||
|
$this->forge->dropTable('med_short_formulation', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -169,6 +169,12 @@
|
||||||
const BASE = '<?= base_url() ?>';
|
const BASE = '<?= base_url() ?>';
|
||||||
const CSRF_NAME = '<?= csrf_token() ?>';
|
const CSRF_NAME = '<?= csrf_token() ?>';
|
||||||
const REFRESH_SECS = 30;
|
const REFRESH_SECS = 30;
|
||||||
|
const STATUS_TRANSITIONS = {
|
||||||
|
PENDING: ['CALLED', 'CANCELLED'],
|
||||||
|
CALLED: ['COMPLETED', 'CANCELLED', 'PENDING'],
|
||||||
|
COMPLETED: ['PENDING'],
|
||||||
|
CANCELLED: ['PENDING']
|
||||||
|
};
|
||||||
let autoTimer = null;
|
let autoTimer = null;
|
||||||
let opdModalCurrentUrl = '';
|
let opdModalCurrentUrl = '';
|
||||||
let originalLoadFormRef = null;
|
let originalLoadFormRef = null;
|
||||||
|
|
@ -331,6 +337,10 @@
|
||||||
|
|
||||||
function renderQueue(data) {
|
function renderQueue(data) {
|
||||||
const tokens = data.data ?? data.tokens ?? [];
|
const tokens = data.data ?? data.tokens ?? [];
|
||||||
|
const statusFilter = String(document.getElementById('queueStatus')?.value || '').toUpperCase();
|
||||||
|
const viewTokens = statusFilter
|
||||||
|
? tokens
|
||||||
|
: tokens.filter(t => ['PENDING', 'CALLED'].includes(String(t.status || '').toUpperCase()));
|
||||||
const body = document.getElementById('abdmQueueBody');
|
const body = document.getElementById('abdmQueueBody');
|
||||||
const summary = document.getElementById('abdmQSummary');
|
const summary = document.getElementById('abdmQSummary');
|
||||||
|
|
||||||
|
|
@ -350,12 +360,14 @@
|
||||||
`<span class="badge rounded-pill bg-secondary px-3 py-2">${tokens.length} Total</span>`,
|
`<span class="badge rounded-pill bg-secondary px-3 py-2">${tokens.length} Total</span>`,
|
||||||
].join('');
|
].join('');
|
||||||
|
|
||||||
if (!tokens.length) {
|
if (!viewTokens.length) {
|
||||||
body.innerHTML = '<tr><td colspan="9" class="text-center py-4 text-muted small">No tokens found for this filter.</td></tr>';
|
body.innerHTML = statusFilter
|
||||||
|
? '<tr><td colspan="9" class="text-center py-4 text-muted small">No tokens found for this filter.</td></tr>'
|
||||||
|
: '<tr><td colspan="9" class="text-center py-4 text-muted small">No active queue tokens. Completed/Cancelled are hidden from active queue.</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.innerHTML = tokens.map(t => {
|
body.innerHTML = viewTokens.map(t => {
|
||||||
const id = t.id ?? 0;
|
const id = t.id ?? 0;
|
||||||
const isPend = t.status === 'PENDING';
|
const isPend = t.status === 'PENDING';
|
||||||
const isCalled = t.status === 'CALLED';
|
const isCalled = t.status === 'CALLED';
|
||||||
|
|
@ -371,7 +383,9 @@
|
||||||
hmsCell = `<a href="javascript:load_form('${esc(t.hms_profile_url ?? '')}','Patient Profile')" class="text-decoration-none">
|
hmsCell = `<a href="javascript:load_form('${esc(t.hms_profile_url ?? '')}','Patient Profile')" class="text-decoration-none">
|
||||||
<span class="badge bg-success-subtle text-success border border-success-subtle">✓ ${lbl}</span>
|
<span class="badge bg-success-subtle text-success border border-success-subtle">✓ ${lbl}</span>
|
||||||
</a>`;
|
</a>`;
|
||||||
if (t.hms_opd_url && !t.hms_opd_id) {
|
if (t.hms_opd_id && t.hms_opd_code) {
|
||||||
|
hmsCell += `<br><span class="badge bg-primary-subtle text-primary border border-primary-subtle">Booked OPD: ${esc(t.hms_opd_code)}</span>`;
|
||||||
|
} else if (t.hms_opd_url) {
|
||||||
hmsCell += `<br><a href="javascript:void(0)" onclick="abdmQOpenOpdInModal('${esc(t.hms_opd_url)}')" class="small text-primary">+ Add OPD Visit</a>`;
|
hmsCell += `<br><a href="javascript:void(0)" onclick="abdmQOpenOpdInModal('${esc(t.hms_opd_url)}')" class="small text-primary">+ Add OPD Visit</a>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -379,11 +393,15 @@
|
||||||
let actions = '';
|
let actions = '';
|
||||||
if (isPend) {
|
if (isPend) {
|
||||||
actions += `<button class="btn btn-xs btn-sm btn-outline-primary me-1" onclick="abdmQCall(${id})">Call</button>`;
|
actions += `<button class="btn btn-xs btn-sm btn-outline-primary me-1" onclick="abdmQCall(${id})">Call</button>`;
|
||||||
}
|
|
||||||
if (isPend || isCalled) {
|
|
||||||
actions += `<button class="btn btn-xs btn-sm btn-outline-success me-1" onclick="abdmQComplete(${id})">Complete</button>`;
|
|
||||||
actions += `<button class="btn btn-xs btn-sm btn-outline-danger me-1" onclick="abdmQCancel(${id})">Cancel</button>`;
|
actions += `<button class="btn btn-xs btn-sm btn-outline-danger me-1" onclick="abdmQCancel(${id})">Cancel</button>`;
|
||||||
}
|
}
|
||||||
|
if (isCalled) {
|
||||||
|
actions += `<button class="btn btn-xs btn-sm btn-outline-success me-1" onclick="abdmQComplete(${id})">Complete</button>`;
|
||||||
|
actions += `<button class="btn btn-xs btn-sm btn-outline-secondary me-1" onclick="abdmQReopen(${id})">Mark Pending</button>`;
|
||||||
|
}
|
||||||
|
if (t.status === 'COMPLETED' || t.status === 'CANCELLED') {
|
||||||
|
actions += `<button class="btn btn-xs btn-sm btn-outline-secondary me-1" onclick="abdmQReopen(${id})">Reopen</button>`;
|
||||||
|
}
|
||||||
if (!isLinked && (isPend || isCalled)) {
|
if (!isLinked && (isPend || isCalled)) {
|
||||||
const pl = encodePayload({
|
const pl = encodePayload({
|
||||||
id: t.id, abha_number: t.abha_number ?? '', abha_address: t.abha_address ?? '',
|
id: t.id, abha_number: t.abha_number ?? '', abha_address: t.abha_address ?? '',
|
||||||
|
|
@ -398,7 +416,7 @@
|
||||||
actions += `<button class="btn btn-xs btn-sm btn-primary" onclick="abdmQRegister('${pl}')">Register OPD</button>`;
|
actions += `<button class="btn btn-xs btn-sm btn-primary" onclick="abdmQRegister('${pl}')">Register OPD</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<tr id="abdmqrow-${id}" class="${isLinked ? 'table-success' : ''}">
|
return `<tr id="abdmqrow-${id}" data-status="${esc(String(t.status || 'PENDING').toUpperCase())}" class="${isLinked ? 'table-success' : ''}">
|
||||||
<td><span class="abdm-q-token-num">#${t.token_number ?? id}</span></td>
|
<td><span class="abdm-q-token-num">#${t.token_number ?? id}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="fw-semibold small">${esc(t.patient_name ?? '—')}</div>
|
<div class="fw-semibold small">${esc(t.patient_name ?? '—')}</div>
|
||||||
|
|
@ -416,6 +434,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatus(tokenId, status) {
|
function setStatus(tokenId, status) {
|
||||||
|
const row = document.getElementById('abdmqrow-' + tokenId);
|
||||||
|
const currentStatus = row ? String(row.getAttribute('data-status') || '').trim().toUpperCase() : '';
|
||||||
|
if (currentStatus && currentStatus !== status) {
|
||||||
|
const next = STATUS_TRANSITIONS[currentStatus] || [];
|
||||||
|
if (next.indexOf(status) === -1) {
|
||||||
|
alert('Invalid transition: ' + currentStatus + ' -> ' + status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
post(BASE + 'AbdmOpdQueue/token_status/' + tokenId, { status })
|
post(BASE + 'AbdmOpdQueue/token_status/' + tokenId, { status })
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
|
|
@ -433,6 +461,7 @@
|
||||||
|
|
||||||
window.abdmQCall = id => setStatus(id, 'CALLED');
|
window.abdmQCall = id => setStatus(id, 'CALLED');
|
||||||
window.abdmQComplete = id => setStatus(id, 'COMPLETED');
|
window.abdmQComplete = id => setStatus(id, 'COMPLETED');
|
||||||
|
window.abdmQReopen = id => setStatus(id, 'PENDING');
|
||||||
window.abdmQCancel = id => { if (confirm('Cancel token #' + id + '?')) setStatus(id, 'CANCELLED'); };
|
window.abdmQCancel = id => { if (confirm('Cancel token #' + id + '?')) setStatus(id, 'CANCELLED'); };
|
||||||
|
|
||||||
window.abdmQRegister = function (payloadEncoded) {
|
window.abdmQRegister = function (payloadEncoded) {
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="small text-muted" id="abdmSandboxStatus">Ready</div>
|
<div class="small text-muted" id="abdmSandboxStatus">Ready</div>
|
||||||
|
<div class="mt-2 d-none" id="abdmScanResolvePane"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -378,6 +379,7 @@
|
||||||
var sandboxCard = document.getElementById('abdmSandboxCard');
|
var sandboxCard = document.getElementById('abdmSandboxCard');
|
||||||
var sandboxContext = document.getElementById('abdmSandboxContext');
|
var sandboxContext = document.getElementById('abdmSandboxContext');
|
||||||
var sandboxStatus = document.getElementById('abdmSandboxStatus');
|
var sandboxStatus = document.getElementById('abdmSandboxStatus');
|
||||||
|
var scanResolvePane = document.getElementById('abdmScanResolvePane');
|
||||||
var sandboxAbhaInput = document.getElementById('tb_abha_id');
|
var sandboxAbhaInput = document.getElementById('tb_abha_id');
|
||||||
var sandboxQrPayloadInput = document.getElementById('tb_abdm_qr_payload');
|
var sandboxQrPayloadInput = document.getElementById('tb_abdm_qr_payload');
|
||||||
var sandboxPurposeCodeInput = document.getElementById('tb_abdm_purpose_code');
|
var sandboxPurposeCodeInput = document.getElementById('tb_abdm_purpose_code');
|
||||||
|
|
@ -578,6 +580,110 @@
|
||||||
sandboxStatus.textContent = msg || 'Ready';
|
sandboxStatus.textContent = msg || 'Ready';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearScanResolvePane() {
|
||||||
|
if (!scanResolvePane) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scanResolvePane.classList.add('d-none');
|
||||||
|
scanResolvePane.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderScanResolvePane(queueId, identity, matches) {
|
||||||
|
if (!scanResolvePane) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
identity = identity || {};
|
||||||
|
matches = Array.isArray(matches) ? matches : [];
|
||||||
|
|
||||||
|
var cardHtml = '<div class="border rounded p-2 bg-light">';
|
||||||
|
cardHtml += '<div class="small fw-semibold mb-1">Scan User ABHA QR Result</div>';
|
||||||
|
cardHtml += '<div class="small text-muted mb-2">Queue ID: ' + (queueId || '-') + '</div>';
|
||||||
|
cardHtml += '<div class="small mb-1"><strong>Name:</strong> ' + ((identity.patient_name || '-') + '') + '</div>';
|
||||||
|
cardHtml += '<div class="small mb-1"><strong>ABHA:</strong> ' + ((identity.abha_number || identity.abha_address || '-') + '') + '</div>';
|
||||||
|
cardHtml += '<div class="small mb-2"><strong>Phone:</strong> ' + ((identity.phone || '-') + '') + '</div>';
|
||||||
|
|
||||||
|
if (matches.length > 0) {
|
||||||
|
cardHtml += '<div class="small fw-semibold mb-1">Matching HMS Patients</div>';
|
||||||
|
matches.forEach(function (m) {
|
||||||
|
var mLabel = ((m.p_code || 'UHID') + ' - ' + (m.p_fname || 'Unnamed'));
|
||||||
|
cardHtml += '<div class="d-flex justify-content-between align-items-center border rounded p-1 mb-1 bg-white">';
|
||||||
|
cardHtml += '<div class="small">' + mLabel + '</div>';
|
||||||
|
cardHtml += '<button type="button" class="btn btn-sm btn-outline-primary" data-scan-use-existing="1" data-patient-id="' + (m.id || 0) + '">Use</button>';
|
||||||
|
cardHtml += '</div>';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cardHtml += '<div class="small text-muted mb-2">No existing patient matched from ABHA/phone.</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
cardHtml += '<div class="d-flex gap-2 mt-2">';
|
||||||
|
cardHtml += '<button type="button" class="btn btn-sm btn-success" id="btnScanCreateNewPatient">Create New Patient</button>';
|
||||||
|
cardHtml += '<button type="button" class="btn btn-sm btn-outline-secondary" id="btnScanClearPane">Clear</button>';
|
||||||
|
cardHtml += '</div>';
|
||||||
|
cardHtml += '</div>';
|
||||||
|
|
||||||
|
scanResolvePane.innerHTML = cardHtml;
|
||||||
|
scanResolvePane.classList.remove('d-none');
|
||||||
|
|
||||||
|
scanResolvePane.querySelectorAll('[data-scan-use-existing="1"]').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var patientId = parseInt(btn.getAttribute('data-patient-id') || '0', 10);
|
||||||
|
if (!patientId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSandboxStatus('Linking selected HMS patient...', 'muted');
|
||||||
|
post('<?= base_url('AbdmGateway/scan_share_link_patient') ?>/' + queueId, {
|
||||||
|
action: 'link_existing',
|
||||||
|
existing_patient_id: patientId,
|
||||||
|
abha_number: (identity.abha_number || ''),
|
||||||
|
abha_address: (identity.abha_address || ''),
|
||||||
|
patient_name: (identity.patient_name || ''),
|
||||||
|
phone: (identity.phone || ''),
|
||||||
|
gender: (identity.gender || ''),
|
||||||
|
dob: (identity.dob || '')
|
||||||
|
}, function (resp) {
|
||||||
|
if (!resp || parseInt(resp.ok || 0, 10) !== 1) {
|
||||||
|
setSandboxStatus((resp && resp.error_text) ? resp.error_text : 'Unable to link patient.', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearScanResolvePane();
|
||||||
|
setSandboxStatus('Patient linked: ' + (resp.p_code || resp.patient_id || '-') + '. You can open profile now.', 'success');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var createBtn = document.getElementById('btnScanCreateNewPatient');
|
||||||
|
if (createBtn) {
|
||||||
|
createBtn.addEventListener('click', function () {
|
||||||
|
setSandboxStatus('Creating new patient from scan result...', 'muted');
|
||||||
|
post('<?= base_url('AbdmGateway/scan_share_link_patient') ?>/' + queueId, {
|
||||||
|
action: 'create_new',
|
||||||
|
abha_number: (identity.abha_number || ''),
|
||||||
|
abha_address: (identity.abha_address || ''),
|
||||||
|
patient_name: (identity.patient_name || ''),
|
||||||
|
phone: (identity.phone || ''),
|
||||||
|
gender: (identity.gender || ''),
|
||||||
|
dob: (identity.dob || '')
|
||||||
|
}, function (resp) {
|
||||||
|
if (!resp || parseInt(resp.ok || 0, 10) !== 1) {
|
||||||
|
setSandboxStatus((resp && resp.error_text) ? resp.error_text : 'Unable to create patient.', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearScanResolvePane();
|
||||||
|
setSandboxStatus('New patient created: ' + (resp.p_code || resp.patient_id || '-') + '.', 'success');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var clearBtn = document.getElementById('btnScanClearPane');
|
||||||
|
if (clearBtn) {
|
||||||
|
clearBtn.addEventListener('click', function () {
|
||||||
|
clearScanResolvePane();
|
||||||
|
setSandboxStatus('Ready', 'muted');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function currentSandboxContext() {
|
function currentSandboxContext() {
|
||||||
if (!sandboxRow) {
|
if (!sandboxRow) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -620,6 +726,7 @@
|
||||||
sandboxPurposeCodeInput.value = sandboxPurposeCodeInput.value || 'CAREMGT';
|
sandboxPurposeCodeInput.value = sandboxPurposeCodeInput.value || 'CAREMGT';
|
||||||
sandboxConsentHandleInput.value = '';
|
sandboxConsentHandleInput.value = '';
|
||||||
sandboxClaimDocumentInput.value = '';
|
sandboxClaimDocumentInput.value = '';
|
||||||
|
clearScanResolvePane();
|
||||||
|
|
||||||
var opdId = row.getAttribute('data-opd-id') || '0';
|
var opdId = row.getAttribute('data-opd-id') || '0';
|
||||||
var opdSessionId = row.getAttribute('data-opd-session-id') || '0';
|
var opdSessionId = row.getAttribute('data-opd-session-id') || '0';
|
||||||
|
|
@ -761,8 +868,61 @@
|
||||||
sandboxCard.classList.add('d-none');
|
sandboxCard.classList.add('d-none');
|
||||||
}
|
}
|
||||||
sandboxRow = null;
|
sandboxRow = null;
|
||||||
|
clearScanResolvePane();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function pollScanLookupStatus(queueId, onTick, onDone) {
|
||||||
|
if (!queueId || queueId <= 0) {
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone({ ok: 0, status: 'unknown', error_text: 'Missing queue id' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tries = 0;
|
||||||
|
var maxTries = 10;
|
||||||
|
var timer = setInterval(function () {
|
||||||
|
tries++;
|
||||||
|
$.getJSON('<?= base_url('AbdmGateway/scan_share_lookup_status') ?>/' + queueId, function (resp) {
|
||||||
|
if (typeof onTick === 'function') {
|
||||||
|
onTick(resp, tries, maxTries);
|
||||||
|
}
|
||||||
|
if (!resp || parseInt(resp.ok || 0, 10) !== 1) {
|
||||||
|
if (tries >= maxTries) {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone(resp || { ok: 0, status: 'failed', error_text: 'Status lookup failed' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var status = (resp.status || '').toString().toLowerCase();
|
||||||
|
if (parseInt(resp.done || 0, 10) === 1 || status === 'sent' || status === 'failed') {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone(resp);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tries >= maxTries) {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone(resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).fail(function () {
|
||||||
|
if (tries >= maxTries) {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone({ ok: 0, status: 'failed', error_text: 'Status polling failed' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('tb_btn_abdm_scan_lookup').addEventListener('click', function () {
|
document.getElementById('tb_btn_abdm_scan_lookup').addEventListener('click', function () {
|
||||||
var qrPayload = (sandboxQrPayloadInput.value || '').trim();
|
var qrPayload = (sandboxQrPayloadInput.value || '').trim();
|
||||||
if (!qrPayload) {
|
if (!qrPayload) {
|
||||||
|
|
@ -780,7 +940,48 @@
|
||||||
if (res.abha_id_hint) {
|
if (res.abha_id_hint) {
|
||||||
sandboxAbhaInput.value = (res.abha_id_hint || '').toString().trim();
|
sandboxAbhaInput.value = (res.abha_id_hint || '').toString().trim();
|
||||||
}
|
}
|
||||||
setSandboxStatus('Scan lookup queued. Queue ID: ' + (res.queue_id || '-'), 'success');
|
var queueId = parseInt(res.queue_id || '0', 10);
|
||||||
|
setSandboxStatus('Scan lookup queued. Queue ID: ' + (queueId || '-') + '. Checking status...', 'success');
|
||||||
|
|
||||||
|
pollScanLookupStatus(queueId, function (scanResp, currentTry, maxTry) {
|
||||||
|
if (!scanResp || parseInt(scanResp.ok || 0, 10) !== 1) {
|
||||||
|
setSandboxStatus('Scan lookup status check (' + currentTry + '/' + maxTry + ')...', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var st = (scanResp.status || 'pending').toString();
|
||||||
|
setSandboxStatus('Scan lookup status: ' + st.toUpperCase() + ' (' + currentTry + '/' + maxTry + ')', st === 'failed' ? 'danger' : 'muted');
|
||||||
|
}, function (finalResp) {
|
||||||
|
if (!finalResp || parseInt(finalResp.ok || 0, 10) !== 1) {
|
||||||
|
setSandboxStatus((finalResp && finalResp.error_text) ? finalResp.error_text : 'Scan lookup status unresolved. Please check Bridge Log.', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var fs = (finalResp.status || '').toString().toLowerCase();
|
||||||
|
if (fs === 'sent') {
|
||||||
|
setSandboxStatus('Scan lookup sent to bridge successfully. Queue ID: ' + queueId + '. Resolving patient...', 'success');
|
||||||
|
$.getJSON('<?= base_url('AbdmGateway/scan_share_resolve_patient') ?>/' + queueId, function (resolveResp) {
|
||||||
|
if (!resolveResp || parseInt(resolveResp.ok || 0, 10) !== 1) {
|
||||||
|
setSandboxStatus((resolveResp && resolveResp.error_text) ? resolveResp.error_text : 'Scan result received but patient data not available yet.', 'warning');
|
||||||
|
clearScanResolvePane();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolveResp.identity && resolveResp.identity.abha_number) {
|
||||||
|
sandboxAbhaInput.value = (resolveResp.identity.abha_number || '').toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderScanResolvePane(queueId, resolveResp.identity || {}, resolveResp.matches || []);
|
||||||
|
setSandboxStatus('Resolve patient using scan result below.', 'success');
|
||||||
|
}).fail(function () {
|
||||||
|
setSandboxStatus('Scan lookup sent, but patient resolution fetch failed.', 'warning');
|
||||||
|
});
|
||||||
|
} else if (fs === 'failed') {
|
||||||
|
setSandboxStatus('Scan lookup failed: ' + (finalResp.last_error || 'Unknown bridge error'), 'danger');
|
||||||
|
clearScanResolvePane();
|
||||||
|
} else {
|
||||||
|
setSandboxStatus('Scan lookup is still ' + fs.toUpperCase() + '. You can recheck from Bridge Log.', 'warning');
|
||||||
|
clearScanResolvePane();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
<th>Age</th>
|
<th>Age</th>
|
||||||
<th>Last Visit</th>
|
<th>Last Visit</th>
|
||||||
<th>Insurance</th>
|
<th>Insurance</th>
|
||||||
<th>ABHA</th>
|
|
||||||
<th>Patient History</th>
|
<th>Patient History</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -27,19 +26,6 @@
|
||||||
<td><?= esc(get_age_1($data[$i]->dob ?? null, $data[$i]->age ?? '', $data[$i]->age_in_month ?? '', $data[$i]->estimate_dob ?? '', $data[$i]->Last_Visit ?? null)) ?></td>
|
<td><?= esc(get_age_1($data[$i]->dob ?? null, $data[$i]->age ?? '', $data[$i]->age_in_month ?? '', $data[$i]->estimate_dob ?? '', $data[$i]->Last_Visit ?? null)) ?></td>
|
||||||
<td><?=$data[$i]->Last_Visit ?></td>
|
<td><?=$data[$i]->Last_Visit ?></td>
|
||||||
<td><?php echo ($data[$i]->insurance_id==0 ? 'Self': 'Insuranced'); ?></td>
|
<td><?php echo ($data[$i]->insurance_id==0 ? 'Self': 'Insuranced'); ?></td>
|
||||||
<td><?php
|
|
||||||
$abhaId = trim((string)($data[$i]->abha_id ?? ''));
|
|
||||||
$linkedAt = $data[$i]->abdm_linked_at ?? null;
|
|
||||||
if ($abhaId !== '') {
|
|
||||||
if (!empty($linkedAt)) {
|
|
||||||
echo '<span class="label label-success"><i class="fa fa-check-circle"></i> Verified ABHA</span>';
|
|
||||||
} else {
|
|
||||||
echo '<span class="label label-warning"><i class="fa fa-exclamation-triangle"></i> Has ABHA</span>';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
echo '<span class="label label-default">No ABHA</span>';
|
|
||||||
}
|
|
||||||
?></td>
|
|
||||||
<td>
|
<td>
|
||||||
<a href="javascript:load_form('<?= base_url('billing/patient/show_profile_opd') ?>/<?=$data[$i]->id ?>/1');" class="btn btn-info btn-xs">
|
<a href="javascript:load_form('<?= base_url('billing/patient/show_profile_opd') ?>/<?=$data[$i]->id ?>/1');" class="btn btn-info btn-xs">
|
||||||
<span class="fa fa-history"></span> Patient History
|
<span class="fa fa-history"></span> Patient History
|
||||||
|
|
@ -56,7 +42,6 @@
|
||||||
<th>Age</th>
|
<th>Age</th>
|
||||||
<th>Last Visit</th>
|
<th>Last Visit</th>
|
||||||
<th>Insurance</th>
|
<th>Insurance</th>
|
||||||
<th>ABHA</th>
|
|
||||||
<th>Patient History</th>
|
<th>Patient History</th>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
|
|
|
||||||
|
|
@ -3939,6 +3939,59 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pollScanLookupStatus(queueId, onTick, onDone) {
|
||||||
|
if (!queueId || queueId <= 0) {
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone({ ok: 0, status: 'unknown', error_text: 'Missing queue id' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tries = 0;
|
||||||
|
var maxTries = 10;
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
tries++;
|
||||||
|
$.getJSON('<?= base_url('AbdmGateway/scan_share_lookup_status') ?>/' + queueId, function(resp) {
|
||||||
|
if (typeof onTick === 'function') {
|
||||||
|
onTick(resp, tries, maxTries);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp || parseInt(resp.ok || 0, 10) !== 1) {
|
||||||
|
if (tries >= maxTries) {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone(resp || { ok: 0, status: 'failed', error_text: 'Status lookup failed' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var status = (resp.status || '').toString().toLowerCase();
|
||||||
|
if (parseInt(resp.done || 0, 10) === 1 || status === 'sent' || status === 'failed') {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone(resp);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tries >= maxTries) {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone(resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).fail(function() {
|
||||||
|
if (tries >= maxTries) {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone({ ok: 0, status: 'failed', error_text: 'Status polling failed' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
// Scan & Share Lookup
|
// Scan & Share Lookup
|
||||||
var btnScan = document.getElementById('btn_ipd_abdm_scan_lookup');
|
var btnScan = document.getElementById('btn_ipd_abdm_scan_lookup');
|
||||||
if (btnScan) {
|
if (btnScan) {
|
||||||
|
|
@ -3953,7 +4006,36 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
|
||||||
var addr = document.getElementById('ipd_abha_address');
|
var addr = document.getElementById('ipd_abha_address');
|
||||||
if (addr) { addr.value = resp.abha_address; }
|
if (addr) { addr.value = resp.abha_address; }
|
||||||
}
|
}
|
||||||
setStatus(resp && resp.message ? resp.message : 'Lookup done.');
|
|
||||||
|
if (!resp || parseInt(resp.ok || 0, 10) !== 1) {
|
||||||
|
setStatus((resp && resp.error_text) ? resp.error_text : 'Scan lookup failed.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var queueId = parseInt(resp.queue_id || '0', 10);
|
||||||
|
setStatus((resp && resp.message ? resp.message : 'Lookup queued.') + ' Queue ID: ' + (queueId || '-') + '. Checking status...', false);
|
||||||
|
|
||||||
|
pollScanLookupStatus(queueId, function(scanResp, currentTry, maxTry) {
|
||||||
|
if (!scanResp || parseInt(scanResp.ok || 0, 10) !== 1) {
|
||||||
|
setStatus('Scan lookup status check (' + currentTry + '/' + maxTry + ')...', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var st = (scanResp.status || 'pending').toString();
|
||||||
|
setStatus('Scan lookup status: ' + st.toUpperCase() + ' (' + currentTry + '/' + maxTry + ')', st === 'failed');
|
||||||
|
}, function(finalResp) {
|
||||||
|
if (!finalResp || parseInt(finalResp.ok || 0, 10) !== 1) {
|
||||||
|
setStatus((finalResp && finalResp.error_text) ? finalResp.error_text : 'Scan lookup status unresolved. Please check Bridge Log.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var fs = (finalResp.status || '').toString().toLowerCase();
|
||||||
|
if (fs === 'sent') {
|
||||||
|
setStatus('Scan lookup sent successfully. Queue ID: ' + queueId, false);
|
||||||
|
} else if (fs === 'failed') {
|
||||||
|
setStatus('Scan lookup failed: ' + (finalResp.last_error || 'Unknown bridge error'), true);
|
||||||
|
} else {
|
||||||
|
setStatus('Scan lookup is still ' + fs.toUpperCase() + '. Please check Bridge Log.', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ $mergedGroups = [
|
||||||
$patientGenderText = $patientGenderCode === 1 ? 'Male' : ($patientGenderCode === 2 ? 'Female' : ($patientGenderCode === 3 ? 'Other' : ''));
|
$patientGenderText = $patientGenderCode === 1 ? 'Male' : ($patientGenderCode === 2 ? 'Female' : ($patientGenderCode === 3 ? 'Other' : ''));
|
||||||
$patientAgeGender = trim($patientAgeText . ($patientGenderText !== '' ? ' / ' . $patientGenderText : ''));
|
$patientAgeGender = trim($patientAgeText . ($patientGenderText !== '' ? ' / ' . $patientGenderText : ''));
|
||||||
$patientDisplayName = esc($patientRawName . ($patientAgeGender !== '' ? ' (' . $patientAgeGender . ')' : ''));
|
$patientDisplayName = esc($patientRawName . ($patientAgeGender !== '' ? ' (' . $patientAgeGender . ')' : ''));
|
||||||
|
$isAbdmSource = (int) ($row->is_abdm_source ?? 0) === 1;
|
||||||
$tabType = $group['tabType'];
|
$tabType = $group['tabType'];
|
||||||
$status = $group['status'];
|
$status = $group['status'];
|
||||||
?>
|
?>
|
||||||
|
|
@ -115,7 +116,12 @@ $mergedGroups = [
|
||||||
data-has-prescription="<?= $showFullActions ? 1 : 0 ?>">
|
data-has-prescription="<?= $showFullActions ? 1 : 0 ?>">
|
||||||
<td><span class="badge <?= esc($group['badge']) ?>"><?= esc($group['label']) ?></span></td>
|
<td><span class="badge <?= esc($group['badge']) ?>"><?= esc($group['label']) ?></span></td>
|
||||||
<td><?= esc($row->opd_code ?? '') ?></td>
|
<td><?= esc($row->opd_code ?? '') ?></td>
|
||||||
<td><?= $patientDisplayName ?></td>
|
<td>
|
||||||
|
<?= $patientDisplayName ?>
|
||||||
|
<?php if ($isAbdmSource) : ?>
|
||||||
|
<span class="badge bg-primary ms-1">ABDM</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
<td data-order="<?= esc($queueDisplay !== '' ? $queueDisplay : 'ZZZZ') ?>"><?= esc($queueDisplay) ?></td>
|
<td data-order="<?= esc($queueDisplay !== '' ? $queueDisplay : 'ZZZZ') ?>"><?= esc($queueDisplay) ?></td>
|
||||||
<td><?= esc($row->p_code ?? '') ?></td>
|
<td><?= esc($row->p_code ?? '') ?></td>
|
||||||
<td><?= esc($row->opd_type ?? '') ?> / Amt: <?= esc($row->opd_fee_amount ?? '') ?></td>
|
<td><?= esc($row->opd_type ?? '') ?> / Amt: <?= esc($row->opd_fee_amount ?? '') ?></td>
|
||||||
|
|
|
||||||
|
|
@ -5789,6 +5789,59 @@
|
||||||
scheduleAutoSave();
|
scheduleAutoSave();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function pollScanLookupStatus(queueId, onTick, onDone) {
|
||||||
|
if (!queueId || queueId <= 0) {
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone({ ok: 0, status: 'unknown', error_text: 'Missing queue id' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tries = 0;
|
||||||
|
var maxTries = 10;
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
tries++;
|
||||||
|
$.getJSON('<?= base_url('AbdmGateway/scan_share_lookup_status') ?>/' + queueId, function(resp) {
|
||||||
|
if (typeof onTick === 'function') {
|
||||||
|
onTick(resp, tries, maxTries);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp || parseInt(resp.ok || 0, 10) !== 1) {
|
||||||
|
if (tries >= maxTries) {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone(resp || { ok: 0, status: 'failed', error_text: 'Status lookup failed' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var status = (resp.status || '').toString().toLowerCase();
|
||||||
|
if (parseInt(resp.done || 0, 10) === 1 || status === 'sent' || status === 'failed') {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone(resp);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tries >= maxTries) {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone(resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).fail(function() {
|
||||||
|
if (tries >= maxTries) {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (typeof onDone === 'function') {
|
||||||
|
onDone({ ok: 0, status: 'failed', error_text: 'Status polling failed' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
$('#btn_abdm_scan_lookup').on('click', function() {
|
$('#btn_abdm_scan_lookup').on('click', function() {
|
||||||
var qrPayload = ($('#abdm_qr_payload').val() || '').toString().trim();
|
var qrPayload = ($('#abdm_qr_payload').val() || '').toString().trim();
|
||||||
if (!qrPayload) {
|
if (!qrPayload) {
|
||||||
|
|
@ -5806,7 +5859,30 @@
|
||||||
if (data.abha_id_hint) {
|
if (data.abha_id_hint) {
|
||||||
$('#abha_address').val(data.abha_id_hint).trigger('input');
|
$('#abha_address').val(data.abha_id_hint).trigger('input');
|
||||||
}
|
}
|
||||||
setAbdmStatus('Scan lookup queued. Queue ID: ' + (data.queue_id || '-'), 'success');
|
var queueId = parseInt(data.queue_id || '0', 10);
|
||||||
|
setAbdmStatus('Scan lookup queued. Queue ID: ' + (queueId || '-') + '. Checking status...', 'success');
|
||||||
|
|
||||||
|
pollScanLookupStatus(queueId, function(scanResp, currentTry, maxTry) {
|
||||||
|
if (!scanResp || parseInt(scanResp.ok || 0, 10) !== 1) {
|
||||||
|
setAbdmStatus('Scan lookup status check (' + currentTry + '/' + maxTry + ')...', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var st = (scanResp.status || 'pending').toString();
|
||||||
|
setAbdmStatus('Scan lookup status: ' + st.toUpperCase() + ' (' + currentTry + '/' + maxTry + ')', st === 'failed' ? 'danger' : 'muted');
|
||||||
|
}, function(finalResp) {
|
||||||
|
if (!finalResp || parseInt(finalResp.ok || 0, 10) !== 1) {
|
||||||
|
setAbdmStatus((finalResp && finalResp.error_text) ? finalResp.error_text : 'Scan lookup status unresolved. Please check Bridge Log.', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var fs = (finalResp.status || '').toString().toLowerCase();
|
||||||
|
if (fs === 'sent') {
|
||||||
|
setAbdmStatus('Scan lookup sent successfully. Queue ID: ' + queueId, 'success');
|
||||||
|
} else if (fs === 'failed') {
|
||||||
|
setAbdmStatus('Scan lookup failed: ' + (finalResp.last_error || 'Unknown bridge error'), 'danger');
|
||||||
|
} else {
|
||||||
|
setAbdmStatus('Scan lookup is still ' + fs.toUpperCase() + '. You can recheck from Bridge Log.', 'warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,78 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body pt-3">
|
<div class="card-body pt-3">
|
||||||
|
<div id="company_master_msg" class="mb-2"></div>
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h5 class="card-title mb-0">Company Master</h5>
|
<h5 class="card-title mb-0">Company Master</h5>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button onclick="syncCompanyMastersFromApi();" type="button" class="btn btn-outline-success btn-sm">Sync From API</button>
|
||||||
<button onclick="load_form_div('<?= base_url('Product_master/CompanyEdit/0') ?>','test_div','Company : New Company :Pharmacy');" type="button" class="btn btn-primary btn-sm">Add New</button>
|
<button onclick="load_form_div('<?= base_url('Product_master/CompanyEdit/0') ?>','test_div','Company : New Company :Pharmacy');" type="button" class="btn btn-primary btn-sm">Add New</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<div id="company_list" class="table-responsive" style="max-height:500px;overflow-y:auto;">
|
<div class="table-responsive">
|
||||||
<?= view('medical/company_master_sub', ['med_company' => $med_company ?? []]) ?>
|
<table id="company_report_list" class="table table-bordered table-striped table-sm align-middle w-100">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Company Name</th>
|
||||||
|
<th>Person / Phone</th>
|
||||||
|
<th style="width:90px;">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6" id="test_div"></div>
|
<div class="col-lg-6" id="test_div"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (!window.jQuery || !$.fn.DataTable) return;
|
||||||
|
|
||||||
|
$('#company_report_list').DataTable({
|
||||||
|
processing : true,
|
||||||
|
serverSide : true,
|
||||||
|
destroy : true,
|
||||||
|
ajax: {
|
||||||
|
url : '<?= base_url('product_master/CompanyListData') ?>',
|
||||||
|
type: 'GET'
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ data: 0, title: 'Company Name' },
|
||||||
|
{ data: 1, title: 'Person / Phone' },
|
||||||
|
{ data: 2, title: 'Action', orderable: false, searchable: false }
|
||||||
|
],
|
||||||
|
pageLength: 25,
|
||||||
|
order: [[0, 'asc']],
|
||||||
|
language: {
|
||||||
|
processing: '<span class="spinner-border spinner-border-sm me-2"></span>Loading…'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
function refreshCompanyMasterList() {
|
||||||
|
if (window.jQuery && $.fn.DataTable && $.fn.DataTable.isDataTable('#company_report_list')) {
|
||||||
|
$('#company_report_list').DataTable().ajax.reload(null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncCompanyMastersFromApi() {
|
||||||
|
$('#company_master_msg').html('<div class="alert alert-info mb-0"><span class="spinner-border spinner-border-sm me-2"></span>Syncing company and formulation masters from gateway API…</div>');
|
||||||
|
$.ajax({
|
||||||
|
url : '<?= base_url('product_master/SyncDrugMasters') ?>',
|
||||||
|
type : 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
success : function (response) {
|
||||||
|
$('#company_master_msg').html(response.show_text || '');
|
||||||
|
refreshCompanyMasterList();
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$('#company_master_msg').html('<div class="alert alert-danger mb-0">Unable to sync masters.</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,6 @@
|
||||||
<table id="company_report_list" class="table table-bordered table-striped table-sm align-middle">
|
<?php
|
||||||
<thead>
|
// This sub-view is kept for backward compatibility but is no longer used
|
||||||
<tr>
|
// to render list data. The company list is now served via server-side
|
||||||
<th>Company Name</th>
|
// DataTables AJAX (product_master/CompanyListData).
|
||||||
<th>Person/Phone</th>
|
?>
|
||||||
<th style="width:90px;">Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach (($med_company ?? []) as $row): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= esc($row->company_name ?? '') ?></td>
|
|
||||||
<td><?= esc(($row->contact_person_name ?? '') . '/' . ($row->contact_phone_no ?? '')) ?></td>
|
|
||||||
<td>
|
|
||||||
<button onclick="load_form_div('<?= base_url('Product_master/CompanyEdit/' . (int) ($row->id ?? 0)) ?>','test_div','Company :<?= esc(($row->company_name ?? '')) ?>:Pharmacy');" type="button" class="btn btn-primary btn-sm">Edit</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
if (window.jQuery && $.fn.DataTable) {
|
|
||||||
if ($.fn.DataTable.isDataTable('#company_report_list')) {
|
|
||||||
$('#company_report_list').DataTable().destroy();
|
|
||||||
}
|
|
||||||
$('#company_report_list').DataTable({
|
|
||||||
pageLength: 25,
|
|
||||||
order: [[0, 'asc']]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
|
||||||
60
app/Views/medical/formulation_edit.php
Normal file
60
app/Views/medical/formulation_edit.php
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
$fid = 0;
|
||||||
|
$name = '';
|
||||||
|
|
||||||
|
if (!empty($med_formulation ?? [])) {
|
||||||
|
$row = $med_formulation[0];
|
||||||
|
$fid = (int) ($row->id ?? 0);
|
||||||
|
// support both field name variants
|
||||||
|
$name = (string) ($row->formulation_length ?? $row->formulation ?? '');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Formulation</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div id="formulation_msg"></div>
|
||||||
|
|
||||||
|
<form id="formulation-form" class="row g-2" method="post" action="javascript:void(0)">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" id="hid_fid" name="hid_fid" value="<?= esc((string) $fid) ?>">
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label">Formulation Name</label>
|
||||||
|
<input class="form-control" name="input_formulation_name" id="input_formulation_name"
|
||||||
|
placeholder="e.g. Tablet, Syrup, Capsule…" type="text" value="<?= esc($name) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="button" class="btn btn-primary" id="btn_formulation_update" accesskey="A">Add & Update Formulation</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function showMessage(html) {
|
||||||
|
$('#formulation_msg').html(html || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#btn_formulation_update').off('click').on('click', function () {
|
||||||
|
$.post('<?= base_url('Product_master/FormulationUpdate') ?>', $('#formulation-form').serialize(), function (data) {
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
showMessage('<div class="alert alert-danger mb-0">Unexpected response.</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showMessage(data.show_text || '');
|
||||||
|
if ((data.insertid || 0) > 0) {
|
||||||
|
$('#hid_fid').val(data.insertid);
|
||||||
|
if (typeof refreshFormulationMasterList === 'function') {
|
||||||
|
refreshFormulationMasterList();
|
||||||
|
}
|
||||||
|
load_form_div('<?= base_url('Product_master/FormulationEdit') ?>/' + data.insertid, 'test_div_form');
|
||||||
|
}
|
||||||
|
}, 'json').fail(function () {
|
||||||
|
showMessage('<div class="alert alert-danger mb-0">Unable to update formulation.</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
75
app/Views/medical/formulation_master.php
Normal file
75
app/Views/medical/formulation_master.php
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div id="formulation_master_msg" class="mb-2"></div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h5 class="card-title mb-0">Formulation Master</h5>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button onclick="syncFormulationMastersFromApi();" type="button" class="btn btn-outline-success btn-sm">Sync From API</button>
|
||||||
|
<button onclick="load_form_div('<?= base_url('Product_master/FormulationEdit/0') ?>','test_div_form','Formulation : New Formulation :Pharmacy');" type="button" class="btn btn-primary btn-sm">Add New</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="formulation_report_list" class="table table-bordered table-striped table-sm align-middle w-100">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Formulation Name</th>
|
||||||
|
<th style="width:90px;">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6" id="test_div_form"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (!window.jQuery || !$.fn.DataTable) return;
|
||||||
|
|
||||||
|
$('#formulation_report_list').DataTable({
|
||||||
|
processing : true,
|
||||||
|
serverSide : true,
|
||||||
|
destroy : true,
|
||||||
|
ajax: {
|
||||||
|
url : '<?= base_url('product_master/FormulationListData') ?>',
|
||||||
|
type: 'GET'
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ data: 0, title: 'Formulation Name' },
|
||||||
|
{ data: 1, title: 'Action', orderable: false, searchable: false }
|
||||||
|
],
|
||||||
|
pageLength: 25,
|
||||||
|
order: [[0, 'asc']],
|
||||||
|
language: {
|
||||||
|
processing: '<span class="spinner-border spinner-border-sm me-2"></span>Loading…'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
function refreshFormulationMasterList() {
|
||||||
|
if (window.jQuery && $.fn.DataTable && $.fn.DataTable.isDataTable('#formulation_report_list')) {
|
||||||
|
$('#formulation_report_list').DataTable().ajax.reload(null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncFormulationMastersFromApi() {
|
||||||
|
$('#formulation_master_msg').html('<div class="alert alert-info mb-0"><span class="spinner-border spinner-border-sm me-2"></span>Syncing formulation masters from gateway API…</div>');
|
||||||
|
$.ajax({
|
||||||
|
url : '<?= base_url('product_master/SyncDrugMasters') ?>',
|
||||||
|
type : 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
success : function (response) {
|
||||||
|
$('#formulation_master_msg').html(response.show_text || '');
|
||||||
|
refreshFormulationMasterList();
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$('#formulation_master_msg').html('<div class="alert alert-danger mb-0">Unable to sync masters.</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -24,6 +24,16 @@
|
||||||
<i class="bi bi-hand-index-thumb"></i> Drug Company
|
<i class="bi bi-hand-index-thumb"></i> Drug Company
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a href="javascript:load_form_div('<?= base_url('product_master/formulation_master_list') ?>','medical-main','Formulation Master :Pharmacy');">
|
||||||
|
<i class="bi bi-hand-index-thumb"></i> Formulation
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a href="javascript:load_form_div('<?= base_url('product_master/short_formulation_master_list') ?>','medical-main','Short Formulation Master :Pharmacy');">
|
||||||
|
<i class="bi bi-hand-index-thumb"></i> Short Formulation
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="mb-2">
|
<li class="mb-2">
|
||||||
<a href="javascript:load_form_div('<?= base_url('product_master/medicine_category') ?>','medical-main','Medicine Category :Pharmacy');">
|
<a href="javascript:load_form_div('<?= base_url('product_master/medicine_category') ?>','medical-main','Medicine Category :Pharmacy');">
|
||||||
<i class="bi bi-hand-index-thumb"></i> Medicine Category
|
<i class="bi bi-hand-index-thumb"></i> Medicine Category
|
||||||
|
|
|
||||||
59
app/Views/medical/short_formulation_edit.php
Normal file
59
app/Views/medical/short_formulation_edit.php
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
$sfid = 0;
|
||||||
|
$name = '';
|
||||||
|
|
||||||
|
if (!empty($med_short_formulation ?? [])) {
|
||||||
|
$row = $med_short_formulation[0];
|
||||||
|
$sfid = (int) ($row->id ?? 0);
|
||||||
|
$name = (string) ($row->short_formulation ?? '');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Short Formulation</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div id="short_formulation_msg"></div>
|
||||||
|
|
||||||
|
<form id="short-formulation-form" class="row g-2" method="post" action="javascript:void(0)">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" id="hid_sfid" name="hid_sfid" value="<?= esc((string) $sfid) ?>">
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label">Short Formulation Name</label>
|
||||||
|
<input class="form-control" name="input_short_formulation_name" id="input_short_formulation_name"
|
||||||
|
placeholder="e.g. Tab, Cap, Syr, Inj…" type="text" value="<?= esc($name) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="button" class="btn btn-primary" id="btn_short_formulation_update" accesskey="A">Add & Update Short Formulation</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function showMessage(html) {
|
||||||
|
$('#short_formulation_msg').html(html || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#btn_short_formulation_update').off('click').on('click', function () {
|
||||||
|
$.post('<?= base_url('Product_master/ShortFormulationUpdate') ?>', $('#short-formulation-form').serialize(), function (data) {
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
showMessage('<div class="alert alert-danger mb-0">Unexpected response.</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showMessage(data.show_text || '');
|
||||||
|
if ((data.insertid || 0) > 0) {
|
||||||
|
$('#hid_sfid').val(data.insertid);
|
||||||
|
if (typeof refreshShortFormulationMasterList === 'function') {
|
||||||
|
refreshShortFormulationMasterList();
|
||||||
|
}
|
||||||
|
load_form_div('<?= base_url('Product_master/ShortFormulationEdit') ?>/' + data.insertid, 'test_div_sform');
|
||||||
|
}
|
||||||
|
}, 'json').fail(function () {
|
||||||
|
showMessage('<div class="alert alert-danger mb-0">Unable to update short formulation.</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
75
app/Views/medical/short_formulation_master.php
Normal file
75
app/Views/medical/short_formulation_master.php
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div id="short_formulation_master_msg" class="mb-2"></div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h5 class="card-title mb-0">Short Formulation Master</h5>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button onclick="syncShortFormulationMastersFromApi();" type="button" class="btn btn-outline-success btn-sm">Sync From API</button>
|
||||||
|
<button onclick="load_form_div('<?= base_url('Product_master/ShortFormulationEdit/0') ?>','test_div_sform','Short Formulation : New :Pharmacy');" type="button" class="btn btn-primary btn-sm">Add New</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="short_formulation_report_list" class="table table-bordered table-striped table-sm align-middle w-100">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Short Formulation</th>
|
||||||
|
<th style="width:90px;">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6" id="test_div_sform"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (!window.jQuery || !$.fn.DataTable) return;
|
||||||
|
|
||||||
|
$('#short_formulation_report_list').DataTable({
|
||||||
|
processing : true,
|
||||||
|
serverSide : true,
|
||||||
|
destroy : true,
|
||||||
|
ajax: {
|
||||||
|
url : '<?= base_url('product_master/ShortFormulationListData') ?>',
|
||||||
|
type: 'GET'
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ data: 0, title: 'Short Formulation' },
|
||||||
|
{ data: 1, title: 'Action', orderable: false, searchable: false }
|
||||||
|
],
|
||||||
|
pageLength: 25,
|
||||||
|
order: [[0, 'asc']],
|
||||||
|
language: {
|
||||||
|
processing: '<span class="spinner-border spinner-border-sm me-2"></span>Loading…'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
function refreshShortFormulationMasterList() {
|
||||||
|
if (window.jQuery && $.fn.DataTable && $.fn.DataTable.isDataTable('#short_formulation_report_list')) {
|
||||||
|
$('#short_formulation_report_list').DataTable().ajax.reload(null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncShortFormulationMastersFromApi() {
|
||||||
|
$('#short_formulation_master_msg').html('<div class="alert alert-info mb-0"><span class="spinner-border spinner-border-sm me-2"></span>Syncing short formulation masters from gateway API…</div>');
|
||||||
|
$.ajax({
|
||||||
|
url : '<?= base_url('product_master/SyncDrugMasters') ?>',
|
||||||
|
type : 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
success : function (response) {
|
||||||
|
$('#short_formulation_master_msg').html(response.show_text || '');
|
||||||
|
refreshShortFormulationMasterList();
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$('#short_formulation_master_msg').html('<div class="alert alert-danger mb-0">Unable to sync masters.</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -31,7 +31,12 @@ ABDM_NPI_ID=your-npi-id
|
||||||
SSL_CERT_PATH=/etc/letsencrypt/live/abdm-bridge.e-atria.in/fullchain.pem
|
SSL_CERT_PATH=/etc/letsencrypt/live/abdm-bridge.e-atria.in/fullchain.pem
|
||||||
SSL_KEY_PATH=/etc/letsencrypt/live/abdm-bridge.e-atria.in/privkey.pem
|
SSL_KEY_PATH=/etc/letsencrypt/live/abdm-bridge.e-atria.in/privkey.pem
|
||||||
|
|
||||||
# Rate Limiting
|
# CDCI Drug Database
|
||||||
|
# Path to the local CDCI SQLite database file used by drug endpoints.
|
||||||
|
# Default: ./data/cdci.db (relative to server.js)
|
||||||
|
CDCI_DB_PATH=./data/cdci.db
|
||||||
|
|
||||||
|
|
||||||
RATE_LIMIT_WINDOW_MS=900000
|
RATE_LIMIT_WINDOW_MS=900000
|
||||||
RATE_LIMIT_MAX_REQUESTS=100
|
RATE_LIMIT_MAX_REQUESTS=100
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"compression": "^1.7.4"
|
"compression": "^1.7.4",
|
||||||
|
"better-sqlite3": "^9.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.2"
|
"nodemon": "^3.0.2"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import morgan from 'morgan';
|
import morgan from 'morgan';
|
||||||
|
|
@ -450,6 +451,99 @@ app.get('/api/v3/gateway/status', authenticateToken, async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drug Masters Endpoint
|
||||||
|
* GET /api/v3/drugs/masters
|
||||||
|
* Serves normalised HMS master lists (company, formulation, short_formulation, generic)
|
||||||
|
* from the local CDCI SQLite database imported into the gateway.
|
||||||
|
* Data is NOT proxied to ABDM M3 — it comes from the gateway's own CDCI dataset.
|
||||||
|
*
|
||||||
|
* Query params:
|
||||||
|
* master = company | formulation | short_formulation | generic (required)
|
||||||
|
* q = optional contains-filter on name
|
||||||
|
* limit = 1–500 (default 100)
|
||||||
|
* offset = pagination offset (default 0)
|
||||||
|
*/
|
||||||
|
app.get('/api/v3/drugs/masters', authenticateToken, (req, res) => {
|
||||||
|
try {
|
||||||
|
const { master = 'company', q = '', limit = '100', offset = '0' } = req.query;
|
||||||
|
|
||||||
|
const validMasters = ['company', 'formulation', 'short_formulation', 'generic'];
|
||||||
|
if (!validMasters.includes(master)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
ok: 0,
|
||||||
|
error_code: 'INVALID_MASTER_TYPE',
|
||||||
|
message: `Invalid master type. Valid values: ${validMasters.join(', ')}`,
|
||||||
|
request_id: req.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedLimit = Math.min(Math.max(parseInt(limit, 10) || 100, 1), 500);
|
||||||
|
const parsedOffset = Math.max(parseInt(offset, 10) || 0, 0);
|
||||||
|
|
||||||
|
logger.info('Drug masters requested', { requestId: req.id, master, limit: parsedLimit, offset: parsedOffset });
|
||||||
|
|
||||||
|
// Open the CDCI SQLite database (path set via CDCI_DB_PATH in .env)
|
||||||
|
const dbPath = process.env.CDCI_DB_PATH || path.join(__dirname, 'data', 'cdci.db');
|
||||||
|
|
||||||
|
let db;
|
||||||
|
try {
|
||||||
|
db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
||||||
|
} catch (dbErr) {
|
||||||
|
logger.error('CDCI DB open failed', { requestId: req.id, error: dbErr.message, dbPath });
|
||||||
|
return res.status(503).json({
|
||||||
|
ok: 0,
|
||||||
|
error_code: 'CDCI_DB_UNAVAILABLE',
|
||||||
|
message: 'CDCI database not available. Contact gateway admin.',
|
||||||
|
request_id: req.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table mapping — each master type maps to a CDCI table
|
||||||
|
const tableMap = {
|
||||||
|
company: 'cdci_manufacturers',
|
||||||
|
formulation: 'cdci_formulations',
|
||||||
|
short_formulation: 'cdci_short_formulations',
|
||||||
|
generic: 'cdci_generics',
|
||||||
|
};
|
||||||
|
const table = tableMap[master];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filterClause = q ? `WHERE name LIKE ?` : '';
|
||||||
|
const filterParam = q ? [`%${q}%`] : [];
|
||||||
|
|
||||||
|
const totalRow = db.prepare(`SELECT COUNT(*) AS cnt FROM ${table} ${filterClause}`).get(...filterParam);
|
||||||
|
const total = totalRow?.cnt ?? 0;
|
||||||
|
|
||||||
|
const rows = db.prepare(
|
||||||
|
`SELECT identifier, name, updated_at FROM ${table} ${filterClause} ORDER BY name ASC LIMIT ? OFFSET ?`
|
||||||
|
).all(...filterParam, parsedLimit, parsedOffset);
|
||||||
|
|
||||||
|
logger.info('Drug masters served', { requestId: req.id, master, count: rows.length, total });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
ok: 1,
|
||||||
|
master,
|
||||||
|
count: rows.length,
|
||||||
|
total,
|
||||||
|
items: rows,
|
||||||
|
request_id: req.id,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Drug masters endpoint error', { requestId: req.id, error: error.message });
|
||||||
|
res.status(500).json({
|
||||||
|
ok: 0,
|
||||||
|
error_code: 'SERVER_ERROR',
|
||||||
|
message: NODE_ENV === 'production' ? 'Internal server error' : error.message,
|
||||||
|
request_id: req.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Error Handlers
|
// Error Handlers
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user