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:
DevSoftTech Server 2026-09-07 00:46:17 +05:30
parent f3faa9c96e
commit bb6589605a
7 changed files with 265 additions and 40 deletions

View File

@ -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),

View File

@ -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;
}
/**

View File

@ -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);

View File

@ -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);

View File

@ -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 + ' &bull; ' + 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'; }
});
}

View File

@ -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>

View File

@ -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();