fix(abdm): align OPConsultRecord vs PrescriptionRecord bundles, separate OPD consultation from prescription discovery, and clean FHIR presentation
This commit is contained in:
parent
5130c6b18a
commit
a72ab50777
|
|
@ -1441,19 +1441,76 @@ class AbdmGateway extends BaseController
|
|||
|
||||
public function sharePrescriptionBundle()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
if (method_exists($this->request, 'isAJAX') && ! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$opdId = (int) $this->request->getPost('opd_id');
|
||||
$sessionId = (int) $this->request->getPost('opd_session_id');
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
||||
$abhaAddressPost = trim((string) $this->request->getPost('abha_address'));
|
||||
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
||||
$jsonBody = [];
|
||||
try {
|
||||
$parsed = $this->request->getJSON(true);
|
||||
if (is_array($parsed)) {
|
||||
$jsonBody = $parsed;
|
||||
}
|
||||
} catch (\Throwable) {}
|
||||
|
||||
$opdId = (int) ($this->request->getPost('opd_id') ?? $jsonBody['opd_id'] ?? $_POST['opd_id'] ?? 0);
|
||||
$sessionId = (int) ($this->request->getPost('opd_session_id') ?? $jsonBody['opd_session_id'] ?? $_POST['opd_session_id'] ?? 0);
|
||||
$patientId = (int) ($this->request->getPost('patient_id') ?? $jsonBody['patient_id'] ?? $_POST['patient_id'] ?? 0);
|
||||
$abhaId = trim((string) ($this->request->getPost('abha_id') ?? $jsonBody['abha_id'] ?? $_POST['abha_id'] ?? ''));
|
||||
$abhaAddressPost = trim((string) ($this->request->getPost('abha_address') ?? $jsonBody['abha_address'] ?? $_POST['abha_address'] ?? ''));
|
||||
$consentHandle = trim((string) ($this->request->getPost('consent_handle') ?? $jsonBody['consent_handle'] ?? $_POST['consent_handle'] ?? ''));
|
||||
$pushToGateway = $this->resolveGatewayPushMode();
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
|
||||
$ccRef = trim((string) ($this->request->getPost('care_context_reference') ?? $this->request->getPost('careContextId') ?? $jsonBody['care_context_reference'] ?? $jsonBody['careContextId'] ?? $_POST['care_context_reference'] ?? $_POST['careContextId'] ?? ''));
|
||||
|
||||
// If opdId passed is actually a prescription ID / session ID
|
||||
if ($opdId > 0 && $this->db->tableExists('opd_master')) {
|
||||
$exists = $this->db->table('opd_master')->where('opd_id', $opdId)->countAllResults();
|
||||
if ($exists === 0 && $this->db->tableExists('opd_prescription')) {
|
||||
$prRow = $this->db->table('opd_prescription')->select('opd_id, session_id')->where('id', $opdId)->get(1)->getRowArray();
|
||||
if (! empty($prRow['opd_id'])) {
|
||||
if ($sessionId <= 0) {
|
||||
$sessionId = (int) ($prRow['session_id'] ?: $opdId);
|
||||
}
|
||||
$opdId = (int) $prRow['opd_id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Resolve opd_id from care_context_reference or patient_id if not supplied
|
||||
if ($opdId <= 0 && $ccRef !== '') {
|
||||
if (preg_match('/^OPD-\d+-S(\d+)/i', $ccRef, $m)) {
|
||||
$sId = (int) $m[1];
|
||||
if ($this->db->tableExists('opd_prescription')) {
|
||||
$prRow = $this->db->table('opd_prescription')->select('opd_id')->where('id', $sId)->orWhere('session_id', $sId)->get(1)->getRowArray();
|
||||
if (! empty($prRow['opd_id'])) {
|
||||
$opdId = (int) $prRow['opd_id'];
|
||||
$sessionId = $sId;
|
||||
}
|
||||
}
|
||||
} elseif (preg_match('/^OPD-(\d+)/i', $ccRef, $m)) {
|
||||
$candId = (int) $m[1];
|
||||
if ($this->db->tableExists('opd_master') && $this->db->table('opd_master')->where('opd_id', $candId)->countAllResults() > 0) {
|
||||
$opdId = $candId;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($opdId <= 0 && $patientId > 0 && $this->db->tableExists('opd_master')) {
|
||||
$latestOpd = $this->db->table('opd_master')->select('opd_id')->where('p_id', $patientId)->orderBy('opd_id', 'DESC')->get(1)->getRowArray();
|
||||
if (! empty($latestOpd['opd_id'])) {
|
||||
$opdId = (int) $latestOpd['opd_id'];
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve patientId if not supplied but opdId is known
|
||||
if ($patientId <= 0 && $opdId > 0 && $this->db->tableExists('opd_master')) {
|
||||
$opdRow = $this->db->table('opd_master')->select('p_id')->where('opd_id', $opdId)->get(1)->getRowArray();
|
||||
if (! empty($opdRow['p_id'])) {
|
||||
$patientId = (int) $opdRow['p_id'];
|
||||
}
|
||||
}
|
||||
|
||||
$logBridge = function (string $status, array $responsePayload, string $errorMessage = '', string $eventType = 'abdm.opd.prescription.share') use ($opdId, $patientId, $now, $pushToGateway): void {
|
||||
if (! $this->db->tableExists('abdm_api_logs')) {
|
||||
return;
|
||||
|
|
@ -1522,14 +1579,33 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
$requestedHiType = trim((string) ($this->request->getPost('hi_type') ?? ''));
|
||||
$targetBundleType = 'OPConsultRecord';
|
||||
if ($ccRef !== '' && preg_match('/^(?:PRESC|RX|PRESCRIPTION)-/i', $ccRef)) {
|
||||
$targetBundleType = 'PrescriptionRecord';
|
||||
} elseif (in_array($requestedHiType, ['MedicationRequestBundle', 'Prescription', 'PrescriptionRecord'], true)) {
|
||||
$targetBundleType = 'PrescriptionRecord';
|
||||
}
|
||||
|
||||
$builder = $this->db->table('opd_fhir_documents')
|
||||
->where('opd_id', $opdId)
|
||||
->whereIn('bundle_type', ['OPConsultRecord', 'MedicationRequestBundle', 'PrescriptionRecord']);
|
||||
->where('bundle_type', $targetBundleType);
|
||||
if ($sessionId > 0) {
|
||||
$builder->where('opd_session_id', $sessionId);
|
||||
}
|
||||
$bundleRow = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
|
||||
|
||||
if (empty($bundleRow) && $opdId > 0) {
|
||||
// Attempt on-demand FHIR bundle regeneration
|
||||
try {
|
||||
$opdCtrl = new \App\Controllers\Opd_prescription();
|
||||
$opdCtrl->initController($this->request, $this->response, service('logger'));
|
||||
$opdCtrl->regenerateFhirBundleInternal($opdId, $sessionId > 0 ? $sessionId : $opdId, $targetBundleType);
|
||||
$bundleRow = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($bundleRow)) {
|
||||
$logBridge('error', ['ok' => 0, 'error_text' => 'No FHIR bundle found for selected OPD/session'], 'No FHIR bundle found for selected OPD/session', 'abdm.opd.prescription.share.validation');
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'No FHIR bundle found for selected OPD/session']);
|
||||
|
|
@ -1554,20 +1630,7 @@ class AbdmGateway extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
$requestedHiType = trim((string) ($this->request->getPost('hi_type') ?? ''));
|
||||
$bundleType = trim((string) ($bundleRow['bundle_type'] ?? 'OPConsultRecord'));
|
||||
if ($requestedHiType !== '') {
|
||||
$hiType = match ($requestedHiType) {
|
||||
'OPConsultation', 'OPConsultRecord' => 'OPConsultRecord',
|
||||
'MedicationRequestBundle', 'Prescription', 'PrescriptionRecord' => 'PrescriptionRecord',
|
||||
default => $requestedHiType,
|
||||
};
|
||||
} else {
|
||||
$hiType = match ($bundleType) {
|
||||
'OPConsultRecord', 'OPConsultation' => 'OPConsultRecord',
|
||||
default => 'PrescriptionRecord',
|
||||
};
|
||||
}
|
||||
$hiType = $targetBundleType;
|
||||
$consentHandleResolved = is_array($consent) ? trim((string) ($consent['consent_handle'] ?? '')) : '';
|
||||
$consentExternalId = is_array($consent) ? $this->resolveConsentExternalId($consent) : '';
|
||||
$sessionForRef = $sessionId > 0 ? $sessionId : (int) ($bundleRow['opd_session_id'] ?? 0);
|
||||
|
|
@ -1581,6 +1644,7 @@ class AbdmGateway extends BaseController
|
|||
->select('care_context_reference')
|
||||
->where('entity_type', 'opd')
|
||||
->where('entity_id', (string) $opdId)
|
||||
->where('hi_type', $hiType)
|
||||
->where('care_context_reference !=', '')
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
|
|
@ -1593,7 +1657,8 @@ class AbdmGateway extends BaseController
|
|||
$careContextRef = $existingCcRef;
|
||||
} else {
|
||||
$dateClean = str_replace('-', '', $visitDate);
|
||||
$careContextRef = 'OPD-' . $patientId . '-S' . ($sessionForRef > 0 ? $sessionForRef : 0) . '-' . $dateClean;
|
||||
$prefix = $hiType === 'PrescriptionRecord' ? 'PRESC-' : 'OPD-';
|
||||
$careContextRef = $prefix . $patientId . '-S' . ($sessionForRef > 0 ? $sessionForRef : 0) . '-' . $dateClean;
|
||||
}
|
||||
}
|
||||
$careContextDisplay = $hiType === 'PrescriptionRecord'
|
||||
|
|
@ -1846,7 +1911,7 @@ class AbdmGateway extends BaseController
|
|||
$logBridge(
|
||||
$connectorError === null ? 'success' : 'error',
|
||||
$responsePayload,
|
||||
$connectorError !== null ? (string) $connectorError : ($consentWarning !== '' ? $consentWarning : ''),
|
||||
$connectorError !== null ? (string) $connectorError : '',
|
||||
'abdm.opd.prescription.share.result'
|
||||
);
|
||||
|
||||
|
|
@ -2151,7 +2216,7 @@ class AbdmGateway extends BaseController
|
|||
$diagnosticReport = [
|
||||
'id' => (string) $labReqId,
|
||||
'title' => $testTitle ?: ($isImaging ? 'Radiology Report' : 'Laboratory Report'),
|
||||
'status' => $labReq->status == 1 ? 'final' : 'preliminary',
|
||||
'status' => in_array((int)$labReq->status, [1, 2], true) ? 'final' : 'preliminary',
|
||||
'conclusion' => $cleanImpression,
|
||||
'reported_at' => $reportedAt,
|
||||
'report_html' => trim((string) ($labReq->Report_Data ?? '')),
|
||||
|
|
@ -5022,51 +5087,60 @@ class AbdmGateway extends BaseController
|
|||
continue;
|
||||
}
|
||||
|
||||
// Strategy 0: Live On-Demand Fresh Generation for OPD Consult & Prescription Records
|
||||
if (preg_match('/^(OPD|PRESCRIPTION|PRESC)-(\d+)(?:-S(\d+))?/i', $ref, $m)) {
|
||||
$refPrefix = strtoupper($m[1]);
|
||||
$targetPatientId = (int) $m[2];
|
||||
$targetSessionId = (int) ($m[3] ?? 0);
|
||||
$targetOpdId = 0;
|
||||
|
||||
if ($targetSessionId > 0 && $db->tableExists('opd_prescription')) {
|
||||
$pRow = $db->table('opd_prescription')->select('opd_id')->where('id', $targetSessionId)->get(1)->getRowArray();
|
||||
$targetOpdId = (int) ($pRow['opd_id'] ?? 0);
|
||||
}
|
||||
if ($targetOpdId <= 0 && $targetPatientId > 0 && $db->tableExists('opd_master')) {
|
||||
$opdRow = $db->table('opd_master')->select('opd_id')->where('opd_id', $targetPatientId)->get(1)->getRowArray();
|
||||
if ($opdRow) {
|
||||
$targetOpdId = (int) $opdRow['opd_id'];
|
||||
} else {
|
||||
$opdRow = $db->table('opd_master')->select('opd_id')->where('p_id', $targetPatientId)->orderBy('opd_id', 'DESC')->get(1)->getRowArray();
|
||||
$targetOpdId = (int) ($opdRow['opd_id'] ?? 0);
|
||||
// Strategy 0a: Live On-Demand Fresh Generation for LAB / Diagnostic Report (LAB-xxxx or RAD-xxxx)
|
||||
if (preg_match('/^(?:LAB|RAD)-/i', $ref)) {
|
||||
try {
|
||||
$diag = $this->buildDiagnosticReportBundle($ref);
|
||||
if ($diag !== null && !empty($diag['bundle'])) {
|
||||
$records[] = [
|
||||
'careContextReference' => $ref,
|
||||
'hiType' => $diag['hiType'] ?? 'DiagnosticReportRecord',
|
||||
'display' => $diag['display'] ?? 'Diagnostic Report',
|
||||
'bundle' => $diag['bundle'],
|
||||
];
|
||||
$this->cacheRecordBundle($ref, $diag['hiType'] ?? 'DiagnosticReportRecord', $diag['bundle'], $diag['display'] ?? 'Diagnostic Report');
|
||||
continue;
|
||||
}
|
||||
} catch (\Throwable $ex) {
|
||||
log_message('warning', 'Live DiagnosticReport FHIR generation in recordsFetch failed: ' . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($targetOpdId > 0) {
|
||||
try {
|
||||
$isPrescriptionReq = in_array($refPrefix, ['PRESC', 'PRESCRIPTION'], true);
|
||||
$targetBundleType = $isPrescriptionReq ? 'PrescriptionRecord' : 'OPConsultRecord';
|
||||
$targetHiType = $isPrescriptionReq ? 'PrescriptionRecord' : 'OPConsultRecord';
|
||||
$targetDisplay = $isPrescriptionReq
|
||||
? ('Prescription - ' . date('d M Y'))
|
||||
: ('Consultation Record - ' . date('d M Y'));
|
||||
|
||||
$opdCtrl = new \App\Controllers\Opd_prescription();
|
||||
$opdCtrl->initController($this->request, $this->response, service('logger'));
|
||||
$regen = $opdCtrl->regenerateFhirBundleInternal($targetOpdId, $targetSessionId, $targetBundleType);
|
||||
if (!empty($regen['ok']) && !empty($regen['bundle'])) {
|
||||
$records[] = [
|
||||
'careContextReference' => $ref,
|
||||
'hiType' => $targetHiType,
|
||||
'display' => $targetDisplay,
|
||||
'bundle' => $regen['bundle'],
|
||||
];
|
||||
continue;
|
||||
}
|
||||
} catch (\Throwable $ex) {
|
||||
log_message('warning', 'Live FHIR bundle generation in recordsFetch failed: ' . $ex->getMessage());
|
||||
// Strategy 0b: Live On-Demand Fresh Generation for Discharge Summary (DISCHARGE-xxxx or IPD-xxxx)
|
||||
if (preg_match('/^(?:DISCHARGE|IPD)-/i', $ref)) {
|
||||
try {
|
||||
$discharge = $this->buildDischargeSummaryBundle($ref);
|
||||
if ($discharge !== null && !empty($discharge['bundle'])) {
|
||||
$records[] = [
|
||||
'careContextReference' => $ref,
|
||||
'hiType' => $discharge['hiType'] ?? 'DischargeSummaryRecord',
|
||||
'display' => $discharge['display'] ?? 'Discharge Summary',
|
||||
'bundle' => $discharge['bundle'],
|
||||
];
|
||||
$this->cacheRecordBundle($ref, $discharge['hiType'] ?? 'DischargeSummaryRecord', $discharge['bundle'], $discharge['display'] ?? 'Discharge Summary');
|
||||
continue;
|
||||
}
|
||||
} catch (\Throwable $ex) {
|
||||
log_message('warning', 'Live DischargeSummary FHIR generation in recordsFetch failed: ' . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 0c: Live On-Demand Fresh Generation for OPD Consult & Prescription Records
|
||||
if (preg_match('/^(OPD|PRESCRIPTION|PRESC|RX)-(\d+)(?:-S(\d+))?/i', $ref, $m)) {
|
||||
try {
|
||||
$opd = $this->buildOpConsultationBundle($ref);
|
||||
if ($opd !== null && !empty($opd['bundle'])) {
|
||||
$records[] = [
|
||||
'careContextReference' => $ref,
|
||||
'hiType' => $opd['hiType'] ?? 'OPConsultRecord',
|
||||
'display' => $opd['display'] ?? 'Consultation Record',
|
||||
'bundle' => $opd['bundle'],
|
||||
];
|
||||
$this->cacheRecordBundle($ref, $opd['hiType'] ?? 'OPConsultRecord', $opd['bundle'], $opd['display'] ?? 'Consultation Record');
|
||||
continue;
|
||||
}
|
||||
} catch (\Throwable $ex) {
|
||||
log_message('warning', 'Live FHIR bundle generation in recordsFetch failed: ' . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5327,6 +5401,320 @@ class AbdmGateway extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an ABDM M3 compliant DiagnosticReportRecord FHIR Document Bundle on the fly
|
||||
* Supports both numeric lab_req_id and care context references like 'LAB-51459-20261008' or 'RAD-51459-20261008'
|
||||
*
|
||||
* @param string|int $labReqIdOrRef
|
||||
* @return array{careContextReference: string, hiType: string, display: string, bundle: array<string,mixed>}|null
|
||||
*/
|
||||
public function buildDiagnosticReportBundle(string|int $labReqIdOrRef): ?array
|
||||
{
|
||||
$labReqId = 0;
|
||||
$refStr = trim((string) $labReqIdOrRef);
|
||||
if (is_numeric($labReqIdOrRef)) {
|
||||
$labReqId = (int) $labReqIdOrRef;
|
||||
} elseif (preg_match('/^(?:LAB|RAD)-(\d+)/i', $refStr, $m)) {
|
||||
$labReqId = (int) $m[1];
|
||||
}
|
||||
|
||||
if ($labReqId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$db = $this->db ?? \Config\Database::connect();
|
||||
if (! $db->tableExists('lab_request')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$selectCols = ['id', 'patient_id', 'patient_name', 'lab_type', 'charge_id', 'report_name', 'Report_Data', 'report_data_Impression', 'status', 'reported_time'];
|
||||
if ($db->fieldExists('charge_item_id', 'lab_request')) {
|
||||
$selectCols[] = 'charge_item_id';
|
||||
}
|
||||
if ($db->fieldExists('lab_repo_id', 'lab_request')) {
|
||||
$selectCols[] = 'lab_repo_id';
|
||||
}
|
||||
$labReq = $db->table('lab_request')
|
||||
->select(implode(', ', $selectCols))
|
||||
->where('id', $labReqId)
|
||||
->get(1)
|
||||
->getRow();
|
||||
|
||||
if (! $labReq) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$patientId = (int) ($labReq->patient_id ?? 0);
|
||||
$patientRow = [];
|
||||
if ($patientId > 0 && $db->tableExists('patient_master')) {
|
||||
$patientRow = $db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
$patName = $this->patientDisplayName($patientRow);
|
||||
if ($patName === '') {
|
||||
$patName = trim((string) ($labReq->patient_name ?? ''));
|
||||
}
|
||||
|
||||
$abhaIdentity = $this->resolvePatientAbhaIdentity($patientId);
|
||||
$abhaNumber = $abhaIdentity['abha_id'] ?? '';
|
||||
$abhaAddress = $abhaIdentity['abha_address'] ?? '';
|
||||
$abhaId = $abhaAddress !== '' ? $abhaAddress : $abhaNumber;
|
||||
|
||||
$testTitle = trim((string) ($labReq->report_name ?? ''));
|
||||
$labType = (int) ($labReq->lab_type ?? 0);
|
||||
$isImaging = in_array($labType, [1, 2, 3, 4, 6], true);
|
||||
|
||||
if ($testTitle === '') {
|
||||
$labRepoId = (int) ($labReq->lab_repo_id ?? 0);
|
||||
if ($labRepoId > 0 && $db->tableExists('lab_repo')) {
|
||||
$repoRow = $db->table('lab_repo')->select('Repo, RepoName')->where('mstRepoKey', $labRepoId)->get(1)->getRowArray() ?? [];
|
||||
$testTitle = trim((string) ($repoRow['RepoName'] ?? $repoRow['Repo'] ?? ''));
|
||||
}
|
||||
}
|
||||
if ($testTitle === '') {
|
||||
$chargeItemId = (int) ($labReq->charge_item_id ?? 0);
|
||||
if ($chargeItemId > 0 && $db->tableExists('invoice_item')) {
|
||||
$invItem = $db->table('invoice_item')->select('item_name')->where('id', $chargeItemId)->get(1)->getRowArray() ?? [];
|
||||
$testTitle = trim((string) ($invItem['item_name'] ?? ''));
|
||||
}
|
||||
}
|
||||
if ($testTitle === '') {
|
||||
$chargeId = (int) ($labReq->charge_id ?? 0);
|
||||
if ($chargeId > 0 && $db->tableExists('charge_master')) {
|
||||
$chargeRow = $db->table('charge_master')->select('charge_name')->where('id', $chargeId)->get(1)->getRowArray() ?? [];
|
||||
$testTitle = trim((string) ($chargeRow['charge_name'] ?? ''));
|
||||
}
|
||||
}
|
||||
if ($testTitle === '') {
|
||||
$testTitle = $this->mapLabTypeToTitle($labType) ?: ($isImaging ? 'Radiology Report' : 'Laboratory Report');
|
||||
}
|
||||
|
||||
$hospitalProfile = $this->getHospitalProfileForFhir();
|
||||
|
||||
$reportedRaw = trim((string) ($labReq->reported_time ?? ''));
|
||||
$reportedAt = $reportedRaw !== '' ? (new \DateTime($reportedRaw, new \DateTimeZone('Asia/Kolkata')))->format('Y-m-d\TH:i:sP') : date('Y-m-d\TH:i:sP');
|
||||
$visitDate = $reportedRaw !== '' ? date('Y-m-d', strtotime($reportedRaw)) : date('Y-m-d');
|
||||
|
||||
$rawImpression = trim((string) ($labReq->report_data_Impression ?? ''));
|
||||
$cleanImpression = trim(html_entity_decode(strip_tags($rawImpression), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
||||
|
||||
$diagnosticReport = [
|
||||
'id' => (string) $labReqId,
|
||||
'title' => $testTitle ?: ($isImaging ? 'Radiology Report' : 'Laboratory Report'),
|
||||
'status' => in_array((int)$labReq->status, [1, 2], true) ? 'final' : 'preliminary',
|
||||
'conclusion' => $cleanImpression,
|
||||
'reported_at' => $reportedAt,
|
||||
'report_html' => trim((string) ($labReq->Report_Data ?? '')),
|
||||
'lab_type' => $labType,
|
||||
];
|
||||
if ($isImaging) {
|
||||
$diagnosticReport['is_imaging'] = true;
|
||||
$diagnosticReport['report_domain'] = 'imaging';
|
||||
$modalityName = $this->mapLabTypeToTitle($labType) ?: 'Diagnostic imaging';
|
||||
$diagnosticReport['section_title'] = $modalityName . ' report';
|
||||
$diagnosticReport['section_snomed_code'] = '371531008';
|
||||
$diagnosticReport['section_snomed_display'] = 'Diagnostic imaging report';
|
||||
}
|
||||
|
||||
$repoLoincCode = '';
|
||||
if ($db->tableExists('lab_repo')) {
|
||||
$labRepoRow = $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')
|
||||
->where('lr.id', $labReqId)
|
||||
->get(1)
|
||||
->getRowArray() ?? [];
|
||||
$repoLoincCode = trim((string) ($labRepoRow['repo_loinc_code'] ?? ''));
|
||||
}
|
||||
if ($repoLoincCode !== '') {
|
||||
$diagnosticReport['loinc_code'] = $repoLoincCode;
|
||||
}
|
||||
|
||||
$observations = $this->buildLabObservations($labReqId, (string) ($labReq->status ?? '0'));
|
||||
$organization = ! empty($hospitalProfile['name'])
|
||||
? ['name' => $hospitalProfile['name'], 'hfr_id' => $hospitalProfile['hfr_id'] ?? '']
|
||||
: null;
|
||||
$practitioner = $this->resolveLabPractitionerForFhir($labReqId, $labReq);
|
||||
$encounter = [
|
||||
'id' => 'LAB-' . $labReqId,
|
||||
'status' => 'finished',
|
||||
'class_code' => 'AMB',
|
||||
'period_start' => $reportedAt,
|
||||
];
|
||||
|
||||
$patient = [
|
||||
'id' => (string) $patientId,
|
||||
'name' => $patName,
|
||||
'gender' => trim((string) ($patientRow['gender'] ?? '')),
|
||||
'birthDate' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
|
||||
'abhaAddress' => $abhaId,
|
||||
];
|
||||
|
||||
$pdfAttachment = null;
|
||||
try {
|
||||
$pdfAttachment = $this->buildDiagnosticDigitalSharePdf(
|
||||
$patientRow,
|
||||
$abhaIdentity,
|
||||
$diagnosticReport,
|
||||
$observations,
|
||||
$practitioner,
|
||||
$hospitalProfile,
|
||||
$labReqId
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'PDF attachment generation failed in buildDiagnosticReportBundle: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$fhir = new FhirR4Builder();
|
||||
$bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, $practitioner, $organization, $encounter, $pdfAttachment);
|
||||
|
||||
$cleanTestTitle = self::sanitizeCareContextDisplay($testTitle !== '' ? $testTitle : ($isImaging ? 'Radiology Report' : 'Lab Report'));
|
||||
$display = self::sanitizeCareContextDisplay(($isImaging ? 'Radiology Report - ' : 'Diagnostic Report - ') . $cleanTestTitle . ' ' . date('d M Y', strtotime($visitDate)));
|
||||
|
||||
$ccRef = $refStr !== '' ? $refStr : (($isImaging ? 'RAD-' : 'LAB-') . $labReqId . '-' . str_replace('-', '', $visitDate));
|
||||
|
||||
return [
|
||||
'careContextReference' => $ccRef,
|
||||
'hiType' => 'DiagnosticReportRecord',
|
||||
'display' => $display,
|
||||
'bundle' => $bundle,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an ABDM M3 compliant DischargeSummaryRecord FHIR Document Bundle on the fly
|
||||
* Supports both numeric ipd_id and care context references like 'DISCHARGE-3211-20260906' or 'IPD-3211'
|
||||
*
|
||||
* @param string|int $ipdIdOrRef
|
||||
* @return array{careContextReference: string, hiType: string, display: string, bundle: array<string,mixed>}|null
|
||||
*/
|
||||
public function buildDischargeSummaryBundle(string|int $ipdIdOrRef): ?array
|
||||
{
|
||||
$ipdId = 0;
|
||||
$refStr = trim((string) $ipdIdOrRef);
|
||||
if (is_numeric($ipdIdOrRef)) {
|
||||
$ipdId = (int) $ipdIdOrRef;
|
||||
} elseif (preg_match('/^(?:DISCHARGE|IPD)-(\d+)/i', $refStr, $m)) {
|
||||
$ipdId = (int) $m[1];
|
||||
}
|
||||
|
||||
if ($ipdId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = $this->buildIpdDischargeGatewayPayload($ipdId, 0, '', false);
|
||||
if ($payload === null || empty($payload['bundle'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$display = (string) ($payload['care_context_display'] ?? ('Discharge Summary - ' . date('d M Y')));
|
||||
$ccRef = $refStr !== '' ? $refStr : (string) ($payload['care_context_reference'] ?? ('DISCHARGE-' . $ipdId . '-' . date('Ymd')));
|
||||
|
||||
return [
|
||||
'careContextReference' => $ccRef,
|
||||
'hiType' => 'DischargeSummaryRecord',
|
||||
'display' => $display,
|
||||
'bundle' => $payload['bundle'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an ABDM M3 compliant OPConsultRecord or PrescriptionRecord FHIR Document Bundle on the fly
|
||||
* Supports care context references like 'OPD-15355-S33729-20261008' or 'PRESC-15355-S33729-20261008'
|
||||
*
|
||||
* @param string $ref
|
||||
* @return array{careContextReference: string, hiType: string, display: string, bundle: array<string,mixed>}|null
|
||||
*/
|
||||
public function buildOpConsultationBundle(string $ref): ?array
|
||||
{
|
||||
$db = $this->db ?? \Config\Database::connect();
|
||||
if (! preg_match('/^(OPD|PRESCRIPTION|PRESC|RX)-(\d+)(?:-S(\d+))?/i', $ref, $m)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$refPrefix = strtoupper($m[1]);
|
||||
$targetPatientId = (int) $m[2];
|
||||
$targetSessionId = (int) ($m[3] ?? 0);
|
||||
$targetOpdId = 0;
|
||||
|
||||
if ($targetSessionId > 0 && $db->tableExists('opd_prescription')) {
|
||||
$pRow = $db->table('opd_prescription')->select('opd_id')->where('id', $targetSessionId)->get(1)->getRowArray();
|
||||
$targetOpdId = (int) ($pRow['opd_id'] ?? 0);
|
||||
}
|
||||
if ($targetOpdId <= 0 && $targetPatientId > 0 && $db->tableExists('opd_master')) {
|
||||
$opdRow = $db->table('opd_master')->select('opd_id')->where('opd_id', $targetPatientId)->get(1)->getRowArray();
|
||||
if ($opdRow) {
|
||||
$targetOpdId = (int) $opdRow['opd_id'];
|
||||
} else {
|
||||
$opdRow = $db->table('opd_master')->select('opd_id')->where('p_id', $targetPatientId)->orderBy('opd_id', 'DESC')->get(1)->getRowArray();
|
||||
$targetOpdId = (int) ($opdRow['opd_id'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
if ($targetOpdId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$isPrescriptionReq = in_array($refPrefix, ['PRESC', 'PRESCRIPTION', 'RX'], true);
|
||||
$targetBundleType = $isPrescriptionReq ? 'PrescriptionRecord' : 'OPConsultRecord';
|
||||
$targetHiType = $isPrescriptionReq ? 'PrescriptionRecord' : 'OPConsultRecord';
|
||||
$targetDisplay = $isPrescriptionReq
|
||||
? ('Prescription - ' . date('d M Y'))
|
||||
: ('Consultation Record - ' . date('d M Y'));
|
||||
|
||||
$opdCtrl = new \App\Controllers\Opd_prescription();
|
||||
$opdCtrl->initController($this->request, $this->response, service('logger'));
|
||||
$regen = $opdCtrl->regenerateFhirBundleInternal($targetOpdId, $targetSessionId, $targetBundleType);
|
||||
if (!empty($regen['ok']) && !empty($regen['bundle'])) {
|
||||
return [
|
||||
'careContextReference' => $ref,
|
||||
'hiType' => $targetHiType,
|
||||
'display' => $targetDisplay,
|
||||
'bundle' => $regen['bundle'],
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $ex) {
|
||||
log_message('warning', 'Live FHIR bundle generation in buildOpConsultationBundle failed: ' . $ex->getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache freshly generated bundle in health_records table for quick retrieval
|
||||
*/
|
||||
private function cacheRecordBundle(string $ref, string $hiType, array $bundle, string $display = ''): void
|
||||
{
|
||||
try {
|
||||
$db = $this->db ?? \Config\Database::connect();
|
||||
if (! $db->tableExists('health_records')) {
|
||||
return;
|
||||
}
|
||||
$bundleJson = json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$exHr = $db->table('health_records')->where('care_context_reference', $ref)->get(1)->getRowArray();
|
||||
if ($exHr) {
|
||||
if (empty($exHr['record_data'])) {
|
||||
$db->table('health_records')->where('id', (int) $exHr['id'])->update([
|
||||
'record_data' => $bundleJson,
|
||||
'push_status' => 'linked',
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$db->table('health_records')->insert([
|
||||
'care_context_reference' => $ref,
|
||||
'hi_type' => $hiType,
|
||||
'record_data' => $bundleJson,
|
||||
'push_status' => 'linked',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'cacheRecordBundle error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to assemble a minimal ABDM-compliant FHIR Document Bundle on the fly
|
||||
*/
|
||||
|
|
@ -6872,7 +7260,7 @@ class AbdmGateway extends BaseController
|
|||
|
||||
$isReady = $hasStoredDoc || $hasHr || $isVisitDone || $hasFindings || $hasMeds || $hasVitals;
|
||||
|
||||
// Single unified care context covering consult note, wellness, prescription, and documents
|
||||
// 1. Doctor Visit / OPD Consultation record
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
|
|
@ -6882,11 +7270,41 @@ class AbdmGateway extends BaseController
|
|||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => $isReady,
|
||||
'is_primary' => false,
|
||||
'entity_id' => (string) $opdId,
|
||||
'opd_id' => $opdId,
|
||||
'session_id' => $sessionId,
|
||||
]);
|
||||
$seenRefs[$aliasRef] = true;
|
||||
$seenRefs['OPD-' . $opdId . '-S' . $sessionId . '-' . $visitDate] = true;
|
||||
$seenRefs['OPD-' . $opdId . '-S' . $sessionId . '-' . $cleanDate] = true;
|
||||
$seenRefs['OPD-' . $patientId . '-S' . $sessionId . '-' . $visitDate] = true;
|
||||
|
||||
// 2. If medicines are prescribed, also discover distinct PrescriptionRecord
|
||||
if ($hasMeds) {
|
||||
$prescCcRef = 'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
|
||||
$prescAliasRef = 'PRESC-' . $opdId . '-S' . $sessionId . '-' . $cleanDate;
|
||||
$prescDisplay = 'Prescription - ' . ($docName !== '' ? 'Dr. ' . $docName . ' - ' : '') . $dateStr;
|
||||
if (strlen($prescDisplay) > 85) {
|
||||
$prescDisplay = 'Prescription - ' . $dateStr;
|
||||
}
|
||||
$addContext([
|
||||
'careContextId' => $prescCcRef,
|
||||
'referenceNumber' => $prescCcRef,
|
||||
'alias_reference' => $prescAliasRef,
|
||||
'display' => $prescDisplay,
|
||||
'record_type' => 'PrescriptionRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => $isReady,
|
||||
'is_primary' => false,
|
||||
'entity_id' => (string) $opdId,
|
||||
'opd_id' => $opdId,
|
||||
'session_id' => $sessionId,
|
||||
]);
|
||||
$seenRefs[$prescAliasRef] = true;
|
||||
$seenRefs['PRESC-' . $opdId . '-S' . $sessionId . '-' . $visitDate] = true;
|
||||
$seenRefs['PRESC-' . $opdId . '-S' . $sessionId . '-' . $cleanDate] = true;
|
||||
$seenRefs['PRESC-' . $patientId . '-S' . $sessionId . '-' . $visitDate] = true;
|
||||
}
|
||||
if ($sessionId > 0) {
|
||||
$seenOpdSessions[$sessionId] = true;
|
||||
}
|
||||
|
|
@ -7283,6 +7701,73 @@ class AbdmGateway extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
$extractDate = static function (array $cc): int {
|
||||
foreach (['visit_date', 'date', 'created_at', 'insert_date', 'date_issue'] as $field) {
|
||||
if (! empty($cc[$field])) {
|
||||
$ts = strtotime((string) $cc[$field]);
|
||||
if ($ts !== false && $ts > 0) {
|
||||
return $ts;
|
||||
}
|
||||
}
|
||||
}
|
||||
$ref = (string) ($cc['referenceNumber'] ?? $cc['careContextId'] ?? '');
|
||||
if (preg_match('/(\d{4})(\d{2})(\d{2})/', $ref, $m)) {
|
||||
$check = $m[1] . '-' . $m[2] . '-' . $m[3];
|
||||
$ts = strtotime($check);
|
||||
if ($ts !== false && $ts > 0) {
|
||||
return $ts;
|
||||
}
|
||||
}
|
||||
if (preg_match('/(\d{4}-\d{2}-\d{2})/', $ref, $m)) {
|
||||
$ts = strtotime($m[1]);
|
||||
if ($ts !== false && $ts > 0) {
|
||||
return $ts;
|
||||
}
|
||||
}
|
||||
$disp = (string) ($cc['display'] ?? '');
|
||||
if (preg_match('/(\d{1,2}\s+[A-Za-z]{3}\s+\d{4})/', $disp, $m)) {
|
||||
$ts = strtotime($m[1]);
|
||||
if ($ts !== false && $ts > 0) {
|
||||
return $ts;
|
||||
}
|
||||
}
|
||||
if (preg_match('/(\d{4}-\d{2}-\d{2})/', $disp, $m)) {
|
||||
$ts = strtotime($m[1]);
|
||||
if ($ts !== false && $ts > 0) {
|
||||
return $ts;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
usort($careContextsFull, function ($a, $b) use ($extractDate) {
|
||||
$pA = ! empty($a['is_primary']) ? 1 : 0;
|
||||
$pB = ! empty($b['is_primary']) ? 1 : 0;
|
||||
if ($pA !== $pB) {
|
||||
return $pB - $pA;
|
||||
}
|
||||
$tA = $extractDate($a);
|
||||
$tB = $extractDate($b);
|
||||
if ($tA !== $tB) {
|
||||
return $tB - $tA;
|
||||
}
|
||||
return strcmp((string) ($b['referenceNumber'] ?? ''), (string) ($a['referenceNumber'] ?? ''));
|
||||
});
|
||||
|
||||
$careContextsV3 = [];
|
||||
foreach ($careContextsFull as $item) {
|
||||
$ref = trim((string) ($item['referenceNumber'] ?? $item['careContextId'] ?? ''));
|
||||
$recType = trim((string) ($item['record_type'] ?? ''));
|
||||
$hiType = $item['hiType'] ?? $this->mapRecordTypeToAbdmHiType($recType);
|
||||
$careContextsV3[] = [
|
||||
'referenceNumber' => $ref,
|
||||
'display' => $item['display'] ?? $ref,
|
||||
'hiType' => $hiType,
|
||||
'record_type' => $recType,
|
||||
'is_fhir_ready' => ! empty($item['is_fhir_ready']),
|
||||
];
|
||||
}
|
||||
|
||||
return [$careContextsV3, $careContextsFull];
|
||||
}
|
||||
|
||||
|
|
@ -12315,6 +12800,7 @@ class AbdmGateway extends BaseController
|
|||
'is_linked' => $isLinked,
|
||||
'is_fhir_ready' => (bool) ($cc['is_fhir_ready'] ?? false),
|
||||
'is_primary' => (bool) ($cc['is_primary'] ?? false),
|
||||
'entity_id' => (string) ($cc['entity_id'] ?? $cc['opd_id'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -780,9 +780,9 @@ class AbdmTaskBoard extends BaseController
|
|||
$linked = $this->db->tableExists('health_records') && ! empty($this->db->table('health_records')
|
||||
->select('id')
|
||||
->where('hi_type', 'ImmunizationRecord')
|
||||
->where('entity_type', 'immunization')
|
||||
->whereIn('entity_type', ['immunization', 'immunization_record_publish'])
|
||||
->where('entity_id', (string) $recordId)
|
||||
->where('push_status', 'linked')
|
||||
->whereIn('push_status', ['linked', 'queued', 'pushed', 'done'])
|
||||
->get(1)
|
||||
->getRowArray());
|
||||
if ($linked) {
|
||||
|
|
@ -792,9 +792,9 @@ class AbdmTaskBoard extends BaseController
|
|||
$exists = $this->db->table('abdm_work_tasks')
|
||||
->select('id')
|
||||
->where('task_type', 'immunization_record_publish')
|
||||
->where('entity_type', 'immunization')
|
||||
->whereIn('entity_type', ['immunization', 'immunization_records', 'immunization_record_publish'])
|
||||
->where('entity_id', (string) $recordId)
|
||||
->whereIn('status', ['pending', 'in_progress'])
|
||||
->whereIn('status', ['pending', 'in_progress', 'completed'])
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($exists)) {
|
||||
|
|
|
|||
|
|
@ -4934,7 +4934,11 @@ class Opd_prescription extends BaseController
|
|||
}
|
||||
$normalizedBundleType = in_array($bundleType, ['Prescription', 'PrescriptionRecord'], true) ? 'PrescriptionRecord' : 'OPConsultRecord';
|
||||
$clinicalContext['bundle_type'] = $normalizedBundleType;
|
||||
$bundle = $this->fhirR4Builder->buildOpConsultBundle($patient, $encounter, $medications, $conditions, $clinicalContext);
|
||||
if ($normalizedBundleType === 'PrescriptionRecord') {
|
||||
$bundle = $this->fhirR4Builder->buildPrescriptionBundle($patient, $encounter, $medications, [], $clinicalContext);
|
||||
} else {
|
||||
$bundle = $this->fhirR4Builder->buildOpConsultBundle($patient, $encounter, $medications, $conditions, $clinicalContext);
|
||||
}
|
||||
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$generatedBy = 'system';
|
||||
|
|
@ -11922,12 +11926,50 @@ class Opd_prescription extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
// Clean up redundant standalone bundles in opd_fhir_documents so only unified OPConsultRecord is kept
|
||||
// If medicines are prescribed, also store a distinct, compliant PrescriptionRecord bundle
|
||||
$prescJson = null;
|
||||
if (! empty($medications)) {
|
||||
$prescContext = $clinicalContext;
|
||||
$prescContext['bundle_type'] = 'PrescriptionRecord';
|
||||
$prescBundle = $builder->buildPrescriptionBundle($patient, $encounter, $medications, [], $prescContext);
|
||||
$prescJson = (string) json_encode($prescBundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$existingPresc = $this->db->table('opd_fhir_documents')
|
||||
->where('opd_id', $opdId)
|
||||
->where('opd_session_id', $sessionId)
|
||||
->where('bundle_type', 'PrescriptionRecord')
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($existingPresc['id'])) {
|
||||
$this->db->table('opd_fhir_documents')
|
||||
->where('id', (int) $existingPresc['id'])
|
||||
->update([
|
||||
'bundle_json' => $prescJson,
|
||||
'generated_by' => $generatedBy,
|
||||
'generated_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
} else {
|
||||
$this->db->table('opd_fhir_documents')->insert([
|
||||
'opd_id' => $opdId,
|
||||
'opd_session_id' => $sessionId,
|
||||
'patient_id' => (int) ($patient['id'] ?? 0),
|
||||
'bundle_type' => 'PrescriptionRecord',
|
||||
'bundle_json' => $prescJson,
|
||||
'generated_by' => $generatedBy,
|
||||
'generated_at' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up legacy/obsolete bundle types
|
||||
try {
|
||||
$this->db->table('opd_fhir_documents')
|
||||
->where('opd_id', $opdId)
|
||||
->where('opd_session_id', $sessionId)
|
||||
->whereIn('bundle_type', ['PrescriptionRecord', 'MedicationRequestBundle', 'WellnessRecord'])
|
||||
->whereIn('bundle_type', ['MedicationRequestBundle', 'WellnessRecord'])
|
||||
->delete();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
|
@ -11944,7 +11986,7 @@ class Opd_prescription extends BaseController
|
|||
$cleanDate = str_replace('-', '', $visitDate);
|
||||
$careContextRef = 'OPD-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
|
||||
|
||||
// 1. Upsert single unified OPConsultRecord (covers Consult, Rx, Vitals, and Attachments)
|
||||
// 1. Upsert OPConsultRecord (covers Doctor Visit Note)
|
||||
$existingHr = $this->db->table('health_records')
|
||||
->select('id')
|
||||
->where('care_context_reference', $careContextRef)
|
||||
|
|
@ -11970,9 +12012,35 @@ class Opd_prescription extends BaseController
|
|||
$this->db->table('health_records')->insert($hrPayload);
|
||||
}
|
||||
|
||||
// Delete redundant standalone PRESC / WELLNESS / alias OPD records so only the single unified bundle is kept
|
||||
// 2. If medications exist, also upsert distinct PrescriptionRecord
|
||||
if (! empty($medications) && $prescJson !== null) {
|
||||
$prescCcRef = 'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
|
||||
$existingPrescHr = $this->db->table('health_records')
|
||||
->select('id')
|
||||
->where('care_context_reference', $prescCcRef)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
$prescHrPayload = [
|
||||
'patient_id' => $patientId > 0 ? $patientId : null,
|
||||
'abha_id' => $abhaAddress !== '' ? $abhaAddress : null,
|
||||
'hi_type' => 'PrescriptionRecord',
|
||||
'entity_type' => 'opd',
|
||||
'entity_id' => (string) $opdId,
|
||||
'record_data' => $prescJson,
|
||||
'care_context_reference' => $prescCcRef,
|
||||
'push_status' => 'local_discovery_ready',
|
||||
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
];
|
||||
if (! empty($existingPrescHr)) {
|
||||
$this->db->table('health_records')->where('id', (int) $existingPrescHr['id'])->update($prescHrPayload);
|
||||
} else {
|
||||
$prescHrPayload['created_at'] = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
$this->db->table('health_records')->insert($prescHrPayload);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete obsolete legacy alias records
|
||||
$redundantRefs = [
|
||||
'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate,
|
||||
'WELLNESS-' . $sessionId . '-' . $cleanDate,
|
||||
'OPD-' . $opdId . '-S' . $sessionId . '-' . $visitDate,
|
||||
'OPD-' . $opdId . '-S' . $sessionId . '-' . $cleanDate,
|
||||
|
|
|
|||
|
|
@ -494,7 +494,7 @@ class AbdmTaskBoardSyncService
|
|||
// 3. Update health_records with final push status
|
||||
if ($healthRecordId > 0 && $this->db->tableExists('health_records')) {
|
||||
$hrUpdate = [
|
||||
'push_status' => $isSuccess ? 'queued' : 'failed',
|
||||
'push_status' => $isSuccess ? 'linked' : 'failed',
|
||||
'updated_at' => $now,
|
||||
];
|
||||
if ($queueId !== null && $queueId !== '') {
|
||||
|
|
@ -982,7 +982,7 @@ class AbdmTaskBoardSyncService
|
|||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityId,
|
||||
'care_context_reference' => $careContextRef,
|
||||
'push_status' => 'queued',
|
||||
'push_status' => 'linked',
|
||||
'abdm_txn_id' => $queueId,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
|
@ -1008,7 +1008,7 @@ class AbdmTaskBoardSyncService
|
|||
'care_context_reference' => $careContextRef,
|
||||
'abdm_txn_id' => $queueId,
|
||||
'abha_id' => $abhaId,
|
||||
'link_status' => 'pending',
|
||||
'link_status' => 'linked',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ class FhirR4Builder
|
|||
$appointments = is_array($context['appointments'] ?? null) ? (array) $context['appointments'] : [];
|
||||
$attachments = is_array($context['attachments'] ?? null) ? (array) $context['attachments'] : [];
|
||||
|
||||
$requestedBundleType = trim((string) ($context['bundle_type'] ?? $context['record_type'] ?? 'PrescriptionRecord'));
|
||||
$isOpConsult = in_array($requestedBundleType, ['OPConsultRecord', 'OPConsultation'], true);
|
||||
|
||||
// UUID-based identity for every resource (ABDM IG v6.5.0 requirement)
|
||||
$bundleUuid = $this->generateUuid();
|
||||
$compositionUuid = $this->generateUuid();
|
||||
|
|
@ -142,221 +145,225 @@ class FhirR4Builder
|
|||
}
|
||||
|
||||
// ── Conditions (diagnoses) ────────────────────────────────────────────
|
||||
foreach ($conditions as $index => $condition) {
|
||||
$text = trim((string) ($condition['text'] ?? ''));
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
if ($isOpConsult) {
|
||||
foreach ($conditions as $index => $condition) {
|
||||
$text = trim((string) ($condition['text'] ?? ''));
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$condUuid = $this->generateUuid();
|
||||
$condRef = 'urn:uuid:' . $condUuid;
|
||||
$verification = trim((string) ($condition['verification_status'] ?? 'provisional'));
|
||||
$verificationCode = strtolower($verification) === 'confirmed' ? 'confirmed' : 'provisional';
|
||||
$useSnomedCode = $verificationCode === 'confirmed' ? '39154008' : '148006';
|
||||
$useSnomedDisplay = $verificationCode === 'confirmed' ? 'Clinical diagnosis' : 'Preliminary diagnosis';
|
||||
$condUuid = $this->generateUuid();
|
||||
$condRef = 'urn:uuid:' . $condUuid;
|
||||
$verification = trim((string) ($condition['verification_status'] ?? 'provisional'));
|
||||
$verificationCode = strtolower($verification) === 'confirmed' ? 'confirmed' : 'provisional';
|
||||
$useSnomedCode = $verificationCode === 'confirmed' ? '39154008' : '148006';
|
||||
$useSnomedDisplay = $verificationCode === 'confirmed' ? 'Clinical diagnosis' : 'Preliminary diagnosis';
|
||||
|
||||
$encounterDiagnosisRefs[] = [
|
||||
'condition' => ['reference' => $condRef, 'display' => 'Condition'],
|
||||
'use' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $useSnomedCode,
|
||||
'display' => $useSnomedDisplay,
|
||||
]]],
|
||||
];
|
||||
$encounterDiagnosisRefs[] = [
|
||||
'condition' => ['reference' => $condRef, 'display' => 'Condition'],
|
||||
'use' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $useSnomedCode,
|
||||
'display' => $useSnomedDisplay,
|
||||
]]],
|
||||
];
|
||||
|
||||
$code = ['text' => $text];
|
||||
$snomedCode = trim((string) ($condition['snomed_code'] ?? ''));
|
||||
if ($snomedCode !== '') {
|
||||
$code['coding'] = [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $snomedCode,
|
||||
'display' => trim((string) ($condition['snomed_display'] ?? $text)),
|
||||
$code = ['text' => $text];
|
||||
$snomedCode = trim((string) ($condition['snomed_code'] ?? ''));
|
||||
if ($snomedCode !== '') {
|
||||
$code['coding'] = [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $snomedCode,
|
||||
'display' => trim((string) ($condition['snomed_display'] ?? $text)),
|
||||
]];
|
||||
}
|
||||
|
||||
$resourceEntries[] = ['fullUrl' => $condRef, 'resource' => [
|
||||
'resourceType' => 'Condition',
|
||||
'id' => $condUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition']],
|
||||
'clinicalStatus' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/condition-clinical',
|
||||
'code' => 'active',
|
||||
'display' => 'Active',
|
||||
]]],
|
||||
'verificationStatus' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/condition-ver-status',
|
||||
'code' => $verificationCode,
|
||||
'display' => ucfirst($verificationCode),
|
||||
]]],
|
||||
'code' => $code,
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'recordedDate' => $issuedAt,
|
||||
]];
|
||||
}
|
||||
|
||||
$resourceEntries[] = ['fullUrl' => $condRef, 'resource' => [
|
||||
'resourceType' => 'Condition',
|
||||
'id' => $condUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition']],
|
||||
'clinicalStatus' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/condition-clinical',
|
||||
'code' => 'active',
|
||||
'display' => 'Active',
|
||||
]]],
|
||||
'verificationStatus' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/condition-ver-status',
|
||||
'code' => $verificationCode,
|
||||
'display' => ucfirst($verificationCode),
|
||||
]]],
|
||||
'code' => $code,
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'recordedDate' => $issuedAt,
|
||||
]];
|
||||
}
|
||||
|
||||
// Append Encounter (now has diagnosis refs)
|
||||
if (! empty($encounterDiagnosisRefs)) {
|
||||
$encounterResource['diagnosis'] = $encounterDiagnosisRefs;
|
||||
// Append Encounter (now has diagnosis refs)
|
||||
if (! empty($encounterDiagnosisRefs)) {
|
||||
$encounterResource['diagnosis'] = $encounterDiagnosisRefs;
|
||||
}
|
||||
}
|
||||
$resourceEntries[] = ['fullUrl' => $encounterRef, 'resource' => $encounterResource];
|
||||
|
||||
// ── Complaints (Chief Complaints) ─────────────────────────────────
|
||||
foreach ($complaints as $index => $complaint) {
|
||||
$text = trim((string) ($complaint['text'] ?? ''));
|
||||
if ($text === '') {
|
||||
continue;
|
||||
if ($isOpConsult) {
|
||||
// ── Complaints (Chief Complaints) ─────────────────────────────────
|
||||
foreach ($complaints as $index => $complaint) {
|
||||
$text = trim((string) ($complaint['text'] ?? ''));
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$complaintUuid = $this->generateUuid();
|
||||
$complaintRef = 'urn:uuid:' . $complaintUuid;
|
||||
$complaintRefs[] = ['reference' => $complaintRef, 'display' => 'Condition'];
|
||||
$code = ['text' => $text];
|
||||
|
||||
$snomedCode = trim((string) ($complaint['snomed_code'] ?? ''));
|
||||
if ($snomedCode !== '') {
|
||||
$code['coding'] = [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $snomedCode,
|
||||
'display' => trim((string) ($complaint['snomed_display'] ?? $text)),
|
||||
]];
|
||||
}
|
||||
|
||||
$complaintResource = [
|
||||
'resourceType' => 'Condition',
|
||||
'id' => $complaintUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition']],
|
||||
'clinicalStatus' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/condition-clinical',
|
||||
'code' => 'active',
|
||||
'display' => 'Active',
|
||||
]]],
|
||||
'code' => $code,
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'recordedDate' => $issuedAt,
|
||||
];
|
||||
|
||||
// Severity (High/Moderate/Low → SNOMED coded)
|
||||
$severityText = trim((string) ($complaint['severity'] ?? ''));
|
||||
if ($severityText !== '') {
|
||||
$severityMap = [
|
||||
'high' => ['24484000', 'Severe'],
|
||||
'severe' => ['24484000', 'Severe'],
|
||||
'moderate' => ['6736007', 'Moderate'],
|
||||
'mild' => ['255604002', 'Mild'],
|
||||
'low' => ['255604002', 'Mild'],
|
||||
];
|
||||
$severityKey = strtolower($severityText);
|
||||
[$sevCode, $sevDisplay] = $severityMap[$severityKey] ?? ['', $severityText];
|
||||
$severityCoding = ['text' => ucfirst($severityText)];
|
||||
if ($sevCode !== '') {
|
||||
$severityCoding['coding'] = [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $sevCode,
|
||||
'display' => $sevDisplay,
|
||||
]];
|
||||
}
|
||||
$complaintResource['severity'] = $severityCoding;
|
||||
}
|
||||
|
||||
// Duration → note
|
||||
$durationText = trim((string) ($complaint['duration'] ?? ''));
|
||||
$frequencyText = trim((string) ($complaint['frequency'] ?? ''));
|
||||
$noteParts = array_filter([$durationText, $frequencyText]);
|
||||
if (! empty($noteParts)) {
|
||||
$complaintResource['note'] = [['text' => implode(' | ', $noteParts)]];
|
||||
}
|
||||
|
||||
$resourceEntries[] = ['fullUrl' => $complaintRef, 'resource' => $complaintResource];
|
||||
}
|
||||
|
||||
$complaintUuid = $this->generateUuid();
|
||||
$complaintRef = 'urn:uuid:' . $complaintUuid;
|
||||
$complaintRefs[] = ['reference' => $complaintRef, 'display' => 'Condition'];
|
||||
$code = ['text' => $text];
|
||||
// ── Observations (Vitals / Physical Examination) ──────────────────────
|
||||
foreach ($observations as $index => $observation) {
|
||||
$value = $observation['value'] ?? null;
|
||||
if (! is_numeric($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snomedCode = trim((string) ($complaint['snomed_code'] ?? ''));
|
||||
if ($snomedCode !== '') {
|
||||
$code['coding'] = [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $snomedCode,
|
||||
'display' => trim((string) ($complaint['snomed_display'] ?? $text)),
|
||||
$num = (float) $value;
|
||||
$loinc = (string) ($observation['loinc'] ?? '');
|
||||
$display = (string) ($observation['display'] ?? '');
|
||||
$unit = (string) ($observation['unit'] ?? '');
|
||||
if ($loinc === '8310-5' || strcasecmp($display, 'Body temperature') === 0 || in_array($unit, ['Cel', 'degF', '[degF]'], true)) {
|
||||
$num = round($num, 1);
|
||||
} elseif ($loinc === '39156-5' || strcasecmp($display, 'Body mass index') === 0) {
|
||||
$num = round($num, 1);
|
||||
} elseif ($loinc === '29463-7' || strcasecmp($display, 'Body weight') === 0) {
|
||||
$num = round($num, 2);
|
||||
} elseif ($loinc === '8302-2' || strcasecmp($display, 'Body height') === 0) {
|
||||
$num = round($num, 1);
|
||||
} elseif (in_array($loinc, ['8867-4', '59408-5', '8480-6', '8462-4', '9279-1'], true)) {
|
||||
$num = (float) round($num);
|
||||
} else {
|
||||
$num = round($num, 2);
|
||||
}
|
||||
|
||||
$obsUuid = $this->generateUuid();
|
||||
$observationRefs[] = ['reference' => 'urn:uuid:' . $obsUuid, 'display' => 'Observation'];
|
||||
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $obsUuid, 'resource' => [
|
||||
'resourceType' => 'Observation',
|
||||
'id' => $obsUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ObservationVitalSigns']],
|
||||
'status' => 'final',
|
||||
'category' => [['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/observation-category',
|
||||
'code' => 'vital-signs',
|
||||
]]]],
|
||||
'code' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => $loinc,
|
||||
'display' => $display,
|
||||
]],
|
||||
'text' => $display,
|
||||
],
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'encounter' => ['reference' => $encounterRef],
|
||||
'effectiveDateTime' => $issuedAt,
|
||||
'valueQuantity' => [
|
||||
'value' => $num,
|
||||
'unit' => $unit,
|
||||
'system' => 'http://unitsofmeasure.org',
|
||||
'code' => (string) ($observation['ucum'] ?? ''),
|
||||
],
|
||||
]];
|
||||
}
|
||||
|
||||
$complaintResource = [
|
||||
'resourceType' => 'Condition',
|
||||
'id' => $complaintUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition']],
|
||||
'clinicalStatus' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/condition-clinical',
|
||||
'code' => 'active',
|
||||
'display' => 'Active',
|
||||
]]],
|
||||
'code' => $code,
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'recordedDate' => $issuedAt,
|
||||
];
|
||||
|
||||
// Severity (High/Moderate/Low → SNOMED coded)
|
||||
$severityText = trim((string) ($complaint['severity'] ?? ''));
|
||||
if ($severityText !== '') {
|
||||
$severityMap = [
|
||||
'high' => ['24484000', 'Severe'],
|
||||
'severe' => ['24484000', 'Severe'],
|
||||
'moderate' => ['6736007', 'Moderate'],
|
||||
'mild' => ['255604002', 'Mild'],
|
||||
'low' => ['255604002', 'Mild'],
|
||||
];
|
||||
$severityKey = strtolower($severityText);
|
||||
[$sevCode, $sevDisplay] = $severityMap[$severityKey] ?? ['', $severityText];
|
||||
$severityCoding = ['text' => ucfirst($severityText)];
|
||||
if ($sevCode !== '') {
|
||||
$severityCoding['coding'] = [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => $sevCode,
|
||||
'display' => $sevDisplay,
|
||||
]];
|
||||
foreach ($allergies as $index => $allergy) {
|
||||
$codeText = trim((string) ($allergy['code_text'] ?? ''));
|
||||
if ($codeText === '') {
|
||||
continue;
|
||||
}
|
||||
$complaintResource['severity'] = $severityCoding;
|
||||
|
||||
$allergyUuid = $this->generateUuid();
|
||||
$allergyRefs[] = ['reference' => 'urn:uuid:' . $allergyUuid, 'display' => 'AllergyIntolerance'];
|
||||
|
||||
$allergyResource = [
|
||||
'resourceType' => 'AllergyIntolerance',
|
||||
'id' => $allergyUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/AllergyIntolerance']],
|
||||
'clinicalStatus' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical',
|
||||
'code' => (string) ($allergy['clinical_status'] ?? 'active'),
|
||||
]]],
|
||||
'verificationStatus' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/allergyintolerance-verification',
|
||||
'code' => (string) ($allergy['verification_status'] ?? 'confirmed'),
|
||||
]]],
|
||||
'code' => ['text' => $codeText],
|
||||
'patient' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'recordedDate' => $issuedAt,
|
||||
];
|
||||
|
||||
$reaction = trim((string) ($allergy['reaction_text'] ?? ''));
|
||||
if ($reaction !== '') {
|
||||
$allergyResource['reaction'] = [['description' => $reaction]];
|
||||
}
|
||||
|
||||
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $allergyUuid, 'resource' => $allergyResource];
|
||||
}
|
||||
|
||||
// Duration → note
|
||||
$durationText = trim((string) ($complaint['duration'] ?? ''));
|
||||
$frequencyText = trim((string) ($complaint['frequency'] ?? ''));
|
||||
$noteParts = array_filter([$durationText, $frequencyText]);
|
||||
if (! empty($noteParts)) {
|
||||
$complaintResource['note'] = [['text' => implode(' | ', $noteParts)]];
|
||||
}
|
||||
|
||||
$resourceEntries[] = ['fullUrl' => $complaintRef, 'resource' => $complaintResource];
|
||||
}
|
||||
|
||||
// ── Observations (Vitals / Physical Examination) ──────────────────────
|
||||
foreach ($observations as $index => $observation) {
|
||||
$value = $observation['value'] ?? null;
|
||||
if (! is_numeric($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$num = (float) $value;
|
||||
$loinc = (string) ($observation['loinc'] ?? '');
|
||||
$display = (string) ($observation['display'] ?? '');
|
||||
$unit = (string) ($observation['unit'] ?? '');
|
||||
if ($loinc === '8310-5' || strcasecmp($display, 'Body temperature') === 0 || in_array($unit, ['Cel', 'degF', '[degF]'], true)) {
|
||||
$num = round($num, 1);
|
||||
} elseif ($loinc === '39156-5' || strcasecmp($display, 'Body mass index') === 0) {
|
||||
$num = round($num, 1);
|
||||
} elseif ($loinc === '29463-7' || strcasecmp($display, 'Body weight') === 0) {
|
||||
$num = round($num, 2);
|
||||
} elseif ($loinc === '8302-2' || strcasecmp($display, 'Body height') === 0) {
|
||||
$num = round($num, 1);
|
||||
} elseif (in_array($loinc, ['8867-4', '59408-5', '8480-6', '8462-4', '9279-1'], true)) {
|
||||
$num = (float) round($num);
|
||||
} else {
|
||||
$num = round($num, 2);
|
||||
}
|
||||
|
||||
$obsUuid = $this->generateUuid();
|
||||
$observationRefs[] = ['reference' => 'urn:uuid:' . $obsUuid, 'display' => 'Observation'];
|
||||
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $obsUuid, 'resource' => [
|
||||
'resourceType' => 'Observation',
|
||||
'id' => $obsUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ObservationVitalSigns']],
|
||||
'status' => 'final',
|
||||
'category' => [['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/observation-category',
|
||||
'code' => 'vital-signs',
|
||||
]]]],
|
||||
'code' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => $loinc,
|
||||
'display' => $display,
|
||||
]],
|
||||
'text' => $display,
|
||||
],
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'encounter' => ['reference' => $encounterRef],
|
||||
'effectiveDateTime' => $issuedAt,
|
||||
'valueQuantity' => [
|
||||
'value' => $num,
|
||||
'unit' => $unit,
|
||||
'system' => 'http://unitsofmeasure.org',
|
||||
'code' => (string) ($observation['ucum'] ?? ''),
|
||||
],
|
||||
]];
|
||||
}
|
||||
|
||||
foreach ($allergies as $index => $allergy) {
|
||||
$codeText = trim((string) ($allergy['code_text'] ?? ''));
|
||||
if ($codeText === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allergyUuid = $this->generateUuid();
|
||||
$allergyRefs[] = ['reference' => 'urn:uuid:' . $allergyUuid, 'display' => 'AllergyIntolerance'];
|
||||
|
||||
$allergyResource = [
|
||||
'resourceType' => 'AllergyIntolerance',
|
||||
'id' => $allergyUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/AllergyIntolerance']],
|
||||
'clinicalStatus' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical',
|
||||
'code' => (string) ($allergy['clinical_status'] ?? 'active'),
|
||||
]]],
|
||||
'verificationStatus' => ['coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/allergyintolerance-verification',
|
||||
'code' => (string) ($allergy['verification_status'] ?? 'confirmed'),
|
||||
]]],
|
||||
'code' => ['text' => $codeText],
|
||||
'patient' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'recordedDate' => $issuedAt,
|
||||
];
|
||||
|
||||
$reaction = trim((string) ($allergy['reaction_text'] ?? ''));
|
||||
if ($reaction !== '') {
|
||||
$allergyResource['reaction'] = [['description' => $reaction]];
|
||||
}
|
||||
|
||||
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $allergyUuid, 'resource' => $allergyResource];
|
||||
}
|
||||
|
||||
// ── Medications ───────────────────────────────────────────────────────
|
||||
|
|
@ -480,75 +487,77 @@ class FhirR4Builder
|
|||
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $medUuid, 'resource' => $medRes];
|
||||
}
|
||||
|
||||
// ── ServiceRequests (Investigation Advice) ────────────────────────────
|
||||
foreach ($serviceRequests as $index => $serviceRequest) {
|
||||
$codeText = trim((string) ($serviceRequest['code_text'] ?? ''));
|
||||
if ($codeText === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$svcUuid = $this->generateUuid();
|
||||
$serviceRequestRefs[] = ['reference' => 'urn:uuid:' . $svcUuid, 'display' => 'ServiceRequest'];
|
||||
$svcRes = [
|
||||
'resourceType' => 'ServiceRequest',
|
||||
'id' => $svcUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ServiceRequest']],
|
||||
'status' => (string) ($serviceRequest['status'] ?? 'active'),
|
||||
'intent' => (string) ($serviceRequest['intent'] ?? 'order'),
|
||||
'code' => ['text' => $codeText],
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'encounter' => ['reference' => $encounterRef],
|
||||
'authoredOn' => $issuedAt,
|
||||
];
|
||||
if ($practitionerRef !== '') {
|
||||
$svcRes['requester'] = ['reference' => $practitionerRef];
|
||||
}
|
||||
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $svcUuid, 'resource' => $svcRes];
|
||||
}
|
||||
|
||||
// ── Appointments (Follow Up) ──────────────────────────────────────────
|
||||
foreach ($appointments as $index => $appointment) {
|
||||
$description = trim((string) ($appointment['description'] ?? ''));
|
||||
if ($description === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$apptUuid = $this->generateUuid();
|
||||
$appointmentRefs[] = ['reference' => 'urn:uuid:' . $apptUuid, 'display' => 'Appointment'];
|
||||
|
||||
$apptStartIso = $issuedAt;
|
||||
if (preg_match('/(\d{2}-\d{2}-\d{4})/', $description, $m) === 1) {
|
||||
$parsed = \DateTimeImmutable::createFromFormat('d-m-Y', $m[1]);
|
||||
if ($parsed !== false) {
|
||||
$apptStartIso = $parsed->setTime(10, 0, 0)->format(DATE_ATOM);
|
||||
if ($isOpConsult) {
|
||||
// ── ServiceRequests (Investigation Advice) ────────────────────────────
|
||||
foreach ($serviceRequests as $index => $serviceRequest) {
|
||||
$codeText = trim((string) ($serviceRequest['code_text'] ?? ''));
|
||||
if ($codeText === '') {
|
||||
continue;
|
||||
}
|
||||
} elseif (preg_match('/(\d{4}-\d{2}-\d{2})/', $description, $m) === 1) {
|
||||
$parsed = \DateTimeImmutable::createFromFormat('Y-m-d', $m[1]);
|
||||
if ($parsed !== false) {
|
||||
$apptStartIso = $parsed->setTime(10, 0, 0)->format(DATE_ATOM);
|
||||
}
|
||||
}
|
||||
|
||||
$apptRes = [
|
||||
'resourceType' => 'Appointment',
|
||||
'id' => $apptUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Appointment']],
|
||||
'status' => (string) ($appointment['status'] ?? 'proposed'),
|
||||
'description' => $description,
|
||||
'start' => (string) ($appointment['start'] ?? $apptStartIso),
|
||||
'end' => (string) ($appointment['end'] ?? $apptStartIso),
|
||||
'participant' => [[
|
||||
'actor' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'status' => 'accepted',
|
||||
]],
|
||||
];
|
||||
if ($practitionerRef !== '') {
|
||||
$apptRes['participant'][] = [
|
||||
'actor' => ['reference' => $practitionerRef, 'display' => $practRawName !== '' ? $practRawName : 'Practitioner'],
|
||||
'status' => 'accepted',
|
||||
$svcUuid = $this->generateUuid();
|
||||
$serviceRequestRefs[] = ['reference' => 'urn:uuid:' . $svcUuid, 'display' => 'ServiceRequest'];
|
||||
$svcRes = [
|
||||
'resourceType' => 'ServiceRequest',
|
||||
'id' => $svcUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ServiceRequest']],
|
||||
'status' => (string) ($serviceRequest['status'] ?? 'active'),
|
||||
'intent' => (string) ($serviceRequest['intent'] ?? 'order'),
|
||||
'code' => ['text' => $codeText],
|
||||
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'encounter' => ['reference' => $encounterRef],
|
||||
'authoredOn' => $issuedAt,
|
||||
];
|
||||
if ($practitionerRef !== '') {
|
||||
$svcRes['requester'] = ['reference' => $practitionerRef];
|
||||
}
|
||||
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $svcUuid, 'resource' => $svcRes];
|
||||
}
|
||||
|
||||
// ── Appointments (Follow Up) ──────────────────────────────────────────
|
||||
foreach ($appointments as $index => $appointment) {
|
||||
$description = trim((string) ($appointment['description'] ?? ''));
|
||||
if ($description === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$apptUuid = $this->generateUuid();
|
||||
$appointmentRefs[] = ['reference' => 'urn:uuid:' . $apptUuid, 'display' => 'Appointment'];
|
||||
|
||||
$apptStartIso = $issuedAt;
|
||||
if (preg_match('/(\d{2}-\d{2}-\d{4})/', $description, $m) === 1) {
|
||||
$parsed = \DateTimeImmutable::createFromFormat('d-m-Y', $m[1]);
|
||||
if ($parsed !== false) {
|
||||
$apptStartIso = $parsed->setTime(10, 0, 0)->format(DATE_ATOM);
|
||||
}
|
||||
} elseif (preg_match('/(\d{4}-\d{2}-\d{2})/', $description, $m) === 1) {
|
||||
$parsed = \DateTimeImmutable::createFromFormat('Y-m-d', $m[1]);
|
||||
if ($parsed !== false) {
|
||||
$apptStartIso = $parsed->setTime(10, 0, 0)->format(DATE_ATOM);
|
||||
}
|
||||
}
|
||||
|
||||
$apptRes = [
|
||||
'resourceType' => 'Appointment',
|
||||
'id' => $apptUuid,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Appointment']],
|
||||
'status' => (string) ($appointment['status'] ?? 'proposed'),
|
||||
'description' => $description,
|
||||
'start' => (string) ($appointment['start'] ?? $apptStartIso),
|
||||
'end' => (string) ($appointment['end'] ?? $apptStartIso),
|
||||
'participant' => [[
|
||||
'actor' => ['reference' => $patientRef, 'display' => 'Patient'],
|
||||
'status' => 'accepted',
|
||||
]],
|
||||
];
|
||||
if ($practitionerRef !== '') {
|
||||
$apptRes['participant'][] = [
|
||||
'actor' => ['reference' => $practitionerRef, 'display' => $practRawName !== '' ? $practRawName : 'Practitioner'],
|
||||
'status' => 'accepted',
|
||||
];
|
||||
}
|
||||
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $apptUuid, 'resource' => $apptRes];
|
||||
}
|
||||
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $apptUuid, 'resource' => $apptRes];
|
||||
}
|
||||
|
||||
// ── OPD scan/upload attachments (DocumentReference + Binary) ─────────
|
||||
|
|
@ -607,51 +616,53 @@ class FhirR4Builder
|
|||
|
||||
// ── Composition sections (ABDM SNOMED section codes) ─────────────────
|
||||
$compositionSections = [];
|
||||
if (! empty($complaintRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Chief complaints',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '422843007',
|
||||
'display' => 'Chief complaint section',
|
||||
]]],
|
||||
'entry' => $complaintRefs,
|
||||
];
|
||||
}
|
||||
if (! empty($allergyRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Allergies',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '722446000',
|
||||
'display' => 'Allergy record',
|
||||
]]],
|
||||
'entry' => $allergyRefs,
|
||||
];
|
||||
}
|
||||
if (! empty($observationRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Physical Examination',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '425044008',
|
||||
'display' => 'Physical exam section',
|
||||
]]],
|
||||
'entry' => $observationRefs,
|
||||
];
|
||||
}
|
||||
if (! empty($encounterDiagnosisRefs)) {
|
||||
// Diagnoses section — conditions referenced from Encounter.diagnosis
|
||||
$diagSectionEntries = array_map(static fn ($d) => $d['condition'], $encounterDiagnosisRefs);
|
||||
$compositionSections[] = [
|
||||
'title' => 'Problems and Diagnoses',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '439401001',
|
||||
'display' => 'Diagnosis',
|
||||
]]],
|
||||
'entry' => $diagSectionEntries,
|
||||
];
|
||||
if ($isOpConsult) {
|
||||
if (! empty($complaintRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Chief complaints',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '422843007',
|
||||
'display' => 'Chief complaint section',
|
||||
]]],
|
||||
'entry' => $complaintRefs,
|
||||
];
|
||||
}
|
||||
if (! empty($allergyRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Allergies',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '722446000',
|
||||
'display' => 'Allergy record',
|
||||
]]],
|
||||
'entry' => $allergyRefs,
|
||||
];
|
||||
}
|
||||
if (! empty($observationRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Physical Examination',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '425044008',
|
||||
'display' => 'Physical exam section',
|
||||
]]],
|
||||
'entry' => $observationRefs,
|
||||
];
|
||||
}
|
||||
if (! empty($encounterDiagnosisRefs)) {
|
||||
// Diagnoses section — conditions referenced from Encounter.diagnosis
|
||||
$diagSectionEntries = array_map(static fn ($d) => $d['condition'], $encounterDiagnosisRefs);
|
||||
$compositionSections[] = [
|
||||
'title' => 'Problems and Diagnoses',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '439401001',
|
||||
'display' => 'Diagnosis',
|
||||
]]],
|
||||
'entry' => $diagSectionEntries,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (! empty($medicationRefs)) {
|
||||
$compositionSections[] = [
|
||||
|
|
@ -664,44 +675,43 @@ class FhirR4Builder
|
|||
'entry' => $medicationRefs,
|
||||
];
|
||||
}
|
||||
if (! empty($serviceRequestRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Investigation Advice',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '721963009',
|
||||
'display' => 'Order document',
|
||||
]]],
|
||||
'entry' => $serviceRequestRefs,
|
||||
];
|
||||
}
|
||||
if (! empty($appointmentRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Follow Up',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '736271009',
|
||||
'display' => 'Outpatient care plan',
|
||||
]]],
|
||||
'entry' => $appointmentRefs,
|
||||
];
|
||||
if ($isOpConsult) {
|
||||
if (! empty($serviceRequestRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Investigation Advice',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '721963009',
|
||||
'display' => 'Order document',
|
||||
]]],
|
||||
'entry' => $serviceRequestRefs,
|
||||
];
|
||||
}
|
||||
if (! empty($appointmentRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Follow Up',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '736271009',
|
||||
'display' => 'Outpatient care plan',
|
||||
]]],
|
||||
'entry' => $appointmentRefs,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (! empty($documentRefs)) {
|
||||
$compositionSections[] = [
|
||||
'title' => 'Document Reference',
|
||||
'code' => ['coding' => [[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '371530004',
|
||||
'display' => 'Clinical consultation report',
|
||||
'code' => $isOpConsult ? '371530004' : '440545006',
|
||||
'display' => $isOpConsult ? 'Clinical consultation report' : 'Prescription record',
|
||||
]]],
|
||||
'entry' => $documentRefs,
|
||||
];
|
||||
}
|
||||
|
||||
// ── Composition (first entry per ABDM spec) ───────────────────────────
|
||||
$requestedBundleType = trim((string) ($context['bundle_type'] ?? $context['record_type'] ?? 'PrescriptionRecord'));
|
||||
$isOpConsult = in_array($requestedBundleType, ['OPConsultRecord', 'OPConsultation'], true);
|
||||
|
||||
$compositionProfile = $isOpConsult
|
||||
? 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/OPConsultRecord'
|
||||
: 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/PrescriptionRecord';
|
||||
|
|
@ -1043,7 +1053,7 @@ class FhirR4Builder
|
|||
]],
|
||||
]];
|
||||
|
||||
if ($isImaging) {
|
||||
if ($isImaging && str_starts_with(strtolower($pdfContentType), 'image/')) {
|
||||
$mediaUuid = $this->generateUuid();
|
||||
$mediaRef = 'urn:uuid:' . $mediaUuid;
|
||||
$resourceEntries[] = ['fullUrl' => $mediaRef, 'resource' => [
|
||||
|
|
@ -1114,12 +1124,13 @@ class FhirR4Builder
|
|||
]];
|
||||
}
|
||||
if ($attachment !== null && ! empty($attachment['data_base64'])) {
|
||||
$pdfPresentedFormUrl = $pdfDocRefRef !== null ? $pdfDocRefRef : null;
|
||||
$reportRes['presentedForm'] = [[
|
||||
$pdfPresentedFormUrl = $pdfBinaryRef ?? null;
|
||||
$reportRes['presentedForm'] = [array_filter([
|
||||
'contentType' => trim((string) ($attachment['content_type'] ?? 'application/pdf')) ?: 'application/pdf',
|
||||
'title' => trim((string) ($attachment['title'] ?? 'Lab Report PDF')) ?: 'Lab Report PDF',
|
||||
'url' => $pdfPresentedFormUrl,
|
||||
]];
|
||||
'title' => trim((string) ($attachment['title'] ?? 'Lab Report PDF')) ?: 'Lab Report PDF',
|
||||
'url' => $pdfPresentedFormUrl,
|
||||
'data' => $pdfData !== '' ? $pdfData : null,
|
||||
], static fn ($v): bool => $v !== null)];
|
||||
}
|
||||
$resourceEntries[] = ['fullUrl' => $reportRef, 'resource' => $reportRes];
|
||||
|
||||
|
|
|
|||
|
|
@ -682,7 +682,17 @@
|
|||
}).catch(function() {});
|
||||
} else if (ctxTaskType === 'opd_prescription_publish') {
|
||||
var pushData = new URLSearchParams();
|
||||
pushData.append('opd_id', ctxEntityId);
|
||||
var targetOpdId = ctxEntityId;
|
||||
if (!targetOpdId || targetOpdId === '0') {
|
||||
var refToParse = firstRef || (window.currentHipTaskContext ? window.currentHipTaskContext.careContext : '');
|
||||
if (refToParse) {
|
||||
var mOpd = refToParse.match(/OPD-(?:\d+)-S(\d+)/i) || refToParse.match(/OPD-(\d+)/i);
|
||||
if (mOpd && mOpd[1]) {
|
||||
targetOpdId = mOpd[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
pushData.append('opd_id', targetOpdId || '0');
|
||||
pushData.append('patient_id', ctxPatientId);
|
||||
pushData.append('abha_id', abhaAddress);
|
||||
pushData.append('push_to_gateway', '1');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user