Align ABDM bridge workflow APIs and add report doctor mapping

This commit is contained in:
DevSoftTech Server 2026-06-01 07:02:34 +05:30
parent e1d9e85e64
commit b12652d6e0
8 changed files with 656 additions and 18 deletions

View File

@ -597,6 +597,8 @@ $routes->post('AbdmGateway/push_health_record', 'AbdmGateway::pushHealthRecord',
$routes->get('AbdmGateway/health_records_list', 'AbdmGateway::healthRecordsList', ['filter' => $abdmPermFilter]);
$routes->get('AbdmGateway/bridge_record_status/(:num)', 'AbdmGateway::bridgeRecordStatus/$1', ['filter' => $abdmPermFilter]);
$routes->post('AbdmGateway/bridge_record_share/(:num)', 'AbdmGateway::bridgeRecordShare/$1', ['filter' => $abdmPermFilter]);
$routes->post('AbdmGateway/bridge_record_link_share/(:num)', 'AbdmGateway::bridgeRecordLinkShare/$1', ['filter' => $abdmPermFilter]);
$routes->get('AbdmGateway/bridge_record_workflow_status/(:num)', 'AbdmGateway::bridgeRecordWorkflowStatus/$1', ['filter' => $abdmPermFilter]);
$routes->get('AbdmGateway/opd_running_token_status', 'AbdmGateway::opdRunningTokenStatus', ['filter' => $abdmPermFilter]);
$routes->get('AbdmGateway/gateway_status', 'AbdmGateway::gatewayStatus', ['filter' => $abdmPermFilter]);
$routes->get('AbdmGateway/bridge_records_list', 'AbdmGateway::bridgeRecordsList', ['filter' => $abdmPermFilter]);
@ -1086,6 +1088,8 @@ $routes->group('setting', static function($routes) {
$routes->get('admin/abdm-gateway', 'Setting\\AbdmGatewaySettings::index', ['filter' => $settingsAdminFilter]);
$routes->post('admin/abdm-gateway/save', 'Setting\\AbdmGatewaySettings::save', ['filter' => $settingsAdminFilter]);
$routes->post('admin/abdm-gateway/test', 'Setting\\AbdmGatewaySettings::testConnection', ['filter' => $settingsAdminFilter]);
$routes->get('admin/abdm-report-doctors', 'Setting\\AbdmReportDoctorSettings::index', ['filter' => $settingsAdminFilter]);
$routes->post('admin/abdm-report-doctors/save', 'Setting\\AbdmReportDoctorSettings::save', ['filter' => $settingsAdminFilter]);
$routes->get('admin/hospital-profile', 'Setting\\HospitalProfile::index');
$routes->post('admin/hospital-profile/save', 'Setting\\HospitalProfile::save');
$routes->post('admin/hospital-profile/delete-logo', 'Setting\\HospitalProfile::deleteLogo');

View File

@ -1851,7 +1851,12 @@ class AbdmGateway extends BaseController
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
$result = $this->connector->getRecord($bridgeId);
try {
$result = $this->connector->workflowStatus($bridgeId);
} catch (\Throwable $e) {
// Backward compatibility with older connectors.
$result = $this->connector->getRecord($bridgeId);
}
return $this->response->setJSON($result);
}
@ -1866,10 +1871,29 @@ class AbdmGateway extends BaseController
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
$result = $this->connector->triggerShare($bridgeId);
try {
$result = $this->connector->linkAndShare($bridgeId);
} catch (\Throwable $e) {
// Backward compatibility with older connectors.
$result = $this->connector->triggerShare($bridgeId);
}
return $this->response->setJSON($result);
}
// =========================================================================
// Explicit e-Atria orchestration aliases for HMS integration docs.
// =========================================================================
public function bridgeRecordLinkShare(int $bridgeId)
{
return $this->bridgeRecordShare($bridgeId);
}
public function bridgeRecordWorkflowStatus(int $bridgeId)
{
return $this->bridgeRecordStatus($bridgeId);
}
// =========================================================================
// OPD running token status — GET /api/v3/opd/running-token-status
// GET /AbdmGateway/opd_running_token_status
@ -2494,6 +2518,8 @@ class AbdmGateway extends BaseController
$doctorId = 0;
$doctorName = '';
$doctorRegNo = '';
$labType = 0;
$chargeId = 0;
$candidateIdCols = ['doctor_id', 'dr_id', 'doc_id', 'r_doc_id', 'consultant_id', 'reported_by_doctor_id'];
$candidateNameCols = ['doctor_name', 'dr_name', 'doc_name', 'r_doc_name', 'consultant_name', 'reported_by_name', 'reported_by'];
@ -2501,6 +2527,8 @@ class AbdmGateway extends BaseController
// Try immediate row first (if caller passed a richer row object/array).
if (is_object($labReqRow)) {
$labType = (int) ($labReqRow->lab_type ?? 0);
$chargeId = (int) ($labReqRow->charge_id ?? 0);
foreach ($candidateIdCols as $c) {
if (isset($labReqRow->{$c}) && (int) $labReqRow->{$c} > 0) {
$doctorId = (int) $labReqRow->{$c};
@ -2522,6 +2550,8 @@ class AbdmGateway extends BaseController
}
}
} elseif (is_array($labReqRow)) {
$labType = (int) ($labReqRow['lab_type'] ?? 0);
$chargeId = (int) ($labReqRow['charge_id'] ?? 0);
foreach ($candidateIdCols as $c) {
if (isset($labReqRow[$c]) && (int) $labReqRow[$c] > 0) {
$doctorId = (int) $labReqRow[$c];
@ -2544,6 +2574,31 @@ class AbdmGateway extends BaseController
}
}
// Priority 1: explicit admin mapping by modality (Pathology/X-Ray/CT/USG/etc.).
$testTitle = '';
if ($chargeId > 0 && $this->db->tableExists('charge_master')) {
try {
$chargeRow = $this->db->table('charge_master')
->select('charge_name')
->where('id', $chargeId)
->get(1)
->getRowArray() ?? [];
$testTitle = trim((string) ($chargeRow['charge_name'] ?? ''));
} catch (\Throwable $e) {
$testTitle = '';
}
}
$mappedDoctorId = $this->resolveMappedDoctorIdForLabContext($labType, $testTitle);
if ($mappedDoctorId > 0) {
$mappedDoctor = $this->loadDoctorIdentityById($mappedDoctorId);
if ($mappedDoctor !== null) {
$doctorId = $mappedDoctorId;
$doctorName = trim((string) ($mappedDoctor['name'] ?? ''));
$doctorRegNo = trim((string) ($mappedDoctor['registration_number'] ?? ''));
}
}
try {
$labFields = $this->db->getFieldNames('lab_request') ?? [];
$selectCols = ['id'];
@ -2635,6 +2690,107 @@ class AbdmGateway extends BaseController
];
}
private function resolveMappedDoctorIdForLabContext(int $labType, string $testTitle): int
{
if (! $this->db->tableExists('hospital_setting')) {
return 0;
}
$title = strtolower(trim($testTitle));
if ($labType === 6) {
if ($title !== '') {
if (preg_match('/\bx\s*-?\s*ray\b|\bxray\b|\bradiograph\b/', $title) === 1) {
return $this->readMappedDoctorId('ABDM_DOC_XRAY');
}
if (preg_match('/\bct\b|\bct\s*scan\b|\bhrct\b|\bcect\b/', $title) === 1) {
return $this->readMappedDoctorId('ABDM_DOC_CTSCAN');
}
if (preg_match('/\busg\b|\bultra\s*sound\b|\bultrasound\b|\bsonograph\b/', $title) === 1) {
return $this->readMappedDoctorId('ABDM_DOC_ULTRASOUND');
}
if (preg_match('/\bmri\b|\bmr\b/', $title) === 1) {
return $this->readMappedDoctorId('ABDM_DOC_MRI');
}
}
$radDefault = $this->readMappedDoctorId('ABDM_DOC_RADIOLOGY');
if ($radDefault > 0) {
return $radDefault;
}
}
return $this->readMappedDoctorId('ABDM_DOC_PATHOLOGY');
}
private function readMappedDoctorId(string $settingKey): int
{
if ($settingKey === '' || ! $this->db->tableExists('hospital_setting')) {
return 0;
}
$row = $this->db->table('hospital_setting')
->select('s_value')
->where('s_name', $settingKey)
->get(1)
->getRowArray();
return (int) ($row['s_value'] ?? 0);
}
/**
* @return array{name:string,registration_number:string}|null
*/
private function loadDoctorIdentityById(int $doctorId): ?array
{
if ($doctorId <= 0 || ! $this->db->tableExists('doctor_master')) {
return null;
}
try {
$dFields = $this->db->getFieldNames('doctor_master') ?? [];
$dSelect = ['id'];
foreach (['p_title', 'p_fname', 'p_lname', 'doctor_reg_no', 'registration_no', 'reg_no', 'doc_reg_no', 'nmc_reg_no', 'mci_reg_no'] as $f) {
if (in_array($f, $dFields, true)) {
$dSelect[] = $f;
}
}
$dRow = $this->db->table('doctor_master')
->select(implode(',', array_unique($dSelect)))
->where('id', $doctorId)
->get(1)
->getRowArray() ?? [];
if (empty($dRow)) {
return null;
}
$name = trim(
trim((string) ($dRow['p_title'] ?? '')) . ' ' .
trim((string) ($dRow['p_fname'] ?? '')) . ' ' .
trim((string) ($dRow['p_lname'] ?? ''))
);
if ($name === '') {
return null;
}
$reg = '';
foreach (['doctor_reg_no', 'registration_no', 'reg_no', 'doc_reg_no', 'nmc_reg_no', 'mci_reg_no'] as $f) {
if (! empty($dRow[$f])) {
$reg = trim((string) $dRow[$f]);
break;
}
}
return [
'name' => $name,
'registration_number' => $reg,
];
} catch (\Throwable $e) {
return null;
}
}
/**
* Map numeric lab_type code to a human-readable title.
*/

View File

@ -0,0 +1,200 @@
<?php
namespace App\Controllers\Setting;
use App\Controllers\BaseController;
class AbdmReportDoctorSettings extends BaseController
{
/**
* @var array<string,string>
*/
private array $mappingKeys = [
'ABDM_DOC_PATHOLOGY' => 'Pathology / Lab Default',
'ABDM_DOC_XRAY' => 'X-Ray',
'ABDM_DOC_CTSCAN' => 'CT Scan',
'ABDM_DOC_ULTRASOUND' => 'Ultrasound (USG)',
'ABDM_DOC_MRI' => 'MRI',
'ABDM_DOC_RADIOLOGY' => 'Radiology Default (fallback)',
];
public function index()
{
if (! $this->canManageSettings()) {
return $this->response->setStatusCode(403)->setBody('Access denied');
}
return view('Setting/Admin/abdm_report_doctor_settings', [
'mapping_keys' => $this->mappingKeys,
'selected' => $this->readExistingMappings(),
'doctors' => $this->fetchDoctorOptions(),
]);
}
public function save()
{
if (! $this->request->isAJAX()) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->canManageSettings()) {
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']);
}
if (! $this->db->tableExists('hospital_setting')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'hospital_setting table not found']);
}
foreach ($this->mappingKeys as $key => $label) {
$doctorId = (int) ($this->request->getPost($key) ?? 0);
if ($doctorId > 0) {
$this->upsertSettingValue($key, (string) $doctorId);
} else {
$this->deleteSettingValue($key);
}
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'ABDM report doctor mapping saved.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
/**
* @return array<string,int>
*/
private function readExistingMappings(): array
{
$output = [];
foreach (array_keys($this->mappingKeys) as $key) {
$output[$key] = 0;
}
if (! $this->db->tableExists('hospital_setting')) {
return $output;
}
$rows = $this->db->table('hospital_setting')
->select('s_name, s_value')
->whereIn('s_name', array_keys($this->mappingKeys))
->get()
->getResultArray();
foreach ($rows as $row) {
$name = trim((string) ($row['s_name'] ?? ''));
if ($name === '' || ! array_key_exists($name, $output)) {
continue;
}
$output[$name] = (int) ($row['s_value'] ?? 0);
}
return $output;
}
/**
* @return array<int,array<string,mixed>>
*/
private function fetchDoctorOptions(): array
{
if (! $this->db->tableExists('doctor_master')) {
return [];
}
$fields = $this->db->getFieldNames('doctor_master') ?? [];
$select = ['id'];
foreach (['p_title', 'p_fname', 'p_lname', 'doctor_reg_no', 'registration_no', 'reg_no', 'doc_reg_no', 'nmc_reg_no', 'mci_reg_no'] as $f) {
if (in_array($f, $fields, true)) {
$select[] = $f;
}
}
$builder = $this->db->table('doctor_master')->select(implode(',', array_unique($select)));
if (in_array('is_active', $fields, true)) {
$builder->where('is_active', 1);
} elseif (in_array('status', $fields, true)) {
$builder->where('status', 1);
}
if (in_array('p_fname', $fields, true)) {
$builder->orderBy('p_fname', 'ASC');
} else {
$builder->orderBy('id', 'DESC');
}
$rows = $builder->get()->getResultArray();
$out = [];
foreach ($rows as $row) {
$id = (int) ($row['id'] ?? 0);
if ($id <= 0) {
continue;
}
$title = trim((string) ($row['p_title'] ?? ''));
$first = trim((string) ($row['p_fname'] ?? ''));
$last = trim((string) ($row['p_lname'] ?? ''));
$name = trim($title . ' ' . $first . ' ' . $last);
if ($name === '') {
$name = 'Doctor #' . $id;
}
$regNo = '';
foreach (['doctor_reg_no', 'registration_no', 'reg_no', 'doc_reg_no', 'nmc_reg_no', 'mci_reg_no'] as $rf) {
$v = trim((string) ($row[$rf] ?? ''));
if ($v !== '') {
$regNo = $v;
break;
}
}
$label = $name;
if ($regNo !== '') {
$label .= ' (' . $regNo . ')';
}
$out[] = [
'id' => $id,
'name' => $name,
'reg_no' => $regNo,
'label' => $label,
];
}
return $out;
}
private function canManageSettings(): bool
{
if (! function_exists('auth') || ! auth()->loggedIn()) {
return false;
}
return true;
}
private function upsertSettingValue(string $name, string $value): bool
{
$existing = $this->db->table('hospital_setting')
->select('id')
->where('s_name', $name)
->get(1)
->getRowArray();
if ($existing) {
return (bool) $this->db->table('hospital_setting')
->where('id', (int) $existing['id'])
->update(['s_value' => $value]);
}
return (bool) $this->db->table('hospital_setting')->insert([
's_name' => $name,
's_value' => $value,
]);
}
private function deleteSettingValue(string $name): bool
{
return (bool) $this->db->table('hospital_setting')->where('s_name', $name)->delete();
}
}

