fix(abdm): resolve health document preview hang and wellness record display in task board
- Fix mPDF single-threaded server deadlock by replacing HTTP logo URL fallbacks with inline base64 data URIs - Add fast path in DoctorDocument to read scanned NABH PDFs directly from disk without mPDF re-rendering - Support doctor_document and patient_document entity types in task board health document preview - Enhance task board preview form with in-modal PDF preview, image thumbnail, and download actions - Populate Composition.encounter with AMB encounter and set Wellness Record display code
This commit is contained in:
parent
f3faa9c96e
commit
bb6589605a
|
|
@ -153,7 +153,14 @@ class DoctorDocument extends BaseController
|
|||
$logoSrc = str_replace('\\', '/', $logoAbsPath);
|
||||
}
|
||||
if ($logoSrc === '') {
|
||||
$logoSrc = base_url('assets/images/' . $logoFile);
|
||||
$globMatches = glob(FCPATH . 'assets/images/hospital_logo_*');
|
||||
if (is_array($globMatches) && ! empty($globMatches)) {
|
||||
usort($globMatches, fn($a, $b) => filemtime($b) <=> filemtime($a));
|
||||
$logoSrc = $this->buildImageDataUriFromPath($globMatches[0]);
|
||||
}
|
||||
}
|
||||
if ($logoSrc === '') {
|
||||
$logoSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAA=';
|
||||
}
|
||||
$logoHtml = '<img style="width:100px;vertical-align:top;" src="'
|
||||
. $logoSrc . '" />';
|
||||
|
|
@ -171,20 +178,22 @@ class DoctorDocument extends BaseController
|
|||
$drSignSrc = str_replace('\\', '/', $drSignAbsPath);
|
||||
}
|
||||
if ($drSignSrc === '') {
|
||||
$drSignSrc = base_url('assets/images/' . $drSignFile);
|
||||
$drSignSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAA=';
|
||||
}
|
||||
|
||||
$drSignImg = '<img style="height:40px;" src="' . $drSignSrc . '" />';
|
||||
}
|
||||
|
||||
$map = [
|
||||
'assets/images/{{H_logo}}' => $logoSrc,
|
||||
'assets/images/{{h_logo}}' => $logoSrc,
|
||||
'{{H_Name}}' => defined('H_Name') ? (string) constant('H_Name') : '',
|
||||
'{{H_address_1}}' => defined('H_address_1') ? (string) constant('H_address_1') : '',
|
||||
'{{H_address_2}}' => defined('H_address_2') ? (string) constant('H_address_2') : '',
|
||||
'{{H_phone_No}}' => defined('H_phone_No') ? (string) constant('H_phone_No') : '',
|
||||
'{{H_Email}}' => defined('H_Email') ? (string) constant('H_Email') : '',
|
||||
'{{H_logo}}' => $logoFile,
|
||||
'{{H_logo_abs}}' => $logoAbsPath,
|
||||
'{{H_logo}}' => $logoSrc,
|
||||
'{{H_logo_abs}}' => $logoSrc,
|
||||
'{{hospital_logo_html}}' => $logoHtml,
|
||||
'{{PATIENT_NAME}}' => (string) ($doc['p_fname'] ?? ''),
|
||||
'{{pName}}' => (string) ($doc['p_fname'] ?? ''),
|
||||
|
|
@ -264,7 +273,7 @@ class DoctorDocument extends BaseController
|
|||
return str_replace('\\', '/', $logoAbsPath);
|
||||
}
|
||||
|
||||
return base_url('assets/images/' . ($logoFile !== '' ? $logoFile : 'logo.png'));
|
||||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAA=';
|
||||
}
|
||||
|
||||
private function hasColumn(string $table, string $column): bool
|
||||
|
|
@ -314,14 +323,16 @@ class DoctorDocument extends BaseController
|
|||
public function health_document_fhir_preview(int $patientDocId = 0)
|
||||
{
|
||||
if ($patientDocId <= 0) {
|
||||
$patientDocId = (int) ($this->request->getGet('patient_doc_id') ?? $this->request->getGet('doc_id') ?? $this->request->getGet('health_doc_id') ?? 0);
|
||||
$patientDocId = (int) ($this->request->getGet('patient_doc_id') ?? $this->request->getGet('doc_id') ?? $this->request->getGet('health_doc_id') ?? $this->request->getGet('record_id') ?? 0);
|
||||
}
|
||||
|
||||
$entityType = trim((string) ($this->request->getGet('entity_type') ?? ''));
|
||||
|
||||
if ($patientDocId <= 0) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'patient_doc_id is required']);
|
||||
}
|
||||
|
||||
$source = $this->buildHealthDocumentSource($patientDocId);
|
||||
$source = $this->buildHealthDocumentSource($patientDocId, $entityType);
|
||||
if (empty($source)) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Health document source data not found for ID: ' . $patientDocId]);
|
||||
}
|
||||
|
|
@ -336,6 +347,9 @@ class DoctorDocument extends BaseController
|
|||
'ok' => 1,
|
||||
'bundle' => $bundle,
|
||||
'fhir_bundle' => $bundle,
|
||||
'patient_id' => (int) ($source['patient']['id'] ?? 0),
|
||||
'abha_id' => (string) ($source['patient']['abha_id'] ?? ''),
|
||||
'document_title' => (string) ($source['document_title'] ?? ''),
|
||||
'hi_type' => 'HealthDocumentRecord',
|
||||
'care_context_reference' => $res['care_context_reference'] ?? '',
|
||||
'care_context_display' => $res['care_context_display'] ?? '',
|
||||
|
|
@ -798,7 +812,15 @@ class DoctorDocument extends BaseController
|
|||
'session_id' => (string) $opdSessionId,
|
||||
'visit_date' => $visitDate,
|
||||
'completed_at' => $completedAt,
|
||||
'encounter' => [
|
||||
'id' => (string) ($rxRow['id'] ?? $patientId),
|
||||
'class_code' => 'AMB',
|
||||
'class_display' => 'ambulatory',
|
||||
'start' => $completedAt,
|
||||
'end' => $completedAt,
|
||||
],
|
||||
'vitals' => $vitals,
|
||||
|
||||
'physical_examination' => $physicalExam,
|
||||
'women_wellness' => $womenWellness,
|
||||
'advice' => $lifestyle,
|
||||
|
|
@ -2026,12 +2048,22 @@ class DoctorDocument extends BaseController
|
|||
return '';
|
||||
}
|
||||
|
||||
private function buildHealthDocumentSource(int $patientDocId): array
|
||||
public function buildHealthDocumentSource(int $patientDocId, string $entityType = ''): array
|
||||
{
|
||||
if ($patientDocId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($entityType === 'patient_document') {
|
||||
if ($this->db->tableExists('file_upload_data')) {
|
||||
$fileRow = $this->db->table('file_upload_data')->where('id', $patientDocId)->get(1)->getRowArray();
|
||||
if (is_array($fileRow) && ! empty($fileRow)) {
|
||||
return $this->buildFileUploadHealthDocumentSource($fileRow);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
$patientDoc = $this->db->table('patient_doc')->where('id', $patientDocId)->get(1)->getRowArray();
|
||||
if (! is_array($patientDoc) || empty($patientDoc)) {
|
||||
if ($this->db->tableExists('file_upload_data')) {
|
||||
|
|
@ -2057,7 +2089,70 @@ class DoctorDocument extends BaseController
|
|||
$templateRow = $this->db->table('doc_format_master')->where('df_id', $docFormatId)->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
|
||||
$docTitle = trim((string) ($templateRow['doc_name'] ?? 'Medical Certificate'));
|
||||
$docTitle = trim((string) ($templateRow['doc_name'] ?? ''));
|
||||
|
||||
$rawDataStr = trim((string) ($patientDoc['raw_data'] ?? ''));
|
||||
$pdfBytes = null;
|
||||
$contentType = 'application/pdf';
|
||||
$existingDocPath = '';
|
||||
|
||||
// Check if raw_data is a JSON payload describing an uploaded/scanned document (e.g. NABH scan)
|
||||
if (str_starts_with($rawDataStr, '{') && str_ends_with($rawDataStr, '}')) {
|
||||
$decoded = json_decode($rawDataStr, true);
|
||||
if (is_array($decoded)) {
|
||||
if (! empty($decoded['title'])) {
|
||||
$docTitle = trim((string) $decoded['title']);
|
||||
}
|
||||
$fileName = trim((string) ($decoded['file_name'] ?? ''));
|
||||
if ($fileName !== '') {
|
||||
$candidates = [
|
||||
FCPATH . 'uploads/nabh_ipd/' . $fileName,
|
||||
FCPATH . 'uploads/' . $fileName,
|
||||
WRITEPATH . 'uploads/nabh_ipd/' . $fileName,
|
||||
defined('ROOTPATH') ? ROOTPATH . 'public/uploads/nabh_ipd/' . $fileName : '',
|
||||
];
|
||||
foreach ($candidates as $cand) {
|
||||
if ($cand !== '' && is_file($cand) && is_readable($cand)) {
|
||||
$existingDocPath = $cand;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check nabh_ipd_scans table if present
|
||||
if ($existingDocPath === '' && $this->db->tableExists('nabh_ipd_scans')) {
|
||||
$scanRow = $this->db->table('nabh_ipd_scans')
|
||||
->where('patient_doc_id', $patientDocId)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (is_array($scanRow) && ! empty($scanRow['file_name'])) {
|
||||
if (! empty($scanRow['category_title'])) {
|
||||
$docTitle = trim((string) $scanRow['category_title']);
|
||||
}
|
||||
$cand = FCPATH . 'uploads/nabh_ipd/' . trim((string) $scanRow['file_name']);
|
||||
if (is_file($cand) && is_readable($cand)) {
|
||||
$existingDocPath = $cand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($existingDocPath !== '') {
|
||||
$pdfBytes = @file_get_contents($existingDocPath);
|
||||
$ext = strtolower(pathinfo($existingDocPath, PATHINFO_EXTENSION));
|
||||
$contentType = match ($ext) {
|
||||
'pdf' => 'application/pdf',
|
||||
'png' => 'image/png',
|
||||
'webp' => 'image/webp',
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
default => 'application/pdf',
|
||||
};
|
||||
} elseif ($docFormatId > 0 && ! empty($rawDataStr)) {
|
||||
// It's a formatted certificate / letterhead document
|
||||
$pdfBytes = $this->generatePatientDocPdfBytes($patientDocId);
|
||||
}
|
||||
|
||||
if ($docTitle === '') {
|
||||
$docTitle = 'Medical Certificate';
|
||||
}
|
||||
|
|
@ -2094,9 +2189,7 @@ class DoctorDocument extends BaseController
|
|||
$doctorName = 'Doctor';
|
||||
}
|
||||
|
||||
$pdfBytes = $this->generatePatientDocPdfBytes($patientDocId);
|
||||
$docDataBase64 = ($pdfBytes !== null && $pdfBytes !== '') ? base64_encode($pdfBytes) : '';
|
||||
$contentType = ($pdfBytes !== null && $pdfBytes !== '') ? 'application/pdf' : 'text/html';
|
||||
|
||||
if ($docDataBase64 === '') {
|
||||
$rawHtml = trim((string) ($patientDoc['raw_data'] ?? ''));
|
||||
|
|
@ -2109,6 +2202,20 @@ class DoctorDocument extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
$encounterId = 'encounter-' . $patientDocId;
|
||||
$encounter = [
|
||||
'id' => $encounterId,
|
||||
'status' => 'finished',
|
||||
'class' => 'AMB',
|
||||
'start' => $visitDate . 'T09:00:00+05:30',
|
||||
'end' => $visitDate . 'T09:30:00+05:30',
|
||||
'period' => [
|
||||
'start' => $visitDate . 'T09:00:00+05:30',
|
||||
'end' => $visitDate . 'T09:30:00+05:30',
|
||||
],
|
||||
'hospitalization' => false,
|
||||
];
|
||||
|
||||
return [
|
||||
'record_id' => (string) $patientDocId,
|
||||
'session_id' => (string) $patientDocId,
|
||||
|
|
@ -2124,6 +2231,8 @@ class DoctorDocument extends BaseController
|
|||
'id' => $hfrId,
|
||||
'name' => $hospitalName,
|
||||
],
|
||||
'encounter' => $encounter,
|
||||
'encounter_reference' => 'urn:uuid:' . $encounterId,
|
||||
'patient' => [
|
||||
'id' => (string) $patientId,
|
||||
'uhid' => (string) ($patientRow['uhid_no'] ?? $patientRow['uhid'] ?? $patientRow['patient_code'] ?? $patientId),
|
||||
|
|
@ -2285,7 +2394,7 @@ class DoctorDocument extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
private function buildFileUploadHealthDocumentSource(array $fileRow): array
|
||||
public function buildFileUploadHealthDocumentSource(array $fileRow): array
|
||||
{
|
||||
$fileUploadId = (int) ($fileRow['id'] ?? 0);
|
||||
$pid = (int) ($fileRow['pid'] ?? 0);
|
||||
|
|
@ -2365,6 +2474,20 @@ class DoctorDocument extends BaseController
|
|||
$drName = 'Medical Officer';
|
||||
}
|
||||
|
||||
$encounterId = 'encounter-file-' . $fileUploadId;
|
||||
$encounter = [
|
||||
'id' => $encounterId,
|
||||
'status' => 'finished',
|
||||
'class' => 'AMB',
|
||||
'start' => $visitDate . 'T09:00:00+05:30',
|
||||
'end' => $visitDate . 'T09:30:00+05:30',
|
||||
'period' => [
|
||||
'start' => $visitDate . 'T09:00:00+05:30',
|
||||
'end' => $visitDate . 'T09:30:00+05:30',
|
||||
],
|
||||
'hospitalization' => false,
|
||||
];
|
||||
|
||||
return [
|
||||
'record_id' => 'file-' . $fileUploadId,
|
||||
'session_id' => (string) ($fileRow['opd_id'] ?? $fileUploadId),
|
||||
|
|
@ -2379,6 +2502,8 @@ class DoctorDocument extends BaseController
|
|||
'id' => $hfrId,
|
||||
'name' => $hospitalName,
|
||||
],
|
||||
'encounter' => $encounter,
|
||||
'encounter_reference' => 'urn:uuid:' . $encounterId,
|
||||
'patient' => [
|
||||
'id' => (string) $pid,
|
||||
'uhid' => (string) ($patientRow['uhid_no'] ?? $patientRow['uhid'] ?? $patientRow['patient_code'] ?? $pid),
|
||||
|
|
|
|||
|
|
@ -201,9 +201,11 @@ abstract class AbstractModuleFhirGenerator
|
|||
if ($patientName === '' || strcasecmp($patientName, 'NA') === 0) {
|
||||
$patientName = 'Patient';
|
||||
}
|
||||
$encounterRef = isset($source['encounter']) ? 'urn:uuid:encounter-' . ((string) ($source['encounter']['id'] ?? $recordId)) : null;
|
||||
$encId = trim((string) ($source['encounter']['id'] ?? $source['record_id'] ?? $recordId));
|
||||
$encounterRef = $encId !== '' ? 'urn:uuid:encounter-' . $encId : null;
|
||||
|
||||
$drId = (string) ($source['practitioner']['id'] ?? $source['doctor']['id'] ?? '1');
|
||||
|
||||
if ($drId === '' || $drId === '0') {
|
||||
$drId = '1';
|
||||
}
|
||||
|
|
@ -278,7 +280,14 @@ abstract class AbstractModuleFhirGenerator
|
|||
if ($id === '') {
|
||||
return null;
|
||||
}
|
||||
$classCode = strtoupper(trim((string) ($enc['class_code'] ?? 'IMP')));
|
||||
$isIpd = (isset($source['hi_type']) && $source['hi_type'] === 'DischargeSummary')
|
||||
|| (isset($source['ipd_no']) && $source['ipd_no'] !== '')
|
||||
|| (isset($source['discharge_status']))
|
||||
|| (isset($source['admission_date']))
|
||||
|| (isset($enc['class_code']) && strtoupper((string) $enc['class_code']) === 'IMP');
|
||||
|
||||
$defaultClass = $isIpd ? 'IMP' : 'AMB';
|
||||
$classCode = strtoupper(trim((string) ($enc['class_code'] ?? $defaultClass)));
|
||||
$classDisplay = $classCode === 'IMP' ? 'inpatient encounter' : 'ambulatory';
|
||||
|
||||
$identifiers = [[
|
||||
|
|
@ -315,6 +324,10 @@ abstract class AbstractModuleFhirGenerator
|
|||
$statusInput = $enc['discharge_disposition'] ?? $enc['outcome'] ?? $source['discharge_status'] ?? $source['discarge_patient_status'] ?? 'home';
|
||||
$disposition = $this->resolveDischargeDisposition($statusInput);
|
||||
|
||||
$divText = $classCode === 'IMP'
|
||||
? '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Encounter:</b> ' . $this->escapeXhtml($classDisplay) . ' ' . $this->escapeXhtml($id) . ' - ' . $this->escapeXhtml($disposition['text']) . '</p></div>'
|
||||
: '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Encounter:</b> ' . $this->escapeXhtml($classDisplay) . ' ' . $this->escapeXhtml($id) . '</p></div>';
|
||||
|
||||
$encounterResource = [
|
||||
'resourceType' => 'Encounter',
|
||||
'id' => 'encounter-' . $id,
|
||||
|
|
@ -323,7 +336,7 @@ abstract class AbstractModuleFhirGenerator
|
|||
]],
|
||||
'text' => [
|
||||
'status' => 'generated',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Encounter:</b> ' . $this->escapeXhtml($classDisplay) . ' ' . $this->escapeXhtml($id) . ' - ' . $this->escapeXhtml($disposition['text']) . '</p></div>',
|
||||
'div' => $divText,
|
||||
],
|
||||
'identifier' => $identifiers,
|
||||
'status' => 'finished',
|
||||
|
|
@ -340,7 +353,10 @@ abstract class AbstractModuleFhirGenerator
|
|||
'start' => $start,
|
||||
'end' => $end,
|
||||
],
|
||||
'hospitalization' => [
|
||||
];
|
||||
|
||||
if ($classCode === 'IMP') {
|
||||
$encounterResource['hospitalization'] = [
|
||||
'dischargeDisposition' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/discharge-disposition',
|
||||
|
|
@ -349,10 +365,11 @@ abstract class AbstractModuleFhirGenerator
|
|||
]],
|
||||
'text' => $disposition['text'],
|
||||
],
|
||||
],
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
return $encounterResource;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -147,11 +147,11 @@ class HealthDocumentFhirGenerator extends AbstractModuleFhirGenerator
|
|||
'description' => $docTitle . ' PDF',
|
||||
'content' => [[
|
||||
'attachment' => [
|
||||
'contentType' => 'application/pdf',
|
||||
'contentType' => $contentType,
|
||||
'language' => 'en-IN',
|
||||
'data' => $docData,
|
||||
'size' => $fileSize,
|
||||
'title' => $docTitle . ' PDF',
|
||||
'title' => $docTitle,
|
||||
'creation' => $timestamp,
|
||||
],
|
||||
]],
|
||||
|
|
@ -210,10 +210,11 @@ class HealthDocumentFhirGenerator extends AbstractModuleFhirGenerator
|
|||
$ext = str_contains($contentType, 'png') ? 'png' : 'jpg';
|
||||
$tempImg = $mpdfTempDir . DIRECTORY_SEPARATOR . 'img_' . md5($rawBytes) . '.' . $ext;
|
||||
@file_put_contents($tempImg, $rawBytes);
|
||||
$tempImgUrl = str_replace('\\', '/', $tempImg);
|
||||
|
||||
$html = '<!DOCTYPE html><html><body style="margin:0;padding:0;text-align:center;">'
|
||||
. '<h3>' . htmlspecialchars($docTitle, ENT_QUOTES, 'UTF-8') . '</h3>'
|
||||
. '<img src="' . $tempImg . '" style="max-width:100%; height:auto;" />'
|
||||
. '<img src="' . $tempImgUrl . '" style="max-width:100%; height:auto;" />'
|
||||
. '</body></html>';
|
||||
$mpdf->WriteHTML($html);
|
||||
@unlink($tempImg);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator
|
|||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '419891008',
|
||||
'display' => 'Record artifact',
|
||||
'display' => 'Wellness Record',
|
||||
],
|
||||
],
|
||||
'text' => 'Wellness Record',
|
||||
|
|
@ -47,8 +47,13 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator
|
|||
$encounter = $this->buildEncounter($source);
|
||||
if (is_array($encounter)) {
|
||||
$builder->addEncounter($encounter);
|
||||
$composition['encounter'] = [
|
||||
'reference' => 'urn:uuid:' . (string) ($encounter['id'] ?? ('encounter-' . $recordId)),
|
||||
'display' => 'Encounter',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
$practitioner = $this->buildPractitioner($source);
|
||||
if (is_array($practitioner)) {
|
||||
$builder->addPractitioner($practitioner);
|
||||
|
|
|
|||
|
|
@ -1118,13 +1118,17 @@
|
|||
} else if (taskType === 'health_document_publish') {
|
||||
var healthDocId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
var healthPatientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
var healthEntityType = (row.getAttribute('data-entity-type') || '').trim();
|
||||
|
||||
if (healthDocId <= 0 || healthPatientId <= 0) {
|
||||
setStatus('Task data missing for Health Document FHIR preview.', true);
|
||||
return true;
|
||||
}
|
||||
|
||||
previewUrl = '<?= base_url('DoctorDocument/health_document_fhir_preview') ?>/' + healthDocId;
|
||||
previewUrl = '<?= base_url('DoctorDocument/health_document_fhir_preview') ?>/' + healthDocId
|
||||
+ '?entity_type=' + encodeURIComponent(healthEntityType)
|
||||
+ '&patient_id=' + encodeURIComponent(healthPatientId)
|
||||
+ '&abha_id=' + encodeURIComponent(abhaId);
|
||||
title = 'HealthDocumentRecord FHIR Preview - Document #' + healthDocId;
|
||||
} else if (taskType === 'wellness_record_publish') {
|
||||
var wellnessPatientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
|
|
@ -2180,19 +2184,67 @@
|
|||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// -- Embedded invoice PDF
|
||||
// -- Health Document Record metadata
|
||||
if (composition && !invoice && (documentRefs.length || (composition.title && (composition.title.toLowerCase().includes('document') || composition.title.toLowerCase().includes('certificate') || composition.title.toLowerCase().includes('form'))))) {
|
||||
var compCoding = ((((composition.type || {}).coding) || [])[0]) || {};
|
||||
var compIdentifier = (composition.identifier || {}).value || '';
|
||||
var bundleIdentifier = (bundle.identifier || {}).value || '';
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light d-flex justify-content-between align-items-center">';
|
||||
html += '<small class="fw-bold text-uppercase text-secondary">' + hesc(composition.title || 'Health Document Record') + '</small>';
|
||||
html += '<span class="badge bg-success text-capitalize">' + hesc(composition.status || 'final') + '</span></div>';
|
||||
html += '<div class="card-body py-2 small"><div class="row g-2">';
|
||||
html += '<div class="col-md-4"><span class="text-muted">Document Title</span><div class="fw-semibold">' + hesc(composition.title || 'Health Document') + '</div></div>';
|
||||
html += '<div class="col-md-3"><span class="text-muted">Document Date</span><div>' + hesc(fmtDate(composition.date || bundle.timestamp) || '-') + '</div></div>';
|
||||
html += '<div class="col-md-5"><span class="text-muted">Record / Bundle ID</span><div><code>' + hesc(bundleIdentifier || compIdentifier || '-') + '</code></div></div>';
|
||||
html += '<div class="col-md-4"><span class="text-muted">Type</span><div>' + hesc(compCoding.display || (composition.type || {}).text || 'Record artifact') + '</div></div>';
|
||||
if (compCoding.code) {
|
||||
html += '<div class="col-md-3"><span class="text-muted">Code</span><div><code>' + hesc(compCoding.code) + '</code></div></div>';
|
||||
}
|
||||
html += '</div></div></div>';
|
||||
}
|
||||
|
||||
// -- Embedded Document / PDF / Image attachments
|
||||
documentRefs.forEach(function(documentReference) {
|
||||
var attachment = (((documentReference.content || [])[0] || {}).attachment) || {};
|
||||
if (attachment.contentType !== 'application/pdf') return;
|
||||
var pdfUrl = attachmentObjectUrl(attachment);
|
||||
html += '<div class="card mb-2"><div class="card-body py-2 d-flex flex-wrap align-items-center gap-2">';
|
||||
html += '<div class="flex-grow-1"><div class="fw-semibold">' + hesc(attachment.title || 'Invoice PDF') + '</div>';
|
||||
html += '<small class="text-muted">Attached to this FHIR InvoiceRecord</small></div>';
|
||||
if (pdfUrl) {
|
||||
html += '<a class="btn btn-sm btn-outline-primary" href="' + hesc(pdfUrl) + '" target="_blank" rel="noopener">Open PDF</a>';
|
||||
html += '<a class="btn btn-sm btn-primary" href="' + hesc(pdfUrl) + '" download="' + hesc(attachment.title || 'invoice.pdf') + '">Download</a>';
|
||||
var cType = (attachment.contentType || '').toLowerCase();
|
||||
var docName = attachment.title || documentReference.description || (invoice ? 'Invoice PDF' : 'Health Document');
|
||||
var isPdf = cType.indexOf('pdf') !== -1;
|
||||
var isImage = cType.indexOf('image/') === 0;
|
||||
var docUrl = attachmentObjectUrl(attachment);
|
||||
|
||||
html += '<div class="card mb-2"><div class="card-header py-1 bg-light d-flex justify-content-between align-items-center">';
|
||||
html += '<small class="fw-bold text-uppercase text-secondary">' + hesc(docName) + '</small>';
|
||||
html += '<span class="badge bg-info text-dark">' + hesc(cType || 'document') + '</span></div>';
|
||||
html += '<div class="card-body py-2">';
|
||||
html += '<div class="d-flex flex-wrap align-items-center gap-2 mb-2">';
|
||||
html += '<div class="flex-grow-1"><div class="fw-semibold">' + hesc(docName) + '</div>';
|
||||
if (attachment.size) {
|
||||
var sizeKb = (attachment.size / 1024).toFixed(1) + ' KB';
|
||||
if (attachment.size > 1048576) sizeKb = (attachment.size / 1048576).toFixed(2) + ' MB';
|
||||
html += '<small class="text-muted">Size: ' + sizeKb + ' • ' + hesc(cType) + '</small>';
|
||||
} else {
|
||||
html += '<span class="badge bg-danger">Invalid PDF attachment</span>';
|
||||
html += '<small class="text-muted">' + (invoice ? 'Attached to this FHIR InvoiceRecord' : 'Attached health document payload') + '</small>';
|
||||
}
|
||||
html += '</div>';
|
||||
if (docUrl) {
|
||||
if (isPdf) {
|
||||
html += '<a class="btn btn-sm btn-outline-primary" href="' + hesc(docUrl) + '" target="_blank" rel="noopener"><i class="bi bi-file-earmark-pdf me-1"></i>Open PDF</a>';
|
||||
html += '<a class="btn btn-sm btn-primary" href="' + hesc(docUrl) + '" download="' + hesc(docName.replace(/\s+/g, '_') + '.pdf') + '"><i class="bi bi-download me-1"></i>Download PDF</a>';
|
||||
} else if (isImage) {
|
||||
html += '<a class="btn btn-sm btn-outline-primary" href="' + hesc(docUrl) + '" target="_blank" rel="noopener"><i class="bi bi-image me-1"></i>View Full Image</a>';
|
||||
html += '<a class="btn btn-sm btn-primary" href="' + hesc(docUrl) + '" download="' + hesc(docName.replace(/\s+/g, '_')) + '"><i class="bi bi-download me-1"></i>Download</a>';
|
||||
} else {
|
||||
html += '<a class="btn btn-sm btn-outline-primary" href="' + hesc(docUrl) + '" target="_blank" rel="noopener">Open Document</a>';
|
||||
}
|
||||
} else {
|
||||
html += '<span class="badge bg-danger">Invalid document attachment</span>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (isImage && docUrl) {
|
||||
html += '<div class="text-center p-2 bg-light rounded border"><img src="' + hesc(docUrl) + '" style="max-width:100%;max-height:400px;object-fit:contain;" class="rounded shadow-sm" alt="Document Preview" /></div>';
|
||||
} else if (isPdf && docUrl) {
|
||||
html += '<div class="ratio ratio-16x9 border rounded overflow-hidden mt-2" style="min-height:350px;max-height:550px;"><iframe src="' + hesc(docUrl) + '" class="w-100 h-100" style="border:none;"></iframe></div>';
|
||||
}
|
||||
html += '</div></div>';
|
||||
});
|
||||
|
|
@ -2723,8 +2775,18 @@
|
|||
|
||||
fhirPreviewModal.show();
|
||||
|
||||
fetch(url, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } })
|
||||
.then(function (r) { return r.text().then(function (b) { return { ok: r.ok, status: r.status, body: b }; }); })
|
||||
var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
||||
var timeoutId = controller ? setTimeout(function () { controller.abort(); }, 25000) : null;
|
||||
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
|
||||
signal: controller ? controller.signal : undefined
|
||||
})
|
||||
.then(function (r) {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
return r.text().then(function (b) { return { ok: r.ok, status: r.status, body: b }; });
|
||||
})
|
||||
.then(function (res) {
|
||||
var parsed = null;
|
||||
try { parsed = JSON.parse(res.body || '{}'); } catch (e) { parsed = { raw: res.body || '' }; }
|
||||
|
|
@ -2757,10 +2819,13 @@
|
|||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
if (fhirFormView) fhirFormView.innerHTML = '<p class="text-danger p-3">Request failed: ' + hesc(e.message) + '</p>';
|
||||
if (fhirModalMeta) { fhirModalMeta.textContent = 'Request failed: ' + e.message; fhirModalMeta.className = 'flex-grow-1 small text-danger'; }
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
var isTimeout = e.name === 'AbortError';
|
||||
var msg = isTimeout ? 'Request timed out after 25 seconds. Please check server or retry.' : e.message;
|
||||
if (fhirFormView) fhirFormView.innerHTML = '<div class="alert alert-danger m-3"><i class="bi bi-exclamation-triangle"></i> <strong>Request failed:</strong> ' + hesc(msg) + '</div>';
|
||||
if (fhirModalMeta) { fhirModalMeta.textContent = 'Request failed: ' + msg; fhirModalMeta.className = 'flex-grow-1 small text-danger'; }
|
||||
if (fhirModalDataBadge) { fhirModalDataBadge.className = 'badge bg-danger'; fhirModalDataBadge.textContent = 'ERROR'; }
|
||||
if (fhirModalHttpBadge) { fhirModalHttpBadge.className = 'badge bg-danger'; fhirModalHttpBadge.textContent = 'HTTP ERR'; }
|
||||
if (fhirModalHttpBadge) { fhirModalHttpBadge.className = 'badge bg-danger'; fhirModalHttpBadge.textContent = isTimeout ? 'TIMEOUT' : 'HTTP ERR'; }
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ $customHeaderHtml = (string) ($custom_header_html ?? '');
|
|||
$customFooterHtml = (string) ($custom_footer_html ?? '');
|
||||
$hasSelectedPrintTemplate = ! empty($has_selected_print_template);
|
||||
$hospitalLogoSrc = trim((string) ($hospital_logo_src ?? ''));
|
||||
if ($hospitalLogoSrc === '') {
|
||||
$hospitalLogoSrc = base_url('assets/images/' . (defined('H_logo') ? constant('H_logo') : 'logo.png'));
|
||||
if ($hospitalLogoSrc === '' || str_starts_with($hospitalLogoSrc, 'http://') || str_starts_with($hospitalLogoSrc, 'https://')) {
|
||||
$hospitalLogoSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAA=';
|
||||
}
|
||||
?>
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -729,10 +729,22 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
|
||||
$composition = $bundle['entry'][0]['resource'];
|
||||
$this->assertSame('https://nrces.in/ndhm/fhir/r4/StructureDefinition/WellnessRecord', $composition['meta']['profile'][0]);
|
||||
$this->assertSame('Wellness Record', $composition['type']['coding'][0]['display'] ?? '');
|
||||
$this->assertNotEmpty($composition['encounter']['reference'] ?? '');
|
||||
|
||||
$this->assertContains('Encounter', $resourceTypes);
|
||||
$encounterEntries = array_values(array_filter($bundle['entry'], static fn ($e) => ($e['resource']['resourceType'] ?? '') === 'Encounter'));
|
||||
$this->assertNotEmpty($encounterEntries);
|
||||
$encResource = $encounterEntries[0]['resource'];
|
||||
$this->assertSame('AMB', $encResource['class']['code'] ?? '');
|
||||
$this->assertSame('ambulatory', $encResource['class']['display'] ?? '');
|
||||
$this->assertArrayNotHasKey('hospitalization', $encResource);
|
||||
|
||||
$this->assertGreaterThanOrEqual(1, count($composition['section']));
|
||||
$this->assertNotEmpty($composition['section'][0]['entry']);
|
||||
}
|
||||
|
||||
|
||||
public function testDischargeSummaryGeneratorCreatesFullSectionsAndComponents(): void
|
||||
{
|
||||
$generator = $this->factory->discharge();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user