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->get('product_master/company_master_list', 'Medical::company_master_list');
|
||||
$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->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/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->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_edit/(:num)', 'Medical::medicine_category_edit/$1');
|
||||
$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';
|
||||
$routes->post('AbdmGateway/abha_validate', 'AbdmGateway::abhaValidate', ['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_callback', 'AbdmGateway::consentCallback');
|
||||
$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']);
|
||||
}
|
||||
|
||||
if (strlen($qrPayload) < 8) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invalid QR payload']);
|
||||
}
|
||||
|
||||
$abhaId = '';
|
||||
if (preg_match('/\b(\d{14})\b/', $qrPayload, $match) === 1) {
|
||||
$abhaId = (string) ($match[1] ?? '');
|
||||
|
|
@ -259,18 +263,277 @@ class AbdmGateway extends BaseController
|
|||
];
|
||||
|
||||
$queueId = null;
|
||||
$result = [];
|
||||
try {
|
||||
$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) {
|
||||
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([
|
||||
'ok' => 1,
|
||||
'status' => 'queued',
|
||||
'queue_id' => $queueId,
|
||||
'abha_id_hint' => $abhaId,
|
||||
'message' => 'Scan & Share lookup queued to center server.',
|
||||
'identity' => $identity,
|
||||
'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);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
$secret = $this->readRuntimeSetting('EKA_WEBHOOK_SECRET');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
namespace App\Controllers;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\Abdm\AbdmConnectorFactory;
|
||||
use App\Libraries\HealthplixService;
|
||||
use App\Models\ClinicalAuditTrailModel;
|
||||
use App\Models\OpdModel;
|
||||
use App\Models\PaymentModel;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
|
@ -883,6 +885,10 @@ class Opd extends BaseController
|
|||
|
||||
$docId = (int) $docId;
|
||||
$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,
|
||||
group_concat(DISTINCT m.SpecName) as Spec
|
||||
|
|
@ -926,6 +932,7 @@ class Opd extends BaseController
|
|||
1,0
|
||||
) as has_vitals,
|
||||
coalesce(pr.queue_no,0) as queue_no
|
||||
," . $abdmSelect . "
|
||||
from opd_master o
|
||||
join patient_master p on o.p_id=p.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,
|
||||
]);
|
||||
|
||||
$this->syncAbdmTokenAfterOpdStatus((int) $opdId, (int) $opdStatus, (int) $userId, (string) $userLabel);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'update' => 1,
|
||||
'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)
|
||||
{
|
||||
$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 CSRF_NAME = '<?= csrf_token() ?>';
|
||||
const REFRESH_SECS = 30;
|
||||
const STATUS_TRANSITIONS = {
|
||||
PENDING: ['CALLED', 'CANCELLED'],
|
||||
CALLED: ['COMPLETED', 'CANCELLED', 'PENDING'],
|
||||
COMPLETED: ['PENDING'],
|
||||
CANCELLED: ['PENDING']
|
||||
};
|
||||
let autoTimer = null;
|
||||
let opdModalCurrentUrl = '';
|
||||
let originalLoadFormRef = null;
|
||||
|
|
@ -331,6 +337,10 @@
|
|||
|
||||
function renderQueue(data) {
|
||||
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 summary = document.getElementById('abdmQSummary');
|
||||
|
||||
|
|
@ -350,12 +360,14 @@
|
|||
`<span class="badge rounded-pill bg-secondary px-3 py-2">${tokens.length} Total</span>`,
|
||||
].join('');
|
||||
|
||||
if (!tokens.length) {
|
||||
body.innerHTML = '<tr><td colspan="9" class="text-center py-4 text-muted small">No tokens found for this filter.</td></tr>';
|
||||
if (!viewTokens.length) {
|
||||
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;
|
||||
}
|
||||
|
||||
body.innerHTML = tokens.map(t => {
|
||||
body.innerHTML = viewTokens.map(t => {
|
||||
const id = t.id ?? 0;
|
||||
const isPend = t.status === 'PENDING';
|
||||
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">
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">✓ ${lbl}</span>
|
||||
</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>`;
|
||||
}
|
||||
}
|
||||
|
|
@ -379,11 +393,15 @@
|
|||
let actions = '';
|
||||
if (isPend) {
|
||||
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>`;
|
||||
}
|
||||
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)) {
|
||||
const pl = encodePayload({
|
||||
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>`;
|
||||
}
|
||||
|
||||
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>
|
||||
<div class="fw-semibold small">${esc(t.patient_name ?? '—')}</div>
|
||||
|
|
@ -416,6 +434,16 @@
|
|||
}
|
||||
|
||||
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 })
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
|
|
@ -433,6 +461,7 @@
|
|||
|
||||
window.abdmQCall = id => setStatus(id, 'CALLED');
|
||||
window.abdmQComplete = id => setStatus(id, 'COMPLETED');
|
||||
window.abdmQReopen = id => setStatus(id, 'PENDING');
|
||||
window.abdmQCancel = id => { if (confirm('Cancel token #' + id + '?')) setStatus(id, 'CANCELLED'); };
|
||||
|
||||
window.abdmQRegister = function (payloadEncoded) {
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@
|
|||
</div>
|
||||
|
||||
<div class="small text-muted" id="abdmSandboxStatus">Ready</div>
|
||||
<div class="mt-2 d-none" id="abdmScanResolvePane"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -378,6 +379,7 @@
|
|||
var sandboxCard = document.getElementById('abdmSandboxCard');
|
||||
var sandboxContext = document.getElementById('abdmSandboxContext');
|
||||
var sandboxStatus = document.getElementById('abdmSandboxStatus');
|
||||
var scanResolvePane = document.getElementById('abdmScanResolvePane');
|
||||
var sandboxAbhaInput = document.getElementById('tb_abha_id');
|
||||
var sandboxQrPayloadInput = document.getElementById('tb_abdm_qr_payload');
|
||||
var sandboxPurposeCodeInput = document.getElementById('tb_abdm_purpose_code');
|
||||
|
|
@ -578,6 +580,110 @@
|
|||
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() {
|
||||
if (!sandboxRow) {
|
||||
return null;
|
||||
|
|
@ -620,6 +726,7 @@
|
|||
sandboxPurposeCodeInput.value = sandboxPurposeCodeInput.value || 'CAREMGT';
|
||||
sandboxConsentHandleInput.value = '';
|
||||
sandboxClaimDocumentInput.value = '';
|
||||
clearScanResolvePane();
|
||||
|
||||
var opdId = row.getAttribute('data-opd-id') || '0';
|
||||
var opdSessionId = row.getAttribute('data-opd-session-id') || '0';
|
||||
|
|
@ -761,8 +868,61 @@
|
|||
sandboxCard.classList.add('d-none');
|
||||
}
|
||||
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 () {
|
||||
var qrPayload = (sandboxQrPayloadInput.value || '').trim();
|
||||
if (!qrPayload) {
|
||||
|
|
@ -780,7 +940,48 @@
|
|||
if (res.abha_id_hint) {
|
||||
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>Last Visit</th>
|
||||
<th>Insurance</th>
|
||||
<th>ABHA</th>
|
||||
<th>Patient History</th>
|
||||
</tr>
|
||||
</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><?=$data[$i]->Last_Visit ?></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>
|
||||
<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
|
||||
|
|
@ -56,7 +42,6 @@
|
|||
<th>Age</th>
|
||||
<th>Last Visit</th>
|
||||
<th>Insurance</th>
|
||||
<th>ABHA</th>
|
||||
<th>Patient History</th>
|
||||
</tr>
|
||||
</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
|
||||
var btnScan = document.getElementById('btn_ipd_abdm_scan_lookup');
|
||||
if (btnScan) {
|
||||
|
|
@ -3953,7 +4006,36 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
|
|||
var addr = document.getElementById('ipd_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' : ''));
|
||||
$patientAgeGender = trim($patientAgeText . ($patientGenderText !== '' ? ' / ' . $patientGenderText : ''));
|
||||
$patientDisplayName = esc($patientRawName . ($patientAgeGender !== '' ? ' (' . $patientAgeGender . ')' : ''));
|
||||
$isAbdmSource = (int) ($row->is_abdm_source ?? 0) === 1;
|
||||
$tabType = $group['tabType'];
|
||||
$status = $group['status'];
|
||||
?>
|
||||
|
|
@ -115,7 +116,12 @@ $mergedGroups = [
|
|||
data-has-prescription="<?= $showFullActions ? 1 : 0 ?>">
|
||||
<td><span class="badge <?= esc($group['badge']) ?>"><?= esc($group['label']) ?></span></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><?= esc($row->p_code ?? '') ?></td>
|
||||
<td><?= esc($row->opd_type ?? '') ?> / Amt: <?= esc($row->opd_fee_amount ?? '') ?></td>
|
||||
|
|
|
|||
|
|
@ -5789,6 +5789,59 @@
|
|||
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() {
|
||||
var qrPayload = ($('#abdm_qr_payload').val() || '').toString().trim();
|
||||
if (!qrPayload) {
|
||||
|
|
@ -5806,7 +5859,30 @@
|
|||
if (data.abha_id_hint) {
|
||||
$('#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-body pt-3">
|
||||
<div id="company_master_msg" class="mb-2"></div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0">Company Master</h5>
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<div id="company_list" class="table-responsive" style="max-height:500px;overflow-y:auto;">
|
||||
<?= view('medical/company_master_sub', ['med_company' => $med_company ?? []]) ?>
|
||||
<div class="table-responsive">
|
||||
<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 class="col-lg-6" id="test_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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Company Name</th>
|
||||
<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>
|
||||
<?php
|
||||
// This sub-view is kept for backward compatibility but is no longer used
|
||||
// to render list data. The company list is now served via server-side
|
||||
// DataTables AJAX (product_master/CompanyListData).
|
||||
?>
|
||||
|
||||
<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
|
||||
</a>
|
||||
</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">
|
||||
<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
|
||||
|
|
|
|||
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_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_MAX_REQUESTS=100
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
"bcryptjs": "^2.4.3",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.11.0",
|
||||
"compression": "^1.7.4"
|
||||
"compression": "^1.7.4",
|
||||
"better-sqlite3": "^9.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import express from 'express';
|
||||
import axios from 'axios';
|
||||
import dotenv from 'dotenv';
|
||||
import Database from 'better-sqlite3';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
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
|
||||
// ============================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user