feat(abdm): enhance IPD discharge FHIR generator with NRCES v6.5.0 single-coding and structured dosage instructions
This commit is contained in:
parent
4e1d5ed7af
commit
52f8d9691b
|
|
@ -768,6 +768,7 @@ $routes->post('AbdmGateway/share_diagnosis_report_bundle', 'AbdmGateway::shareDi
|
|||
$routes->post('AbdmGateway/shareDiagnosisReportBundle', 'AbdmGateway::shareDiagnosisReportBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmGateway/diagnosis_report_fhir_preview', 'AbdmGateway::diagnosisReportFhirPreview', ['filter' => $abdmPermFilter]);
|
||||
$routes->match(['GET', 'POST'], 'AbdmGateway/ipd_discharge_fhir_preview', 'AbdmGateway::ipdDischargeFhirPreview', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/save_ipd_discharge_fhir', 'AbdmGateway::saveIpdDischargeFhir', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmGateway/immunization_fhir_preview', 'AbdmGateway::immunizationFhirPreview', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/share_immunization_bundle', 'AbdmGateway::shareImmunizationBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/share_wellness_bundle', 'AbdmGateway::shareWellnessBundle', ['filter' => $abdmPermFilter]);
|
||||
|
|
|
|||
|
|
@ -874,8 +874,8 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// HIP Consent Notify Callback — gateway forwards consent notify updates.
|
||||
// POST /AbdmGateway/hip_consent_notify_callback (no auth filter — public webhook)
|
||||
// HIP Consent Notify Callback - gateway forwards consent notify updates.
|
||||
// POST /AbdmGateway/hip_consent_notify_callback (no auth filter - public webhook)
|
||||
// =========================================================================
|
||||
|
||||
public function hipConsentNotifyCallback()
|
||||
|
|
@ -1711,11 +1711,34 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
$effectiveConsent = (string) ($consent['consent_handle'] ?? $consentHandle);
|
||||
|
||||
$fhirPayload = $this->buildIpdDischargeGatewayPayload($ipdId, $patientId, $abhaId);
|
||||
$forceNewRecord = (int) $this->request->getPost('force_new_record');
|
||||
|
||||
$fhirPayload = $this->buildIpdDischargeGatewayPayload($ipdId, $patientId, $abhaId, $forceNewRecord === 1);
|
||||
if ($fhirPayload === null) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Unable to prepare IPD discharge FHIR payload']);
|
||||
}
|
||||
|
||||
$bundle = (array) ($fhirPayload['bundle'] ?? []);
|
||||
|
||||
// Override bundle with user edits if provided
|
||||
$overrideJson = trim((string) $this->request->getPost('fhir_override_json'));
|
||||
if ($overrideJson !== '') {
|
||||
$parsedOverride = json_decode($overrideJson, true);
|
||||
if (is_array($parsedOverride)) {
|
||||
if (($parsedOverride['resourceType'] ?? '') === 'Bundle') {
|
||||
$bundle = $parsedOverride;
|
||||
$fhirPayload['bundle'] = $bundle;
|
||||
} elseif (isset($parsedOverride['bundle']) && is_array($parsedOverride['bundle']) && ($parsedOverride['bundle']['resourceType'] ?? '') === 'Bundle') {
|
||||
$bundle = $parsedOverride['bundle'];
|
||||
$fhirPayload['bundle'] = $bundle;
|
||||
} else {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invalid JSON provided in the override editor.']);
|
||||
}
|
||||
} else {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invalid JSON provided in the override editor.']);
|
||||
}
|
||||
}
|
||||
|
||||
$ipdRow = (array) ($fhirPayload['ipd_row'] ?? []);
|
||||
$patName = (string) ($fhirPayload['patient_name'] ?? '');
|
||||
$doctorName = (string) ($fhirPayload['doctor_name'] ?? '');
|
||||
|
|
@ -1723,11 +1746,10 @@ class AbdmGateway extends BaseController
|
|||
$birthYear = (int) ($fhirPayload['year_of_birth'] ?? 0);
|
||||
$ccRef = (string) ($fhirPayload['care_context_reference'] ?? ('IPD-' . $ipdId . '-' . $visitDate));
|
||||
$ccDisplay = (string) ($fhirPayload['care_context_display'] ?? ('Discharge Summary ' . $visitDate));
|
||||
$bundle = (array) ($fhirPayload['bundle'] ?? []);
|
||||
$attachmentPath = trim((string) ($fhirPayload['attachment_path'] ?? ''));
|
||||
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
// ── Store health_record ───────────────────────────────────────────────
|
||||
// -- Store health_record -----------------------------------------------
|
||||
$healthRecordId = $this->storeHealthRecord([
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
|
|
@ -1769,7 +1791,7 @@ class AbdmGateway extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
// ── Push via pushRecord() (POST /v3/records/push) ─────────────────────
|
||||
// -- Push via pushRecord() (POST /v3/records/push) ---------------------
|
||||
$queueId = null;
|
||||
$bridgeRecordId = 0;
|
||||
$connectorError = null;
|
||||
|
|
@ -1846,7 +1868,7 @@ class AbdmGateway extends BaseController
|
|||
return $this->response->setJSON(['ok' => 0, 'error' => 'lab_req_id and patient_id are required']);
|
||||
}
|
||||
|
||||
// ── Load lab request ──────────────────────────────────────────────────
|
||||
// -- Load lab request --------------------------------------------------
|
||||
$labReq = $this->db->table('lab_request')
|
||||
->select('id, patient_name, lab_type, charge_id, Report_Data, report_data_Impression, status, reported_time')
|
||||
->where('id', $labReqId)
|
||||
|
|
@ -1873,7 +1895,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
$effectiveConsent = (string) ($consentRecord['consent_handle'] ?? $consentHandle);
|
||||
|
||||
// ── Load patient ──────────────────────────────────────────────────────
|
||||
// -- Load patient ------------------------------------------------------
|
||||
$patientRow = [];
|
||||
if ($this->db->tableExists('patient_master')) {
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
|
||||
|
|
@ -1884,7 +1906,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
$patientBirthYear = $this->resolvePatientBirthYear($patientRow, str_contains($abhaId, '@') ? $abhaId : '', str_contains($abhaId, '@') ? '' : $abhaId);
|
||||
|
||||
// ── Load test / charge name ────────────────────────────────────────────
|
||||
// -- Load test / charge name --------------------------------------------
|
||||
$testTitle = '';
|
||||
$chargeId = (int) ($labReq->charge_id ?? 0);
|
||||
if ($chargeId > 0 && $this->db->tableExists('charge_master')) {
|
||||
|
|
@ -1895,15 +1917,15 @@ class AbdmGateway extends BaseController
|
|||
$testTitle = $this->mapLabTypeToTitle((int) ($labReq->lab_type ?? 0));
|
||||
}
|
||||
|
||||
// ── Load hospital profile ─────────────────────────────────────────────
|
||||
// -- Load hospital profile ---------------------------------------------
|
||||
$hospitalProfile = $this->getHospitalProfileForFhir();
|
||||
|
||||
// ── Parse reported time ───────────────────────────────────────────────
|
||||
// -- Parse reported time -----------------------------------------------
|
||||
$reportedRaw = trim((string) ($labReq->reported_time ?? ''));
|
||||
$reportedAt = $reportedRaw !== '' ? (new \DateTime($reportedRaw, new \DateTimeZone('Asia/Kolkata')))->format('Y-m-d\TH:i:sP') : '';
|
||||
$visitDate = $reportedRaw !== '' ? date('Y-m-d', strtotime($reportedRaw)) : date('Y-m-d');
|
||||
|
||||
// ── Build FHIR DiagnosticReportRecord bundle ───────────────────────────
|
||||
// -- Build FHIR DiagnosticReportRecord bundle ---------------------------
|
||||
$fhir = new FhirR4Builder();
|
||||
|
||||
$patient = [
|
||||
|
|
@ -1931,7 +1953,7 @@ class AbdmGateway extends BaseController
|
|||
$diagnosticReport['section_snomed_display'] = 'Computed tomography imaging report';
|
||||
}
|
||||
|
||||
// ── Load LOINC code for the panel from lab_repo ───────────────────────
|
||||
// -- Load LOINC code for the panel from lab_repo -----------------------
|
||||
$labRepoRow = $this->db->table('lab_request lr')
|
||||
->select('lr.lab_repo_id, repo.loinc_code AS repo_loinc_code, repo.Title')
|
||||
->join('lab_repo repo', 'repo.mstRepoKey = lr.lab_repo_id', 'left')
|
||||
|
|
@ -1944,7 +1966,7 @@ class AbdmGateway extends BaseController
|
|||
$diagnosticReport['loinc_code'] = $repoLoincCode;
|
||||
}
|
||||
|
||||
// ── Build structured observations from lab_request_item + lab_tests ───
|
||||
// -- Build structured observations from lab_request_item + lab_tests ---
|
||||
$observations = $this->buildLabObservations($labReqId, (string) ($labReq->status ?? '0'));
|
||||
|
||||
$organization = $hospitalProfile['name'] !== ''
|
||||
|
|
@ -1972,7 +1994,7 @@ class AbdmGateway extends BaseController
|
|||
$bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, $practitioner, $organization, $encounter, $pdfAttachment);
|
||||
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
// ── Store health_record ───────────────────────────────────────────────
|
||||
// -- Store health_record -----------------------------------------------
|
||||
$ccRef = 'LAB-' . $labReqId . '-' . $visitDate;
|
||||
$healthRecordId = $this->storeHealthRecord([
|
||||
'patient_id' => $patientId,
|
||||
|
|
@ -2014,7 +2036,7 @@ class AbdmGateway extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
// ── Push via pushRecord() (POST /v3/records/push) ─────────────────────
|
||||
// -- Push via pushRecord() (POST /v3/records/push) ---------------------
|
||||
$queueId = null;
|
||||
$bridgeRecordId = 0;
|
||||
$connectorError = null;
|
||||
|
|
@ -2609,8 +2631,39 @@ class AbdmGateway extends BaseController
|
|||
$abhaId = $ident['abha_address'] ?: $ident['abha_id'];
|
||||
}
|
||||
|
||||
$forceNewRecord = (int) ($this->request->getGet('force_rebuild') ?? 0);
|
||||
try {
|
||||
$fhirPayload = $this->buildIpdDischargeGatewayPayload($ipdId, $patientId, $abhaId);
|
||||
if ($forceNewRecord !== 1 && $this->db && $this->db->tableExists('health_records')) {
|
||||
$existing = $this->db->table('health_records')
|
||||
->where('entity_type', 'ipd')
|
||||
->where('entity_id', (string) $ipdId)
|
||||
->whereIn('hi_type', ['DischargeSummaryRecord', 'DischargeSummary'])
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($existing)) {
|
||||
$raw = (string) ($existing['record_data'] ?? '');
|
||||
if ($raw === '' && ! empty($existing['fhir_bundle_enc'])) {
|
||||
$enc = new FhirEncryptionService();
|
||||
$raw = $enc->decrypt((string) $existing['fhir_bundle_enc']);
|
||||
}
|
||||
if ($raw !== '') {
|
||||
$savedBundle = json_decode($raw, true);
|
||||
if (is_array($savedBundle) && ($savedBundle['resourceType'] ?? '') === 'Bundle') {
|
||||
return $this->response->setJSON([
|
||||
'status' => 'ok',
|
||||
'ipd_id' => $ipdId,
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'bundle' => $savedBundle,
|
||||
'source' => 'saved_record',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$fhirPayload = $this->buildIpdDischargeGatewayPayload($ipdId, $patientId, $abhaId, $forceNewRecord === 1);
|
||||
if ($fhirPayload === null) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'status' => 'error',
|
||||
|
|
@ -2636,10 +2689,113 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
public function saveIpdDischargeFhir()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$ipdId = (int) ($this->request->getPost('ipd_id') ?? 0);
|
||||
$patientId = (int) ($this->request->getPost('patient_id') ?? 0);
|
||||
$abhaId = trim((string) ($this->request->getPost('abha_id') ?? ''));
|
||||
$rawJson = trim((string) ($this->request->getPost('fhir_json') ?? ''));
|
||||
|
||||
if ($ipdId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ipd_id is required']);
|
||||
}
|
||||
|
||||
if ($rawJson === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'fhir_json is required']);
|
||||
}
|
||||
|
||||
$parsed = json_decode($rawJson, true);
|
||||
if (! is_array($parsed)) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invalid JSON syntax: ' . json_last_error_msg()]);
|
||||
}
|
||||
|
||||
$bundle = ($parsed['resourceType'] ?? '') === 'Bundle' ? $parsed : ($parsed['bundle'] ?? null);
|
||||
if (! is_array($bundle) || ($bundle['resourceType'] ?? '') !== 'Bundle') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'The provided JSON is not a valid FHIR Bundle']);
|
||||
}
|
||||
|
||||
if ($patientId <= 0 && $this->db && $this->db->tableExists('ipd_master')) {
|
||||
$ipdRow = $this->db->table('ipd_master')->where('id', $ipdId)->get(1)->getRowArray() ?? [];
|
||||
$patientId = (int) ($ipdRow['p_id'] ?? $ipdRow['patient_id'] ?? 0);
|
||||
}
|
||||
|
||||
if ($abhaId === '' && $patientId > 0) {
|
||||
$ident = $this->resolvePatientAbhaIdentity($patientId);
|
||||
$abhaId = $ident['abha_address'] ?: $ident['abha_id'];
|
||||
}
|
||||
|
||||
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$ccRef = 'IPD-' . $ipdId . '-' . date('Y-m-d');
|
||||
|
||||
$healthRecordId = $this->storeHealthRecord([
|
||||
'patient_id' => $patientId > 0 ? $patientId : null,
|
||||
'abha_id' => $abhaId,
|
||||
'hi_type' => 'DischargeSummaryRecord',
|
||||
'entity_type' => 'ipd',
|
||||
'entity_id' => (string) $ipdId,
|
||||
'fhir_bundle' => $bundleJson,
|
||||
'care_context_reference' => $ccRef,
|
||||
]);
|
||||
|
||||
$abhaIdentity = $this->resolvePatientAbhaIdentity($patientId, $abhaId);
|
||||
$abhaNumber = $abhaIdentity['abha_id'];
|
||||
$abhaAddress = $abhaIdentity['abha_address'];
|
||||
|
||||
$patName = '';
|
||||
if ($this->db && $this->db->tableExists('patient_master')) {
|
||||
$pRow = $this->db->table('patient_master')->where('p_id', $patientId)->get(1)->getRowArray() ?? [];
|
||||
$patName = trim((string) (($pRow['p_fname'] ?? '') . ' ' . ($pRow['p_lname'] ?? '')));
|
||||
}
|
||||
|
||||
$queueId = null;
|
||||
$bridgeRecordId = 0;
|
||||
$connectorError = null;
|
||||
if ($abhaNumber !== '' || $abhaAddress !== '') {
|
||||
try {
|
||||
$pushResult = $this->connector->pushRecord([
|
||||
'patient_id' => (string) $patientId,
|
||||
'patient_name' => $patName !== '' ? $patName : ('PATIENT-' . $patientId),
|
||||
'abha_id' => $abhaNumber,
|
||||
'abha_address' => $abhaAddress,
|
||||
'hi_type' => 'DischargeSummaryRecord',
|
||||
'record_type' => 'DischargeSummaryRecord',
|
||||
'visit_date' => date('Y-m-d'),
|
||||
'care_context_reference' => $ccRef,
|
||||
'care_context_display' => 'Discharge Summary ' . date('Y-m-d'),
|
||||
'notes' => 'Discharge Summary ' . date('Y-m-d'),
|
||||
'queue_id' => 'IPD-' . $ipdId . '-' . date('Y-m-d'),
|
||||
'record_data' => $bundle,
|
||||
]);
|
||||
$this->logGatewayPushResolution('ipd_discharge', $pushResult);
|
||||
$queueId = $this->extractGatewayPushQueueId($pushResult);
|
||||
$bridgeRecordId = $this->extractGatewayPushRecordId($pushResult);
|
||||
} catch (\Throwable $e) {
|
||||
$connectorError = $e->getMessage();
|
||||
log_message('error', 'saveIpdDischargeFhir pushRecord error: ' . $e->getMessage());
|
||||
}
|
||||
if ($healthRecordId > 0) {
|
||||
$this->updateHealthRecordTxn($healthRecordId, $queueId ?? '', $connectorError, $bridgeRecordId);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'status' => 'ok',
|
||||
'message' => 'IPD discharge FHIR record updated, saved and pushed to bridge.',
|
||||
'health_record_id' => $healthRecordId,
|
||||
'queue_id' => $queueId,
|
||||
'bundle' => $bundle,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function buildIpdDischargeGatewayPayload(int $ipdId, int $patientId, string $preferredAbhaId = ''): ?array
|
||||
private function buildIpdDischargeGatewayPayload(int $ipdId, int $patientId, string $preferredAbhaId = '', bool $isNewRecord = false): ?array
|
||||
{
|
||||
$ipdRow = ($this->db && $this->db->tableExists('ipd_master'))
|
||||
? ($this->db->table('ipd_master')->where('id', $ipdId)->get(1)->getRowArray() ?? [])
|
||||
|
|
@ -2985,9 +3141,12 @@ class AbdmGateway extends BaseController
|
|||
$documents = $this->buildIpdPdfDocuments($ipdId, $dischargeRaw);
|
||||
$locationInfo = $this->resolveIpdLocation($ipdId);
|
||||
|
||||
$uniqueSuffix = $isNewRecord ? ('-v' . time()) : '';
|
||||
|
||||
$source = [
|
||||
'record_id' => (string) $ipdId,
|
||||
'bundle_identifier' => 'discharge-' . (trim((string) ($ipdRow['ipd_code'] ?? '')) ?: (string) $ipdId),
|
||||
'care_context_reference' => 'DISCHARGE-' . $ipdId . '-S' . $ipdId . '-' . $visitDate . $uniqueSuffix,
|
||||
'bundle_identifier' => 'discharge-' . (trim((string) ($ipdRow['ipd_code'] ?? '')) ?: (string) $ipdId) . $uniqueSuffix,
|
||||
'session_id' => (string) $ipdId,
|
||||
'visit_date' => $visitDate,
|
||||
'completed_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
|
||||
|
|
@ -3013,8 +3172,8 @@ class AbdmGateway extends BaseController
|
|||
'abha_address' => $abhaAddress,
|
||||
],
|
||||
'encounter' => [
|
||||
'id' => trim((string) ($ipdRow['ipd_code'] ?? '')) ?: (string) $ipdId,
|
||||
'ipd_no' => trim((string) ($ipdRow['ipd_code'] ?? '')) ?: ('IPD-' . $ipdId),
|
||||
'id' => (trim((string) ($ipdRow['ipd_code'] ?? '')) ?: (string) $ipdId) . $uniqueSuffix,
|
||||
'ipd_no' => (trim((string) ($ipdRow['ipd_code'] ?? '')) ?: ('IPD-' . $ipdId)) . $uniqueSuffix,
|
||||
'class_code' => 'IMP',
|
||||
'start' => $this->toIsoDateTimeOrNow($admissionRaw),
|
||||
'end' => $this->toIsoDateTimeOrNow($dischargeRaw),
|
||||
|
|
@ -3446,41 +3605,41 @@ class AbdmGateway extends BaseController
|
|||
'BT' => 'BT (BED TIME)',
|
||||
];
|
||||
static $whenHindiMap = [
|
||||
'BF' => 'भोजन से पहले', 'BEFORE FOOD' => 'भोजन से पहले',
|
||||
'AF' => 'भोजन के बाद', 'AFTER FOOD' => 'भोजन के बाद',
|
||||
'WF' => 'भोजन के साथ', 'WITH FOOD' => 'भोजन के साथ',
|
||||
'ES' => 'सुबह खाली पेट', 'EMPTY STOMACH' => 'सुबह खाली पेट',
|
||||
'BBF'=> 'नाश्ते से पहले', 'BEFORE BREAKFAST' => 'नाश्ते से पहले',
|
||||
'ABF'=> 'नाश्ते के बाद', 'AFTER BREAKFAST' => 'नाश्ते के बाद',
|
||||
'BL' => 'दोपहर के भोजन से पहले', 'BEFORE LUNCH' => 'दोपहर के भोजन से पहले',
|
||||
'AL' => 'दोपहर के भोजन के बाद', 'AFTER LUNCH' => 'दोपहर के भोजन के बाद',
|
||||
'BD' => 'रात के भोजन से पहले', 'BEFORE DINNER'=> 'रात के भोजन से पहले',
|
||||
'AD' => 'रात के भोजन के बाद', 'AFTER DINNER' => 'रात के भोजन के बाद',
|
||||
'BT' => 'रात को सोते समय', 'BED TIME' => 'रात को सोते समय',
|
||||
'BF' => ' ', 'BEFORE FOOD' => ' ',
|
||||
'AF' => ' ', 'AFTER FOOD' => ' ',
|
||||
'WF' => ' ', 'WITH FOOD' => ' ',
|
||||
'ES' => ' ', 'EMPTY STOMACH' => ' ',
|
||||
'BBF'=> ' ', 'BEFORE BREAKFAST' => ' ',
|
||||
'ABF'=> ' ', 'AFTER BREAKFAST' => ' ',
|
||||
'BL' => ' ', 'BEFORE LUNCH' => ' ',
|
||||
'AL' => ' ', 'AFTER LUNCH' => ' ',
|
||||
'BD' => ' ', 'BEFORE DINNER'=> ' ',
|
||||
'AD' => ' ', 'AFTER DINNER' => ' ',
|
||||
'BT' => ' ', 'BED TIME' => ' ',
|
||||
];
|
||||
static $freqHindiMap = [
|
||||
'OD' => 'दिन में एक बार (OD)', 'BD' => 'दिन में दो बार (BD)',
|
||||
'TDS' => 'दिन में तीन बार (TDS)', 'TID' => 'दिन में तीन बार (TID)',
|
||||
'QID' => 'दिन में चार बार (QID)', 'HS' => 'रात को सोते समय (HS)',
|
||||
'SOS' => 'ज़रूरत पड़ने पर (SOS)', 'STAT'=> 'तुरंत एक बार (STAT)',
|
||||
'ALTERNATE DAY' => 'एक दिन छोड़कर',
|
||||
'DAILY' => 'प्रतिदिन',
|
||||
'WEEKLY' => 'हफ़्ते में एक बार',
|
||||
'MONTHLY' => 'महीने में एक बार',
|
||||
'OD' => ' (OD)', 'BD' => ' (BD)',
|
||||
'TDS' => ' (TDS)', 'TID' => ' (TID)',
|
||||
'QID' => ' (QID)', 'HS' => ' (HS)',
|
||||
'SOS' => ' (SOS)', 'STAT'=> ' (STAT)',
|
||||
'ALTERNATE DAY' => ' ',
|
||||
'DAILY' => '',
|
||||
'WEEKLY' => ' ',
|
||||
'MONTHLY' => ' ',
|
||||
];
|
||||
static $remarkHindiMap = [
|
||||
'TAKE WITH MILK' => 'दूध के साथ लें',
|
||||
'TAKE WITH WARM WATER' => 'गुनगुने पानी के साथ लें',
|
||||
'AVOID SOUR FOOD AND DAIRY PRODUCTS'=> 'खट्टा और डेयरी उत्पाद न लें',
|
||||
'TAKE AFTER MEALS' => 'भोजन के बाद लें',
|
||||
'TAKE ON AN EMPTY STOMACH EARLY MORNING' => 'सुबह खाली पेट लें',
|
||||
'CHEW WELL BEFORE SWALLOWING' => 'चबाकर खाएं',
|
||||
'DISSOLVE IN HALF GLASS OF WATER' => 'आधे गिलास पानी में घोलकर लें',
|
||||
'APPLY LOCALLY TWICE DAILY' => 'दिन में दो बार लगाएं',
|
||||
'DO NOT CRUSH OR CHEW TABLET' => 'गोली को तोड़े या चबाएं नहीं',
|
||||
'AVOID ALCOHOL WHILE TAKING THIS MEDICINE' => 'शराब का सेवन न करें',
|
||||
'COMPLETE FULL COURSE OF ANTIBIOTICS' => 'एंटीबायोटिक का पूरा कोर्स लें',
|
||||
'DRINK PLENTY OF FLUIDS / WATER' => 'प्रचुर मात्रा में पानी पिएं',
|
||||
'TAKE WITH MILK' => ' ',
|
||||
'TAKE WITH WARM WATER' => ' ',
|
||||
'AVOID SOUR FOOD AND DAIRY PRODUCTS'=> ' ',
|
||||
'TAKE AFTER MEALS' => ' ',
|
||||
'TAKE ON AN EMPTY STOMACH EARLY MORNING' => ' ',
|
||||
'CHEW WELL BEFORE SWALLOWING' => ' ',
|
||||
'DISSOLVE IN HALF GLASS OF WATER' => ' ',
|
||||
'APPLY LOCALLY TWICE DAILY' => ' ',
|
||||
'DO NOT CRUSH OR CHEW TABLET' => ' ',
|
||||
'AVOID ALCOHOL WHILE TAKING THIS MEDICINE' => ' ',
|
||||
'COMPLETE FULL COURSE OF ANTIBIOTICS' => ' ',
|
||||
'DRINK PLENTY OF FLUIDS / WATER' => ' ',
|
||||
];
|
||||
|
||||
static $doseCache = null;
|
||||
|
|
@ -3618,7 +3777,7 @@ class AbdmGateway extends BaseController
|
|||
$daysNum = is_numeric($daysClean) ? (int) $daysClean : 0;
|
||||
$dirParts[] = $daysNum > 0 ? ('for ' . $daysNum . ' days') : ('for ' . $daysClean);
|
||||
if ($daysNum > 0) {
|
||||
$dirLocalParts[] = $daysNum . ' दिन';
|
||||
$dirLocalParts[] = $daysNum . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3881,9 +4040,9 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// Record Linked Callback — ABDM notifies HMS when a record is successfully
|
||||
// Record Linked Callback - ABDM notifies HMS when a record is successfully
|
||||
// linked to an ABHA address.
|
||||
// POST /AbdmGateway/record_linked_callback (no auth filter — public webhook)
|
||||
// POST /AbdmGateway/record_linked_callback (no auth filter - public webhook)
|
||||
// =========================================================================
|
||||
|
||||
public function recordLinkedCallback()
|
||||
|
|
@ -3987,8 +4146,8 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// Consent Revoked Callback — ABDM notifies HMS when consent is revoked.
|
||||
// POST /AbdmGateway/consent_revoked_callback (no auth filter — public webhook)
|
||||
// Consent Revoked Callback - ABDM notifies HMS when consent is revoked.
|
||||
// POST /AbdmGateway/consent_revoked_callback (no auth filter - public webhook)
|
||||
// =========================================================================
|
||||
|
||||
public function consentRevokedCallback()
|
||||
|
|
@ -4044,7 +4203,7 @@ class AbdmGateway extends BaseController
|
|||
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'consent_handle not found']);
|
||||
}
|
||||
|
||||
// Idempotency — already revoked
|
||||
// Idempotency - already revoked
|
||||
if (strtolower((string) ($existing['consent_status'] ?? '')) === 'revoked') {
|
||||
return $this->response->setJSON(['ok' => 1, 'consent_handle' => $consentHandle, 'status' => 'revoked', 'duplicate' => 1]);
|
||||
}
|
||||
|
|
@ -4183,7 +4342,7 @@ class AbdmGateway extends BaseController
|
|||
);
|
||||
|
||||
// Use the new store-and-link flow (POST /v3/records/push) for re-push:
|
||||
// No consent needed — bridge stores record and serves it when ABDM requests data.
|
||||
// No consent needed - bridge stores record and serves it when ABDM requests data.
|
||||
$result = $this->connector->pushRecord([
|
||||
'patient_id' => (string) $patientId,
|
||||
'patient_name' => $sanitizedPatientName !== '' ? $sanitizedPatientName : ('PATIENT-' . $patientId),
|
||||
|
|
@ -4238,7 +4397,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// List health_records for a patient (AJAX — used by ABDM task board)
|
||||
// List health_records for a patient (AJAX - used by ABDM task board)
|
||||
// GET /AbdmGateway/health_records_list?patient_id=X&abha_id=Y
|
||||
// =========================================================================
|
||||
|
||||
|
|
@ -4516,7 +4675,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// Bridge record status — proxy to GET /api/v3/records/{id}
|
||||
// Bridge record status - proxy to GET /api/v3/records/{id}
|
||||
// GET /AbdmGateway/bridge_record_status/:id
|
||||
// =========================================================================
|
||||
|
||||
|
|
@ -4536,7 +4695,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// Trigger HIP-initiated ABDM linking — POST /api/v3/records/{id}/share
|
||||
// Trigger HIP-initiated ABDM linking - POST /api/v3/records/{id}/share
|
||||
// POST /AbdmGateway/bridge_record_share/:id
|
||||
// =========================================================================
|
||||
|
||||
|
|
@ -4570,7 +4729,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// OPD running token status — GET /api/v3/opd/running-token-status
|
||||
// OPD running token status - GET /api/v3/opd/running-token-status
|
||||
// GET /AbdmGateway/opd_running_token_status
|
||||
// =========================================================================
|
||||
|
||||
|
|
@ -5084,7 +5243,7 @@ class AbdmGateway extends BaseController
|
|||
$append($rows, 'ABHA', 4);
|
||||
}
|
||||
|
||||
// Demographic matches — primary criteria
|
||||
// Demographic matches - primary criteria
|
||||
$nameUpper = strtoupper(trim((string) ($identity['patient_name'] ?? '')));
|
||||
$rawGender = strtoupper(trim((string) ($identity['gender'] ?? '')));
|
||||
$genderDb = ($rawGender === 'F' || $rawGender === '2') ? 2 : ($rawGender !== '' ? 1 : null);
|
||||
|
|
@ -5096,7 +5255,7 @@ class AbdmGateway extends BaseController
|
|||
$ageYears = preg_match('/^\d{1,3}$/', $age) ? (int) $age : null;
|
||||
|
||||
if ($nameUpper !== '' && $dobValid !== '') {
|
||||
// Name + exact DOB [+ gender] — high confidence (score 3)
|
||||
// Name + exact DOB [+ gender] - high confidence (score 3)
|
||||
$q = $this->db->table('patient_master')->select($select)
|
||||
->where('UPPER(p_fname)', $nameUpper)
|
||||
->where('dob', $dobValid);
|
||||
|
|
@ -5111,7 +5270,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
if ($nameUpper !== '' && $yearOnly > 0) {
|
||||
// Name + year of birth [+ gender] — medium confidence (score 2)
|
||||
// Name + year of birth [+ gender] - medium confidence (score 2)
|
||||
$q = $this->db->table('patient_master')->select($select)
|
||||
->where('UPPER(p_fname)', $nameUpper)
|
||||
->where('YEAR(dob)', $yearOnly);
|
||||
|
|
@ -5132,7 +5291,7 @@ class AbdmGateway extends BaseController
|
|||
$append([$row], $reason, $score);
|
||||
}
|
||||
|
||||
// Phone match — supplementary only (score 1), ambiguous for families
|
||||
// Phone match - supplementary only (score 1), ambiguous for families
|
||||
if (($identity['phone'] ?? '') !== '') {
|
||||
$rows = $this->db->table('patient_master')->select($select)->where('mphone1', $identity['phone'])->limit(10)->get()->getResultArray();
|
||||
$append($rows, 'Phone', 1);
|
||||
|
|
@ -5506,7 +5665,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
/**
|
||||
* HMS sits behind NAT, so ABDM/bridge discovery callbacks can never reach it —
|
||||
* HMS sits behind NAT, so ABDM/bridge discovery callbacks can never reach it -
|
||||
* records must be pushed to the gateway to become discoverable in the PHR.
|
||||
* Set ABDM_GATEWAY_PUSH_MODE=0 only for a callback-reachable deployment.
|
||||
*/
|
||||
|
|
@ -8211,7 +8370,7 @@ class AbdmGateway extends BaseController
|
|||
|
||||
// =========================================================================
|
||||
// M1 ABHA OTP Flows
|
||||
// Calls EAtriaBridgeConnector synchronously — requires eatria_bridge connector.
|
||||
// Calls EAtriaBridgeConnector synchronously - requires eatria_bridge connector.
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
|
|
@ -8777,7 +8936,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// Gateway status — hospital info + ABDM connectivity
|
||||
// Gateway status - hospital info + ABDM connectivity
|
||||
// GET /AbdmGateway/gateway_status
|
||||
// =========================================================================
|
||||
|
||||
|
|
@ -8792,7 +8951,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// Bridge records — list stored records from bridge
|
||||
// Bridge records - list stored records from bridge
|
||||
// GET /AbdmGateway/bridge_records_list
|
||||
// =========================================================================
|
||||
|
||||
|
|
@ -8816,7 +8975,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// HIP-Initiated Linking — Step 1: request link token
|
||||
// HIP-Initiated Linking - Step 1: request link token
|
||||
// POST /AbdmGateway/hip_link_token
|
||||
// Body: { abha_address, name, gender, year_of_birth [, abha_number] }
|
||||
// =========================================================================
|
||||
|
|
@ -8841,7 +9000,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// HIP-Initiated Linking — Step 2: link care contexts
|
||||
// HIP-Initiated Linking - Step 2: link care contexts
|
||||
// POST /AbdmGateway/hip_link_carecontext
|
||||
// Body: { abha_address, link_token_id, patient_ref, display, hi_type, care_contexts[] }
|
||||
// =========================================================================
|
||||
|
|
@ -8866,7 +9025,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// HIP-Initiated Linking — fetch all linked care contexts for a patient
|
||||
// HIP-Initiated Linking - fetch all linked care contexts for a patient
|
||||
// GET /AbdmGateway/hip_patient_links?abha_address=xxx&limit=20
|
||||
// =========================================================================
|
||||
|
||||
|
|
@ -8890,7 +9049,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// HIP-Initiated Linking — notify ABDM of care-context update
|
||||
// HIP-Initiated Linking - notify ABDM of care-context update
|
||||
// POST /AbdmGateway/hip_link_notify
|
||||
// Body: { abha_address, care_context_reference, hi_type, date_of_record }
|
||||
// =========================================================================
|
||||
|
|
@ -8915,7 +9074,7 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// HIP-Initiated Linking — send ABDM deep-link SMS to patient
|
||||
// HIP-Initiated Linking - send ABDM deep-link SMS to patient
|
||||
// POST /AbdmGateway/hip_sms_notify
|
||||
// Body: { phone_number [, hip_name] }
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -9189,6 +9189,13 @@ class Ipd_discharge extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
$uiSummaryInvestigation = '';
|
||||
if ($this->tableHasColumns('ipd_discharge_investigtions_inhos', ['ipd_id', 'comp_remark'])) {
|
||||
$inhosRow = $this->firstRowByIpd('ipd_discharge_investigtions_inhos', $ipdId);
|
||||
$uiSummaryInvestigation = trim((string) ($inhosRow['comp_remark'] ?? ''));
|
||||
}
|
||||
|
||||
$investigationRows = [];
|
||||
if ($this->tableHasColumns('ipd_discharge_1_d', ['ipd_d_id'])) {
|
||||
$cols = $this->db->getFieldNames('ipd_discharge_1_d') ?? [];
|
||||
|
|
@ -9329,6 +9336,59 @@ class Ipd_discharge extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
$uiClinicalHistoryText = '';
|
||||
$historyList = [];
|
||||
|
||||
$opdHistorySnapshot = [];
|
||||
if ($patientId > 0 && $this->db->tableExists('patient_master')) {
|
||||
$historyFields = [
|
||||
'is_smoking' => 'Smoking',
|
||||
'is_alcohol' => 'Alcohol',
|
||||
'is_drug_abuse' => 'Drug Abuse',
|
||||
'is_tobacoo' => 'Tobacco',
|
||||
'is_hypertesion' => 'Hypertension',
|
||||
'is_diabetes' => 'Diabetes',
|
||||
'is_ischaemic_heart_ds' => 'Ischemic Heart Disease',
|
||||
'is_asthma_copd' => 'Asthma/COPD',
|
||||
'is_bleeding_disorder' => 'Bleeding Disorder',
|
||||
'is_heapatitis_type' => 'Hepatitis',
|
||||
'is_blood_transfusion' => 'Blood Transfusion',
|
||||
'is_hiv_std' => 'HIV/STD',
|
||||
'is_epilepsy' => 'Epilepsy',
|
||||
'is_tb' => 'TB',
|
||||
'is_psychiatric_illness' => 'Psychiatric Illness',
|
||||
'is_thyroid' => 'Thyroid',
|
||||
];
|
||||
foreach ($historyFields as $f => $label) {
|
||||
if ((int) ($patientRow[$f] ?? 0) === 1) {
|
||||
$historyList[] = $label . ': Yes';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($instructionRows)) {
|
||||
$instructionRow = $instructionRows[0];
|
||||
$instructionMeta = $this->parseInstructionMetaPayload((string) ($instructionRow['comp_report'] ?? ''));
|
||||
$nabhMeta = is_array($instructionMeta['nabh'] ?? null) ? $instructionMeta['nabh'] : [];
|
||||
|
||||
$historyLabels = [
|
||||
'drug_allergy_status' => 'Drug Allergy Status',
|
||||
'drug_allergy_details' => 'Drug Allergy Details',
|
||||
'adr_history' => 'ADR History',
|
||||
'current_medications' => 'Current Medications',
|
||||
'co_morbidities' => 'Comorbidities',
|
||||
'hpi_note' => 'HPI Note'
|
||||
];
|
||||
foreach ($historyLabels as $key => $label) {
|
||||
$val = trim((string) ($nabhMeta[$key] ?? ''));
|
||||
if ($val !== '') {
|
||||
$historyList[] = $label . ': ' . $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
$uiClinicalHistoryText = implode("\n", $historyList);
|
||||
|
||||
$documentsList = $this->buildIpdPdfDocumentsList($ipdId, $dischargeIso);
|
||||
$locationInfo = $this->resolveIpdLocation($ipdId);
|
||||
|
||||
|
|
@ -9377,15 +9437,18 @@ class Ipd_discharge extends BaseController
|
|||
'room' => (string) ($locationInfo['room'] ?? ''),
|
||||
'bed' => (string) ($locationInfo['bed'] ?? ''),
|
||||
],
|
||||
'chief_complaints' => $chiefComplaintsList,
|
||||
'chief_complaint_narrative' => $complaintRemarkText,
|
||||
'conditions' => $conditionRows,
|
||||
'procedures' => $procedureRows,
|
||||
'medications' => $medicationRows,
|
||||
'observations' => $observationRows,
|
||||
'investigations' => $investigationRows,
|
||||
'ui_complaints' => $chiefComplaintsList,
|
||||
'ui_complaint_narrative' => $complaintRemarkText,
|
||||
'ui_clinical_history' => $uiClinicalHistoryText,
|
||||
'ui_physical_exam' => $observationRows,
|
||||
'ui_investigations' => $investigationRows,
|
||||
'ui_surgeries' => $procedureRows,
|
||||
'ui_final_diagnosis' => $conditionRows,
|
||||
'ui_summary_investigation' => $uiSummaryInvestigation,
|
||||
'ui_course_treatment' => $courseRows,
|
||||
'ui_discharge_medicine' => $medicationRows,
|
||||
'ui_discharge_summary' => $carePlanRows,
|
||||
'allergies' => $allergyRows,
|
||||
'care_plans' => $carePlanRows,
|
||||
'documents' => $documentsList,
|
||||
'template' => $this->resolveAbdmDischargeTemplate(),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@ class FhirDocumentBuilder
|
|||
{
|
||||
$this->bundleMeta = [
|
||||
'resourceType' => 'Bundle',
|
||||
'id' => $identifierValue,
|
||||
'meta' => [
|
||||
'profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle'],
|
||||
'security' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/v3-Confidentiality',
|
||||
'code' => 'V',
|
||||
'display' => 'very restricted'
|
||||
]]
|
||||
],
|
||||
'identifier' => [
|
||||
'system' => 'https://hms.local/fhir/document',
|
||||
'value' => $identifierValue,
|
||||
|
|
@ -174,13 +183,16 @@ class FhirDocumentBuilder
|
|||
private function normalizeEntryReferences(array $entries): array
|
||||
{
|
||||
$referenceMap = [];
|
||||
$idMap = [];
|
||||
foreach ($entries as $entry) {
|
||||
$resource = (array) ($entry['resource'] ?? []);
|
||||
$id = (string) ($resource['id'] ?? '');
|
||||
if ($id === '') {
|
||||
continue;
|
||||
}
|
||||
$referenceMap['urn:uuid:' . $id] = 'urn:uuid:' . $this->uuidForResource($resource);
|
||||
$newUuid = $this->uuidForResource($resource);
|
||||
$referenceMap['urn:uuid:' . $id] = 'urn:uuid:' . $newUuid;
|
||||
$idMap[$id] = $newUuid;
|
||||
}
|
||||
|
||||
foreach ($entries as &$entry) {
|
||||
|
|
@ -189,7 +201,11 @@ class FhirDocumentBuilder
|
|||
if ($id !== '') {
|
||||
$entry['fullUrl'] = $referenceMap['urn:uuid:' . $id];
|
||||
}
|
||||
$entry['resource'] = $this->rewriteReferences($resource, $referenceMap);
|
||||
$resource = $this->rewriteReferences($resource, $referenceMap);
|
||||
if ($id !== '') {
|
||||
$resource['id'] = $idMap[$id];
|
||||
}
|
||||
$entry['resource'] = $resource;
|
||||
}
|
||||
unset($entry);
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ abstract class AbstractModuleFhirGenerator
|
|||
]],
|
||||
'text' => [
|
||||
'status' => 'generated',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Patient:</b> ' . htmlspecialchars((string) ($source['patient']['name'] ?? 'Patient')) . ' (' . htmlspecialchars(ucfirst($gender)) . ')</p></div>',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Patient:</b> ' . $this->escapeXhtml((string) ($source['patient']['name'] ?? 'Patient')) . ' (' . $this->escapeXhtml(ucfirst($gender)) . ')</p></div>',
|
||||
],
|
||||
'identifier' => [
|
||||
[
|
||||
|
|
@ -185,12 +185,12 @@ abstract class AbstractModuleFhirGenerator
|
|||
*/
|
||||
protected function buildEncounter(array $source): ?array
|
||||
{
|
||||
if (! isset($source['encounter']) || ! is_array($source['encounter'])) {
|
||||
return null;
|
||||
$enc = $source['encounter'] ?? [];
|
||||
if (! is_array($enc)) {
|
||||
$enc = [];
|
||||
}
|
||||
|
||||
$enc = $source['encounter'];
|
||||
$id = (string) ($enc['id'] ?? '');
|
||||
$id = trim((string) ($enc['id'] ?? $source['record_id'] ?? ''));
|
||||
if ($id === '') {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -251,7 +251,7 @@ abstract class AbstractModuleFhirGenerator
|
|||
'location' => [
|
||||
'display' => $locDisplay,
|
||||
],
|
||||
'status' => 'active',
|
||||
'status' => 'completed',
|
||||
'period' => [
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
|
|
@ -270,7 +270,7 @@ abstract class AbstractModuleFhirGenerator
|
|||
]],
|
||||
'text' => [
|
||||
'status' => 'generated',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Encounter:</b> ' . htmlspecialchars($classDisplay) . ' ' . htmlspecialchars($id) . '</p></div>',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Encounter:</b> ' . $this->escapeXhtml($classDisplay) . ' ' . $this->escapeXhtml($id) . '</p></div>',
|
||||
],
|
||||
'status' => 'finished',
|
||||
'class' => [
|
||||
|
|
@ -287,6 +287,16 @@ abstract class AbstractModuleFhirGenerator
|
|||
'end' => $end,
|
||||
],
|
||||
'identifier' => $identifiers,
|
||||
'hospitalization' => [
|
||||
'dischargeDisposition' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/discharge-disposition',
|
||||
'code' => 'home',
|
||||
'display' => 'Home',
|
||||
]],
|
||||
'text' => 'Discharged to Home Care',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
if (! empty($locationList)) {
|
||||
|
|
@ -329,7 +339,7 @@ abstract class AbstractModuleFhirGenerator
|
|||
]],
|
||||
'text' => [
|
||||
'status' => 'generated',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Practitioner:</b> ' . htmlspecialchars($name) . '</p></div>',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Practitioner:</b> ' . $this->escapeXhtml($name) . '</p></div>',
|
||||
],
|
||||
'identifier' => [[
|
||||
'system' => 'https://doctor.ndhm.gov.in',
|
||||
|
|
@ -364,7 +374,7 @@ abstract class AbstractModuleFhirGenerator
|
|||
]],
|
||||
'text' => [
|
||||
'status' => 'generated',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Organization:</b> ' . htmlspecialchars($name) . '</p></div>',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Organization:</b> ' . $this->escapeXhtml($name) . '</p></div>',
|
||||
],
|
||||
'name' => $name,
|
||||
'identifier' => [[
|
||||
|
|
@ -403,4 +413,13 @@ abstract class AbstractModuleFhirGenerator
|
|||
|
||||
return trim(implode("\n", $lines));
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely escapes text for inclusion in FHIR XHTML div sections.
|
||||
* Ensures valid UTF-8 encoding so non-ASCII characters like Hindi script are preserved.
|
||||
*/
|
||||
protected function escapeXhtml(string $text): string
|
||||
{
|
||||
return htmlspecialchars($text, ENT_QUOTES | ENT_XML1, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,32 +20,32 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
$bundleIdentifier = 'discharge-' . $recordId . '-' . strtotime($timestamp);
|
||||
}
|
||||
|
||||
$careContextReference = 'DISCHARGE-' . $recordId . '-S' . $sessionId . '-' . $visitDate;
|
||||
$careContextReference = (string) ($source['care_context_reference'] ?? ('DISCHARGE-' . $recordId . '-S' . $sessionId . '-' . $visitDate));
|
||||
$careContextDisplay = 'Discharge Summary ' . $visitDate;
|
||||
|
||||
$builder = new \App\Libraries\Abdm\Fhir\FhirDocumentBuilder();
|
||||
$builder
|
||||
->buildBundleMeta($bundleIdentifier, $timestamp)
|
||||
->updateBundleMeta(['meta' => ['profile' => [
|
||||
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle',
|
||||
]]])
|
||||
->buildComposition($this->buildBaseComposition($source, 'IPD Discharge Summary', '373942005', 'Discharge summary'))
|
||||
->buildComposition($this->buildBaseComposition($source, 'IPD Discharge Summary', '18842-5', 'Discharge summary'))
|
||||
->updateComposition([
|
||||
'meta' => ['profile' => [
|
||||
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DischargeSummaryRecord',
|
||||
]],
|
||||
'type' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '373942005',
|
||||
'display' => 'Discharge summary',
|
||||
]], 'text' => 'Discharge Summary'],
|
||||
'meta' => [
|
||||
'versionId' => '1',
|
||||
'lastUpdated' => $timestamp,
|
||||
'profile' => [
|
||||
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DischargeSummaryRecord',
|
||||
],
|
||||
],
|
||||
'type' => ['coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '373942005',
|
||||
'display' => 'Discharge summary',
|
||||
],
|
||||
], 'text' => 'Discharge Summary'],
|
||||
])
|
||||
->addPatient($this->buildBasePatient($source));
|
||||
|
||||
$encounter = $this->buildEncounter($source);
|
||||
if (is_array($encounter)) {
|
||||
$builder->addEncounter($encounter);
|
||||
}
|
||||
|
||||
$practitioner = $this->buildPractitioner($source);
|
||||
if (! is_array($practitioner)) {
|
||||
|
|
@ -98,18 +98,22 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
$diagnosesList = $chiefComplaintsList;
|
||||
}
|
||||
|
||||
$encounterDiagnoses = [];
|
||||
|
||||
$conditionGroups = [
|
||||
[
|
||||
'items' => $chiefComplaintsList,
|
||||
'prefix' => 'discharge-chief-complaint-',
|
||||
'category_code' => 'problem-list-item',
|
||||
'category_display' => 'Problem List Item',
|
||||
'is_encounter_diagnosis' => false,
|
||||
],
|
||||
[
|
||||
'items' => $diagnosesList,
|
||||
'prefix' => 'discharge-diagnosis-',
|
||||
'category_code' => 'encounter-diagnosis',
|
||||
'category_display' => 'Encounter Diagnosis',
|
||||
'is_encounter_diagnosis' => true,
|
||||
],
|
||||
];
|
||||
foreach ($conditionGroups as $group) {
|
||||
|
|
@ -129,9 +133,25 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
'display' => $fallback['display'],
|
||||
]];
|
||||
}
|
||||
|
||||
$conditionId = $group['prefix'] . $recordId . '-' . $idx;
|
||||
|
||||
if ($group['is_encounter_diagnosis']) {
|
||||
$encounterDiagnoses[] = [
|
||||
'condition' => ['reference' => 'urn:uuid:' . $conditionId, 'display' => $text],
|
||||
'use' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/diagnosis-role',
|
||||
'code' => 'DD',
|
||||
'display' => 'Discharge diagnosis'
|
||||
]]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$builder->addCondition([
|
||||
'resourceType' => 'Condition',
|
||||
'id' => $group['prefix'] . $recordId . '-' . $idx,
|
||||
'id' => $conditionId,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition']],
|
||||
'text' => [
|
||||
'status' => 'generated',
|
||||
|
|
@ -159,6 +179,13 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($encounter)) {
|
||||
if ($encounterDiagnoses !== []) {
|
||||
$encounter['diagnosis'] = $encounterDiagnoses;
|
||||
}
|
||||
$builder->addEncounter($encounter);
|
||||
}
|
||||
|
||||
foreach ((array) ($source['procedures'] ?? []) as $idx => $proc) {
|
||||
$text = trim((string) ($proc['text'] ?? $proc['name'] ?? ''));
|
||||
|
|
@ -226,12 +253,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
}
|
||||
|
||||
$dosageText = trim((string) ($med['dosage'] ?? ''));
|
||||
$dosageInstruction = null;
|
||||
if ($this->isMeaningfulValue($dosageText)) {
|
||||
$dosageInstruction = [[
|
||||
'text' => $dosageText,
|
||||
]];
|
||||
}
|
||||
$structuredDosage = $this->buildStructuredDosageInstruction($dosageText, $name);
|
||||
|
||||
$medResource = [
|
||||
'resourceType' => 'MedicationRequest',
|
||||
|
|
@ -243,21 +265,43 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
],
|
||||
'status' => 'active',
|
||||
'intent' => 'order',
|
||||
'category' => [
|
||||
[
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/medicationrequest-category',
|
||||
'code' => 'community',
|
||||
'display' => 'Discharge Medication',
|
||||
],
|
||||
],
|
||||
'text' => 'Discharge Medication',
|
||||
],
|
||||
],
|
||||
'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
|
||||
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
|
||||
'authoredOn' => (string) ($med['authored_on'] ?? $timestamp),
|
||||
'requester' => is_array($practitioner) ? [
|
||||
'reference' => 'urn:uuid:' . (string) $practitioner['id'],
|
||||
'display' => (string) ($practitioner['name'][0]['text'] ?? ''),
|
||||
] : null,
|
||||
'medicationCodeableConcept' => [
|
||||
'coding' => $medCoding,
|
||||
'text' => $name,
|
||||
],
|
||||
];
|
||||
|
||||
if ($dosageInstruction !== null) {
|
||||
$medResource['dosageInstruction'] = $dosageInstruction;
|
||||
if ($encounterRef) {
|
||||
$medResource['encounter'] = ['reference' => $encounterRef];
|
||||
}
|
||||
|
||||
if (is_array($practitioner)) {
|
||||
$medResource['requester'] = [
|
||||
'reference' => 'urn:uuid:' . (string) $practitioner['id'],
|
||||
'display' => (string) ($practitioner['name'][0]['text'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($structuredDosage['dosageInstruction'])) {
|
||||
$medResource['dosageInstruction'] = $structuredDosage['dosageInstruction'];
|
||||
}
|
||||
|
||||
if (!empty($structuredDosage['dispenseRequest'])) {
|
||||
$medResource['dispenseRequest'] = $structuredDosage['dispenseRequest'];
|
||||
}
|
||||
|
||||
$builder->addMedicationRequest($medResource);
|
||||
|
|
@ -274,12 +318,6 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
$isAdm = (stripos($catText, 'Admission') !== false || stripos($text, 'Admission') !== false);
|
||||
$isDis = (stripos($catText, 'Discharge') !== false || stripos($text, 'Discharge') !== false);
|
||||
$obsCategoryDisplay = $isAdm ? 'Condition on Admission Time' : ($isDis ? 'Condition on Discharge Time' : 'Vital Signs');
|
||||
// ABDM PHR: use exam category for admission/discharge condition observations;
|
||||
// vital-signs category only for true vital measurements.
|
||||
$obsCategoryCode = ($isAdm || $isDis) ? 'exam' : 'vital-signs';
|
||||
$obsCategoryCanonicalDisplay = ($isAdm || $isDis) ? 'Exam' : 'Vital Signs';
|
||||
$obsCategorySystem = 'http://terminology.hl7.org/CodeSystem/observation-category';
|
||||
|
||||
$coding = [];
|
||||
$loincCode = trim((string) ($obs['loinc_code'] ?? ''));
|
||||
$loincDisplay = trim((string) ($obs['loinc_display'] ?? ''));
|
||||
|
|
@ -291,6 +329,11 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
}
|
||||
}
|
||||
|
||||
$isVital = ($loincCode !== '' && $loincCode !== '108252007');
|
||||
$obsCategoryCode = $isVital ? 'vital-signs' : 'exam';
|
||||
$obsCategoryCanonicalDisplay = $isVital ? 'Vital Signs' : 'Exam';
|
||||
$obsCategorySystem = 'http://terminology.hl7.org/CodeSystem/observation-category';
|
||||
|
||||
if ($loincCode !== '') {
|
||||
$coding[] = [
|
||||
'system' => 'http://loinc.org',
|
||||
|
|
@ -570,7 +613,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
continue;
|
||||
}
|
||||
|
||||
$builder->addCarePlan([
|
||||
$carePlanResource = [
|
||||
'resourceType' => 'CarePlan',
|
||||
'id' => 'discharge-careplan-' . $recordId . '-' . $idx,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/CarePlan']],
|
||||
|
|
@ -583,9 +626,49 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
'title' => $title !== '' ? $title : 'Discharge Advice',
|
||||
'description' => $description,
|
||||
'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
|
||||
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
|
||||
'created' => (string) ($cp['created_at'] ?? $timestamp),
|
||||
]);
|
||||
'category' => [
|
||||
[
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '736271009',
|
||||
'display' => 'Outpatient care plan'
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'activity' => [
|
||||
[
|
||||
'detail' => [
|
||||
'kind' => 'ServiceRequest',
|
||||
'code' => [
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '385676005',
|
||||
'display' => 'General advice and education'
|
||||
]
|
||||
]
|
||||
],
|
||||
'status' => 'scheduled',
|
||||
'description' => $description,
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
if ($encounterRef) {
|
||||
$carePlanResource['encounter'] = ['reference' => $encounterRef];
|
||||
}
|
||||
if (is_array($practitioner)) {
|
||||
$carePlanResource['author'] = [
|
||||
'reference' => 'urn:uuid:' . (string) $practitioner['id'],
|
||||
'display' => (string) ($practitioner['name'][0]['text'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
$builder->addCarePlan($carePlanResource);
|
||||
}
|
||||
|
||||
$documentsList = (array) ($source['documents'] ?? []);
|
||||
|
|
@ -602,16 +685,21 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
$docTitle = trim((string) ($document['title'] ?? 'Clinical document'));
|
||||
$snomedCode = trim((string) ($document['snomed_code'] ?? ''));
|
||||
$snomedDisplay = trim((string) ($document['snomed_display'] ?? ''));
|
||||
if ($snomedCode === '') {
|
||||
if (stripos($docTitle, 'Bill') !== false || stripos($docTitle, 'Invoice') !== false) {
|
||||
$snomedCode = '823651000000106'; // Billing record
|
||||
$snomedDisplay = 'Billing record';
|
||||
} else {
|
||||
$snomedCode = '373942005'; // Discharge summary
|
||||
$snomedDisplay = 'Discharge summary';
|
||||
}
|
||||
} elseif ($snomedDisplay === '') {
|
||||
$snomedDisplay = $docTitle;
|
||||
$loincCode = trim((string) ($document['loinc_code'] ?? ''));
|
||||
|
||||
$typeCoding = [];
|
||||
if (stripos($docTitle, 'Bill') !== false || stripos($docTitle, 'Invoice') !== false) {
|
||||
$typeCoding[] = [
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $snomedCode !== '' ? $snomedCode : '823651000000106',
|
||||
'display' => $snomedDisplay !== '' ? $snomedDisplay : 'Billing record',
|
||||
];
|
||||
} else {
|
||||
$typeCoding[] = [
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $snomedCode !== '' ? $snomedCode : '373942005',
|
||||
'display' => $snomedDisplay !== '' ? $snomedDisplay : 'Discharge summary',
|
||||
];
|
||||
}
|
||||
|
||||
$rawBytes = base64_decode($data);
|
||||
|
|
@ -628,11 +716,10 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
],
|
||||
'status' => 'current',
|
||||
'docStatus' => 'final',
|
||||
'type' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $snomedCode,
|
||||
'display' => $snomedDisplay,
|
||||
]], 'text' => $docTitle],
|
||||
'type' => [
|
||||
'coding' => $typeCoding,
|
||||
'text' => $docTitle,
|
||||
],
|
||||
'subject' => [
|
||||
'reference' => $patientRef,
|
||||
'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient'),
|
||||
|
|
@ -666,7 +753,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
$builder->addDocumentReference($docRef);
|
||||
}
|
||||
|
||||
$rawObservations = (array) ($source['observations'] ?? []);
|
||||
$rawObservations = (array) ($source['ui_physical_exam'] ?? $source['observations'] ?? []);
|
||||
$admissionObsIndexed = [];
|
||||
$dischargeObsIndexed = [];
|
||||
foreach ($rawObservations as $idx => $obs) {
|
||||
|
|
@ -679,40 +766,84 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
}
|
||||
}
|
||||
|
||||
$chiefComplaintsList = (array) ($source['ui_complaints'] ?? $source['chief_complaints'] ?? []);
|
||||
$complaintsNarrative = trim((string) ($source['ui_complaint_narrative'] ?? $source['chief_complaint_narrative'] ?? ''));
|
||||
|
||||
$diagnosesList = (array) ($source['ui_final_diagnosis'] ?? $source['diagnoses'] ?? $source['conditions'] ?? []);
|
||||
$clinicalHistoryNarrative = trim((string) ($source['ui_clinical_history'] ?? ''));
|
||||
|
||||
$admissionNarrative = trim((string) ($source['ui_admission_discharge'] ?? $source['admission_discharge_narrative'] ?? ''));
|
||||
|
||||
$sectionDefinitions = [
|
||||
['title' => 'Chief complaints', 'code' => '422843007', 'display' => 'Chief complaint section', 'prefix' => 'discharge-chief-complaint-', 'items' => $chiefComplaintsList, 'narrative' => trim((string) ($source['chief_complaint_narrative'] ?? ''))],
|
||||
['title' => 'Medical History', 'code' => '1003642006', 'display' => 'Past medical history section', 'prefix' => 'discharge-diagnosis-', 'items' => $diagnosesList, 'narrative' => trim((string) ($source['diagnosis_narrative'] ?? ''))],
|
||||
['title' => 'Procedures', 'code' => '1003640003', 'display' => 'History of past procedure section', 'prefix' => 'discharge-procedure-', 'items' => (array) ($source['procedures'] ?? [])],
|
||||
['title' => 'Medications', 'code' => '1003606003', 'display' => 'Medication history section', 'prefix' => 'discharge-medication-', 'items' => (array) ($source['medications'] ?? [])],
|
||||
// Chief complaints
|
||||
['title' => 'Chief complaints', 'code' => '422843007', 'display' => 'Chief complaint section', 'prefix' => 'discharge-chief-complaint-', 'items' => $chiefComplaintsList, 'narrative' => $complaintsNarrative],
|
||||
// Discharge Diagnosis
|
||||
['title' => 'Discharge Diagnosis', 'code' => '397659008', 'display' => 'Discharge diagnosis', 'prefix' => 'discharge-diagnosis-', 'items' => $diagnosesList, 'narrative' => $clinicalHistoryNarrative],
|
||||
];
|
||||
|
||||
if (! empty($admissionObsIndexed) && ! empty($dischargeObsIndexed)) {
|
||||
$sectionDefinitions[] = ['title' => 'Condition on Admission Time', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $admissionObsIndexed];
|
||||
$sectionDefinitions[] = ['title' => 'Condition on Discharge Time', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $dischargeObsIndexed];
|
||||
} elseif (! empty($admissionObsIndexed)) {
|
||||
$sectionDefinitions[] = ['title' => 'Condition on Admission Time', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $admissionObsIndexed];
|
||||
if (! empty($rawObservations)) {
|
||||
$otherObs = array_diff_key($rawObservations, $admissionObsIndexed);
|
||||
if (! empty($otherObs)) {
|
||||
$sectionDefinitions[] = ['title' => 'Physical examination', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $otherObs];
|
||||
}
|
||||
}
|
||||
} elseif (! empty($dischargeObsIndexed)) {
|
||||
$sectionDefinitions[] = ['title' => 'Condition on Discharge Time', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $dischargeObsIndexed];
|
||||
if (! empty($rawObservations)) {
|
||||
$otherObs = array_diff_key($rawObservations, $dischargeObsIndexed);
|
||||
if (! empty($otherObs)) {
|
||||
$sectionDefinitions[] = ['title' => 'Physical examination', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $otherObs];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$sectionDefinitions[] = ['title' => 'Physical examination', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items' => $rawObservations];
|
||||
// Surgery / Procedure
|
||||
$surgeries = (array) ($source['ui_surgeries'] ?? $source['procedures'] ?? []);
|
||||
if (! empty($surgeries)) {
|
||||
$sectionDefinitions[] = ['title' => 'Procedures', 'code' => '1003640003', 'display' => 'History of past procedure section', 'prefix' => 'discharge-procedure-', 'items' => $surgeries];
|
||||
}
|
||||
|
||||
$sectionDefinitions[] = ['title' => 'Investigations', 'code' => '721981007', 'display' => 'Diagnostic studies report', 'prefix' => 'discharge-investigation-', 'items' => (array) ($source['investigations'] ?? [])];
|
||||
$sectionDefinitions[] = ['title' => 'Allergies', 'code' => '722446000', 'display' => 'Allergy record', 'prefix' => 'discharge-allergy-', 'items' => (array) ($source['allergies'] ?? [])];
|
||||
$sectionDefinitions[] = ['title' => 'Care Plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => (array) ($source['care_plans'] ?? [])];
|
||||
$sectionDefinitions[] = ['title' => 'Document Reference', 'code' => '373942005', 'display' => 'Discharge summary', 'prefix' => 'discharge-document-', 'items' => $documentsList];
|
||||
// Medications
|
||||
$meds = (array) ($source['ui_discharge_medicine'] ?? $source['medications'] ?? []);
|
||||
if (! empty($meds)) {
|
||||
$sectionDefinitions[] = ['title' => 'Medications', 'code' => '1003606003', 'display' => 'Medication history section', 'prefix' => 'discharge-medication-', 'items' => $meds];
|
||||
}
|
||||
|
||||
// Physical Examinations on Admission
|
||||
if (! empty($admissionObsIndexed)) {
|
||||
$sectionDefinitions[] = ['title' => 'Condition on Admission Time', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $admissionObsIndexed];
|
||||
}
|
||||
|
||||
// Physical Examinations on Discharge
|
||||
if (! empty($dischargeObsIndexed)) {
|
||||
$sectionDefinitions[] = ['title' => 'Condition on Discharge Time', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $dischargeObsIndexed];
|
||||
}
|
||||
|
||||
// Additional Observations not captured in Admission/Discharge
|
||||
$otherObs = array_diff_key($rawObservations, $admissionObsIndexed, $dischargeObsIndexed);
|
||||
if (! empty($otherObs)) {
|
||||
$sectionDefinitions[] = ['title' => 'Other Physical examination', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $otherObs];
|
||||
}
|
||||
|
||||
// Investigations
|
||||
$investigations = (array) ($source['ui_investigations'] ?? $source['investigations'] ?? []);
|
||||
$invNarrative = trim((string) ($source['ui_summary_investigation'] ?? ''));
|
||||
if (! empty($investigations) || $invNarrative !== '') {
|
||||
$sectionDefinitions[] = ['title' => 'Clinical Investigation Reports', 'code' => '721981007', 'display' => 'Diagnostic studies report', 'prefix' => 'discharge-investigation-', 'items' => $investigations, 'narrative' => $invNarrative];
|
||||
}
|
||||
|
||||
// Admission / Discharge Details
|
||||
if ($admissionNarrative !== '') {
|
||||
$admItems = $encounterRef ? [['name' => 'Admission and Discharge Details', 'id' => 'encounter-' . $recordId]] : [];
|
||||
$sectionDefinitions[] = ['title' => 'Admission / Discharge Information', 'code' => '422838006', 'display' => 'Admission details', 'prefix' => 'discharge-admission-', 'items' => $admItems, 'narrative' => $admissionNarrative];
|
||||
}
|
||||
|
||||
// Course / Treatment
|
||||
$course = (array) ($source['ui_course_treatment'] ?? []);
|
||||
if (! empty($course)) {
|
||||
$sectionDefinitions[] = ['title' => 'Course / Treatment in the hospital', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-course-', 'items' => $course];
|
||||
}
|
||||
|
||||
// Care plans / Discharge advice
|
||||
$carePlans = (array) ($source['ui_discharge_summary'] ?? $source['care_plans'] ?? []);
|
||||
if (! empty($carePlans)) {
|
||||
$sectionDefinitions[] = ['title' => 'Care plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => $carePlans];
|
||||
}
|
||||
|
||||
// Document Reference
|
||||
if (! empty($documentsList)) {
|
||||
$sectionDefinitions[] = ['title' => 'Document reference', 'code' => '373942005', 'display' => 'Discharge summary', 'prefix' => 'discharge-document-', 'items' => $documentsList];
|
||||
}
|
||||
|
||||
// Allergies
|
||||
$allergies = (array) ($source['allergies'] ?? []);
|
||||
if (! empty($allergies)) {
|
||||
$sectionDefinitions[] = ['title' => 'Allergies', 'code' => '722446000', 'display' => 'Allergy record', 'prefix' => 'discharge-allergy-', 'items' => $allergies];
|
||||
}
|
||||
|
||||
$sections = [];
|
||||
foreach ($sectionDefinitions as $definition) {
|
||||
|
|
@ -740,21 +871,26 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
if ($sectionNarrative !== '') {
|
||||
array_unshift($narrativeParts, $sectionNarrative);
|
||||
}
|
||||
$sections[] = [
|
||||
$sec = [
|
||||
'title' => $definition['title'],
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $definition['code'],
|
||||
'display' => $definition['display'],
|
||||
]]],
|
||||
'entry' => $references,
|
||||
'text' => $narrativeParts !== [] ? [
|
||||
];
|
||||
if ($references !== []) {
|
||||
$sec['entry'] = $references;
|
||||
}
|
||||
if ($narrativeParts !== []) {
|
||||
$sec['text'] = [
|
||||
'status' => 'generated',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><ul><li>'
|
||||
. implode('</li><li>', array_map('htmlspecialchars', $narrativeParts))
|
||||
. '</li></ul></div>',
|
||||
] : null,
|
||||
];
|
||||
];
|
||||
}
|
||||
$sections[] = $sec;
|
||||
}
|
||||
|
||||
$patientNameClean = trim((string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient'));
|
||||
|
|
@ -786,12 +922,17 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
'div' => $fullNarrativeHtml,
|
||||
],
|
||||
];
|
||||
|
||||
if ($sections !== []) {
|
||||
$compositionFinalUpdate['section'] = $sections;
|
||||
}
|
||||
|
||||
$builder->updateComposition($compositionFinalUpdate);
|
||||
|
||||
$bundle = $builder->toBundle();
|
||||
|
||||
|
||||
|
||||
$validation = $this->validator->validate($bundle, 'discharge', ['resolved' => 1, 'unresolved' => 0, 'fallback_used' => 0]);
|
||||
|
||||
return [
|
||||
|
|
@ -1434,4 +1575,203 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build structured dosageInstruction and dispenseRequest from free text dosage and drug name
|
||||
*
|
||||
* @param string $dosageText
|
||||
* @param string $medName
|
||||
* @return array{dosageInstruction: array<int,array<string,mixed>>, dispenseRequest?: array<string,mixed>}
|
||||
*/
|
||||
private function buildStructuredDosageInstruction(string $dosageText, string $medName): array
|
||||
{
|
||||
$raw = strtoupper($dosageText . ' ' . $medName);
|
||||
|
||||
// Timing detection
|
||||
$timingCode = 'QD';
|
||||
$timingDisplay = 'Once daily';
|
||||
$frequency = 1;
|
||||
$period = 1;
|
||||
|
||||
if (preg_match('/\b(TDS|TID|THRICE|3 TIMES|THREE TIMES)\b/', $raw)) {
|
||||
$timingCode = 'TID';
|
||||
$timingDisplay = 'Three times daily';
|
||||
$frequency = 3;
|
||||
} elseif (preg_match('/\b(QID|4 TIMES|FOUR TIMES)\b/', $raw)) {
|
||||
$timingCode = 'QID';
|
||||
$timingDisplay = 'Four times daily';
|
||||
$frequency = 4;
|
||||
} elseif (preg_match('/\b(BD|BID|TWICE|2 TIMES|TWO TIMES)\b/', $raw)) {
|
||||
$timingCode = 'BID';
|
||||
$timingDisplay = 'Twice daily';
|
||||
$frequency = 2;
|
||||
} elseif (preg_match('/\b(SOS|PRN|AS NEEDED|WHEN REQUIRED)\b/', $raw)) {
|
||||
$timingCode = 'PRN';
|
||||
$timingDisplay = 'As needed';
|
||||
$frequency = 1;
|
||||
} elseif (preg_match('/\b(HS|BEDTIME|AT NIGHT)\b/', $raw)) {
|
||||
$timingCode = 'HS';
|
||||
$timingDisplay = 'At bedtime';
|
||||
$frequency = 1;
|
||||
}
|
||||
|
||||
// Additional Instructions detection (Food relation)
|
||||
$addInstruct = null;
|
||||
if (preg_match('/\b(EMPTY STOMACH|BEFORE FOOD|BEFORE MEALS?|BF|AC)\b/', $raw)) {
|
||||
$addInstruct = [
|
||||
[
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '307165006',
|
||||
'display' => 'Before meals',
|
||||
],
|
||||
],
|
||||
'text' => 'Before food / Empty stomach',
|
||||
],
|
||||
];
|
||||
} elseif (preg_match('/\b(AFTER FOOD|AFTER MEALS?|AF|PC)\b/', $raw)) {
|
||||
$addInstruct = [
|
||||
[
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '307166007',
|
||||
'display' => 'After meals',
|
||||
],
|
||||
],
|
||||
'text' => 'After food',
|
||||
],
|
||||
];
|
||||
} elseif (preg_match('/\b(WITH FOOD|WITH MEALS?)\b/', $raw)) {
|
||||
$addInstruct = [
|
||||
[
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '311504000',
|
||||
'display' => 'With or after food',
|
||||
],
|
||||
],
|
||||
'text' => 'With food',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// Route detection
|
||||
$routeCode = '260548002';
|
||||
$routeDisplay = 'Oral route';
|
||||
$routeText = 'Oral';
|
||||
|
||||
if (preg_match('/\b(INJ|IV|INTRAVENOUS|INFUSION)\b/', $raw)) {
|
||||
$routeCode = '47625008';
|
||||
$routeDisplay = 'Intravenous route';
|
||||
$routeText = 'Intravenous (IV)';
|
||||
} elseif (preg_match('/\b(IM|INTRAMUSCULAR)\b/', $raw)) {
|
||||
$routeCode = '78421000';
|
||||
$routeDisplay = 'Intramuscular route';
|
||||
$routeText = 'Intramuscular (IM)';
|
||||
} elseif (preg_match('/\b(DROPS?|EYE|EAR|OPHTHALMIC)\b/', $raw)) {
|
||||
$routeCode = '54485002';
|
||||
$routeDisplay = 'Ophthalmic route';
|
||||
$routeText = 'Drops';
|
||||
} elseif (preg_match('/\b(CREAM|OINTMENT|GEL|TOPICAL)\b/', $raw)) {
|
||||
$routeCode = '6064005';
|
||||
$routeDisplay = 'Topical route';
|
||||
$routeText = 'Topical';
|
||||
}
|
||||
|
||||
// Dose unit detection
|
||||
$doseUnit = 'Tablet';
|
||||
$doseUnitCode = 'tbl';
|
||||
$doseValue = 1;
|
||||
|
||||
if (preg_match('/\b(SACHET|POWDER)\b/', $raw)) {
|
||||
$doseUnit = 'Sachet';
|
||||
$doseUnitCode = 'sachet';
|
||||
} elseif (preg_match('/\b(SYRUP|SUSPENSION|ML)\b/', $raw)) {
|
||||
$doseUnit = 'mL';
|
||||
$doseUnitCode = 'mL';
|
||||
$doseValue = 5;
|
||||
} elseif (preg_match('/\b(CAPSULE|CAP)\b/', $raw)) {
|
||||
$doseUnit = 'Capsule';
|
||||
$doseUnitCode = 'capsule';
|
||||
} elseif (preg_match('/\b(DROPS?)\b/', $raw)) {
|
||||
$doseUnit = 'drops';
|
||||
$doseUnitCode = '[drp]';
|
||||
$doseValue = 2;
|
||||
}
|
||||
|
||||
// Duration detection
|
||||
$durationDays = 5;
|
||||
if (preg_match('/(?:FOR\s*)?(\d+)\s*DAYS?/i', $dosageText, $dm)) {
|
||||
$durationDays = (int) $dm[1];
|
||||
}
|
||||
|
||||
$instructionItem = [
|
||||
'text' => $dosageText !== '' ? $dosageText : ($doseValue . ' ' . $doseUnit . ' ' . $timingDisplay . ' for ' . $durationDays . ' days'),
|
||||
'timing' => [
|
||||
'repeat' => [
|
||||
'frequency' => $frequency,
|
||||
'period' => $period,
|
||||
'periodUnit' => 'd',
|
||||
],
|
||||
'code' => [
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation',
|
||||
'code' => $timingCode,
|
||||
'display' => $timingDisplay,
|
||||
],
|
||||
],
|
||||
'text' => $timingCode . ' (' . $timingDisplay . ')',
|
||||
],
|
||||
],
|
||||
'route' => [
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $routeCode,
|
||||
'display' => $routeDisplay,
|
||||
],
|
||||
],
|
||||
'text' => $routeText,
|
||||
],
|
||||
'doseAndRate' => [
|
||||
[
|
||||
'type' => [
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/dose-rate-type',
|
||||
'code' => 'ordered',
|
||||
'display' => 'Ordered',
|
||||
],
|
||||
],
|
||||
],
|
||||
'doseQuantity' => [
|
||||
'value' => $doseValue,
|
||||
'unit' => $doseUnit,
|
||||
'system' => 'http://unitsofmeasure.org',
|
||||
'code' => $doseUnitCode,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if ($addInstruct !== null) {
|
||||
$instructionItem['additionalInstruction'] = $addInstruct;
|
||||
}
|
||||
|
||||
return [
|
||||
'dosageInstruction' => [$instructionItem],
|
||||
'dispenseRequest' => [
|
||||
'expectedSupplyDuration' => [
|
||||
'value' => $durationDays,
|
||||
'unit' => 'days',
|
||||
'system' => 'http://unitsofmeasure.org',
|
||||
'code' => 'd',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@
|
|||
<div class="card shadow-sm mt-3 d-none" id="opdBookCard">
|
||||
<div class="card-header py-2 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<strong>OPD Book — ABDM Tokens</strong>
|
||||
<strong>OPD Book - ABDM Tokens</strong>
|
||||
<span class="badge bg-primary ms-2" id="hmsOpdBadge"><?= count($opd_book_rows ?? []) ?></span>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
|
|
@ -308,7 +308,7 @@
|
|||
<div class="px-3 pt-2 pb-1 bg-light border-bottom border-top d-flex align-items-center gap-2 mt-1">
|
||||
<strong class="small">HMS ABDM OPD Token List</strong>
|
||||
<span class="badge bg-success"><?= count($opd_book_rows ?? []) ?></span>
|
||||
<span class="text-muted small">Last 7 days · synced and manual ABDM tokens only</span>
|
||||
<span class="text-muted small">Last 7 days synced and manual ABDM tokens only</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
|
|
@ -340,9 +340,9 @@
|
|||
<!-- OPD Consult / Prescription Records Card -->
|
||||
<div class="card shadow-sm mt-3 d-none" id="opdConsultCard">
|
||||
<div class="card-header py-2 d-flex align-items-center gap-2">
|
||||
<strong>OPD Consult / Prescription Records — Done Appointments with ABHA</strong>
|
||||
<strong>OPD Consult / Prescription Records - Done Appointments with ABHA</strong>
|
||||
<span class="badge bg-success"><?= count($opd_consult_rows ?? []) ?></span>
|
||||
<small class="text-muted ms-1">Last 30 days · opd_status=Done · ABHA linked</small>
|
||||
<small class="text-muted ms-1">Last 30 days opd_status=Done ABHA linked</small>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
|
|
@ -431,7 +431,7 @@
|
|||
<!-- Invoice Status Card -->
|
||||
<div class="card shadow-sm mt-3 d-none" id="invoiceCard">
|
||||
<div class="card-header py-2 d-flex align-items-center gap-2 flex-wrap">
|
||||
<strong>Invoice — ABDM Billing Records</strong>
|
||||
<strong>Invoice - ABDM Billing Records</strong>
|
||||
<span class="badge bg-primary"><?= count($invoice_rows ?? []) ?></span>
|
||||
<small class="text-muted">OPD, Charges and IPD Billing</small>
|
||||
</div>
|
||||
|
|
@ -526,10 +526,14 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-warning" id="btnRegenerateFhirModal" title="Rebuild FHIR bundle from current prescription data"><i class="bi bi-arrow-clockwise"></i> Regenerate</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-success" id="btnSubmitFhirToAbdm" title="Push FHIR bundle to ABDM bridge"><i class="bi bi-cloud-upload"></i> Submit to ABDM</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnCopyFhirModal">Copy JSON</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<button type="button" class="btn btn-sm btn-outline-warning" id="btnRegenerateFhirModal" title="Rebuild FHIR bundle from current clinical data"><i class="bi bi-arrow-clockwise"></i> Regenerate</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="btnUpdateFhirJson" title="Validate and apply edited JSON changes to preview and database"><i class="bi bi-check2-circle"></i> Update JSON</button>
|
||||
<div>
|
||||
<label class="me-3 small text-muted"><input type="checkbox" id="chkForceNewRecord" checked> New Record ID (for PHR testing)</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-success" id="btnSubmitFhirToAbdm" title="Push FHIR bundle to ABDM bridge"><i class="bi bi-cloud-upload"></i> Submit to ABDM</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnCopyFhirModal">Copy JSON</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
|
|
@ -546,7 +550,7 @@
|
|||
</div>
|
||||
<div class="px-3 pb-2 pt-1" id="fhirModalQuality" style="display:none;"></div>
|
||||
<div id="fhirFormView" style="overflow-y:auto;max-height:62vh;padding:16px;"></div>
|
||||
<pre id="fhirModalJson" style="display:none;margin:0;max-height:62vh;overflow:auto;background:#0b1020;color:#d9e2ff;padding:16px;border-radius:0;">{}</pre>
|
||||
<textarea id="fhirModalJson" class="form-control" style="display:none;margin:0;max-height:62vh;height:500px;font-family:monospace;background:#0b1020;color:#d9e2ff;padding:16px;border-radius:0;" spellcheck="false">{}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -844,7 +848,7 @@
|
|||
var confBadgeClass = { definitive: 'bg-success', high: 'bg-primary', medium: 'bg-warning text-dark', low: 'bg-secondary' };
|
||||
var confBadge = '<span class="badge ' + (confBadgeClass[conf] || 'bg-secondary') + ' ms-1 small">' + (confLabels[conf] || 'Low') + '</span>';
|
||||
var reasons = Array.isArray(m.match_reasons) ? m.match_reasons.join(', ') : '';
|
||||
var genderLabel = m.gender == 2 ? 'F' : m.gender == 1 ? 'M' : '—';
|
||||
var genderLabel = m.gender == 2 ? 'F' : m.gender == 1 ? 'M' : '-';
|
||||
var borderClass = conf === 'definitive' ? 'border-success' : conf === 'high' ? 'border-primary' : conf === 'medium' ? 'border-warning' : 'border-secondary';
|
||||
var mLabel = ((m.p_code || 'UHID') + ' - ' + (m.p_fname || 'Unnamed'));
|
||||
cardHtml += '<div class="border rounded p-1 mb-1 bg-white ' + borderClass + '">';
|
||||
|
|
@ -852,14 +856,14 @@
|
|||
cardHtml += '<div class="small"><span class="fw-semibold">' + mLabel + '</span>' + confBadge + '</div>';
|
||||
cardHtml += '<button type="button" class="btn btn-sm btn-outline-primary ms-2" data-scan-use-existing="1" data-patient-id="' + (m.id || 0) + '">Link</button>';
|
||||
cardHtml += '</div>';
|
||||
cardHtml += '<div class="small text-muted">DOB: ' + (m.dob || '—') + ' | Gender: ' + genderLabel + ' | Phone: ' + (m.mphone1 || '—') + '</div>';
|
||||
cardHtml += '<div class="small text-muted">DOB: ' + (m.dob || '-') + ' | Gender: ' + genderLabel + ' | Phone: ' + (m.mphone1 || '-') + '</div>';
|
||||
cardHtml += '<div class="small text-muted fst-italic">Matched by: ' + reasons + '</div>';
|
||||
cardHtml += '</div>';
|
||||
});
|
||||
// Add "Create New Patient" as a selectable card option at the end of the list
|
||||
cardHtml += '<div class="border rounded border-2 border-secondary p-1 mb-1 bg-white">';
|
||||
cardHtml += '<div class="d-flex justify-content-between align-items-start">';
|
||||
cardHtml += '<div class="small"><span class="fw-semibold text-secondary">None of the above — Register as New Patient</span></div>';
|
||||
cardHtml += '<div class="small"><span class="fw-semibold text-secondary">None of the above - Register as New Patient</span></div>';
|
||||
cardHtml += '<button type="button" class="btn btn-sm btn-success ms-2" data-scan-create-new="1">Create New</button>';
|
||||
cardHtml += '</div>';
|
||||
cardHtml += '<div class="small text-muted">This ABHA does not belong to any listed record.</div>';
|
||||
|
|
@ -1069,7 +1073,7 @@
|
|||
+ '?lab_req_id=' + encodeURIComponent(labReqId)
|
||||
+ '&patient_id=' + encodeURIComponent(patientId)
|
||||
+ '&abha_id=' + encodeURIComponent(abhaId);
|
||||
title = (taskType === 'radiology_report_publish' ? 'Radiology' : 'Lab') + ' FHIR Preview — Request #' + labReqId;
|
||||
title = (taskType === 'radiology_report_publish' ? 'Radiology' : 'Lab') + ' FHIR Preview - Request #' + labReqId;
|
||||
} else if (taskType === 'opd_prescription_publish') {
|
||||
var opdId = parseInt(row.getAttribute('data-opd-id') || row.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
var opdSessionId = parseInt(row.getAttribute('data-opd-session-id') || '0', 10) || 0;
|
||||
|
|
@ -1082,7 +1086,7 @@
|
|||
previewUrl = opdSessionId > 0
|
||||
? '<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId + '/' + opdSessionId
|
||||
: '<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId;
|
||||
title = 'OPD FHIR Preview — OPD #' + opdId;
|
||||
title = 'OPD FHIR Preview - OPD #' + opdId;
|
||||
} else if (taskType === 'ipd_discharge_publish') {
|
||||
var ipdId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
var ipdPatientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
|
|
@ -1096,7 +1100,7 @@
|
|||
+ '?ipd_id=' + encodeURIComponent(ipdId)
|
||||
+ '&patient_id=' + encodeURIComponent(ipdPatientId)
|
||||
+ '&abha_id=' + encodeURIComponent(abhaId);
|
||||
title = 'IPD Discharge FHIR Preview — IPD #' + ipdId;
|
||||
title = 'IPD Discharge FHIR Preview - IPD #' + ipdId;
|
||||
} else if (taskType === 'immunization_record_publish') {
|
||||
var immunizationRecordId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
var immunizationPatientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
|
|
@ -1110,7 +1114,7 @@
|
|||
+ '?record_id=' + encodeURIComponent(immunizationRecordId)
|
||||
+ '&patient_id=' + encodeURIComponent(immunizationPatientId)
|
||||
+ '&abha_id=' + encodeURIComponent(abhaId);
|
||||
title = 'ImmunizationRecord FHIR Preview — Record #' + immunizationRecordId;
|
||||
title = 'ImmunizationRecord FHIR Preview - Record #' + immunizationRecordId;
|
||||
} else if (taskType === 'health_document_publish') {
|
||||
var healthDocId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
var healthPatientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
|
|
@ -1121,7 +1125,7 @@
|
|||
}
|
||||
|
||||
previewUrl = '<?= base_url('DoctorDocument/health_document_fhir_preview') ?>/' + healthDocId;
|
||||
title = 'HealthDocumentRecord FHIR Preview — Document #' + healthDocId;
|
||||
title = 'HealthDocumentRecord FHIR Preview - Document #' + healthDocId;
|
||||
} else if (taskType === 'wellness_record_publish') {
|
||||
var wellnessPatientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
var wellnessOpdId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
|
|
@ -1132,7 +1136,7 @@
|
|||
}
|
||||
|
||||
previewUrl = '<?= base_url('DoctorDocument/wellness_record_fhir_preview') ?>/' + wellnessPatientId + (wellnessOpdId > 0 ? ('/' + wellnessOpdId) : '');
|
||||
title = 'WellnessRecord FHIR Preview — Patient #' + wellnessPatientId;
|
||||
title = 'WellnessRecord FHIR Preview - Patient #' + wellnessPatientId;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1609,7 +1613,7 @@
|
|||
}
|
||||
navigator.clipboard.writeText(jsonText).then(function () {
|
||||
setStatus('FHIR JSON copied to clipboard.');
|
||||
btn.textContent = 'Copied ✓';
|
||||
btn.textContent = 'Copied v';
|
||||
btn.className = 'btn btn-sm btn-success';
|
||||
window.setTimeout(function () {
|
||||
btn.textContent = originalText;
|
||||
|
|
@ -1645,13 +1649,13 @@
|
|||
var stat = (t.status || t.queue_status || '').toString();
|
||||
var token = (t.token_number || t.tokenNumber || t.gateway_token_id || t.id || '').toString();
|
||||
var time = (t.created_at || t.token_time || t.arrival_time || '').toString();
|
||||
var hms = t.patient_id ? ('#' + t.patient_id + (t.p_fname ? ' ' + t.p_fname : '')) : '<span class="text-muted">—</span>';
|
||||
var hms = t.patient_id ? ('#' + t.patient_id + (t.p_fname ? ' ' + t.p_fname : '')) : '<span class="text-muted">-</span>';
|
||||
return '<tr>' +
|
||||
'<td>' + (token || '—') + '</td>' +
|
||||
'<td><span class="text-primary small">' + (abha || '<span class="text-muted">—</span>') + '</span></td>' +
|
||||
'<td>' + (name || '—') + '</td>' +
|
||||
'<td><span class="badge bg-' + (stat === 'PENDING' ? 'warning text-dark' : 'secondary') + '">' + (stat || '—') + '</span></td>' +
|
||||
'<td class="small">' + (time ? time.substring(0, 16) : '—') + '</td>' +
|
||||
'<td>' + (token || '-') + '</td>' +
|
||||
'<td><span class="text-primary small">' + (abha || '<span class="text-muted">-</span>') + '</span></td>' +
|
||||
'<td>' + (name || '-') + '</td>' +
|
||||
'<td><span class="badge bg-' + (stat === 'PENDING' ? 'warning text-dark' : 'secondary') + '">' + (stat || '-') + '</span></td>' +
|
||||
'<td class="small">' + (time ? time.substring(0, 16) : '-') + '</td>' +
|
||||
'<td>' + hms + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
|
|
@ -1673,18 +1677,38 @@
|
|||
var fhirModalDataBadge = document.getElementById('fhirModalDataBadge');
|
||||
var fhirModalHttpBadge = document.getElementById('fhirModalHttpBadge');
|
||||
var btnRegenerateFhirModal = document.getElementById('btnRegenerateFhirModal');
|
||||
var btnUpdateFhirJson = document.getElementById('btnUpdateFhirJson');
|
||||
var btnSubmitFhirToAbdm = document.getElementById('btnSubmitFhirToAbdm');
|
||||
|
||||
var defaultSubmitBtnHtml = btnSubmitFhirToAbdm ? btnSubmitFhirToAbdm.innerHTML : '';
|
||||
var defaultSubmitBtnClass = btnSubmitFhirToAbdm ? btnSubmitFhirToAbdm.className : 'btn btn-sm btn-outline-success';
|
||||
var defaultSubmitBtnTitle = btnSubmitFhirToAbdm ? btnSubmitFhirToAbdm.title : 'Push FHIR bundle to ABDM bridge';
|
||||
|
||||
// ── Tab switching ──────────────────────────────────────────────────
|
||||
// -- Tab switching --------------------------------------------------
|
||||
fhirTabForm.addEventListener('click', function () {
|
||||
fhirTabForm.classList.add('active');
|
||||
fhirTabJson.classList.remove('active');
|
||||
fhirFormView.style.display = '';
|
||||
fhirModalJson.style.display = 'none';
|
||||
|
||||
// Auto-refresh Form View from JSON if valid
|
||||
try {
|
||||
var curText = (fhirModalJson ? fhirModalJson.value : '').trim();
|
||||
if (curText && curText !== '{\n "loading": true\n}' && curText !== '{}') {
|
||||
var p = JSON.parse(curText);
|
||||
var b = (p && p.resourceType === 'Bundle') ? p : ((p && p.bundle) ? p.bundle : null);
|
||||
if (b && fhirFormView) {
|
||||
fhirFormView.innerHTML = renderFhirBundleForm(b, _fhirLiveComplaints, _fhirLiveDiagnoses);
|
||||
if (fhirModalQuality) {
|
||||
var qs = fhirCodeQuality(b);
|
||||
if (qs) {
|
||||
fhirModalQuality.innerHTML = qs;
|
||||
fhirModalQuality.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
fhirTabJson.addEventListener('click', function () {
|
||||
fhirTabJson.classList.add('active');
|
||||
|
|
@ -1693,8 +1717,9 @@
|
|||
fhirFormView.style.display = 'none';
|
||||
});
|
||||
|
||||
// ── Track current bundle context for edits ──────────────────────
|
||||
// -- Track current bundle context for edits ----------------------
|
||||
var _fhirOpdId = 0, _fhirSessionId = 0;
|
||||
var _fhirIpdId = 0, _fhirPatientId = 0, _fhirAbhaId = '';
|
||||
var _fhirLiveComplaints = [], _fhirLiveDiagnoses = [];
|
||||
var _fhirSubmitMode = 'opd';
|
||||
var _fhirSubmitTaskRow = null;
|
||||
|
|
@ -1704,7 +1729,7 @@
|
|||
var _fhirInvoicePatientId = 0;
|
||||
var _fhirPreviewUrl = '';
|
||||
|
||||
// ── Inline complaint SNOMED edit (event delegation on fhirFormView) ─
|
||||
// -- Inline complaint SNOMED edit (event delegation on fhirFormView) -
|
||||
var _editTimer = null;
|
||||
fhirFormView.addEventListener('click', function(e) {
|
||||
var editBtn = e.target.closest('.fhir-complaint-edit');
|
||||
|
|
@ -1753,7 +1778,7 @@
|
|||
panel2.querySelector('.fhir-snomed-search').value = ddItem.dataset.term || '';
|
||||
panel2.querySelector('.fhir-snomed-dd').style.display = 'none';
|
||||
var sel = panel2.querySelector('.fhir-snomed-selected');
|
||||
sel.textContent = '✓ ' + (ddItem.dataset.id || '') + ' — ' + (ddItem.dataset.term || '');
|
||||
sel.textContent = 'v ' + (ddItem.dataset.id || '') + ' - ' + (ddItem.dataset.term || '');
|
||||
sel.style.display = '';
|
||||
}
|
||||
if (saveBtn) {
|
||||
|
|
@ -1776,7 +1801,7 @@
|
|||
});
|
||||
body.append(csrfName, csrfHash);
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving…';
|
||||
saveBtn.textContent = 'Saving...';
|
||||
fetch('<?= base_url('Opd_prescription/fhir_complaint_recode') ?>', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
|
|
@ -1935,7 +1960,7 @@
|
|||
}, 200);
|
||||
});
|
||||
|
||||
// ── Arrow-key / Enter navigation for SNOMED dropdowns ─────────────
|
||||
// -- Arrow-key / Enter navigation for SNOMED dropdowns -------------
|
||||
fhirFormView.addEventListener('keydown', function(e) {
|
||||
var key = e.key;
|
||||
if (key !== 'ArrowDown' && key !== 'ArrowUp' && key !== 'Enter' && key !== 'Escape') return;
|
||||
|
|
@ -1995,7 +2020,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
// ── Render FHIR bundle as human-readable form ──────────────────────
|
||||
// -- Render FHIR bundle as human-readable form ----------------------
|
||||
function hesc(s) {
|
||||
return String(s || '')
|
||||
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
|
|
@ -2066,7 +2091,7 @@
|
|||
|
||||
var html = '';
|
||||
|
||||
// ── Patient header
|
||||
// -- Patient header
|
||||
if (patient) {
|
||||
var pname = ((patient.name || [{}])[0].text || '').trim();
|
||||
var gender = (patient.gender || '').toUpperCase().charAt(0);
|
||||
|
|
@ -2089,7 +2114,7 @@
|
|||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// ── Invoice Record document metadata
|
||||
// -- Invoice Record document metadata
|
||||
if (composition && invoice) {
|
||||
var compositionCoding = ((((composition.type || {}).coding) || [])[0]) || {};
|
||||
var compositionIdentifier = (composition.identifier || {}).value || '';
|
||||
|
|
@ -2105,7 +2130,7 @@
|
|||
html += '</div></div></div>';
|
||||
}
|
||||
|
||||
// ── Invoice details and line items
|
||||
// -- Invoice details and line items
|
||||
if (invoice) {
|
||||
var invoiceIdentifier = ((invoice.identifier || [])[0] || {}).value || '';
|
||||
var invoiceCoding = ((((invoice.type || {}).coding) || [])[0]) || {};
|
||||
|
|
@ -2155,7 +2180,7 @@
|
|||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// ── Embedded invoice PDF
|
||||
// -- Embedded invoice PDF
|
||||
documentRefs.forEach(function(documentReference) {
|
||||
var attachment = (((documentReference.content || [])[0] || {}).attachment) || {};
|
||||
if (attachment.contentType !== 'application/pdf') return;
|
||||
|
|
@ -2172,7 +2197,7 @@
|
|||
html += '</div></div>';
|
||||
});
|
||||
|
||||
// ── Immunization Record
|
||||
// -- Immunization Record
|
||||
if (immunizations.length) {
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Vaccination Details</small></div>';
|
||||
html += '<div class="table-responsive"><table class="table table-sm table-bordered align-middle mb-0"><thead class="table-light"><tr>';
|
||||
|
|
@ -2200,7 +2225,7 @@
|
|||
html += '</tbody></table></div></div>';
|
||||
}
|
||||
|
||||
// ── Complaints
|
||||
// -- Complaints
|
||||
if (complaints.length) {
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Chief Complaints</small></div><div class="card-body py-2">';
|
||||
html += '<ul class="mb-0 ps-3">';
|
||||
|
|
@ -2237,7 +2262,7 @@
|
|||
html += '<div class="fhir-edit-panel border rounded p-2 mt-1 bg-light" data-idx="' + idx + '" data-complaint-text="' + hesc(doctorText) + '" style="display:none;">';
|
||||
html += '<div class="mb-1 small text-muted">Doctor wrote: <em>' + hesc(doctorText) + '</em> — assign SNOMED code:</div>';
|
||||
html += '<div class="position-relative mb-1">';
|
||||
html += '<input type="text" class="form-control form-control-sm fhir-snomed-search" placeholder="Type to search SNOMED…" value="' + hesc(doctorText) + '">';
|
||||
html += '<input type="text" class="form-control form-control-sm fhir-snomed-search" placeholder="Type to search SNOMED..." value="' + hesc(doctorText) + '">';
|
||||
html += '<div class="list-group shadow position-absolute w-100 fhir-snomed-dd" style="z-index:2000;display:none;max-height:180px;overflow-y:auto;"></div>';
|
||||
html += '</div>';
|
||||
html += '<div class="fhir-snomed-selected small text-success mb-1" style="display:none;"></div>';
|
||||
|
|
@ -2251,7 +2276,7 @@
|
|||
html += '</ul></div></div>';
|
||||
}
|
||||
|
||||
// ── Diagnoses
|
||||
// -- Diagnoses
|
||||
if (diagnoses.length) {
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Diagnosis</small></div><div class="card-body py-2">';
|
||||
html += '<ul class="mb-0 ps-3">';
|
||||
|
|
@ -2300,9 +2325,9 @@
|
|||
html += '</ul></div></div>';
|
||||
}
|
||||
|
||||
// ── Observation Groups (Vitals vs Lab Results)
|
||||
// -- Observation Groups (Vitals vs Lab Results)
|
||||
if (observations.length) {
|
||||
var loincLabel = { '8867-4':'HR','59408-5':'SpO₂','8480-6':'BP Sys','8462-4':'BP Dia','8310-5':'Temp','9279-1':'RR','8302-2':'Height','29463-7':'Weight' };
|
||||
var loincLabel = { '8867-4':'HR','59408-5':'SpO','8480-6':'BP Sys','8462-4':'BP Dia','8310-5':'Temp','9279-1':'RR','8302-2':'Height','29463-7':'Weight' };
|
||||
var vitalLoincCodes = { '8867-4':1, '59408-5':1, '8480-6':1, '8462-4':1, '8310-5':1, '9279-1':1, '8302-2':1, '29463-7':1 };
|
||||
|
||||
function meaningfulText(v) {
|
||||
|
|
@ -2391,7 +2416,7 @@
|
|||
renderObservationCard('Lab Results', labObservations);
|
||||
}
|
||||
|
||||
// ── Medications
|
||||
// -- Medications
|
||||
if (medications.length) {
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Medications</small></div><div class="card-body py-2">';
|
||||
html += '<ol class="mb-0 ps-3">';
|
||||
|
|
@ -2401,13 +2426,13 @@
|
|||
var dosage = di.text || '';
|
||||
var method = (di.method || {}).text || '';
|
||||
html += '<li class="mb-1"><strong>' + hesc(drug) + '</strong>';
|
||||
if (dosage) html += ' <span class="text-muted small">— ' + hesc(dosage) + (method ? ' (' + hesc(method) + ')' : '') + '</span>';
|
||||
if (dosage) html += ' <span class="text-muted small">- ' + hesc(dosage) + (method ? ' (' + hesc(method) + ')' : '') + '</span>';
|
||||
html += '</li>';
|
||||
});
|
||||
html += '</ol></div></div>';
|
||||
}
|
||||
|
||||
// ── Procedures / Surgeries
|
||||
// -- Procedures / Surgeries
|
||||
if (procedures.length) {
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Procedures</small></div><div class="card-body py-2">';
|
||||
html += '<ul class="mb-0 ps-3">';
|
||||
|
|
@ -2421,7 +2446,7 @@
|
|||
html += '</ul></div></div>';
|
||||
}
|
||||
|
||||
// ── Investigations
|
||||
// -- Investigations
|
||||
if (serviceReqs.length) {
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Investigations</small></div><div class="card-body py-2">';
|
||||
html += '<ul class="mb-0 ps-3">';
|
||||
|
|
@ -2439,7 +2464,7 @@
|
|||
html += '</ul></div></div>';
|
||||
}
|
||||
|
||||
// ── Allergy & Risk Profile
|
||||
// -- Allergy & Risk Profile
|
||||
if (allergies.length) {
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Allergies / Risk Profile</small></div><div class="card-body py-2">';
|
||||
html += '<ul class="mb-0 ps-3">';
|
||||
|
|
@ -2455,7 +2480,7 @@
|
|||
html += '</ul></div></div>';
|
||||
}
|
||||
|
||||
// ── Course In Hospital
|
||||
// -- Course In Hospital
|
||||
if (coursePlans.length) {
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Course In Hospital</small></div><div class="card-body py-2">';
|
||||
html += '<ul class="mb-0 ps-3">';
|
||||
|
|
@ -2469,7 +2494,7 @@
|
|||
html += '</ul></div></div>';
|
||||
}
|
||||
|
||||
// ── Discharge Advice / Follow-up
|
||||
// -- Discharge Advice / Follow-up
|
||||
if (advicePlans.length) {
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Discharge Advice & Follow-up</small></div><div class="card-body py-2">';
|
||||
html += '<ul class="mb-0 ps-3">';
|
||||
|
|
@ -2483,7 +2508,7 @@
|
|||
html += '</ul></div></div>';
|
||||
}
|
||||
|
||||
// ── Attached Documents & Reports (PDFs, Images, Scanned Documents, Invoices)
|
||||
// -- Attached Documents & Reports (PDFs, Images, Scanned Documents, Invoices)
|
||||
if (documentRefs.length) {
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light d-flex justify-content-between align-items-center"><small class="fw-bold text-uppercase text-secondary">Attached Documents & Reports</small><span class="badge bg-secondary">' + documentRefs.length + '</span></div><div class="card-body py-2">';
|
||||
html += '<div class="row g-2">';
|
||||
|
|
@ -2536,7 +2561,7 @@
|
|||
html += '</div></div></div>';
|
||||
}
|
||||
|
||||
// ── Practitioner
|
||||
// -- Practitioner
|
||||
if (practitioner) {
|
||||
var pName = ((practitioner.name || [{}])[0].text || '').trim();
|
||||
var pGiven = (((practitioner.name || [{}])[0].given) || []).join(' ').trim();
|
||||
|
|
@ -2551,7 +2576,7 @@
|
|||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// ── Organization
|
||||
// -- Organization
|
||||
if (organization) {
|
||||
var oName = (organization.name || '').trim();
|
||||
var oIdent = (organization.identifier || []).find(function(i){ return (i.system||'').includes('facility') || (i.system||'').includes('hfr'); });
|
||||
|
|
@ -2638,11 +2663,19 @@
|
|||
var rowPushStatus = (options.pushStatus || '').toString().trim().toLowerCase();
|
||||
|
||||
_fhirOpdId = 0; _fhirSessionId = 0;
|
||||
_fhirIpdId = 0; _fhirPatientId = 0; _fhirAbhaId = '';
|
||||
try {
|
||||
var urlObj = new URL(url, window.location.origin);
|
||||
if (urlObj.searchParams.has('ipd_id')) _fhirIpdId = parseInt(urlObj.searchParams.get('ipd_id') || '0', 10) || 0;
|
||||
if (urlObj.searchParams.has('patient_id')) _fhirPatientId = parseInt(urlObj.searchParams.get('patient_id') || '0', 10) || 0;
|
||||
if (urlObj.searchParams.has('abha_id')) _fhirAbhaId = (urlObj.searchParams.get('abha_id') || '').trim();
|
||||
} catch (e) {}
|
||||
|
||||
if (fhirModalTitle) fhirModalTitle.textContent = title || 'FHIR Preview';
|
||||
if (fhirModalMeta) { fhirModalMeta.textContent = 'Loading...'; fhirModalMeta.className = 'flex-grow-1 small text-muted'; }
|
||||
if (fhirModalQuality) { fhirModalQuality.innerHTML = ''; fhirModalQuality.style.display = 'none'; }
|
||||
if (fhirFormView) fhirFormView.innerHTML = '<p class="text-muted p-3">Loading...</p>';
|
||||
if (fhirModalJson) fhirModalJson.textContent = '{\n "loading": true\n}';
|
||||
if (fhirModalJson) fhirModalJson.value = '{\n "loading": true\n}';
|
||||
// Reset to Form View tab
|
||||
fhirTabForm.classList.add('active'); fhirTabJson.classList.remove('active');
|
||||
fhirFormView.style.display = ''; fhirModalJson.style.display = 'none';
|
||||
|
|
@ -2651,7 +2684,7 @@
|
|||
|
||||
if (btnSubmitFhirToAbdm) {
|
||||
btnSubmitFhirToAbdm.disabled = false;
|
||||
if (_fhirSubmitMode === 'task') {
|
||||
if (_fhirSubmitMode === 'task' || _fhirIpdId > 0 || (url && url.indexOf('ipd_discharge') !== -1)) {
|
||||
btnSubmitFhirToAbdm.className = 'btn btn-sm btn-outline-warning';
|
||||
btnSubmitFhirToAbdm.innerHTML = '<i class="bi bi-check2-square"></i> Verify & Submit to Gateway';
|
||||
btnSubmitFhirToAbdm.title = 'Submit directly to ABDM gateway after FHIR verification';
|
||||
|
|
@ -2699,9 +2732,12 @@
|
|||
var hasFhir = bundle !== null;
|
||||
_fhirOpdId = parseInt(parsed.opd_id || '0', 10) || 0;
|
||||
_fhirSessionId = parseInt(parsed.opd_session_id || '0', 10) || 0;
|
||||
if (parsed.ipd_id) _fhirIpdId = parseInt(parsed.ipd_id, 10) || _fhirIpdId;
|
||||
if (parsed.patient_id) _fhirPatientId = parseInt(parsed.patient_id, 10) || _fhirPatientId;
|
||||
if (parsed.abha_id) _fhirAbhaId = (parsed.abha_id || _fhirAbhaId).trim();
|
||||
_fhirLiveComplaints = Array.isArray(parsed.live_complaints) ? parsed.live_complaints : [];
|
||||
_fhirLiveDiagnoses = Array.isArray(parsed.live_diagnoses) ? parsed.live_diagnoses : [];
|
||||
if (fhirModalJson) fhirModalJson.textContent = JSON.stringify(parsed, null, 2);
|
||||
if (fhirModalJson) fhirModalJson.value = JSON.stringify(bundle || parsed, null, 2);
|
||||
if (fhirFormView) {
|
||||
if (hasFhir) {
|
||||
fhirFormView.innerHTML = renderFhirBundleForm(bundle, _fhirLiveComplaints, _fhirLiveDiagnoses);
|
||||
|
|
@ -2728,6 +2764,121 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Apply JSON changes function
|
||||
function applyFhirJsonChanges(onSaved) {
|
||||
var raw = (fhirModalJson ? fhirModalJson.value : '').trim();
|
||||
if (!raw) {
|
||||
alert('JSON content is empty.');
|
||||
return false;
|
||||
}
|
||||
var parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (e) {
|
||||
alert('Invalid JSON syntax:\n' + e.message);
|
||||
if (fhirModalMeta) {
|
||||
fhirModalMeta.textContent = 'JSON syntax error: ' + e.message;
|
||||
fhirModalMeta.className = 'flex-grow-1 small text-danger';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var bundle = (parsed && parsed.resourceType === 'Bundle') ? parsed : ((parsed && parsed.bundle) ? parsed.bundle : ((parsed && parsed.fhir_bundle) ? parsed.fhir_bundle : null));
|
||||
if (!bundle || bundle.resourceType !== 'Bundle') {
|
||||
alert('The JSON must be a valid FHIR Bundle (resourceType: "Bundle").');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Format textarea neatly
|
||||
if (fhirModalJson) {
|
||||
fhirModalJson.value = JSON.stringify(bundle, null, 2);
|
||||
}
|
||||
|
||||
// Refresh Form View
|
||||
if (fhirFormView) {
|
||||
fhirFormView.innerHTML = renderFhirBundleForm(bundle, _fhirLiveComplaints, _fhirLiveDiagnoses);
|
||||
}
|
||||
|
||||
// Refresh Code Quality badges
|
||||
if (fhirModalQuality) {
|
||||
var qs = fhirCodeQuality(bundle);
|
||||
if (qs) {
|
||||
fhirModalQuality.innerHTML = qs;
|
||||
fhirModalQuality.style.display = 'block';
|
||||
} else {
|
||||
fhirModalQuality.innerHTML = '';
|
||||
fhirModalQuality.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (fhirModalDataBadge) {
|
||||
fhirModalDataBadge.className = 'badge bg-success';
|
||||
fhirModalDataBadge.textContent = 'HAS FHIR';
|
||||
}
|
||||
|
||||
var targetIpdId = _fhirIpdId || (_fhirSubmitTaskRow && (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() === 'ipd_discharge_publish' ? parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) : 0);
|
||||
var targetPatientId = _fhirPatientId || (_fhirSubmitTaskRow ? parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) : 0);
|
||||
var targetAbha = (_fhirSubmitAbha || _fhirAbhaId || '').trim();
|
||||
|
||||
if (targetIpdId > 0) {
|
||||
var origBtnHtml = btnUpdateFhirJson ? btnUpdateFhirJson.innerHTML : '';
|
||||
if (btnUpdateFhirJson) {
|
||||
btnUpdateFhirJson.disabled = true;
|
||||
btnUpdateFhirJson.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving...';
|
||||
}
|
||||
|
||||
post('<?= base_url('AbdmGateway/save_ipd_discharge_fhir') ?>', {
|
||||
ipd_id: targetIpdId,
|
||||
patient_id: targetPatientId,
|
||||
abha_id: targetAbha,
|
||||
fhir_json: fhirModalJson.value
|
||||
}, function (res) {
|
||||
if (btnUpdateFhirJson) {
|
||||
btnUpdateFhirJson.disabled = false;
|
||||
btnUpdateFhirJson.innerHTML = '<i class="bi bi-check-lg"></i> JSON Updated!';
|
||||
window.setTimeout(function() {
|
||||
btnUpdateFhirJson.innerHTML = origBtnHtml || '<i class="bi bi-check2-circle"></i> Update JSON';
|
||||
}, 2000);
|
||||
}
|
||||
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
||||
var err = (res && (res.error_text || res.error || res.message)) || 'Failed to save updated JSON';
|
||||
if (fhirModalMeta) {
|
||||
fhirModalMeta.textContent = 'Updated in preview: ' + err;
|
||||
fhirModalMeta.className = 'flex-grow-1 small text-warning';
|
||||
}
|
||||
if (typeof onSaved === 'function') onSaved(false);
|
||||
return;
|
||||
}
|
||||
if (fhirModalMeta) {
|
||||
fhirModalMeta.textContent = 'JSON validated & saved to database.';
|
||||
fhirModalMeta.className = 'flex-grow-1 small text-success';
|
||||
}
|
||||
if (typeof onSaved === 'function') onSaved(true);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (btnUpdateFhirJson) {
|
||||
var prevHtml = btnUpdateFhirJson.innerHTML;
|
||||
btnUpdateFhirJson.innerHTML = '<i class="bi bi-check-lg"></i> Updated!';
|
||||
window.setTimeout(function() {
|
||||
btnUpdateFhirJson.innerHTML = prevHtml;
|
||||
}, 2000);
|
||||
}
|
||||
if (fhirModalMeta) {
|
||||
fhirModalMeta.textContent = 'JSON validated and applied.';
|
||||
fhirModalMeta.className = 'flex-grow-1 small text-success';
|
||||
}
|
||||
if (typeof onSaved === 'function') onSaved(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (btnUpdateFhirJson) {
|
||||
btnUpdateFhirJson.addEventListener('click', function () {
|
||||
applyFhirJsonChanges();
|
||||
});
|
||||
}
|
||||
|
||||
// Regenerate / Recreate FHIR bundle in modal
|
||||
btnRegenerateFhirModal.addEventListener('click', function () {
|
||||
if (!_fhirPreviewUrl) {
|
||||
|
|
@ -2746,7 +2897,7 @@
|
|||
});
|
||||
});
|
||||
|
||||
// Update Codes dropdown — navigate to master workspaces
|
||||
// Update Codes dropdown - navigate to master workspaces
|
||||
(function () {
|
||||
function navTo(url, label) {
|
||||
fhirPreviewModal.hide();
|
||||
|
|
@ -2826,28 +2977,27 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (_fhirSubmitMode === 'task') {
|
||||
if (!_fhirSubmitTaskRow) {
|
||||
alert('No ABDM task selected for submission.');
|
||||
return;
|
||||
}
|
||||
if (!/^\d{14}$/.test(_fhirSubmitAbha)) {
|
||||
alert('ABHA must be a 14-digit number before queueing.');
|
||||
return;
|
||||
}
|
||||
var isIpdMode = (_fhirSubmitTaskRow && (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() === 'ipd_discharge_publish') || (_fhirIpdId > 0 && !_fhirInvoiceSource);
|
||||
|
||||
if (_fhirSubmitMode === 'task' || isIpdMode) {
|
||||
var effectiveAbha = (_fhirSubmitAbha || _fhirAbhaId || (_fhirSubmitTaskRow && _fhirSubmitTaskRow.querySelector('.abha-input') ? _fhirSubmitTaskRow.querySelector('.abha-input').value : '')).trim();
|
||||
|
||||
if (!confirm('FHIR verified. Submit this record to ABDM gateway now?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rowInput = _fhirSubmitTaskRow.querySelector('.abha-input');
|
||||
if (rowInput) {
|
||||
rowInput.value = _fhirSubmitAbha;
|
||||
if (_fhirSubmitTaskRow) {
|
||||
var rowInput = _fhirSubmitTaskRow.querySelector('.abha-input');
|
||||
if (rowInput && effectiveAbha) {
|
||||
rowInput.value = effectiveAbha;
|
||||
}
|
||||
}
|
||||
|
||||
var taskType = (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase();
|
||||
var taskType = _fhirSubmitTaskRow ? (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() : (isIpdMode ? 'ipd_discharge_publish' : '');
|
||||
|
||||
if (taskType === 'lab_report_publish' || taskType === 'radiology_report_publish') {
|
||||
var labReqId = parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
var patientId = parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
var labReqId = _fhirSubmitTaskRow ? parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) : 0;
|
||||
var patientId = _fhirSubmitTaskRow ? parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) : 0;
|
||||
if (labReqId <= 0 || patientId <= 0) {
|
||||
alert('Task data missing (lab_req_id/patient_id).');
|
||||
return;
|
||||
|
|
@ -2855,12 +3005,12 @@
|
|||
|
||||
var origHtmlTask = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Submitting…';
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Submitting...';
|
||||
|
||||
post('<?= base_url('AbdmGateway/share_diagnosis_report_bundle') ?>', {
|
||||
lab_req_id: labReqId,
|
||||
patient_id: patientId,
|
||||
abha_id: _fhirSubmitAbha,
|
||||
abha_id: effectiveAbha,
|
||||
consent_handle: ''
|
||||
}, function (res) {
|
||||
btn.disabled = false;
|
||||
|
|
@ -2876,10 +3026,12 @@
|
|||
var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : '';
|
||||
var warningText = res.warning ? (' Warning: ' + res.warning) : '';
|
||||
setSubmissionStatus('Submitted to ABDM gateway.' + queueText + warningText, res.queue_id || '');
|
||||
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
||||
if (badge) {
|
||||
badge.textContent = 'IN_PROGRESS';
|
||||
badge.className = 'badge bg-info status-pill';
|
||||
if (_fhirSubmitTaskRow) {
|
||||
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
||||
if (badge) {
|
||||
badge.textContent = 'IN_PROGRESS';
|
||||
badge.className = 'badge bg-info status-pill';
|
||||
}
|
||||
}
|
||||
alert('Submitted to ABDM gateway.' + queueText + warningText);
|
||||
fhirPreviewModal.hide();
|
||||
|
|
@ -2887,23 +3039,35 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (taskType === 'ipd_discharge_publish') {
|
||||
var ipdId = parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
var ipdPatientId = parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
if (ipdId <= 0 || ipdPatientId <= 0) {
|
||||
alert('Task data missing (ipd_id/patient_id).');
|
||||
if (taskType === 'ipd_discharge_publish' || isIpdMode) {
|
||||
var ipdId = _fhirIpdId || (_fhirSubmitTaskRow ? parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) : 0);
|
||||
var ipdPatientId = _fhirPatientId || (_fhirSubmitTaskRow ? parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) : 0);
|
||||
if (ipdId <= 0) {
|
||||
alert('Task data missing (ipd_id).');
|
||||
return;
|
||||
}
|
||||
|
||||
var currentJson = (fhirModalJson ? fhirModalJson.value : '').trim();
|
||||
if (currentJson) {
|
||||
try {
|
||||
JSON.parse(currentJson);
|
||||
} catch (e) {
|
||||
alert('Cannot submit: JSON contains syntax errors:\n' + e.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var origHtmlIpd = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Submitting…';
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Submitting...';
|
||||
|
||||
post('<?= base_url('AbdmGateway/share_ipd_discharge_bundle') ?>', {
|
||||
ipd_id: ipdId,
|
||||
patient_id: ipdPatientId,
|
||||
abha_id: _fhirSubmitAbha,
|
||||
consent_handle: ''
|
||||
abha_id: effectiveAbha,
|
||||
consent_handle: '',
|
||||
fhir_override_json: currentJson,
|
||||
force_new_record: document.getElementById('chkForceNewRecord') && document.getElementById('chkForceNewRecord').checked ? 1 : 0
|
||||
}, function (res) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtmlIpd;
|
||||
|
|
@ -2918,10 +3082,12 @@
|
|||
var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : '';
|
||||
var warningText = res.warning ? (' Warning: ' + res.warning) : '';
|
||||
setSubmissionStatus('Submitted to ABDM gateway.' + queueText + warningText, res.queue_id || '');
|
||||
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
||||
if (badge) {
|
||||
badge.textContent = 'IN_PROGRESS';
|
||||
badge.className = 'badge bg-info status-pill';
|
||||
if (_fhirSubmitTaskRow) {
|
||||
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
||||
if (badge) {
|
||||
badge.textContent = 'IN_PROGRESS';
|
||||
badge.className = 'badge bg-info status-pill';
|
||||
}
|
||||
}
|
||||
alert('Submitted to ABDM gateway.' + queueText + warningText);
|
||||
fhirPreviewModal.hide();
|
||||
|
|
@ -3029,7 +3195,7 @@
|
|||
if (!confirm(submitConfirm)) return;
|
||||
var origHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Submitting…';
|
||||
btn.textContent = 'Submitting...';
|
||||
var body = new URLSearchParams({ opd_id: _fhirOpdId, opd_session_id: _fhirSessionId });
|
||||
body.append('push_to_gateway', '1');
|
||||
body.append(csrfName, csrfHash);
|
||||
|
|
@ -3063,7 +3229,7 @@
|
|||
if (res.csrfName && res.csrfHash) { csrfName = res.csrfName; csrfHash = res.csrfHash; }
|
||||
if (res.ok == 1) {
|
||||
btn.className = 'btn btn-sm btn-success';
|
||||
btn.innerHTML = '<i class="bi bi-check-circle"></i> Submitted ✓';
|
||||
btn.innerHTML = '<i class="bi bi-check-circle"></i> Submitted v';
|
||||
|
||||
var rowSubmitBtn = document.querySelector('.btn-opd-consult-submit-gateway[data-opd-id="' + String(_fhirOpdId) + '"]');
|
||||
if (rowSubmitBtn) {
|
||||
|
|
@ -3088,11 +3254,11 @@
|
|||
}
|
||||
|
||||
var qInfo = res.queue_id ? ' Queue ID: ' + res.queue_id : '';
|
||||
alert('✅ Bundle submitted to ABDM bridge.\n' + (res.message || '') + qInfo);
|
||||
alert(' Bundle submitted to ABDM bridge.\n' + (res.message || '') + qInfo);
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
alert('❌ Submission failed:\n' + (res.message || res.error_text || 'Check bridge configuration.') + (res.http_code ? ' (HTTP ' + res.http_code + ')' : ''));
|
||||
alert(' Submission failed:\n' + (res.message || res.error_text || 'Check bridge configuration.') + (res.http_code ? ' (HTTP ' + res.http_code + ')' : ''));
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
|
|
@ -3102,7 +3268,7 @@
|
|||
var msg = (e && e.name === 'AbortError')
|
||||
? 'Request timed out before server response. Please retry.'
|
||||
: (e && e.message ? e.message : 'Request failed');
|
||||
alert('❌ Error: ' + msg);
|
||||
alert(' Error: ' + msg);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -3111,19 +3277,19 @@
|
|||
var jsonText = fhirModalJson ? (fhirModalJson.textContent || '{}') : '{}';
|
||||
if (! navigator.clipboard || ! navigator.clipboard.writeText) { setStatus('Clipboard API unavailable.', true); return; }
|
||||
navigator.clipboard.writeText(jsonText).then(function () {
|
||||
var orig = btn.textContent; btn.textContent = 'Copied ✓'; btn.className = 'btn btn-sm btn-success';
|
||||
var orig = btn.textContent; btn.textContent = 'Copied v'; btn.className = 'btn btn-sm btn-success';
|
||||
window.setTimeout(function () { btn.textContent = orig; btn.className = 'btn btn-sm btn-outline-secondary'; }, 1500);
|
||||
}).catch(function (e) { setStatus('Copy failed: ' + e.message, true); });
|
||||
});
|
||||
|
||||
// OPD Consult Publish — FHIR preview buttons → open in modal
|
||||
// OPD Consult Publish - FHIR preview buttons open in modal
|
||||
document.querySelectorAll('.btn-opd-consult-fhir').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var opdId = parseInt(btn.getAttribute('data-opd-id') || '0', 10);
|
||||
var tr = btn.closest('tr');
|
||||
var pushStatus = tr ? (tr.getAttribute('data-push-status') || '') : '';
|
||||
if (opdId <= 0) return;
|
||||
openFhirModal('<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId, 'FHIR Preview — OPD #' + opdId, { pushStatus: pushStatus });
|
||||
openFhirModal('<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId, 'FHIR Preview - OPD #' + opdId, { pushStatus: pushStatus });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -3134,7 +3300,7 @@
|
|||
if (opdId <= 0) return;
|
||||
openFhirModal(
|
||||
'<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId,
|
||||
'Gateway Submit — OPD #' + opdId,
|
||||
'Gateway Submit - OPD #' + opdId,
|
||||
{ submitMode: 'opd_gateway', abhaId: abhaId }
|
||||
);
|
||||
});
|
||||
|
|
@ -3153,7 +3319,7 @@
|
|||
+ '?source=' + encodeURIComponent(source)
|
||||
+ '&bill_id=' + encodeURIComponent(String(billId))
|
||||
+ '&patient_id=' + encodeURIComponent(String(patientId));
|
||||
openFhirModal(previewUrl, (pushIntent ? 'Review & Push' : 'FHIR Preview') + ' — Invoice #' + billId, {
|
||||
openFhirModal(previewUrl, (pushIntent ? 'Review & Push' : 'FHIR Preview') + ' - Invoice #' + billId, {
|
||||
submitMode: 'invoice',
|
||||
taskRow: row,
|
||||
pushStatus: pushStatus,
|
||||
|
|
|
|||
|
|
@ -344,7 +344,7 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
$this->assertSame('2026-09-04T04:58:00+05:30', $encounterRes['period']['end'] ?? null);
|
||||
$this->assertNotEmpty($encounterRes['location'] ?? []);
|
||||
$this->assertSame('Ward: General Ward, Bed: GW-05', $encounterRes['location'][0]['location']['display'] ?? null);
|
||||
$this->assertSame('active', $encounterRes['location'][0]['status'] ?? null);
|
||||
$this->assertSame('completed', $encounterRes['location'][0]['status'] ?? null);
|
||||
$this->assertSame('2026-09-01T04:53:00+05:30', $encounterRes['location'][0]['period']['start'] ?? null);
|
||||
$this->assertSame('2026-09-04T04:58:00+05:30', $encounterRes['location'][0]['period']['end'] ?? null);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user