View File

@ -197,6 +197,16 @@ interface AbdmConnectorInterface
*/
public function triggerShare(int $bridgeId): array;
/**
* POST /api/v3/records/{id}/link-and-share — orchestrated care-context link + share.
*/
public function linkAndShare(int $bridgeId): array;
/**
* GET /api/v3/records/{id}/workflow-status — end-to-end workflow status.
*/
public function workflowStatus(int $bridgeId): array;
/**
* GET /api/v3/records — list stored bridge records with optional filters.
*

View File

@ -189,6 +189,50 @@ class DreamsoftConnector implements AbdmConnectorInterface
return $this->sharePrescriptionBundle($data, (string) ($data['entity_id'] ?? ''));
}
public function getRecord(int $bridgeId): array
{
// No direct pull API in Dreamsoft async adapter; return queued placeholder.
return [
'ok' => 1,
'id' => $bridgeId,
'status' => 'queued',
'message' => 'Dreamsoft connector does not provide synchronous record fetch.',
];
}
public function triggerShare(int $bridgeId): array
{
// Keep compatibility with older UI actions.
return [
'ok' => 1,
'id' => $bridgeId,
'status' => 'queued',
'message' => 'Dreamsoft connector does not provide synchronous share trigger.',
];
}
public function linkAndShare(int $bridgeId): array
{
// New e-Atria orchestration API is not available in Dreamsoft adapter.
return [
'ok' => 1,
'id' => $bridgeId,
'status' => 'queued',
'message' => 'Dreamsoft connector does not provide synchronous link-and-share orchestration.',
];
}
public function workflowStatus(int $bridgeId): array
{
// No synchronous status API in Dreamsoft adapter.
return [
'ok' => 1,
'id' => $bridgeId,
'status' => 'queued',
'next_action' => 'Track status via bridge queue/callback events.',
];
}
public function nhcxClaimStatusRequest(
int $documentId,
string $externalRef,
@ -209,4 +253,96 @@ class DreamsoftConnector implements AbdmConnectorInterface
);
return ['ok' => 1, 'queue_id' => $queueId, 'status' => 'queued'];
}
public function getRecords(array $filters = []): array
{
return [
'ok' => 1,
'records' => [],
'status' => 'queued',
'message' => 'Dreamsoft connector does not provide synchronous records listing.',
];
}
public function gatewayStatus(): array
{
return [
'ok' => 1,
'status' => 'queued',
'connector' => $this->getConnectorName(),
'message' => 'Dreamsoft connector does not expose synchronous gateway status.',
];
}
public function hipLinkToken(array $payload): array
{
return [
'ok' => 0,
'error_text' => 'HIP link-token flow is not supported by DreamsoftConnector. Use eatria_bridge connector.',
];
}
public function hipLinkCareContext(array $payload): array
{
return [
'ok' => 0,
'error_text' => 'HIP link care-context flow is not supported by DreamsoftConnector. Use eatria_bridge connector.',
];
}
public function hipGetPatientLinks(array $filters = []): array
{
return [
'ok' => 0,
'error_text' => 'HIP patient links flow is not supported by DreamsoftConnector. Use eatria_bridge connector.',
];
}
public function hipLinkNotify(array $payload): array
{
return [
'ok' => 0,
'error_text' => 'HIP link notify flow is not supported by DreamsoftConnector. Use eatria_bridge connector.',
];
}
public function hipSmsNotify(array $payload): array
{
return [
'ok' => 0,
'error_text' => 'HIP SMS notify flow is not supported by DreamsoftConnector. Use eatria_bridge connector.',
];
}
public function opdQueueFetch(string $date = '', string $status = '', int $page = 1, int $limit = 100): array
{
return [
'ok' => 0,
'error_text' => 'OPD queue API is not supported by DreamsoftConnector. Use eatria_bridge connector.',
];
}
public function opdTokenCreate(array $payload): array
{
return [
'ok' => 0,
'error_text' => 'OPD token API is not supported by DreamsoftConnector. Use eatria_bridge connector.',
];
}
public function opdTokenUpdateStatus(int $tokenId, string $status): array
{
return [
'ok' => 0,
'error_text' => 'OPD token status API is not supported by DreamsoftConnector. Use eatria_bridge connector.',
];
}
public function opdRunningTokenStatus(): array
{
return [
'ok' => 0,
'error_text' => 'OPD running token status API is not supported by DreamsoftConnector. Use eatria_bridge connector.',
];
}
}

View File

@ -338,16 +338,16 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
public function pushRecord(array $data): array
{
// Normalise to the 8 official ABDM HI types that the gateway validates.
// Valid values: OPConsultRecord, PrescriptionRecord, DiagnosticReportRecord,
// DischargeSummaryRecord, ImmunizationRecord, WellnessRecord,
// Normalise to gateway accepted HI types from integration docs.
// Valid values: OPConsultation, Prescription, DiagnosticReport,
// DischargeSummary, ImmunizationRecord, WellnessRecord,
// HealthDocumentRecord, InvoiceRecord
$hiTypeRaw = (string) ($data['hi_type'] ?? '');
$hiType = match ($hiTypeRaw) {
'OPConsultRecord', 'OPConsultation' => 'OPConsultRecord',
'PrescriptionRecord' => 'PrescriptionRecord',
'DiagnosticReportRecord', 'DiagnosticReport' => 'DiagnosticReportRecord',
'DischargeSummaryRecord', 'DischargeSummary' => 'DischargeSummaryRecord',
'OPConsultRecord', 'OPConsultation' => 'OPConsultation',
'PrescriptionRecord', 'Prescription' => 'Prescription',
'DiagnosticReportRecord', 'DiagnosticReport' => 'DiagnosticReport',
'DischargeSummaryRecord', 'DischargeSummary' => 'DischargeSummary',
'WellnessRecord' => 'WellnessRecord',
'ImmunizationRecord' => 'ImmunizationRecord',
'InvoiceRecord' => 'InvoiceRecord',
@ -364,9 +364,9 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
} else {
// Derive record_type from hi_type
$recordType = match ($hiType) {
'OPConsultRecord', 'PrescriptionRecord' => 'prescription',
'DiagnosticReportRecord' => 'lab_report',
'DischargeSummaryRecord' => 'discharge_summary',
'OPConsultation', 'Prescription' => 'prescription',
'DiagnosticReport' => 'lab_report',
'DischargeSummary' => 'discharge_summary',
'WellnessRecord', 'ImmunizationRecord' => 'wellness_record',
default => 'health_document',
};
@ -376,9 +376,9 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
// Gateway requires hi_type with the exact ABDM HI type name.
if ($hiType === '') {
$hiType = match ($recordType) {
'prescription' => 'OPConsultRecord',
'lab_report' => 'DiagnosticReportRecord',
'discharge_summary' => 'DischargeSummaryRecord',
'prescription' => 'OPConsultation',
'lab_report' => 'DiagnosticReport',
'discharge_summary' => 'DischargeSummary',
'wellness_record' => 'WellnessRecord',
default => 'HealthDocumentRecord',
};
@ -392,6 +392,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
'hi_type' => $hiType,
'visit_date' => (string) ($data['visit_date'] ?? date('Y-m-d')),
'record_data' => $data['record_data'] ?? $data['bundle'] ?? (object) [],
'fhir_bundle' => $data['record_data'] ?? $data['bundle'] ?? (object) [],
];
// hfr_id is required in every push request alongside the Bearer token.
@ -399,7 +400,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
$body['hfr_id'] = $this->hfrId;
}
foreach (['abha_id', 'abha_address', 'doctor_name', 'department', 'care_context_reference', 'notes', 'queue_id'] as $optional) {
foreach (['abha_id', 'abha_address', 'doctor_name', 'department', 'care_context_reference', 'care_context_display', 'notes', 'queue_id', 'gender', 'year_of_birth'] as $optional) {
if (! empty($data[$optional])) {
$body[$optional] = (string) $data[$optional];
}
@ -458,12 +459,38 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
public function getRecord(int $bridgeId): array
{
return $this->get('/v3/records/' . $bridgeId);
$query = [];
if ($this->hfrId !== '') {
$query['hfr_id'] = $this->hfrId;
}
return $this->get('/v3/records/' . $bridgeId, $query);
}
public function triggerShare(int $bridgeId): array
{
return $this->post('/v3/records/' . $bridgeId . '/share', []);
$body = [];
if ($this->hfrId !== '') {
$body['hfr_id'] = $this->hfrId;
}
return $this->post('/v3/records/' . $bridgeId . '/share', $body);
}
public function linkAndShare(int $bridgeId): array
{
$body = [];
if ($this->hfrId !== '') {
$body['hfr_id'] = $this->hfrId;
}
return $this->post('/v3/records/' . $bridgeId . '/link-and-share', $body);
}
public function workflowStatus(int $bridgeId): array
{
$query = [];
if ($this->hfrId !== '') {
$query['hfr_id'] = $this->hfrId;
}
return $this->get('/v3/records/' . $bridgeId . '/workflow-status', $query);
}
public function opdQueueFetch(string $date = '', string $status = '', int $page = 1, int $limit = 100): array

View File

@ -0,0 +1,99 @@
<div class="card">
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
<h3 class="card-title mb-0">
<i class="bi bi-person-vcard me-2 text-primary"></i>ABDM Report Doctor Mapping
</h3>
<span class="badge bg-primary">Lab & Radiology</span>
</div>
<div class="card-body">
<?= csrf_field() ?>
<div class="alert alert-info py-2 mb-3" style="font-size:13px;">
Select which doctor should be used in FHIR <code>Practitioner</code> for each report modality.
If a modality-specific doctor is not found, system falls back to default resolution.
</div>
<?php if (empty($doctors ?? [])) : ?>
<div class="alert alert-warning">No doctors found in Doctor Master. Add doctors first, then map them here.</div>
<?php else : ?>
<div class="row g-3">
<?php foreach (($mapping_keys ?? []) as $key => $label) : ?>
<div class="col-md-6">
<label class="form-label fw-semibold"><?= esc($label) ?></label>
<select class="form-select abdm-doc-map" id="<?= esc($key) ?>" data-key="<?= esc($key) ?>">
<option value="">-- Not mapped --</option>
<?php foreach (($doctors ?? []) as $doc) : ?>
<option value="<?= (int) ($doc['id'] ?? 0) ?>" <?= ((int) (($selected[$key] ?? 0)) === (int) ($doc['id'] ?? 0)) ? 'selected' : '' ?>>
<?= esc((string) ($doc['label'] ?? ('Doctor #' . (int) ($doc['id'] ?? 0)))) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="mt-3 d-flex gap-2 flex-wrap">
<button type="button" class="btn btn-primary" id="btn_save_abdm_doc_map">
<i class="bi bi-save me-1"></i>Save Mapping
</button>
</div>
<div id="abdm_doc_map_msg" class="mt-3"></div>
</div>
</div>
<script>
(function () {
'use strict';
function getCsrfPair() {
var input = document.querySelector('input[name="<?= csrf_token() ?>"]');
if (!input) {
return { name: '<?= csrf_token() ?>', value: '<?= csrf_hash() ?>' };
}
return { name: input.getAttribute('name'), value: input.value };
}
function updateCsrf(data) {
if (!data || !data.csrfName || !data.csrfHash) return;
var input = document.querySelector('input[name="' + data.csrfName + '"]');
if (input) input.value = data.csrfHash;
}
function showMsg(type, html) {
var cls = type === 'success' ? 'alert alert-success' : 'alert alert-danger';
$('#abdm_doc_map_msg').html('<div class="' + cls + ' py-2">' + html + '</div>');
}
function collectPayload() {
var payload = {};
document.querySelectorAll('.abdm-doc-map').forEach(function (el) {
payload[el.getAttribute('data-key')] = (el.value || '').trim();
});
return payload;
}
$('#btn_save_abdm_doc_map').on('click', function () {
var payload = collectPayload();
var csrf = getCsrfPair();
payload[csrf.name] = csrf.value;
var $btn = $(this).prop('disabled', true).text('Saving...');
$('#abdm_doc_map_msg').html('');
$.post('<?= base_url('setting/admin/abdm-report-doctors/save') ?>', payload, function (res) {
updateCsrf(res);
if (res && parseInt(res.update || '0', 10) === 1) {
showMsg('success', '<i class="bi bi-check-circle me-1"></i>' + (res.error_text || 'Saved.'));
} else {
showMsg('danger', '<i class="bi bi-x-circle me-1"></i>' + ((res && res.error_text) ? res.error_text : 'Save failed.'));
}
$btn.prop('disabled', false).html('<i class="bi bi-save me-1"></i>Save Mapping');
}, 'json').fail(function () {
showMsg('danger', '<i class="bi bi-x-circle me-1"></i>Save failed.');
$btn.prop('disabled', false).html('<i class="bi bi-save me-1"></i>Save Mapping');
});
});
})();
</script>

View File

@ -80,6 +80,12 @@
<span>ABDM Gateway</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/abdm-report-doctors') ?>','maindiv','ABDM Report Doctors');">
<i class="bi bi-person-vcard" style="color:#0d6efd"></i>
<span>ABDM Report Doctors</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/hospital-profile') ?>','maindiv','Hospital Profile');">
<i class="bi bi-building"></i>