ABDM: enforce PDF-backed report submit and improve gateway tracking

This commit is contained in:
DevSoftTech Server 2026-05-31 04:17:39 +05:30
parent 7253fa0cca
commit 0ebf8d2a8a
6 changed files with 999 additions and 42 deletions

View File

@ -589,6 +589,9 @@ $routes->post('AbdmCodingPanel/mark_fhir_ready', 'AbdmCodingPanel::markFhirReady
$routes->get('AbdmCodingPanel/tip_check', 'AbdmCodingPanel::tipCheck'); $routes->get('AbdmCodingPanel/tip_check', 'AbdmCodingPanel::tipCheck');
$routes->post('AbdmGateway/share_ipd_discharge_bundle', 'AbdmGateway::shareIpdDischargeBundle', ['filter' => $abdmPermFilter]); $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/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->post('AbdmGateway/push_health_record', 'AbdmGateway::pushHealthRecord', ['filter' => $abdmPermFilter]);
$routes->get('AbdmGateway/health_records_list', 'AbdmGateway::healthRecordsList', ['filter' => $abdmPermFilter]); $routes->get('AbdmGateway/health_records_list', 'AbdmGateway::healthRecordsList', ['filter' => $abdmPermFilter]);
$routes->get('AbdmGateway/bridge_record_status/(:num)', 'AbdmGateway::bridgeRecordStatus/$1', ['filter' => $abdmPermFilter]); $routes->get('AbdmGateway/bridge_record_status/(:num)', 'AbdmGateway::bridgeRecordStatus/$1', ['filter' => $abdmPermFilter]);

View File

@ -8,6 +8,8 @@ use App\Libraries\FhirR4Builder;
use App\Libraries\FhirEncryptionService; use App\Libraries\FhirEncryptionService;
use App\Libraries\AbdmAuditService; use App\Libraries\AbdmAuditService;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use Mpdf\HTMLParserMode;
use Mpdf\Mpdf;
class AbdmGateway extends BaseController class AbdmGateway extends BaseController
{ {
@ -936,6 +938,14 @@ class AbdmGateway extends BaseController
'reported_at' => $reportedAt, 'reported_at' => $reportedAt,
'report_html' => trim((string) ($labReq->Report_Data ?? '')), '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 ─────────────────────── // ── Load LOINC code for the panel from lab_repo ───────────────────────
$labRepoRow = $this->db->table('lab_request lr') $labRepoRow = $this->db->table('lab_request lr')
@ -964,7 +974,21 @@ class AbdmGateway extends BaseController
'period_start' => $reportedAt, '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); $bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// ── Store health_record ─────────────────────────────────────────────── // ── 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() public function nhcxClaimCreate()
{ {
if (! $this->request->isAJAX()) { if (! $this->request->isAJAX()) {
@ -1610,6 +1883,176 @@ class AbdmGateway extends BaseController
return $this->response->setJSON($result); 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 = '<!doctype html><html><head><meta charset="utf-8">'
. '<style>body{font-family:DejaVu Sans,sans-serif;font-size:11px;color:#111} table{border-collapse:collapse;width:100%} td,th{vertical-align:top}</style>'
. '</head><body>' . $reportHtml . '</body></html>';
$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 private function getScanShareResultPayload(int $queueId): ?array
{ {
if (! $this->db->tableExists('abdm_api_logs')) { if (! $this->db->tableExists('abdm_api_logs')) {

View File

@ -2145,9 +2145,19 @@ class Diagnosis extends BaseController
'report_compile' => $compiledBy, '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([ return $this->response->setJSON([
'status' => 'success', 'status' => 'success',
'message' => 'Data Compile', 'message' => $message,
'pdf_file_id' => (int) ($pdfStore['file_id'] ?? 0),
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return $this->response->setJSON([ 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 = '<!doctype html><html><head><meta charset="utf-8">'
. '<style>body{font-family:DejaVu Sans,sans-serif;font-size:11px;color:#111} h1,h2,h3{margin:8px 0} table{border-collapse:collapse;width:100%} td,th{vertical-align:top}</style>'
. '</head><body>' . $compiledHtml . '</body></html>';
$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) public function printPdfCreate($invoiceId, $labType, $print = 0, $printOnType = 0)
{ {
// CI4 equivalent of old Lab_Admin/print_pdf_create/{inv}/{lab}/{print}/{print_on_type} // CI4 equivalent of old Lab_Admin/print_pdf_create/{inv}/{lab}/{print}/{print_on_type}

View File

@ -636,6 +636,7 @@ class FhirR4Builder
* @param array<string, mixed>|null $practitioner {name, registration_number} * @param array<string, mixed>|null $practitioner {name, registration_number}
* @param array<string, mixed>|null $organization {name, hfr_id} * @param array<string, mixed>|null $organization {name, hfr_id}
* @param array<string, mixed>|null $encounter {id, status, class_code (AMB|IMP|EMER), period_start, period_end} * @param array<string, mixed>|null $encounter {id, status, class_code (AMB|IMP|EMER), period_start, period_end}
* @param array<string, mixed>|null $attachment Optional PDF attachment {content_type, data_base64, title}
* *
* @return array<string, mixed> * @return array<string, mixed>
*/ */
@ -645,7 +646,8 @@ class FhirR4Builder
array $observations = [], array $observations = [],
?array $practitioner = null, ?array $practitioner = null,
?array $organization = null, ?array $organization = null,
?array $encounter = null ?array $encounter = null,
?array $attachment = null
): array { ): array {
$issuedAt = $this->isoTimestamp(); $issuedAt = $this->isoTimestamp();
$bundleUuid = $this->generateUuid(); $bundleUuid = $this->generateUuid();
@ -658,6 +660,8 @@ class FhirR4Builder
$reportedAt = trim((string) ($diagnosticReport['reported_at'] ?? '')) ?: $issuedAt; $reportedAt = trim((string) ($diagnosticReport['reported_at'] ?? '')) ?: $issuedAt;
$reportTitle = trim((string) ($diagnosticReport['title'] ?? 'Diagnostic Report')); $reportTitle = trim((string) ($diagnosticReport['title'] ?? 'Diagnostic Report'));
$reportStatus = trim((string) ($diagnosticReport['status'] ?? 'final')); $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'] ?? '')) !== ''; $hasPractitioner = $practitioner !== null && trim((string) ($practitioner['name'] ?? '')) !== '';
$hasOrganization = $organization !== null && trim((string) ($organization['name'] ?? '')) !== ''; $hasOrganization = $organization !== null && trim((string) ($organization['name'] ?? '')) !== '';
@ -827,6 +831,8 @@ class FhirR4Builder
// ── Binary + DocumentReference for HTML report ───────────────────────── // ── Binary + DocumentReference for HTML report ─────────────────────────
$docRefEntry = null; $docRefEntry = null;
$docRefRef = null; $docRefRef = null;
$pdfDocRefRef = null;
$mediaRef = null;
$reportHtml = trim((string) ($diagnosticReport['report_html'] ?? '')); $reportHtml = trim((string) ($diagnosticReport['report_html'] ?? ''));
if ($reportHtml !== '') { if ($reportHtml !== '') {
$binaryUuid = $this->generateUuid(); $binaryUuid = $this->generateUuid();
@ -837,6 +843,7 @@ class FhirR4Builder
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $binaryUuid, 'resource' => [ $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $binaryUuid, 'resource' => [
'resourceType' => 'Binary', 'resourceType' => 'Binary',
'id' => $binaryUuid, 'id' => $binaryUuid,
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Binary']],
'contentType' => $contentType, 'contentType' => $contentType,
'data' => base64_encode($reportHtml), 'data' => base64_encode($reportHtml),
]]; ]];
@ -848,16 +855,79 @@ class FhirR4Builder
'status' => 'current', 'status' => 'current',
'type' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]], 'type' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]],
'subject' => ['reference' => $patientRef], '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]; $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 ────────────────────────────────────────────────── // ── DiagnosticReport ──────────────────────────────────────────────────
$reportRes = [ $reportRes = [
'resourceType' => 'DiagnosticReport', 'resourceType' => 'DiagnosticReport',
'id' => $reportUuid, '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, 'status' => $reportStatus,
'code' => array_filter([ 'code' => array_filter([
'coding' => trim((string) ($diagnosticReport['loinc_code'] ?? '')) !== '' 'coding' => trim((string) ($diagnosticReport['loinc_code'] ?? '')) !== ''
@ -876,6 +946,11 @@ class FhirR4Builder
} elseif ($practitionerRef !== '') { } elseif ($practitionerRef !== '') {
$reportRes['performer'] = [['reference' => $practitionerRef]]; $reportRes['performer'] = [['reference' => $practitionerRef]];
} }
if ($practitionerRef !== '') {
$reportRes['resultsInterpreter'] = [['reference' => $practitionerRef]];
} elseif ($organizationRef !== '') {
$reportRes['resultsInterpreter'] = [['reference' => $organizationRef]];
}
if ($encounterRef !== '') { if ($encounterRef !== '') {
$reportRes['encounter'] = ['reference' => $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))]]]]; $reportRes['category'] = [['coding' => [['system' => 'http://snomed.info/sct', 'code' => $catSnomedCode, 'display' => trim((string) ($diagnosticReport['category_snomed_display'] ?? $catSnomedCode))]]]];
} }
$conclusion = trim((string) ($diagnosticReport['conclusion'] ?? '')); $conclusion = trim((string) ($diagnosticReport['conclusion'] ?? ''));
if ($conclusion !== '') { $reportRes['conclusion'] = $conclusion !== '' ? $conclusion : 'Laboratory report generated';
$reportRes['conclusion'] = $conclusion; 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]; $resourceEntries[] = ['fullUrl' => $reportRef, 'resource' => $reportRes];
// ── Composition section entries ──────────────────────────────────────── // ── Composition section entries ────────────────────────────────────────
$sectionEntries = [['reference' => $reportRef]]; $sectionEntries = [[
if ($docRefRef !== null) { 'reference' => $reportRef,
$sectionEntries[] = ['reference' => $docRefRef]; '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 ─────────────────────────────────────────────────────── // ── Composition ───────────────────────────────────────────────────────
$authorEntry = $practitionerRef !== '' ? ['reference' => $practitionerRef] : ['display' => 'Unknown']; $authorEntry = $practitionerRef !== ''
? ['reference' => $practitionerRef]
: ($organizationRef !== '' ? ['reference' => $organizationRef] : ['reference' => $patientRef]);
$composition = [ $composition = [
'resourceType' => 'Composition', 'resourceType' => 'Composition',
'id' => $compositionUuid, 'id' => $compositionUuid,
@ -905,16 +1003,20 @@ class FhirR4Builder
'identifier' => ['system' => 'https://ndhm.in/phr', 'value' => $compositionUuid], 'identifier' => ['system' => 'https://ndhm.in/phr', 'value' => $compositionUuid],
'status' => 'final', 'status' => 'final',
'type' => [ 'type' => [
'coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']], 'coding' => [[
'text' => 'Diagnostic Report', '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'], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
'date' => $issuedAt, 'date' => $issuedAt,
'author' => [[$authorEntry]], 'author' => [$authorEntry],
'title' => $reportTitle, 'title' => $reportTitle,
'section' => [[ 'section' => [[
'title' => 'Diagnostic Report', 'title' => $sectionTitle,
'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]], 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => $sectionCode, 'display' => $sectionDisplay]]],
'entry' => $sectionEntries, 'entry' => $sectionEntries,
]], ]],
]; ];

View File

@ -426,6 +426,32 @@ const today = new Date().toISOString().slice(0, 10);
document.getElementById('filterDateFrom').value = today; document.getElementById('filterDateFrom').value = today;
document.getElementById('filterDateTo').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); loadList(1);
</script> </script>
</body> </body>

View File

@ -81,7 +81,8 @@
$opdId = (int) ($meta['opd_id'] ?? 0); $opdId = (int) ($meta['opd_id'] ?? 0);
$opdSessionId = (int) ($meta['opd_session_id'] ?? 0); $opdSessionId = (int) ($meta['opd_session_id'] ?? 0);
$showSandbox = ($type === 'opd_prescription_publish' && $opdId > 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);
?> ?>
<tr <tr
data-task-id="<?= (int) ($t['id'] ?? 0) ?>" data-task-id="<?= (int) ($t['id'] ?? 0) ?>"
@ -743,9 +744,9 @@
sandboxCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); sandboxCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} }
function runAction(row, action) { function runAction(row, action, overrideAbha) {
var taskId = row.getAttribute('data-task-id'); var taskId = row.getAttribute('data-task-id');
var abha = modalAbhaInput.value.trim(); var abha = (overrideAbha || modalAbhaInput.value || '').trim();
setStatus('Submitting action...'); setStatus('Submitting action...');
post('<?= base_url('AbdmTaskBoard/perform_action') ?>', { post('<?= base_url('AbdmTaskBoard/perform_action') ?>', {
@ -765,10 +766,87 @@
badge.className = 'badge bg-info status-pill'; badge.className = 'badge bg-info status-pill';
} }
setStatus('Action queued: ' + (res.event_type || '-')); 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 = '<?= base_url('AbdmGateway/diagnosis_report_fhir_preview') ?>'
+ '?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
? '<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId + '/' + opdSessionId
: '<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + 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 = '<?= base_url('AbdmGateway/ipd_discharge_fhir_preview') ?>'
+ '?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) { document.querySelectorAll('.action-btn').forEach(function (btn) {
btn.addEventListener('click', function () { btn.addEventListener('click', function () {
selectedRow = btn.closest('tr'); selectedRow = btn.closest('tr');
@ -777,10 +855,17 @@
} }
selectedAction = btn.getAttribute('data-action') || 'submit'; selectedAction = btn.getAttribute('data-action') || 'submit';
if (selectedAction === 'submit') {
var previewHandled = openTaskFhirPreviewForSubmit(selectedRow);
if (previewHandled) {
return;
}
}
var abhaInput = selectedRow.querySelector('.abha-input'); 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() : ''; modalAbhaInput.value = abhaInput ? abhaInput.value.trim() : '';
modalTaskSummary.textContent = 'Task: ' + taskType + ' | Action: ' + selectedAction.toUpperCase(); modalTaskSummary.textContent = 'Task: ' + taskTypeLabel + ' | Action: ' + selectedAction.toUpperCase();
actionModal.show(); actionModal.show();
}); });
}); });
@ -804,6 +889,11 @@
return; return;
} }
var handled = openTaskFhirPreviewForSubmit(row);
if (handled) {
return;
}
var opdId = parseInt(row.getAttribute('data-opd-id') || '0', 10) || 0; var opdId = parseInt(row.getAttribute('data-opd-id') || '0', 10) || 0;
var opdSessionId = parseInt(row.getAttribute('data-opd-session-id') || '0', 10) || 0; var opdSessionId = parseInt(row.getAttribute('data-opd-session-id') || '0', 10) || 0;
if (opdId <= 0) { if (opdId <= 0) {
@ -1190,6 +1280,12 @@
var fhirTabJson = document.getElementById('fhirTabJson'); var fhirTabJson = document.getElementById('fhirTabJson');
var fhirModalDataBadge = document.getElementById('fhirModalDataBadge'); var fhirModalDataBadge = document.getElementById('fhirModalDataBadge');
var fhirModalHttpBadge = document.getElementById('fhirModalHttpBadge'); 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 ────────────────────────────────────────────────── // ── Tab switching ──────────────────────────────────────────────────
fhirTabForm.addEventListener('click', function () { fhirTabForm.addEventListener('click', function () {
@ -1208,6 +1304,9 @@
// ── Track current bundle context for edits ────────────────────── // ── Track current bundle context for edits ──────────────────────
var _fhirOpdId = 0, _fhirSessionId = 0; var _fhirOpdId = 0, _fhirSessionId = 0;
var _fhirLiveComplaints = [], _fhirLiveDiagnoses = []; var _fhirLiveComplaints = [], _fhirLiveDiagnoses = [];
var _fhirSubmitMode = 'opd';
var _fhirSubmitTaskRow = null;
var _fhirSubmitAbha = '';
// ── Inline complaint SNOMED edit (event delegation on fhirFormView) ─ // ── Inline complaint SNOMED edit (event delegation on fhirFormView) ─
var _editTimer = null; var _editTimer = null;
@ -1656,24 +1755,66 @@
html += '</ul></div></div>'; html += '</ul></div></div>';
} }
// ── Vitals // ── Observation Groups (Vitals vs Lab Results)
if (observations.length) { if (observations.length) {
var loincLabel = { '8867-4':'HR','59408-5':'SpO₂','8480-6':'BP Sys','8462-4':'BP Dia','8310-5':'Temp','9279-1':'RR','8302-2':'Height','29463-7':'Weight' }; var loincLabel = { '8867-4':'HR','59408-5':'SpO₂','8480-6':'BP Sys','8462-4':'BP Dia','8310-5':'Temp','9279-1':'RR','8302-2':'Height','29463-7':'Weight' };
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Vitals</small></div><div class="card-body py-2"><div class="d-flex flex-wrap gap-2">'; 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 };
observations.forEach(function(o) {
var loinc = (((o.code || {}).coding) || [])[0] || {}; function isVitalObservation(o) {
var label = loincLabel[loinc.code] || (o.code || {}).text || loinc.display || ''; var categories = o.category || [];
var val = (o.valueQuantity || {}).value; for (var ci = 0; ci < categories.length; ci++) {
var unit = (o.valueQuantity || {}).unit || ''; var cat = categories[ci] || {};
if (val === undefined || val === null) return; var catText = (cat.text || '').toLowerCase();
html += '<div class="border rounded px-2 py-1 text-center" style="min-width:80px;">'; if (catText.indexOf('vital') !== -1) {
html += '<div class="small text-muted" style="font-size:.7rem;">' + hesc(label) + '</div>'; return true;
html += '<div class="fw-bold">' + hesc(val) + '</div>'; }
html += '<div class="text-muted" style="font-size:.7rem;">' + hesc(unit); var codings = cat.coding || [];
if (loinc.code) html += ' <span class="text-secondary" style="font-size:.65rem;">'+hesc(loinc.code)+'</span>'; for (var cj = 0; cj < codings.length; cj++) {
html += '</div></div>'; var c = codings[cj] || {};
}); var system = (c.system || '').toLowerCase();
html += '</div></div></div>'; 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 += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">' + hesc(sectionTitle) + '</small></div><div class="card-body py-2"><div class="d-flex flex-wrap gap-2">';
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 += '<div class="border rounded px-2 py-1 text-center" style="min-width:80px;">';
html += '<div class="small text-muted" style="font-size:.7rem;">' + hesc(label) + '</div>';
html += '<div class="fw-bold">' + hesc(val) + '</div>';
html += '<div class="text-muted" style="font-size:.7rem;">' + hesc(unit);
if (loinc.code) html += ' <span class="text-secondary" style="font-size:.65rem;">' + hesc(loinc.code) + '</span>';
html += '</div></div>';
});
html += '</div></div></div>';
}
var vitalObservations = observations.filter(isVitalObservation);
var labObservations = observations.filter(function(o) { return !isVitalObservation(o); });
renderObservationCard('Vitals', vitalObservations);
renderObservationCard('Lab Results', labObservations);
} }
// ── Medications // ── Medications
@ -1768,7 +1909,23 @@
items.push('<span class="badge ' + (n === diagnoses.length ? 'bg-success' : 'bg-warning text-dark') + ' me-1">Diagnoses ' + n + '/' + diagnoses.length + ' SNOMED</span>'); items.push('<span class="badge ' + (n === diagnoses.length ? 'bg-success' : 'bg-warning text-dark') + ' me-1">Diagnoses ' + n + '/' + diagnoses.length + ' SNOMED</span>');
} }
if (obs.length) { if (obs.length) {
items.push('<span class="badge bg-success me-1">Vitals ' + obs.length + ' LOINC</span>'); 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('<span class="badge bg-success me-1">Vitals ' + vitalObs + ' LOINC</span>');
}
if (labObs > 0) {
items.push('<span class="badge bg-primary me-1">Lab Results ' + labObs + ' LOINC</span>');
}
} }
if (services.length) { if (services.length) {
var n = services.filter(hasLoincCoding).length; var n = services.filter(hasLoincCoding).length;
@ -1780,7 +1937,12 @@
return items.length ? '<small class="text-muted me-1">Code quality:</small>' + items.join('') : ''; return items.length ? '<small class="text-muted me-1">Code quality:</small>' + 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; _fhirOpdId = 0; _fhirSessionId = 0;
if (fhirModalTitle) fhirModalTitle.textContent = title || 'FHIR Preview'; if (fhirModalTitle) fhirModalTitle.textContent = title || 'FHIR Preview';
if (fhirModalMeta) { fhirModalMeta.textContent = 'Loading...'; fhirModalMeta.className = 'flex-grow-1 small text-muted'; } if (fhirModalMeta) { fhirModalMeta.textContent = 'Loading...'; fhirModalMeta.className = 'flex-grow-1 small text-muted'; }
@ -1792,6 +1954,23 @@
fhirFormView.style.display = ''; fhirModalJson.style.display = 'none'; fhirFormView.style.display = ''; fhirModalJson.style.display = 'none';
if (fhirModalDataBadge) { fhirModalDataBadge.className = 'badge bg-warning text-dark'; fhirModalDataBadge.textContent = 'CHECKING'; } 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 (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 = '<i class="bi bi-check2-square"></i> 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(); fhirPreviewModal.show();
fetch(url, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } }) fetch(url, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } })
@ -1824,7 +2003,7 @@
} }
// Regenerate FHIR bundle in modal // Regenerate FHIR bundle in modal
document.getElementById('btnRegenerateFhirModal').addEventListener('click', function () { btnRegenerateFhirModal.addEventListener('click', function () {
var btn = this; var btn = this;
if (_fhirOpdId <= 0) { alert('No FHIR bundle loaded.'); return; } 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; 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; 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 = '<span class="spinner-border spinner-border-sm me-1"></span>Submitting…';
post('<?= base_url('AbdmGateway/share_diagnosis_report_bundle') ?>', {
lab_req_id: labReqId,
patient_id: patientId,
abha_id: _fhirSubmitAbha,
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 = '<?= base_url('AbdmBridgeLog') ?>' + '?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 (_fhirOpdId <= 0) { alert('No FHIR bundle loaded.'); return; }
if (!confirm('Submit FHIR bundle for OPD #' + _fhirOpdId + ' to ABDM bridge?')) return; if (!confirm('Submit FHIR bundle for OPD #' + _fhirOpdId + ' to ABDM bridge?')) return;
var origHtml = btn.innerHTML; var origHtml = btn.innerHTML;