ABDM: enforce PDF-backed report submit and improve gateway tracking
This commit is contained in:
parent
7253fa0cca
commit
0ebf8d2a8a
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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 = '<!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
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_api_logs')) {
|
||||
|
|
|
|||
|
|
@ -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 = '<!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)
|
||||
{
|
||||
// CI4 equivalent of old Lab_Admin/print_pdf_create/{inv}/{lab}/{print}/{print_on_type}
|
||||
|
|
|
|||
|
|
@ -636,6 +636,7 @@ class FhirR4Builder
|
|||
* @param array<string, mixed>|null $practitioner {name, registration_number}
|
||||
* @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 $attachment Optional PDF attachment {content_type, data_base64, title}
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
|
|
@ -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,
|
||||
]],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
?>
|
||||
<tr
|
||||
data-task-id="<?= (int) ($t['id'] ?? 0) ?>"
|
||||
|
|
@ -743,9 +744,9 @@
|
|||
sandboxCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
function runAction(row, action) {
|
||||
function runAction(row, action, overrideAbha) {
|
||||
var taskId = row.getAttribute('data-task-id');
|
||||
var abha = modalAbhaInput.value.trim();
|
||||
var abha = (overrideAbha || modalAbhaInput.value || '').trim();
|
||||
|
||||
setStatus('Submitting action...');
|
||||
post('<?= base_url('AbdmTaskBoard/perform_action') ?>', {
|
||||
|
|
@ -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 = '<?= 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) {
|
||||
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 += '</ul></div></div>';
|
||||
}
|
||||
|
||||
// ── 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 += '<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">';
|
||||
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 += '<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 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 += '<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
|
||||
|
|
@ -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>');
|
||||
}
|
||||
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) {
|
||||
var n = services.filter(hasLoincCoding).length;
|
||||
|
|
@ -1780,7 +1937,12 @@
|
|||
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;
|
||||
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 = '<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();
|
||||
|
||||
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 = '<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 (!confirm('Submit FHIR bundle for OPD #' + _fhirOpdId + ' to ABDM bridge?')) return;
|
||||
var origHtml = btn.innerHTML;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user