From 0ebf8d2a8a6311bb227cb9ca36ca406431e693aa Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Sun, 31 May 2026 04:17:39 +0530 Subject: [PATCH] ABDM: enforce PDF-backed report submit and improve gateway tracking --- app/Config/Routes.php | 3 + app/Controllers/AbdmGateway.php | 445 +++++++++++++++++++++++++++++++- app/Controllers/Diagnosis.php | 136 +++++++++- app/Libraries/FhirR4Builder.php | 130 +++++++++- app/Views/abdm/bridge_log.php | 26 ++ app/Views/abdm/task_board.php | 301 +++++++++++++++++++-- 6 files changed, 999 insertions(+), 42 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d217d6d..aad3f4e 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -589,6 +589,9 @@ $routes->post('AbdmCodingPanel/mark_fhir_ready', 'AbdmCodingPanel::markFhirReady $routes->get('AbdmCodingPanel/tip_check', 'AbdmCodingPanel::tipCheck'); $routes->post('AbdmGateway/share_ipd_discharge_bundle', 'AbdmGateway::shareIpdDischargeBundle', ['filter' => $abdmPermFilter]); $routes->post('AbdmGateway/share_diagnosis_report_bundle', 'AbdmGateway::shareDiagnosisReportBundle', ['filter' => $abdmPermFilter]); +$routes->post('AbdmGateway/shareDiagnosisReportBundle', 'AbdmGateway::shareDiagnosisReportBundle', ['filter' => $abdmPermFilter]); +$routes->get('AbdmGateway/diagnosis_report_fhir_preview', 'AbdmGateway::diagnosisReportFhirPreview', ['filter' => $abdmPermFilter]); +$routes->get('AbdmGateway/ipd_discharge_fhir_preview', 'AbdmGateway::ipdDischargeFhirPreview', ['filter' => $abdmPermFilter]); $routes->post('AbdmGateway/push_health_record', 'AbdmGateway::pushHealthRecord', ['filter' => $abdmPermFilter]); $routes->get('AbdmGateway/health_records_list', 'AbdmGateway::healthRecordsList', ['filter' => $abdmPermFilter]); $routes->get('AbdmGateway/bridge_record_status/(:num)', 'AbdmGateway::bridgeRecordStatus/$1', ['filter' => $abdmPermFilter]); diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index 533a962..41b1ea2 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -8,6 +8,8 @@ use App\Libraries\FhirR4Builder; use App\Libraries\FhirEncryptionService; use App\Libraries\AbdmAuditService; use CodeIgniter\I18n\Time; +use Mpdf\HTMLParserMode; +use Mpdf\Mpdf; class AbdmGateway extends BaseController { @@ -936,6 +938,14 @@ class AbdmGateway extends BaseController 'reported_at' => $reportedAt, 'report_html' => trim((string) ($labReq->Report_Data ?? '')), ]; + $isImaging = (int) ($labReq->lab_type ?? 0) === 6; + if ($isImaging) { + $diagnosticReport['is_imaging'] = true; + $diagnosticReport['report_domain'] = 'imaging'; + $diagnosticReport['section_title'] = 'Computed tomography imaging report'; + $diagnosticReport['section_snomed_code'] = '371531008'; + $diagnosticReport['section_snomed_display'] = 'Computed tomography imaging report'; + } // ── Load LOINC code for the panel from lab_repo ─────────────────────── $labRepoRow = $this->db->table('lab_request lr') @@ -964,7 +974,21 @@ class AbdmGateway extends BaseController 'period_start' => $reportedAt, ]; - $bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, null, $organization, $encounter); + $pdfAttachment = $this->loadLatestLabPdfAttachment( + (int) ($labReq->charge_id ?? 0), + (int) ($labReq->lab_type ?? 0), + $labReqId, + $testTitle !== '' ? ($testTitle . ' PDF Report') : 'Lab Report PDF' + ); + + if ($pdfAttachment === null) { + return $this->response->setStatusCode(422)->setJSON([ + 'ok' => 0, + 'error' => 'PDF attachment missing for this lab request. Compile and store the report first, then retry ABDM submit.', + ]); + } + + $bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, null, $organization, $encounter, $pdfAttachment); $bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); // ── Store health_record ─────────────────────────────────────────────── @@ -1026,6 +1050,255 @@ class AbdmGateway extends BaseController ]); } + public function diagnosisReportFhirPreview() + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(403)->setJSON(['status' => 'error', 'message' => 'AJAX only']); + } + + $labReqId = (int) ($this->request->getGet('lab_req_id') ?? $this->request->getPost('lab_req_id') ?? 0); + $patientId = (int) ($this->request->getGet('patient_id') ?? $this->request->getPost('patient_id') ?? 0); + $abhaId = trim((string) ($this->request->getGet('abha_id') ?? $this->request->getPost('abha_id') ?? '')); + + if ($labReqId <= 0 || $patientId <= 0 || $abhaId === '') { + return $this->response->setStatusCode(400)->setJSON([ + 'status' => 'error', + 'message' => 'lab_req_id, patient_id and abha_id are required.', + ]); + } + + $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) + ->get(1) + ->getRow(); + + if (! $labReq) { + return $this->response->setStatusCode(404)->setJSON([ + 'status' => 'error', + 'message' => 'Lab request not found.', + ]); + } + + $patientRow = []; + if ($this->db->tableExists('patient_master')) { + $patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? []; + } + $patName = trim( + trim((string) ($patientRow['p_fname'] ?? '')) . ' ' . + trim((string) ($patientRow['p_lname'] ?? '')) + ) ?: trim((string) ($labReq->patient_name ?? '')); + + $testTitle = ''; + $chargeId = (int) ($labReq->charge_id ?? 0); + if ($chargeId > 0 && $this->db->tableExists('charge_master')) { + $chargeRow = $this->db->table('charge_master')->select('charge_name')->where('id', $chargeId)->get(1)->getRowArray() ?? []; + $testTitle = trim((string) ($chargeRow['charge_name'] ?? '')); + } + if ($testTitle === '') { + $testTitle = $this->mapLabTypeToTitle((int) ($labReq->lab_type ?? 0)); + } + + $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') : ''; + + $diagnosticReport = [ + 'id' => (string) $labReqId, + 'title' => $testTitle ?: 'Laboratory Report', + 'status' => ((string) ($labReq->status ?? '0')) === '1' ? 'final' : 'preliminary', + 'conclusion' => trim((string) ($labReq->report_data_Impression ?? '')), + 'reported_at' => $reportedAt, + 'report_html' => trim((string) ($labReq->Report_Data ?? '')), + ]; + $isImaging = (int) ($labReq->lab_type ?? 0) === 6; + if ($isImaging) { + $diagnosticReport['is_imaging'] = true; + $diagnosticReport['report_domain'] = 'imaging'; + $diagnosticReport['section_title'] = 'Computed tomography imaging report'; + $diagnosticReport['section_snomed_code'] = '371531008'; + $diagnosticReport['section_snomed_display'] = 'Computed tomography imaging report'; + } + + $labRepoRow = $this->db->table('lab_request lr') + ->select('lr.lab_repo_id, repo.loinc_code AS repo_loinc_code') + ->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 = $hospitalProfile['name'] !== '' + ? ['name' => $hospitalProfile['name'], 'hfr_id' => $hospitalProfile['hfr_id']] + : null; + + $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, + ]; + + $fhir = new FhirR4Builder(); + $pdfAttachment = $this->loadLatestLabPdfAttachment( + (int) ($labReq->charge_id ?? 0), + (int) ($labReq->lab_type ?? 0), + $labReqId, + $testTitle !== '' ? ($testTitle . ' PDF Report') : 'Lab Report PDF' + ); + + if ($pdfAttachment === null) { + return $this->response->setStatusCode(422)->setJSON([ + 'status' => 'error', + 'message' => 'PDF attachment missing for this lab request. Compile and store the report first, then retry preview.', + ]); + } + + $bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, null, $organization, $encounter, $pdfAttachment); + + return $this->response->setJSON([ + 'status' => 'ok', + 'lab_req_id' => $labReqId, + 'patient_id' => $patientId, + 'abha_id' => $abhaId, + 'bundle' => $bundle, + ]); + } + + public function ipdDischargeFhirPreview() + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(403)->setJSON(['status' => 'error', 'message' => 'AJAX only']); + } + + $ipdId = (int) ($this->request->getGet('ipd_id') ?? $this->request->getPost('ipd_id') ?? 0); + $patientId = (int) ($this->request->getGet('patient_id') ?? $this->request->getPost('patient_id') ?? 0); + $abhaId = trim((string) ($this->request->getGet('abha_id') ?? $this->request->getPost('abha_id') ?? '')); + + if ($ipdId <= 0 || $patientId <= 0 || $abhaId === '') { + return $this->response->setStatusCode(400)->setJSON([ + 'status' => 'error', + 'message' => 'ipd_id, patient_id and abha_id are required.', + ]); + } + + $ipdRow = $this->db->tableExists('ipd_master') + ? ($this->db->table('ipd_master')->where('id', $ipdId)->get(1)->getRowArray() ?? []) + : []; + + if (empty($ipdRow)) { + return $this->response->setStatusCode(404)->setJSON([ + 'status' => 'error', + 'message' => 'IPD record not found.', + ]); + } + + $summaryHtml = ''; + if ($this->db->tableExists('ipd_discharge') && $this->db->fieldExists('content', 'ipd_discharge')) { + $summaryRow = $this->db->table('ipd_discharge') + ->select('content') + ->where('ipd_id', $ipdId) + ->orderBy('id', 'DESC') + ->get(1) + ->getRowArray(); + $summaryHtml = trim((string) ($summaryRow['content'] ?? '')); + } + + $patientRow = []; + if ($this->db->tableExists('patient_master')) { + $patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? []; + } + $patName = trim( + trim((string) ($patientRow['p_fname'] ?? '')) . ' ' . + trim((string) ($patientRow['p_lname'] ?? '')) + ) ?: trim((string) ($ipdRow['P_name'] ?? '')); + + $hospitalProfile = $this->getHospitalProfileForFhir(); + + $doctorId = (int) ($ipdRow['r_doc_id'] ?? 0); + $doctorName = trim((string) ($ipdRow['r_doc_name'] ?? '')); + $doctorRegNo = ''; + if ($doctorId > 0 && $this->db->tableExists('doctor_master')) { + $dFields = $this->db->getFieldNames('doctor_master') ?? []; + $dSelect = ['id']; + foreach (['p_fname', 'p_lname', 'doctor_reg_no', 'registration_no', 'reg_no'] as $f) { + if (in_array($f, $dFields, true)) { + $dSelect[] = $f; + } + } + $dRow = $this->db->table('doctor_master')->select(implode(',', $dSelect))->where('id', $doctorId)->get(1)->getRowArray() ?? []; + if ($dRow !== [] && $doctorName === '') { + $doctorName = trim(trim((string) ($dRow['p_fname'] ?? '')) . ' ' . trim((string) ($dRow['p_lname'] ?? ''))); + } + foreach (['doctor_reg_no', 'registration_no', 'reg_no'] as $f) { + if (! empty($dRow[$f])) { + $doctorRegNo = trim((string) $dRow[$f]); + break; + } + } + } + + $admissionRaw = trim((string) ($ipdRow['register_date'] ?? '')); + $dischargeRaw = trim((string) ($ipdRow['discharge_date'] ?? '')); + $admissionDate = $admissionRaw !== '' ? (new \DateTime($admissionRaw, new \DateTimeZone('Asia/Kolkata')))->format('Y-m-d\TH:i:sP') : ''; + $dischargeDate = $dischargeRaw !== '' ? (new \DateTime($dischargeRaw, new \DateTimeZone('Asia/Kolkata')))->format('Y-m-d\TH:i:sP') : ''; + + $fhir = new FhirR4Builder(); + + $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, + ]; + + $encounter = [ + 'id' => (string) $ipdId, + 'admission_date' => $admissionDate, + 'discharge_date' => $dischargeDate, + 'ipd_code' => trim((string) ($ipdRow['ipd_code'] ?? '')), + ]; + + $summary = [ + 'title' => 'Discharge Summary', + 'chief_complaints' => trim((string) ($ipdRow['problem'] ?? '')), + 'clinical_summary_html' => $summaryHtml, + ]; + + $practitioner = $doctorName !== '' + ? ['name' => $doctorName, 'registration_number' => $doctorRegNo] + : null; + $organization = $hospitalProfile['name'] !== '' + ? ['name' => $hospitalProfile['name'], 'hfr_id' => $hospitalProfile['hfr_id']] + : null; + + $bundle = $fhir->buildDischargeSummaryBundle($patient, $encounter, $summary, $practitioner, $organization); + + return $this->response->setJSON([ + 'status' => 'ok', + 'ipd_id' => $ipdId, + 'patient_id' => $patientId, + 'abha_id' => $abhaId, + 'bundle' => $bundle, + ]); + } + public function nhcxClaimCreate() { if (! $this->request->isAJAX()) { @@ -1610,6 +1883,176 @@ class AbdmGateway extends BaseController return $this->response->setJSON($result); } + /** + * Resolve latest PDF for a lab/radiology context and return base64 attachment payload. + * + * @return array{content_type: string, data_base64: string, title: string}|null + */ + private function loadLatestLabPdfAttachment(int $invoiceId, int $labType, int $labReqId = 0, string $defaultTitle = 'Lab Report PDF'): ?array + { + if ($invoiceId <= 0 || $labType <= 0 || ! $this->db->tableExists('file_upload_data')) { + return null; + } + + $fields = $this->db->getFieldNames('file_upload_data') ?? []; + if (empty($fields)) { + return null; + } + + $selectParts = []; + foreach (['id', 'file_name', 'orig_name', 'full_path', 'file_type', 'repo_id'] as $col) { + if (in_array($col, $fields, true)) { + $selectParts[] = $col; + } + } + if (empty($selectParts)) { + return null; + } + + $builder = $this->db->table('file_upload_data') + ->select(implode(',', $selectParts)) + ->where('charge_id', $invoiceId) + ->where('charge_type', $labType); + + if (in_array('isdelete', $fields, true)) { + $builder->where('isdelete', 0); + } + + // Prefer exact report-level file; fall back to invoice-level compiled (repo_id=0). + if (in_array('repo_id', $fields, true)) { + $builder->whereIn('repo_id', [$labReqId, 0]); + $builder->orderBy('CASE WHEN repo_id = ' . (int) $labReqId . ' THEN 0 WHEN repo_id = 0 THEN 1 ELSE 2 END', '', false); + } + + $rows = $builder->orderBy('id', 'DESC')->limit(20)->get()->getResultArray(); + if (empty($rows)) { + return null; + } + + foreach ($rows as $row) { + $fullPath = trim((string) ($row['full_path'] ?? '')); + $fileName = trim((string) ($row['file_name'] ?? '')); + $mimeType = trim((string) ($row['file_type'] ?? '')); + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $isPdf = $ext === 'pdf' || $mimeType === 'application/pdf'; + if (! $isPdf) { + continue; + } + + $absolutePath = $this->resolveStoredFileAbsolutePath($fullPath); + if ($absolutePath === '' || ! is_file($absolutePath)) { + continue; + } + + $bytes = @file_get_contents($absolutePath); + if ($bytes === false || $bytes === '') { + continue; + } + + $title = trim((string) ($row['orig_name'] ?? '')); + if ($title === '') { + $title = $fileName !== '' ? $fileName : $defaultTitle; + } + + return [ + 'content_type' => 'application/pdf', + 'data_base64' => base64_encode($bytes), + 'title' => $title, + ]; + } + + if ($labReqId > 0) { + $fallbackRow = $this->db->table('lab_request') + ->select('Report_Data, patient_name') + ->where('id', $labReqId) + ->get(1) + ->getRowArray() ?? []; + + $reportHtml = trim((string) ($fallbackRow['Report_Data'] ?? '')); + if ($reportHtml !== '') { + $fallbackTitle = trim((string) ($defaultTitle !== '' ? $defaultTitle : 'Lab Report PDF')); + $generatedPdf = $this->renderReportHtmlToPdfAttachment($reportHtml, $fallbackTitle); + if ($generatedPdf !== null) { + return $generatedPdf; + } + } + } + + return null; + } + + /** + * Render report HTML to a PDF attachment payload without persisting it. + * + * @return array{content_type: string, data_base64: string, title: string}|null + */ + private function renderReportHtmlToPdfAttachment(string $reportHtml, string $title = 'Lab Report PDF'): ?array + { + $reportHtml = trim($reportHtml); + if ($reportHtml === '') { + return null; + } + + try { + $wrappedHtml = '' + . '' + . '' . $reportHtml . ''; + + $mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf'; + if (! is_dir($mpdfTempDir)) { + @mkdir($mpdfTempDir, 0755, true); + } + + $mpdf = new Mpdf([ + 'format' => 'A4', + 'margin_top' => 10, + 'margin_bottom' => 10, + 'margin_left' => 10, + 'margin_right' => 10, + 'tempDir' => $mpdfTempDir, + ]); + $mpdf->WriteHTML($wrappedHtml, HTMLParserMode::HTML_BODY); + + return [ + 'content_type' => 'application/pdf', + 'data_base64' => base64_encode($mpdf->Output($title . '.pdf', 'S')), + 'title' => $title, + ]; + } catch (\Throwable $e) { + return null; + } + } + + private function resolveStoredFileAbsolutePath(string $fullPath): string + { + $normalized = str_replace('\\', '/', trim($fullPath)); + if ($normalized === '') { + return ''; + } + + if (is_file($normalized)) { + return $normalized; + } + + if (preg_match('/^[A-Za-z]:\//', $normalized) === 1 || str_starts_with($normalized, '/')) { + return is_file($normalized) ? $normalized : ''; + } + + $candidates = [ + ROOTPATH . ltrim(str_replace('/', DIRECTORY_SEPARATOR, $normalized), DIRECTORY_SEPARATOR), + ROOTPATH . 'public' . DIRECTORY_SEPARATOR . ltrim(str_replace('/', DIRECTORY_SEPARATOR, $normalized), DIRECTORY_SEPARATOR), + WRITEPATH . ltrim(str_replace('/', DIRECTORY_SEPARATOR, $normalized), DIRECTORY_SEPARATOR), + ]; + + foreach ($candidates as $path) { + if (is_file($path)) { + return $path; + } + } + + return ''; + } + private function getScanShareResultPayload(int $queueId): ?array { if (! $this->db->tableExists('abdm_api_logs')) { diff --git a/app/Controllers/Diagnosis.php b/app/Controllers/Diagnosis.php index 44c8e4f..0feca4d 100644 --- a/app/Controllers/Diagnosis.php +++ b/app/Controllers/Diagnosis.php @@ -2145,9 +2145,19 @@ class Diagnosis extends BaseController 'report_compile' => $compiledBy, ]); + $pdfStore = $this->autoStoreCompiledPdfAttachment($invoiceId, $labType, $rawData, $header); + + $message = 'Data Compile'; + if (($pdfStore['ok'] ?? false) && ! empty($pdfStore['file_id'])) { + $message .= ' + PDF stored'; + } elseif (! empty($pdfStore['error'])) { + $message .= ' (PDF store skipped: ' . (string) $pdfStore['error'] . ')'; + } + return $this->response->setJSON([ 'status' => 'success', - 'message' => 'Data Compile', + 'message' => $message, + 'pdf_file_id' => (int) ($pdfStore['file_id'] ?? 0), ]); } catch (\Throwable $e) { return $this->response->setJSON([ @@ -2157,6 +2167,130 @@ class Diagnosis extends BaseController } } + /** + * Generate compiled report PDF and persist metadata in file_upload_data. + * Fail-open: returns error details without throwing so compile flow stays successful. + * + * @return array{ok: bool, file_id?: int, error?: string} + */ + private function autoStoreCompiledPdfAttachment(int $invoiceId, int $labType, string $reportHtml, string $reportHeader = ''): array + { + if ($invoiceId <= 0 || $labType <= 0) { + return ['ok' => false, 'error' => 'invalid_context']; + } + + if (! $this->db->tableExists('file_upload_data')) { + return ['ok' => false, 'error' => 'file_upload_data table not found']; + } + + try { + $compiledHtml = trim($reportHeader) . "\n" . trim($reportHtml); + if ($compiledHtml === '') { + return ['ok' => false, 'error' => 'empty_compiled_report']; + } + + $wrappedHtml = '' + . '' + . '' . $compiledHtml . ''; + + $mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf'; + if (! is_dir($mpdfTempDir)) { + @mkdir($mpdfTempDir, 0755, true); + } + + $mpdf = new Mpdf([ + 'format' => 'A4', + 'margin_top' => 10, + 'margin_bottom' => 10, + 'margin_left' => 10, + 'margin_right' => 10, + 'tempDir' => $mpdfTempDir, + ]); + $mpdf->WriteHTML($wrappedHtml); + + $invoiceCode = ''; + if ($this->db->tableExists('invoice_master')) { + $inv = $this->db->table('invoice_master')->select('invoice_code')->where('id', $invoiceId)->get(1)->getRowArray(); + $invoiceCode = trim((string) ($inv['invoice_code'] ?? '')); + } + $invoiceCodeSafe = preg_replace('/[^A-Za-z0-9\-_]/', '_', $invoiceCode !== '' ? $invoiceCode : ('INV_' . $invoiceId)); + $storedName = 'compiled_' . $invoiceCodeSafe . '_L' . $labType . '_' . date('Ymd_His') . '.pdf'; + + $targetDir = ROOTPATH . 'public' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'diagnosis' . DIRECTORY_SEPARATOR . date('Y') . DIRECTORY_SEPARATOR . date('m'); + if (! is_dir($targetDir) && ! @mkdir($targetDir, 0775, true) && ! is_dir($targetDir)) { + return ['ok' => false, 'error' => 'Unable to create upload directory']; + } + + $absolutePath = $targetDir . DIRECTORY_SEPARATOR . $storedName; + file_put_contents($absolutePath, $mpdf->Output($storedName, 'S')); + + if (! is_file($absolutePath)) { + return ['ok' => false, 'error' => 'compiled_pdf_write_failed']; + } + + $fields = $this->db->getFieldNames('file_upload_data'); + + // Keep only one latest auto-compiled PDF per invoice/lab context. + if (in_array('scan_type', $fields, true) && in_array('isdelete', $fields, true)) { + $this->db->table('file_upload_data') + ->where('charge_id', $invoiceId) + ->where('charge_type', $labType) + ->where('scan_type', 'compiled_pdf_auto') + ->where('isdelete', 0) + ->update(['isdelete' => 1]); + } + + $user = function_exists('auth') ? auth()->user() : null; + $uploadBy = trim((string) ($user->username ?? $user->email ?? 'system')); + $uploadById = (int) ($user->id ?? 0); + + $insert = []; + $fieldMap = [ + 'name' => $storedName, + 'file_name' => $storedName, + 'orig_name' => 'Compiled_Report_' . ($invoiceCode !== '' ? $invoiceCode : $invoiceId) . '_' . $labType . '.pdf', + 'client_name' => 'Compiled_Report_' . ($invoiceCode !== '' ? $invoiceCode : $invoiceId) . '_' . $labType . '.pdf', + 'file_ext' => '.pdf', + 'file_type' => 'application/pdf', + 'full_path' => str_replace('\\', '/', $absolutePath), + 'file_path' => str_replace('\\', '/', $targetDir) . '/', + 'raw_name' => pathinfo($storedName, PATHINFO_FILENAME), + 'store_file_name' => $storedName, + 'file_size' => round(((float) filesize($absolutePath)) / 1024, 2), + 'is_image' => 0, + 'image_type' => 'pdf', + 'insert_date' => Time::now('Asia/Kolkata')->toDateTimeString(), + 'insert_time' => Time::now('Asia/Kolkata')->toDateTimeString(), + 'file_desc' => 'Auto compiled report PDF', + 'repo_id' => 0, + 'charge_id' => $invoiceId, + 'charge_type' => $labType, + 'upload_by' => $uploadBy, + 'upload_by_id' => $uploadById, + 'scan_type' => 'compiled_pdf_auto', + 'show_type' => 0, + 'isdelete' => 0, + 'document_type' => 'diagnosis-compiled-report', + 'content_description' => 'Auto-generated from diagnosis/report-compile', + ]; + + foreach ($fieldMap as $key => $value) { + if (in_array($key, $fields, true)) { + $insert[$key] = $value; + } + } + + $ok = $this->db->table('file_upload_data')->insert($insert); + if (! $ok) { + return ['ok' => false, 'error' => 'Unable to save compiled PDF metadata']; + } + + return ['ok' => true, 'file_id' => (int) $this->db->insertID()]; + } catch (\Throwable $e) { + return ['ok' => false, 'error' => $e->getMessage()]; + } + } + public function printPdfCreate($invoiceId, $labType, $print = 0, $printOnType = 0) { // CI4 equivalent of old Lab_Admin/print_pdf_create/{inv}/{lab}/{print}/{print_on_type} diff --git a/app/Libraries/FhirR4Builder.php b/app/Libraries/FhirR4Builder.php index 7768ffd..8b0f976 100644 --- a/app/Libraries/FhirR4Builder.php +++ b/app/Libraries/FhirR4Builder.php @@ -636,6 +636,7 @@ class FhirR4Builder * @param array|null $practitioner {name, registration_number} * @param array|null $organization {name, hfr_id} * @param array|null $encounter {id, status, class_code (AMB|IMP|EMER), period_start, period_end} + * @param array|null $attachment Optional PDF attachment {content_type, data_base64, title} * * @return array */ @@ -645,7 +646,8 @@ class FhirR4Builder array $observations = [], ?array $practitioner = null, ?array $organization = null, - ?array $encounter = null + ?array $encounter = null, + ?array $attachment = null ): array { $issuedAt = $this->isoTimestamp(); $bundleUuid = $this->generateUuid(); @@ -658,6 +660,8 @@ class FhirR4Builder $reportedAt = trim((string) ($diagnosticReport['reported_at'] ?? '')) ?: $issuedAt; $reportTitle = trim((string) ($diagnosticReport['title'] ?? 'Diagnostic Report')); $reportStatus = trim((string) ($diagnosticReport['status'] ?? 'final')); + $isImaging = (bool) ($diagnosticReport['is_imaging'] ?? false) + || strtolower(trim((string) ($diagnosticReport['report_domain'] ?? ''))) === 'imaging'; $hasPractitioner = $practitioner !== null && trim((string) ($practitioner['name'] ?? '')) !== ''; $hasOrganization = $organization !== null && trim((string) ($organization['name'] ?? '')) !== ''; @@ -827,6 +831,8 @@ class FhirR4Builder // ── Binary + DocumentReference for HTML report ───────────────────────── $docRefEntry = null; $docRefRef = null; + $pdfDocRefRef = null; + $mediaRef = null; $reportHtml = trim((string) ($diagnosticReport['report_html'] ?? '')); if ($reportHtml !== '') { $binaryUuid = $this->generateUuid(); @@ -837,6 +843,7 @@ class FhirR4Builder $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $binaryUuid, 'resource' => [ 'resourceType' => 'Binary', 'id' => $binaryUuid, + 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Binary']], 'contentType' => $contentType, 'data' => base64_encode($reportHtml), ]]; @@ -848,16 +855,79 @@ class FhirR4Builder 'status' => 'current', 'type' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]], 'subject' => ['reference' => $patientRef], - 'content' => [['attachment' => ['contentType' => $contentType, 'url' => 'urn:uuid:' . $binaryUuid, 'title' => $reportTitle]]], + 'content' => [['attachment' => ['contentType' => $contentType, 'url' => 'urn:uuid:' . $binaryUuid, 'data' => base64_encode($reportHtml), 'title' => $reportTitle]]], ]; $resourceEntries[] = ['fullUrl' => $docRefRef, 'resource' => $docRefEntry]; } + // ── Optional PDF attachment as Binary + DocumentReference ─────────── + if ($attachment !== null && ! empty($attachment['data_base64'])) { + $pdfData = trim((string) ($attachment['data_base64'] ?? '')); + if ($pdfData !== '') { + $pdfBinaryUuid = $this->generateUuid(); + $pdfDocRefUuid = $this->generateUuid(); + $pdfDocRefRef = 'urn:uuid:' . $pdfDocRefUuid; + $pdfContentType = trim((string) ($attachment['content_type'] ?? 'application/pdf')) ?: 'application/pdf'; + $pdfTitle = trim((string) ($attachment['title'] ?? 'Lab Report PDF')) ?: 'Lab Report PDF'; + + $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $pdfBinaryUuid, 'resource' => [ + 'resourceType' => 'Binary', + 'id' => $pdfBinaryUuid, + 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Binary']], + 'contentType' => $pdfContentType, + 'data' => $pdfData, + ]]; + + $resourceEntries[] = ['fullUrl' => $pdfDocRefRef, 'resource' => [ + 'resourceType' => 'DocumentReference', + 'id' => $pdfDocRefUuid, + 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentReference']], + 'status' => 'current', + 'type' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]], + 'subject' => ['reference' => $patientRef], + 'content' => [[ + 'attachment' => [ + 'contentType' => $pdfContentType, + 'url' => 'urn:uuid:' . $pdfBinaryUuid, + 'data' => $pdfData, + 'title' => $pdfTitle, + ], + ]], + ]]; + + if ($isImaging) { + $mediaUuid = $this->generateUuid(); + $mediaRef = 'urn:uuid:' . $mediaUuid; + $resourceEntries[] = ['fullUrl' => $mediaRef, 'resource' => [ + 'resourceType' => 'Media', + 'id' => $mediaUuid, + 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Media']], + 'status' => 'completed', + 'type' => 'photo', + 'subject' => ['reference' => $patientRef], + 'createdDateTime' => $reportedAt, + 'content' => [ + 'contentType' => $pdfContentType, + 'url' => 'urn:uuid:' . $pdfBinaryUuid, + 'data' => $pdfData, + 'title' => $pdfTitle, + ], + ]]; + } + } + } + + // Pick a single DocumentReference for Composition.section slicing (0..1). + $effectiveDocRefRef = $pdfDocRefRef !== null ? $pdfDocRefRef : $docRefRef; + // ── DiagnosticReport ────────────────────────────────────────────────── $reportRes = [ 'resourceType' => 'DiagnosticReport', 'id' => $reportUuid, - 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DiagnosticReportLab']], + 'meta' => ['profile' => [$isImaging + ? 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DiagnosticReportImaging' + : 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DiagnosticReportLab' + ]], 'status' => $reportStatus, 'code' => array_filter([ 'coding' => trim((string) ($diagnosticReport['loinc_code'] ?? '')) !== '' @@ -876,6 +946,11 @@ class FhirR4Builder } elseif ($practitionerRef !== '') { $reportRes['performer'] = [['reference' => $practitionerRef]]; } + if ($practitionerRef !== '') { + $reportRes['resultsInterpreter'] = [['reference' => $practitionerRef]]; + } elseif ($organizationRef !== '') { + $reportRes['resultsInterpreter'] = [['reference' => $organizationRef]]; + } if ($encounterRef !== '') { $reportRes['encounter'] = ['reference' => $encounterRef]; } @@ -884,19 +959,42 @@ class FhirR4Builder $reportRes['category'] = [['coding' => [['system' => 'http://snomed.info/sct', 'code' => $catSnomedCode, 'display' => trim((string) ($diagnosticReport['category_snomed_display'] ?? $catSnomedCode))]]]]; } $conclusion = trim((string) ($diagnosticReport['conclusion'] ?? '')); - if ($conclusion !== '') { - $reportRes['conclusion'] = $conclusion; + $reportRes['conclusion'] = $conclusion !== '' ? $conclusion : 'Laboratory report generated'; + if ($isImaging && $mediaRef !== null) { + $reportRes['media'] = [[ + 'link' => ['reference' => $mediaRef], + ]]; + } + if ($attachment !== null && ! empty($attachment['data_base64'])) { + $pdfPresentedFormUrl = $pdfDocRefRef !== null ? $pdfDocRefRef : null; + $reportRes['presentedForm'] = [[ + 'contentType' => trim((string) ($attachment['content_type'] ?? 'application/pdf')) ?: 'application/pdf', + 'title' => trim((string) ($attachment['title'] ?? 'Lab Report PDF')) ?: 'Lab Report PDF', + 'url' => $pdfPresentedFormUrl, + ]]; } $resourceEntries[] = ['fullUrl' => $reportRef, 'resource' => $reportRes]; // ── Composition section entries ──────────────────────────────────────── - $sectionEntries = [['reference' => $reportRef]]; - if ($docRefRef !== null) { - $sectionEntries[] = ['reference' => $docRefRef]; + $sectionEntries = [[ + 'reference' => $reportRef, + 'type' => 'DiagnosticReport', + ]]; + if ($effectiveDocRefRef !== null) { + $sectionEntries[] = [ + 'reference' => $effectiveDocRefRef, + 'type' => 'DocumentReference', + ]; } + $sectionTitle = trim((string) ($diagnosticReport['section_title'] ?? ($isImaging ? 'Computed tomography imaging report' : 'Hematology report'))); + $sectionCode = trim((string) ($diagnosticReport['section_snomed_code'] ?? ($isImaging ? '371531008' : '4321000179101'))); + $sectionDisplay = trim((string) ($diagnosticReport['section_snomed_display'] ?? $sectionTitle)); + // ── Composition ─────────────────────────────────────────────────────── - $authorEntry = $practitionerRef !== '' ? ['reference' => $practitionerRef] : ['display' => 'Unknown']; + $authorEntry = $practitionerRef !== '' + ? ['reference' => $practitionerRef] + : ($organizationRef !== '' ? ['reference' => $organizationRef] : ['reference' => $patientRef]); $composition = [ 'resourceType' => 'Composition', 'id' => $compositionUuid, @@ -905,16 +1003,20 @@ class FhirR4Builder 'identifier' => ['system' => 'https://ndhm.in/phr', 'value' => $compositionUuid], 'status' => 'final', 'type' => [ - 'coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']], - 'text' => 'Diagnostic Report', + 'coding' => [[ + 'system' => 'http://snomed.info/sct', + 'code' => $isImaging ? '721979004' : '721981007', + 'display' => $isImaging ? 'Diagnostic Report- Imaging' : 'Diagnostic Report- Lab', + ]], + 'text' => $isImaging ? 'Diagnostic Report- Imaging' : 'Diagnostic Report- Lab', ], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'date' => $issuedAt, - 'author' => [[$authorEntry]], + 'author' => [$authorEntry], 'title' => $reportTitle, 'section' => [[ - 'title' => 'Diagnostic Report', - 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]], + 'title' => $sectionTitle, + 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => $sectionCode, 'display' => $sectionDisplay]]], 'entry' => $sectionEntries, ]], ]; diff --git a/app/Views/abdm/bridge_log.php b/app/Views/abdm/bridge_log.php index edfbcbe..f87c1c0 100644 --- a/app/Views/abdm/bridge_log.php +++ b/app/Views/abdm/bridge_log.php @@ -426,6 +426,32 @@ const today = new Date().toISOString().slice(0, 10); document.getElementById('filterDateFrom').value = today; document.getElementById('filterDateTo').value = today; +// Allow deep-link filters, e.g. AbdmBridgeLog?search=REC-xxxx&date_from=2026-05-31 +(function applyUrlFilters() { + const qs = new URLSearchParams(window.location.search || ''); + const map = [ + ['channel', 'filterChannel'], + ['status', 'filterStatus'], + ['search', 'filterSearch'], + ['date_from', 'filterDateFrom'], + ['date_to', 'filterDateTo'], + ]; + map.forEach(([key, id]) => { + const value = (qs.get(key) || '').trim(); + if (value === '') return; + const el = document.getElementById(id); + if (!el) return; + el.value = value; + }); + + // If a queue search is provided without date range, clear date defaults + // so older queue ids are still discoverable. + if ((qs.get('search') || '').trim() !== '' && (qs.get('date_from') || '').trim() === '' && (qs.get('date_to') || '').trim() === '') { + document.getElementById('filterDateFrom').value = ''; + document.getElementById('filterDateTo').value = ''; + } +})(); + loadList(1); diff --git a/app/Views/abdm/task_board.php b/app/Views/abdm/task_board.php index bba4b9d..5882814 100644 --- a/app/Views/abdm/task_board.php +++ b/app/Views/abdm/task_board.php @@ -81,7 +81,8 @@ $opdId = (int) ($meta['opd_id'] ?? 0); $opdSessionId = (int) ($meta['opd_session_id'] ?? 0); $showSandbox = ($type === 'opd_prescription_publish' && $opdId > 0); - $showPreview = ($type === 'opd_prescription_publish' && $opdId > 0); + $showPreview = ($type === 'opd_prescription_publish' && $opdId > 0) + || in_array($type, ['lab_report_publish', 'radiology_report_publish', 'ipd_discharge_publish'], true); ?> ', { @@ -765,10 +766,87 @@ badge.className = 'badge bg-info status-pill'; } setStatus('Action queued: ' + (res.event_type || '-')); - actionModal.hide(); + if (modalEl && modalEl.classList.contains('show')) { + actionModal.hide(); + } }); } + function openTaskFhirPreviewForSubmit(row) { + if (!row) { + return false; + } + + var taskType = (row.getAttribute('data-task-type') || '').toLowerCase(); + var abhaInput = row.querySelector('.abha-input'); + var abhaId = (abhaInput ? abhaInput.value : (row.getAttribute('data-abha-id') || '')).trim(); + + if (!/^\d{14}$/.test(abhaId)) { + setStatus('ABHA must be a 14-digit number before FHIR preview.', true); + return true; + } + + var previewUrl = ''; + var title = 'FHIR Preview'; + + if (taskType === 'lab_report_publish' || taskType === 'radiology_report_publish') { + var labReqId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0; + var patientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0; + + if (labReqId <= 0 || patientId <= 0) { + setStatus('Task data missing for FHIR preview.', true); + return true; + } + + previewUrl = '' + + '?lab_req_id=' + encodeURIComponent(labReqId) + + '&patient_id=' + encodeURIComponent(patientId) + + '&abha_id=' + encodeURIComponent(abhaId); + 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; + + if (opdId <= 0) { + setStatus('Task data missing for OPD FHIR preview.', true); + return true; + } + + previewUrl = opdSessionId > 0 + ? '/' + opdId + '/' + opdSessionId + : '/' + 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; + + if (ipdId <= 0 || ipdPatientId <= 0) { + setStatus('Task data missing for IPD discharge FHIR preview.', true); + return true; + } + + previewUrl = '' + + '?ipd_id=' + encodeURIComponent(ipdId) + + '&patient_id=' + encodeURIComponent(ipdPatientId) + + '&abha_id=' + encodeURIComponent(abhaId); + title = 'IPD Discharge FHIR Preview — IPD #' + ipdId; + } else { + return false; + } + + _fhirSubmitMode = 'task'; + _fhirSubmitTaskRow = row; + _fhirSubmitAbha = abhaId; + + openFhirModal(previewUrl, title, { + submitMode: 'task', + taskRow: row, + abhaId: abhaId + }); + + return true; + } + document.querySelectorAll('.action-btn').forEach(function (btn) { btn.addEventListener('click', function () { selectedRow = btn.closest('tr'); @@ -777,10 +855,17 @@ } selectedAction = btn.getAttribute('data-action') || 'submit'; + if (selectedAction === 'submit') { + var previewHandled = openTaskFhirPreviewForSubmit(selectedRow); + if (previewHandled) { + return; + } + } + var abhaInput = selectedRow.querySelector('.abha-input'); - var taskType = selectedRow.getAttribute('data-task-type') || ''; + var taskTypeLabel = selectedRow.getAttribute('data-task-type') || ''; modalAbhaInput.value = abhaInput ? abhaInput.value.trim() : ''; - modalTaskSummary.textContent = 'Task: ' + taskType + ' | Action: ' + selectedAction.toUpperCase(); + modalTaskSummary.textContent = 'Task: ' + taskTypeLabel + ' | Action: ' + selectedAction.toUpperCase(); actionModal.show(); }); }); @@ -804,6 +889,11 @@ return; } + var handled = openTaskFhirPreviewForSubmit(row); + if (handled) { + return; + } + var opdId = parseInt(row.getAttribute('data-opd-id') || '0', 10) || 0; var opdSessionId = parseInt(row.getAttribute('data-opd-session-id') || '0', 10) || 0; if (opdId <= 0) { @@ -1190,6 +1280,12 @@ var fhirTabJson = document.getElementById('fhirTabJson'); var fhirModalDataBadge = document.getElementById('fhirModalDataBadge'); var fhirModalHttpBadge = document.getElementById('fhirModalHttpBadge'); + var btnRegenerateFhirModal = document.getElementById('btnRegenerateFhirModal'); + 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 ────────────────────────────────────────────────── fhirTabForm.addEventListener('click', function () { @@ -1208,6 +1304,9 @@ // ── Track current bundle context for edits ────────────────────── var _fhirOpdId = 0, _fhirSessionId = 0; var _fhirLiveComplaints = [], _fhirLiveDiagnoses = []; + var _fhirSubmitMode = 'opd'; + var _fhirSubmitTaskRow = null; + var _fhirSubmitAbha = ''; // ── Inline complaint SNOMED edit (event delegation on fhirFormView) ─ var _editTimer = null; @@ -1656,24 +1755,66 @@ html += ''; } - // ── Vitals + // ── 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' }; - html += '
Vitals
'; - observations.forEach(function(o) { - var loinc = (((o.code || {}).coding) || [])[0] || {}; - var label = loincLabel[loinc.code] || (o.code || {}).text || loinc.display || ''; - var val = (o.valueQuantity || {}).value; - var unit = (o.valueQuantity || {}).unit || ''; - if (val === undefined || val === null) return; - html += '
'; - html += '
' + hesc(label) + '
'; - html += '
' + hesc(val) + '
'; - html += '
' + hesc(unit); - if (loinc.code) html += ' '+hesc(loinc.code)+''; - html += '
'; - }); - html += '
'; + 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 isVitalObservation(o) { + var categories = o.category || []; + for (var ci = 0; ci < categories.length; ci++) { + var cat = categories[ci] || {}; + var catText = (cat.text || '').toLowerCase(); + if (catText.indexOf('vital') !== -1) { + return true; + } + var codings = cat.coding || []; + for (var cj = 0; cj < codings.length; cj++) { + var c = codings[cj] || {}; + var system = (c.system || '').toLowerCase(); + var code = (c.code || '').toLowerCase(); + if ((system.indexOf('observation-category') !== -1 && code === 'vital-signs') || code === 'vital-signs') { + return true; + } + } + } + + var coding = (((o.code || {}).coding) || [])[0] || {}; + var code = coding.code || ''; + return !!vitalLoincCodes[code]; + } + + function renderObservationCard(sectionTitle, items) { + if (!items.length) { + return; + } + html += '
' + hesc(sectionTitle) + '
'; + items.forEach(function(o) { + var loinc = (((o.code || {}).coding) || [])[0] || {}; + var label = loincLabel[loinc.code] || (o.code || {}).text || loinc.display || ''; + var val = (o.valueQuantity || {}).value; + var unit = (o.valueQuantity || {}).unit || ''; + if (val === undefined || val === null) { + val = o.valueString || o.valueCodeableConcept && o.valueCodeableConcept.text || ''; + } + if (val === undefined || val === null || val === '') { + return; + } + html += '
'; + html += '
' + hesc(label) + '
'; + html += '
' + hesc(val) + '
'; + html += '
' + hesc(unit); + if (loinc.code) html += ' ' + hesc(loinc.code) + ''; + html += '
'; + }); + html += '
'; + } + + var vitalObservations = observations.filter(isVitalObservation); + var labObservations = observations.filter(function(o) { return !isVitalObservation(o); }); + + renderObservationCard('Vitals', vitalObservations); + renderObservationCard('Lab Results', labObservations); } // ── Medications @@ -1768,7 +1909,23 @@ items.push('Diagnoses ' + n + '/' + diagnoses.length + ' SNOMED'); } if (obs.length) { - items.push('Vitals ' + obs.length + ' LOINC'); + 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 }; + var vitalObs = 0; + var labObs = 0; + obs.forEach(function(o) { + var code = ((((o.code || {}).coding) || [])[0] || {}).code || ''; + if (vitalLoincCodes[code]) { + vitalObs++; + } else { + labObs++; + } + }); + if (vitalObs > 0) { + items.push('Vitals ' + vitalObs + ' LOINC'); + } + if (labObs > 0) { + items.push('Lab Results ' + labObs + ' LOINC'); + } } if (services.length) { var n = services.filter(hasLoincCoding).length; @@ -1780,7 +1937,12 @@ return items.length ? 'Code quality:' + items.join('') : ''; } - function openFhirModal(url, title) { + function openFhirModal(url, title, options) { + options = options || {}; + _fhirSubmitMode = options.submitMode || 'opd'; + _fhirSubmitTaskRow = options.taskRow || null; + _fhirSubmitAbha = (options.abhaId || '').trim(); + _fhirOpdId = 0; _fhirSessionId = 0; if (fhirModalTitle) fhirModalTitle.textContent = title || 'FHIR Preview'; if (fhirModalMeta) { fhirModalMeta.textContent = 'Loading...'; fhirModalMeta.className = 'flex-grow-1 small text-muted'; } @@ -1792,6 +1954,23 @@ fhirFormView.style.display = ''; fhirModalJson.style.display = 'none'; if (fhirModalDataBadge) { fhirModalDataBadge.className = 'badge bg-warning text-dark'; fhirModalDataBadge.textContent = 'CHECKING'; } if (fhirModalHttpBadge) { fhirModalHttpBadge.className = 'badge bg-warning text-dark'; fhirModalHttpBadge.textContent = 'HTTP ...'; } + + if (btnSubmitFhirToAbdm) { + if (_fhirSubmitMode === 'task') { + btnSubmitFhirToAbdm.className = 'btn btn-sm btn-outline-warning'; + btnSubmitFhirToAbdm.innerHTML = ' Verify & Submit to Gateway'; + btnSubmitFhirToAbdm.title = 'Submit directly to ABDM gateway after FHIR verification'; + } else { + btnSubmitFhirToAbdm.className = defaultSubmitBtnClass; + btnSubmitFhirToAbdm.innerHTML = defaultSubmitBtnHtml; + btnSubmitFhirToAbdm.title = defaultSubmitBtnTitle; + } + } + + if (btnRegenerateFhirModal) { + btnRegenerateFhirModal.style.display = _fhirSubmitMode === 'task' ? 'none' : ''; + } + fhirPreviewModal.show(); fetch(url, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } }) @@ -1824,7 +2003,7 @@ } // Regenerate FHIR bundle in modal - document.getElementById('btnRegenerateFhirModal').addEventListener('click', function () { + btnRegenerateFhirModal.addEventListener('click', function () { var btn = this; if (_fhirOpdId <= 0) { alert('No FHIR bundle loaded.'); return; } if (!confirm('Rebuild FHIR bundle for OPD #' + _fhirOpdId + '?\nThis overwrites the stored bundle with fresh data.')) return; @@ -1880,8 +2059,78 @@ }); })(); - document.getElementById('btnSubmitFhirToAbdm').addEventListener('click', function () { + btnSubmitFhirToAbdm.addEventListener('click', function () { var btn = this; + + 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; + } + if (!confirm('FHIR verified. Submit this record to ABDM gateway now?')) { + return; + } + + var rowInput = _fhirSubmitTaskRow.querySelector('.abha-input'); + if (rowInput) { + rowInput.value = _fhirSubmitAbha; + } + + var taskType = (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase(); + 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; + if (labReqId <= 0 || patientId <= 0) { + alert('Task data missing (lab_req_id/patient_id).'); + return; + } + + var origHtmlTask = btn.innerHTML; + btn.disabled = true; + btn.innerHTML = 'Submitting…'; + + post('', { + lab_req_id: labReqId, + patient_id: patientId, + abha_id: _fhirSubmitAbha, + consent_handle: '' + }, function (res) { + btn.disabled = false; + btn.innerHTML = origHtmlTask; + + if (!res || parseInt(res.ok || '0', 10) !== 1) { + var err = (res && (res.error || res.error_text || res.message)) ? (res.error || res.error_text || res.message) : 'Gateway submit failed'; + setStatus(err, true); + alert('Submission failed:\n' + err); + return; + } + + var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : ''; + setStatus('Submitted to ABDM gateway.' + queueText); + 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); + if (res.queue_id) { + var logUrl = '' + '?search=' + encodeURIComponent(res.queue_id); + window.open(logUrl, '_blank'); + } + fhirPreviewModal.hide(); + }); + return; + } + + runAction(_fhirSubmitTaskRow, 'submit', _fhirSubmitAbha); + fhirPreviewModal.hide(); + return; + } + if (_fhirOpdId <= 0) { alert('No FHIR bundle loaded.'); return; } if (!confirm('Submit FHIR bundle for OPD #' + _fhirOpdId + ' to ABDM bridge?')) return; var origHtml = btn.innerHTML;