feat(abdm): add digital share records
Generate standardized Digital Share PDFs for invoice and diagnostic records, strengthen FHIR document validation and sync handling, and keep task-board submissions AJAX-only.
This commit is contained in:
parent
625987955e
commit
2e8304636b
|
|
@ -4,6 +4,23 @@ Date: 2026-02-08
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ABDM Immunization Duplicate Control — 2026-08-15
|
||||||
|
|
||||||
|
- Made ImmunizationRecord care-context references stable per source record (`IMM-{record_id}`), while preserving previously stored references and deterministic aggregate fallbacks.
|
||||||
|
- Removed the timestamp-based Immunization fallback from `FhirR4Builder` so retries cannot create a new gateway identity.
|
||||||
|
- Normalized direct and outbox push responses: HTTP 201 succeeds; HTTP 409 succeeds only for `DUPLICATE_RECORD`; HTTP 422 remains a non-retryable validation failure with field details.
|
||||||
|
- Preserved returned gateway `record_id`, `queue_id`, and `first_pushed_at`, persisting each when the deployed schema provides a matching column.
|
||||||
|
- Added focused regression tests for stable Immunization references, accepted duplicate responses, generic conflicts, HTTP 201, and detailed HTTP 422 errors.
|
||||||
|
|
||||||
|
## ABDM M2 Link Status + Immunization Task Board — 2026-08-15
|
||||||
|
|
||||||
|
- Fixed M2 discovery/fetch to use canonical `health_records` care-context references and HI types first, with `patient_records` retained as a backward-compatible fallback.
|
||||||
|
- Fixed link callbacks to accept snake_case and camelCase request/care-context identifiers, persist request IDs as transaction metadata, update both `record_links` and `health_records`, and complete source-keyed work tasks.
|
||||||
|
- Made link synchronization idempotent: linked records cannot be downgraded, stale pending callbacks cannot erase failures, and duplicate callbacks preserve the confirmed link timestamp.
|
||||||
|
- Added the `Immunization Record` task-board tab for completed vaccinations and local M2 registration using the exact `ImmunizationRecord` HI type.
|
||||||
|
- Task-board Immunization registration forces `m2_only=1`; it stores the FHIR record for M2 discovery/linking and does not call a Push API.
|
||||||
|
- Added focused tests for callback identifier parsing and status transition behavior.
|
||||||
|
|
||||||
## Storestock Module — 2026-04-19
|
## Storestock Module — 2026-04-19
|
||||||
|
|
||||||
Ported Hospital Stock Management (Storestock) module from CI3 to CI4.
|
Ported Hospital Stock Management (Storestock) module from CI3 to CI4.
|
||||||
|
|
|
||||||
|
|
@ -1955,20 +1955,16 @@ class AbdmGateway extends BaseController
|
||||||
'period_start' => $reportedAt,
|
'period_start' => $reportedAt,
|
||||||
];
|
];
|
||||||
|
|
||||||
$pdfAttachment = $this->loadLatestLabPdfAttachment(
|
$pdfAttachment = $this->buildDiagnosticDigitalSharePdf(
|
||||||
(int) ($labReq->charge_id ?? 0),
|
$patientRow,
|
||||||
(int) ($labReq->lab_type ?? 0),
|
$abhaIdentity,
|
||||||
$labReqId,
|
$diagnosticReport,
|
||||||
$testTitle !== '' ? ($testTitle . ' PDF Report') : 'Lab Report PDF'
|
$observations,
|
||||||
|
$practitioner,
|
||||||
|
$hospitalProfile,
|
||||||
|
$labReqId
|
||||||
);
|
);
|
||||||
|
|
||||||
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, $practitioner, $organization, $encounter, $pdfAttachment);
|
$bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, $practitioner, $organization, $encounter, $pdfAttachment);
|
||||||
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
|
||||||
|
|
@ -2180,20 +2176,17 @@ class AbdmGateway extends BaseController
|
||||||
];
|
];
|
||||||
|
|
||||||
$fhir = new FhirR4Builder();
|
$fhir = new FhirR4Builder();
|
||||||
$pdfAttachment = $this->loadLatestLabPdfAttachment(
|
$abhaIdentity = $this->resolvePatientAbhaIdentity($patientId, $abhaId);
|
||||||
(int) ($labReq->charge_id ?? 0),
|
$pdfAttachment = $this->buildDiagnosticDigitalSharePdf(
|
||||||
(int) ($labReq->lab_type ?? 0),
|
$patientRow,
|
||||||
$labReqId,
|
$abhaIdentity,
|
||||||
$testTitle !== '' ? ($testTitle . ' PDF Report') : 'Lab Report PDF'
|
$diagnosticReport,
|
||||||
|
$observations,
|
||||||
|
$practitioner,
|
||||||
|
$hospitalProfile,
|
||||||
|
$labReqId
|
||||||
);
|
);
|
||||||
|
|
||||||
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, $practitioner, $organization, $encounter, $pdfAttachment);
|
$bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, $practitioner, $organization, $encounter, $pdfAttachment);
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
|
|
@ -4260,6 +4253,186 @@ class AbdmGateway extends BaseController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $patientRow
|
||||||
|
* @param array{abha_id:string,abha_address:string} $abhaIdentity
|
||||||
|
* @param array<string,mixed> $diagnosticReport
|
||||||
|
* @param array<int,array<string,mixed>> $observations
|
||||||
|
* @param array<string,mixed>|null $practitioner
|
||||||
|
* @param array<string,mixed> $hospitalProfile
|
||||||
|
*/
|
||||||
|
private function buildDiagnosticDigitalSharePdf(
|
||||||
|
array $patientRow,
|
||||||
|
array $abhaIdentity,
|
||||||
|
array $diagnosticReport,
|
||||||
|
array $observations,
|
||||||
|
?array $practitioner,
|
||||||
|
array $hospitalProfile,
|
||||||
|
int $labReqId
|
||||||
|
): ?array {
|
||||||
|
$escape = static fn (string $value): string => htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
$contentHtml = trim((string) ($diagnosticReport['report_html'] ?? ''));
|
||||||
|
if ($contentHtml === '' && $observations !== []) {
|
||||||
|
$rows = '';
|
||||||
|
foreach ($observations as $observation) {
|
||||||
|
$value = trim((string) ($observation['value'] ?? ''));
|
||||||
|
$unit = trim((string) ($observation['unit'] ?? ''));
|
||||||
|
$range = trim(implode(' - ', array_filter([
|
||||||
|
trim((string) ($observation['ref_low'] ?? '')),
|
||||||
|
trim((string) ($observation['ref_high'] ?? '')),
|
||||||
|
], static fn (string $part): bool => $part !== '')));
|
||||||
|
$rows .= '<tr><td>' . $escape((string) ($observation['test_name'] ?? 'Test')) . '</td>'
|
||||||
|
. '<td>' . $escape(trim($value . ' ' . $unit)) . '</td>'
|
||||||
|
. '<td>' . $escape($range) . '</td>'
|
||||||
|
. '<td>' . $escape((string) ($observation['interpretation'] ?? '')) . '</td></tr>';
|
||||||
|
}
|
||||||
|
$contentHtml = '<table class="share-content-table"><thead><tr><th>Test</th><th>Result</th><th>Reference</th>'
|
||||||
|
. '<th>Flag</th></tr></thead><tbody>' . $rows . '</tbody></table>';
|
||||||
|
}
|
||||||
|
if ($contentHtml === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$conclusion = trim((string) ($diagnosticReport['conclusion'] ?? ''));
|
||||||
|
if ($conclusion !== '') {
|
||||||
|
$contentHtml .= '<div class="share-conclusion"><strong>Conclusion:</strong> ' . $escape($conclusion) . '</div>';
|
||||||
|
}
|
||||||
|
$title = trim((string) ($diagnosticReport['title'] ?? 'Diagnostic Report')) ?: 'Diagnostic Report';
|
||||||
|
$abha = trim(implode(' / ', array_filter([
|
||||||
|
$abhaIdentity['abha_id'],
|
||||||
|
$abhaIdentity['abha_address'],
|
||||||
|
], static fn (string $value): bool => $value !== '')));
|
||||||
|
|
||||||
|
return $this->renderDigitalSharePdf($contentHtml, 'Digital-Share-LAB-' . $labReqId, [
|
||||||
|
'document_title' => $title,
|
||||||
|
'patient_name' => $this->patientDisplayName($patientRow),
|
||||||
|
'uhid' => trim((string) ($patientRow['p_code'] ?? '')),
|
||||||
|
'abha' => $abha,
|
||||||
|
'record_label' => 'Diagnostic Report',
|
||||||
|
'record_code' => 'LAB-' . $labReqId,
|
||||||
|
'reported_at' => (string) ($diagnosticReport['reported_at'] ?? ''),
|
||||||
|
'practitioner' => trim((string) ($practitioner['name'] ?? '')),
|
||||||
|
'facility_name' => trim((string) ($hospitalProfile['name'] ?? '')),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the stable PDF layout used for digital/PHR sharing.
|
||||||
|
*
|
||||||
|
* @param array<string,string> $metadata
|
||||||
|
* @return array{content_type:string,data_base64:string,title:string,size:int,hash:string}|null
|
||||||
|
*/
|
||||||
|
private function renderDigitalSharePdf(string $contentHtml, string $fileStem, array $metadata): ?array
|
||||||
|
{
|
||||||
|
$contentHtml = trim($contentHtml);
|
||||||
|
if ($contentHtml === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$escape = static fn (string $value): string => htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
$hospital = $this->getHospitalProfileForFhir();
|
||||||
|
$hospitalName = trim((string) ($metadata['facility_name'] ?? $hospital['name'] ?? '')) ?: 'Healthcare Facility';
|
||||||
|
$hospitalAddress = trim((string) ($hospital['address'] ?? ''));
|
||||||
|
$hospitalContact = trim(implode(' | ', array_filter([
|
||||||
|
trim((string) ($hospital['phone'] ?? '')),
|
||||||
|
trim((string) ($hospital['email'] ?? '')),
|
||||||
|
], static fn (string $value): bool => $value !== '')));
|
||||||
|
$logoHtml = '';
|
||||||
|
$logoName = trim((string) ($hospital['logo'] ?? ''));
|
||||||
|
foreach (array_filter([
|
||||||
|
$logoName !== '' ? FCPATH . 'assets/images/' . ltrim($logoName, '/\\') : '',
|
||||||
|
$logoName !== '' ? FCPATH . 'assets/img/' . ltrim($logoName, '/\\') : '',
|
||||||
|
FCPATH . 'assets/img/logo.png',
|
||||||
|
]) as $logoPath) {
|
||||||
|
if (is_file($logoPath)) {
|
||||||
|
$logoHtml = '<img src="' . $escape(str_replace('\\', '/', $logoPath)) . '" style="height:52px;max-width:110px">';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$patientName = trim((string) ($metadata['patient_name'] ?? '')) ?: 'Patient';
|
||||||
|
$uhid = trim((string) ($metadata['uhid'] ?? ''));
|
||||||
|
$abha = trim((string) ($metadata['abha'] ?? ''));
|
||||||
|
$recordCode = trim((string) ($metadata['record_code'] ?? ''));
|
||||||
|
$footerIdentity = trim(implode(' | ', array_filter([$patientName, $uhid !== '' ? 'UHID: ' . $uhid : ''])));
|
||||||
|
$identityFields = [
|
||||||
|
['Patient', $patientName],
|
||||||
|
['UHID', $uhid],
|
||||||
|
['ABHA', $abha],
|
||||||
|
[trim((string) ($metadata['record_label'] ?? 'Record')) ?: 'Record', $recordCode],
|
||||||
|
['Reported / Issued', trim((string) ($metadata['reported_at'] ?? date('Y-m-d H:i:s')))],
|
||||||
|
['Practitioner', trim((string) ($metadata['practitioner'] ?? ''))],
|
||||||
|
];
|
||||||
|
$identityFields = array_values(array_filter(
|
||||||
|
$identityFields,
|
||||||
|
static fn (array $field): bool => trim((string) ($field[1] ?? '')) !== ''
|
||||||
|
));
|
||||||
|
$identityRows = '';
|
||||||
|
for ($index = 0, $count = count($identityFields); $index < $count; $index += 2) {
|
||||||
|
$identityRows .= '<tr>';
|
||||||
|
for ($column = 0; $column < 2; $column++) {
|
||||||
|
$field = $identityFields[$index + $column] ?? null;
|
||||||
|
if ($field === null) {
|
||||||
|
$identityRows .= '<td></td>';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$identityRows .= '<td><span class="field-label">' . $escape((string) $field[0]) . ':</span> '
|
||||||
|
. '<strong>' . $escape((string) $field[1]) . '</strong></td>';
|
||||||
|
}
|
||||||
|
$identityRows .= '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$css = 'body{font-family:DejaVu Sans,sans-serif;color:#17212b;font-size:10.5px}'
|
||||||
|
. '.brand{width:100%;border-collapse:collapse;border-bottom:2px solid #176b87;margin-bottom:10px}.brand td{vertical-align:middle;padding:0 0 9px 0}'
|
||||||
|
. '.brand-logo{width:125px}.facility{font-size:19px;font-weight:bold;color:#123b4a}.facility-meta{font-size:9px;color:#52606d;margin-top:2px}'
|
||||||
|
. '.document-title{font-size:14px;font-weight:bold;text-align:right;color:#176b87}.share-identity{width:100%;border-collapse:collapse;margin-bottom:14px}'
|
||||||
|
. '.share-identity td{width:50%;border:1px solid #cad7de;background:#f5f9fb;padding:6px;vertical-align:top}.field-label{color:#52606d}'
|
||||||
|
. '.share-content-table{width:100%;border-collapse:collapse}'
|
||||||
|
. '.share-content-table th,.share-content-table td{border:1px solid #cbd5df;padding:6px}.share-content-table th{background:#eaf2f5;text-align:left}'
|
||||||
|
. '.items{width:100%;border-collapse:collapse}.items th,.items td{border:1px solid #cbd5df;padding:7px}.items th{background:#eef2f6;text-align:left}'
|
||||||
|
. '.num{text-align:right}.totals{margin-top:12px;text-align:right;font-size:12px;line-height:1.6}'
|
||||||
|
. '.share-conclusion{margin-top:14px;padding:9px;border-left:3px solid #176b87;background:#f5f9fb}table{border-collapse:collapse;max-width:100%}img{max-width:100%}'
|
||||||
|
. 'h1,h2,h3{margin:8px 0}p{margin:5px 0}';
|
||||||
|
$bodyHtml = '<table class="brand"><tr><td class="brand-logo">' . $logoHtml . '</td><td><div class="facility">' . $escape($hospitalName) . '</div>'
|
||||||
|
. '<div class="facility-meta">' . $escape($hospitalAddress) . '</div><div class="facility-meta">' . $escape($hospitalContact) . '</div></td>'
|
||||||
|
. '<td class="document-title">' . $escape((string) ($metadata['document_title'] ?? 'Digital Health Record')) . '</td></tr></table>'
|
||||||
|
. '<table class="share-identity">' . $identityRows . '</table>' . $contentHtml;
|
||||||
|
|
||||||
|
$mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
|
||||||
|
if (! is_dir($mpdfTempDir)) {
|
||||||
|
@mkdir($mpdfTempDir, 0755, true);
|
||||||
|
}
|
||||||
|
$mpdf = new Mpdf([
|
||||||
|
'format' => 'A4',
|
||||||
|
'margin_top' => 10,
|
||||||
|
'margin_bottom' => 16,
|
||||||
|
'margin_left' => 10,
|
||||||
|
'margin_right' => 10,
|
||||||
|
'tempDir' => $mpdfTempDir,
|
||||||
|
'default_font' => 'freeserif',
|
||||||
|
]);
|
||||||
|
$mpdf->SetHTMLFooter('<div style="border-top:1px solid #9fb3bd;padding-top:5px;font-size:8px;color:#52606d">'
|
||||||
|
. $escape($footerIdentity) . '<span style="float:right">Page {PAGENO} of {nbpg}</span></div>');
|
||||||
|
$mpdf->WriteHTML($css, HTMLParserMode::HEADER_CSS);
|
||||||
|
$mpdf->WriteHTML($bodyHtml, HTMLParserMode::HTML_BODY);
|
||||||
|
$bytes = $mpdf->Output($fileStem . '.pdf', 'S');
|
||||||
|
if (! is_string($bytes) || ! str_starts_with($bytes, '%PDF-')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'content_type' => 'application/pdf',
|
||||||
|
'data_base64' => base64_encode($bytes),
|
||||||
|
'title' => $fileStem . '.pdf',
|
||||||
|
'size' => strlen($bytes),
|
||||||
|
'hash' => base64_encode(sha1($bytes, true)),
|
||||||
|
];
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function resolveStoredFileAbsolutePath(string $fullPath): string
|
private function resolveStoredFileAbsolutePath(string $fullPath): string
|
||||||
{
|
{
|
||||||
$normalized = str_replace('\\', '/', trim($fullPath));
|
$normalized = str_replace('\\', '/', trim($fullPath));
|
||||||
|
|
@ -5444,23 +5617,44 @@ class AbdmGateway extends BaseController
|
||||||
/**
|
/**
|
||||||
* Query hospital_setting for FHIR-required hospital profile fields.
|
* Query hospital_setting for FHIR-required hospital profile fields.
|
||||||
*
|
*
|
||||||
* @return array{name: string, hfr_id: string}
|
* @return array{name:string,hfr_id:string,address:string,phone:string,email:string,logo:string}
|
||||||
*/
|
*/
|
||||||
private function getHospitalProfileForFhir(): array
|
private function getHospitalProfileForFhir(): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$rows = $this->db->table('hospital_setting')
|
$rows = $this->db->table('hospital_setting')
|
||||||
->select('s_name, s_value')
|
->select('s_name, s_value')
|
||||||
->whereIn('s_name', ['ABDM_HMS_NAME', 'ABDM_HFR_ID', 'H_Name'])
|
->whereIn('s_name', [
|
||||||
|
'ABDM_HMS_NAME', 'ABDM_HFR_ID', 'H_Name', 'H_address_1', 'H_address_2',
|
||||||
|
'H_phone_No', 'H_Email', 'H_logo',
|
||||||
|
])
|
||||||
->get()->getResultArray();
|
->get()->getResultArray();
|
||||||
$map = array_column($rows, 's_value', 's_name');
|
$map = array_column($rows, 's_value', 's_name');
|
||||||
$name = trim((string) ($map['ABDM_HMS_NAME'] ?? $map['H_Name'] ?? ''));
|
$name = trim((string) ($map['ABDM_HMS_NAME'] ?? $map['H_Name'] ?? ''));
|
||||||
$hfrId = trim((string) ($map['ABDM_HFR_ID'] ?? ''));
|
$hfrId = trim((string) ($map['ABDM_HFR_ID'] ?? ''));
|
||||||
|
$address = trim(implode(', ', array_filter([
|
||||||
|
trim((string) ($map['H_address_1'] ?? '')),
|
||||||
|
trim((string) ($map['H_address_2'] ?? '')),
|
||||||
|
], static fn (string $value): bool => $value !== '')));
|
||||||
|
$phone = trim((string) ($map['H_phone_No'] ?? ''));
|
||||||
|
$email = trim((string) ($map['H_Email'] ?? ''));
|
||||||
|
$logo = trim((string) ($map['H_logo'] ?? ''));
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$name = '';
|
$name = '';
|
||||||
$hfrId = '';
|
$hfrId = '';
|
||||||
|
$address = '';
|
||||||
|
$phone = '';
|
||||||
|
$email = '';
|
||||||
|
$logo = '';
|
||||||
}
|
}
|
||||||
return ['name' => $name, 'hfr_id' => $hfrId];
|
return [
|
||||||
|
'name' => $name,
|
||||||
|
'hfr_id' => $hfrId,
|
||||||
|
'address' => $address,
|
||||||
|
'phone' => $phone,
|
||||||
|
'email' => $email,
|
||||||
|
'logo' => $logo,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -6476,11 +6670,29 @@ class AbdmGateway extends BaseController
|
||||||
if (! empty($patientRow['dob'])) {
|
if (! empty($patientRow['dob'])) {
|
||||||
$patient['birthDate'] = date('Y-m-d', strtotime((string) $patientRow['dob']));
|
$patient['birthDate'] = date('Y-m-d', strtotime((string) $patientRow['dob']));
|
||||||
}
|
}
|
||||||
if ($abhaId !== '') {
|
$patient['identifier'] = [];
|
||||||
$patient['identifier'] = [[
|
$uhid = trim((string) ($patientRow['p_code'] ?? ''));
|
||||||
'system' => str_contains($abhaId, '@') ? 'https://healthid.ndhm.gov.in/abha-address' : 'https://healthid.ndhm.gov.in',
|
if ($uhid !== '') {
|
||||||
'value' => $abhaId,
|
$patient['identifier'][] = [
|
||||||
]];
|
'system' => 'https://hms.local/fhir/uhid',
|
||||||
|
'value' => $uhid,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$abhaIdentity = $this->resolvePatientAbhaIdentity($patientId, $abhaId);
|
||||||
|
if ($abhaIdentity['abha_id'] !== '') {
|
||||||
|
$patient['identifier'][] = [
|
||||||
|
'system' => 'https://healthid.ndhm.gov.in',
|
||||||
|
'value' => $abhaIdentity['abha_id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($abhaIdentity['abha_address'] !== '') {
|
||||||
|
$patient['identifier'][] = [
|
||||||
|
'system' => 'https://healthid.ndhm.gov.in/abha-address',
|
||||||
|
'value' => $abhaIdentity['abha_address'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($patient['identifier'] === []) {
|
||||||
|
unset($patient['identifier']);
|
||||||
}
|
}
|
||||||
return $patient;
|
return $patient;
|
||||||
}
|
}
|
||||||
|
|
@ -6791,33 +7003,43 @@ class AbdmGateway extends BaseController
|
||||||
if ($invoiceRows === '') {
|
if ($invoiceRows === '') {
|
||||||
$invoiceRows = '<tr><td colspan="5">No line items</td></tr>';
|
$invoiceRows = '<tr><td colspan="5">No line items</td></tr>';
|
||||||
}
|
}
|
||||||
$invoicePdfHtml = '<style>'
|
$invoicePdfHtml = '<table class="items"><thead><tr><th>#</th><th>Item</th><th class="num">Qty</th><th class="num">Rate (INR)</th>'
|
||||||
. 'body{font-family:DejaVu Sans,sans-serif;color:#17212b;font-size:11px}'
|
|
||||||
. 'h1{font-size:20px;margin:0 0 3px}.muted{color:#52606d}.meta{width:100%;margin:18px 0;border-collapse:collapse}'
|
|
||||||
. '.meta td{width:50%;vertical-align:top;border:1px solid #d7dee5;padding:9px}.label{font-weight:bold}'
|
|
||||||
. 'table.items{width:100%;border-collapse:collapse}.items th,.items td{border:1px solid #cbd5df;padding:7px}'
|
|
||||||
. '.items th{background:#eef2f6;text-align:left}.num{text-align:right}.totals{margin-top:12px;text-align:right;font-size:12px}'
|
|
||||||
. '</style><h1>' . $escape($hospitalName) . '</h1><div class="muted">Invoice ' . $escape($invoiceNumber) . '</div>'
|
|
||||||
. '<table class="meta"><tr><td><span class="label">Patient:</span> ' . $escape($patientName)
|
|
||||||
. '<br><span class="label">Gender:</span> ' . $escape((string) ($patient['gender'] ?? ''))
|
|
||||||
. '<br><span class="label">Date of Birth:</span> ' . $escape((string) ($patient['birthDate'] ?? ''))
|
|
||||||
. '</td><td><span class="label">Visit:</span> ' . $escape($invoiceTypeDisplay)
|
|
||||||
. '<br><span class="label">Visit Date:</span> ' . $escape($encounterStart)
|
|
||||||
. '<br><span class="label">Practitioner:</span> ' . $escape($practitionerName)
|
|
||||||
. '<br><span class="label">Invoice Date:</span> ' . $escape($invoiceDate) . '</td></tr></table>'
|
|
||||||
. '<table class="items"><thead><tr><th>#</th><th>Item</th><th class="num">Qty</th><th class="num">Rate (INR)</th>'
|
|
||||||
. '<th class="num">Amount (INR)</th></tr></thead><tbody>' . $invoiceRows . '</tbody></table>'
|
. '<th class="num">Amount (INR)</th></tr></thead><tbody>' . $invoiceRows . '</tbody></table>'
|
||||||
. '<div class="totals"><div>Gross: INR ' . number_format($grossAmount, 2) . '</div><div><strong>Net: INR '
|
. '<div class="totals"><div>Gross: INR ' . number_format($grossAmount, 2) . '</div><div><strong>Net: INR '
|
||||||
. number_format($netAmount, 2) . '</strong></div></div>';
|
. number_format($netAmount, 2) . '</strong></div></div>';
|
||||||
$invoicePdf = $this->renderReportHtmlToPdfAttachment($invoicePdfHtml, $pdfFileStem);
|
$patientUhid = '';
|
||||||
if ($invoicePdf === null) {
|
$patientAbhaValues = [];
|
||||||
throw new \RuntimeException('Unable to render invoice PDF attachment.');
|
foreach ((array) ($patient['identifier'] ?? []) as $identifier) {
|
||||||
|
$system = (string) ($identifier['system'] ?? '');
|
||||||
|
if (str_contains($system, '/uhid')) {
|
||||||
|
$patientUhid = trim((string) ($identifier['value'] ?? ''));
|
||||||
|
} elseif (str_contains($system, 'healthid.ndhm.gov.in')) {
|
||||||
|
$value = trim((string) ($identifier['value'] ?? ''));
|
||||||
|
if ($value !== '') {
|
||||||
|
$patientAbhaValues[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$invoicePdfBytes = base64_decode((string) $invoicePdf['data_base64'], true);
|
$patientAbha = implode(' / ', array_values(array_unique($patientAbhaValues)));
|
||||||
if (! is_string($invoicePdfBytes) || ! str_starts_with($invoicePdfBytes, '%PDF-')) {
|
$invoicePdf = $this->renderDigitalSharePdf(
|
||||||
throw new \RuntimeException('Generated invoice PDF attachment is invalid.');
|
$invoicePdfHtml,
|
||||||
}
|
$pdfFileStem,
|
||||||
$documentReference = [
|
[
|
||||||
|
'document_title' => 'Invoice ' . $invoiceNumber,
|
||||||
|
'patient_name' => $patientName,
|
||||||
|
'uhid' => $patientUhid,
|
||||||
|
'abha' => $patientAbha,
|
||||||
|
'record_label' => $invoiceTypeDisplay . ' / Invoice',
|
||||||
|
'record_code' => $invoiceNumber,
|
||||||
|
'reported_at' => $invoiceDate,
|
||||||
|
'practitioner' => $practitionerName,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$documentReference = null;
|
||||||
|
if ($invoicePdf !== null) {
|
||||||
|
$invoicePdfBytes = base64_decode((string) $invoicePdf['data_base64'], true);
|
||||||
|
if (is_string($invoicePdfBytes) && str_starts_with($invoicePdfBytes, '%PDF-')) {
|
||||||
|
$documentReference = [
|
||||||
'resourceType' => 'DocumentReference',
|
'resourceType' => 'DocumentReference',
|
||||||
'id' => $documentReferenceId,
|
'id' => $documentReferenceId,
|
||||||
'meta' => [
|
'meta' => [
|
||||||
|
|
@ -6854,7 +7076,9 @@ class AbdmGateway extends BaseController
|
||||||
'encounter' => [['reference' => $encounterRef]],
|
'encounter' => [['reference' => $encounterRef]],
|
||||||
'related' => [['reference' => $invoiceRef, 'display' => 'Invoice ' . $invoiceNumber]],
|
'related' => [['reference' => $invoiceRef, 'display' => 'Invoice ' . $invoiceNumber]],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$encounter = [
|
$encounter = [
|
||||||
'resourceType' => 'Encounter',
|
'resourceType' => 'Encounter',
|
||||||
|
|
@ -6956,7 +7180,10 @@ class AbdmGateway extends BaseController
|
||||||
]],
|
]],
|
||||||
];
|
];
|
||||||
|
|
||||||
$resources = [$composition, $patient, $organization, $encounter, $invoiceResource, $documentReference];
|
$resources = [$composition, $patient, $organization, $encounter, $invoiceResource];
|
||||||
|
if ($documentReference !== null) {
|
||||||
|
$resources[] = $documentReference;
|
||||||
|
}
|
||||||
if ($practitioner !== null) {
|
if ($practitioner !== null) {
|
||||||
$resources[] = $practitioner;
|
$resources[] = $practitioner;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -988,13 +988,30 @@ class Ipd extends BaseController
|
||||||
$ipdCode = (string) ($data['ipd_info']->ipd_code ?? ('IPD-' . $ipdId));
|
$ipdCode = (string) ($data['ipd_info']->ipd_code ?? ('IPD-' . $ipdId));
|
||||||
$fileName = 'IPD_Bill_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $ipdCode) . '_M' . $mode . '.pdf';
|
$fileName = 'IPD_Bill_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $ipdCode) . '_M' . $mode . '.pdf';
|
||||||
|
|
||||||
|
$pdfBinary = $mpdf->Output($fileName, 'S');
|
||||||
|
$this->cacheAbdmIpdBillPdf($ipdId, $pdfBinary);
|
||||||
|
|
||||||
return $this->response
|
return $this->response
|
||||||
->setHeader('Content-Type', 'application/pdf')
|
->setHeader('Content-Type', 'application/pdf')
|
||||||
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
|
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
|
||||||
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||||
->setHeader('Pragma', 'no-cache')
|
->setHeader('Pragma', 'no-cache')
|
||||||
->setHeader('Expires', '0')
|
->setHeader('Expires', '0')
|
||||||
->setBody($mpdf->Output($fileName, 'S'));
|
->setBody($pdfBinary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cacheAbdmIpdBillPdf(int $ipdId, string $pdfBinary): void
|
||||||
|
{
|
||||||
|
if ($ipdId <= 0 || $pdfBinary === '' || ! str_starts_with($pdfBinary, '%PDF-')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId;
|
||||||
|
if (! is_dir($directory) && ! mkdir($directory, 0755, true) && ! is_dir($directory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf', $pdfBinary, LOCK_EX);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function showIpdForm(int $ipdId, int $formNo = 1)
|
public function showIpdForm(int $ipdId, int $formNo = 1)
|
||||||
|
|
|
||||||
|
|
@ -7140,10 +7140,14 @@ class Ipd_discharge extends BaseController
|
||||||
try {
|
try {
|
||||||
$headerHtml = '';
|
$headerHtml = '';
|
||||||
if ($withHeader) {
|
if ($withHeader) {
|
||||||
$headerHtml = $this->sanitizeDischargePdfHtml(
|
$configuredHeader = $this->applyDischargeTemplateTokens(
|
||||||
$this->applyDischargeTemplateTokens(trim((string) ($templateSettings['header_html'] ?? '')), $templateTokenVars),
|
trim((string) ($templateSettings['header_html'] ?? '')),
|
||||||
false
|
$templateTokenVars
|
||||||
);
|
);
|
||||||
|
if ($this->isDischargeHeaderBlank($configuredHeader)) {
|
||||||
|
$configuredHeader = $this->buildDefaultDischargeHeader($templateTokenVars);
|
||||||
|
}
|
||||||
|
$headerHtml = $this->sanitizeDischargePdfHtml($configuredHeader, false);
|
||||||
$headerHtml = mpdf_normalize_font_weight_css($headerHtml);
|
$headerHtml = mpdf_normalize_font_weight_css($headerHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -7192,6 +7196,7 @@ class Ipd_discharge extends BaseController
|
||||||
|
|
||||||
return $mpdf->Output($fileName, Destination::STRING_RETURN);
|
return $mpdf->Output($fileName, Destination::STRING_RETURN);
|
||||||
});
|
});
|
||||||
|
$this->cacheAbdmIpdPdf($ipdId, 'discharge-summary.pdf', $pdfBinary);
|
||||||
|
|
||||||
// Clear any output buffers to prevent corruption of binary PDF data
|
// Clear any output buffers to prevent corruption of binary PDF data
|
||||||
while (ob_get_level() > 0) {
|
while (ob_get_level() > 0) {
|
||||||
|
|
@ -7719,6 +7724,46 @@ class Ipd_discharge extends BaseController
|
||||||
. '</body></html>';
|
. '</body></html>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function cacheAbdmIpdPdf(int $ipdId, string $fileName, string $pdfBinary): void
|
||||||
|
{
|
||||||
|
if ($ipdId <= 0 || $pdfBinary === '' || ! str_starts_with($pdfBinary, '%PDF-')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId;
|
||||||
|
if (! is_dir($directory) && ! mkdir($directory, 0755, true) && ! is_dir($directory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($directory . DIRECTORY_SEPARATOR . $fileName, $pdfBinary, LOCK_EX);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isDischargeHeaderBlank(string $headerHtml): bool
|
||||||
|
{
|
||||||
|
$withoutMedia = (string) preg_replace('/<(img|svg)\b[^>]*>[\s\S]*?<\/\1>|<img\b[^>]*\/?\s*>/i', 'media', $headerHtml);
|
||||||
|
$visibleText = html_entity_decode(strip_tags($withoutMedia), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
|
||||||
|
return trim((string) preg_replace('/\s+/u', ' ', $visibleText)) === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,string> $tokens */
|
||||||
|
private function buildDefaultDischargeHeader(array $tokens): string
|
||||||
|
{
|
||||||
|
$logoPath = trim((string) ($tokens['H_logo_abs'] ?? ''));
|
||||||
|
$logoHtml = $logoPath !== '' && is_file(html_entity_decode($logoPath, ENT_QUOTES | ENT_HTML5, 'UTF-8'))
|
||||||
|
? '<td style="width:70px;vertical-align:middle;"><img src="' . $logoPath . '" style="max-width:60px;max-height:60px;"></td>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return '<table style="width:100%;border-collapse:collapse;border-bottom:1px solid #64748b;padding-bottom:6px;">'
|
||||||
|
. '<tr>' . $logoHtml
|
||||||
|
. '<td style="vertical-align:middle;text-align:center;">'
|
||||||
|
. '<div style="font-family:freeserif,serif;font-size:18pt;font-weight:bold;">' . ($tokens['H_Name'] ?? '') . '</div>'
|
||||||
|
. '<div style="font-family:freeserif,serif;font-size:9pt;">' . ($tokens['hospital_address'] ?? '') . '</div>'
|
||||||
|
. '<div style="font-family:freeserif,serif;font-size:9pt;">Phone: ' . ($tokens['H_phone_No'] ?? '')
|
||||||
|
. (($tokens['H_Email'] ?? '') !== '' ? ' | Email: ' . $tokens['H_Email'] : '') . '</div>'
|
||||||
|
. '</td><td style="width:70px;"></td></tr></table>';
|
||||||
|
}
|
||||||
|
|
||||||
public function show_file3(int $ipdId)
|
public function show_file3(int $ipdId)
|
||||||
{
|
{
|
||||||
return $this->show_discharge($ipdId, 3);
|
return $this->show_discharge($ipdId, 3);
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,14 @@ class FhirDocumentBuilder
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $values */
|
||||||
|
public function updateBundleMeta(array $values): self
|
||||||
|
{
|
||||||
|
$this->bundleMeta = array_replace($this->bundleMeta, $values);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string,mixed> $composition
|
* @param array<string,mixed> $composition
|
||||||
*/
|
*/
|
||||||
|
|
@ -40,6 +48,16 @@ class FhirDocumentBuilder
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $values */
|
||||||
|
public function updateComposition(array $values): self
|
||||||
|
{
|
||||||
|
if (is_array($this->composition)) {
|
||||||
|
$this->composition = array_replace($this->composition, $values);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string,mixed> $resource
|
* @param array<string,mixed> $resource
|
||||||
*/
|
*/
|
||||||
|
|
@ -120,6 +138,12 @@ class FhirDocumentBuilder
|
||||||
return $this->addResource($resource);
|
return $this->addResource($resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $resource */
|
||||||
|
public function addDocumentReference(array $resource): self
|
||||||
|
{
|
||||||
|
return $this->addResource($resource);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string,mixed>
|
* @return array<string,mixed>
|
||||||
*/
|
*/
|
||||||
|
|
@ -136,10 +160,71 @@ class FhirDocumentBuilder
|
||||||
$entries[] = $entry;
|
$entries[] = $entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
$bundle['entry'] = $entries;
|
$bundle['entry'] = $this->normalizeEntryReferences($entries);
|
||||||
return $this->stripNulls($bundle);
|
return $this->stripNulls($bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A urn:uuid fullUrl must contain an RFC 4122 UUID. Resource ids remain
|
||||||
|
* human-readable while all document references use deterministic UUID URNs.
|
||||||
|
*
|
||||||
|
* @param array<int,array<string,mixed>> $entries
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
private function normalizeEntryReferences(array $entries): array
|
||||||
|
{
|
||||||
|
$referenceMap = [];
|
||||||
|
foreach ($entries as $entry) {
|
||||||
|
$resource = (array) ($entry['resource'] ?? []);
|
||||||
|
$id = (string) ($resource['id'] ?? '');
|
||||||
|
if ($id === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$referenceMap['urn:uuid:' . $id] = 'urn:uuid:' . $this->uuidForResource($resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($entries as &$entry) {
|
||||||
|
$resource = (array) ($entry['resource'] ?? []);
|
||||||
|
$id = (string) ($resource['id'] ?? '');
|
||||||
|
if ($id !== '') {
|
||||||
|
$entry['fullUrl'] = $referenceMap['urn:uuid:' . $id];
|
||||||
|
}
|
||||||
|
$entry['resource'] = $this->rewriteReferences($resource, $referenceMap);
|
||||||
|
}
|
||||||
|
unset($entry);
|
||||||
|
|
||||||
|
return $entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $resource */
|
||||||
|
private function uuidForResource(array $resource): string
|
||||||
|
{
|
||||||
|
$hex = md5((string) ($resource['resourceType'] ?? 'Resource') . '/' . (string) ($resource['id'] ?? ''));
|
||||||
|
return substr($hex, 0, 8) . '-' . substr($hex, 8, 4) . '-4' . substr($hex, 13, 3)
|
||||||
|
. '-a' . substr($hex, 17, 3) . '-' . substr($hex, 20, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
* @param array<string,string> $referenceMap
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
private function rewriteReferences($value, array $referenceMap)
|
||||||
|
{
|
||||||
|
if (is_string($value)) {
|
||||||
|
return $referenceMap[$value] ?? $value;
|
||||||
|
}
|
||||||
|
if (! is_array($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($value as $key => $nested) {
|
||||||
|
$value[$key] = $this->rewriteReferences($nested, $referenceMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string,mixed> $resource
|
* @param array<string,mixed> $resource
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ abstract class AbstractModuleFhirGenerator
|
||||||
$patient = [
|
$patient = [
|
||||||
'resourceType' => 'Patient',
|
'resourceType' => 'Patient',
|
||||||
'id' => 'patient-' . $patientId,
|
'id' => 'patient-' . $patientId,
|
||||||
|
'meta' => ['profile' => [
|
||||||
|
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Patient',
|
||||||
|
]],
|
||||||
'identifier' => [
|
'identifier' => [
|
||||||
[
|
[
|
||||||
'system' => 'https://hms.local/patient-id',
|
'system' => 'https://hms.local/patient-id',
|
||||||
|
|
@ -116,11 +119,21 @@ abstract class AbstractModuleFhirGenerator
|
||||||
if ($id === '') {
|
if ($id === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
$classCode = strtoupper(trim((string) ($enc['class_code'] ?? 'AMB')));
|
||||||
|
$classDisplay = $classCode === 'IMP' ? 'inpatient encounter' : 'ambulatory';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'resourceType' => 'Encounter',
|
'resourceType' => 'Encounter',
|
||||||
'id' => 'encounter-' . $id,
|
'id' => 'encounter-' . $id,
|
||||||
|
'meta' => ['profile' => [
|
||||||
|
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Encounter',
|
||||||
|
]],
|
||||||
'status' => 'finished',
|
'status' => 'finished',
|
||||||
|
'class' => [
|
||||||
|
'system' => 'http://terminology.hl7.org/CodeSystem/v3-ActCode',
|
||||||
|
'code' => $classCode,
|
||||||
|
'display' => $classDisplay,
|
||||||
|
],
|
||||||
'subject' => [
|
'subject' => [
|
||||||
'reference' => 'urn:uuid:patient-' . (string) ($source['patient']['id'] ?? ''),
|
'reference' => 'urn:uuid:patient-' . (string) ($source['patient']['id'] ?? ''),
|
||||||
],
|
],
|
||||||
|
|
@ -152,6 +165,9 @@ abstract class AbstractModuleFhirGenerator
|
||||||
return [
|
return [
|
||||||
'resourceType' => 'Practitioner',
|
'resourceType' => 'Practitioner',
|
||||||
'id' => 'practitioner-' . ($id !== '' ? $id : md5($name)),
|
'id' => 'practitioner-' . ($id !== '' ? $id : md5($name)),
|
||||||
|
'meta' => ['profile' => [
|
||||||
|
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner',
|
||||||
|
]],
|
||||||
'name' => [[
|
'name' => [[
|
||||||
'text' => $name,
|
'text' => $name,
|
||||||
]],
|
]],
|
||||||
|
|
@ -179,6 +195,9 @@ abstract class AbstractModuleFhirGenerator
|
||||||
return [
|
return [
|
||||||
'resourceType' => 'Organization',
|
'resourceType' => 'Organization',
|
||||||
'id' => 'organization-' . ($id !== '' ? $id : md5($name)),
|
'id' => 'organization-' . ($id !== '' ? $id : md5($name)),
|
||||||
|
'meta' => ['profile' => [
|
||||||
|
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization',
|
||||||
|
]],
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'identifier' => $id !== '' ? [[
|
'identifier' => $id !== '' ? [[
|
||||||
'system' => 'https://hms.local/organization-id',
|
'system' => 'https://hms.local/organization-id',
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,24 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
$patientId = (string) ($source['patient']['id'] ?? '0');
|
$patientId = (string) ($source['patient']['id'] ?? '0');
|
||||||
$sessionId = (string) ($source['session_id'] ?? '0');
|
$sessionId = (string) ($source['session_id'] ?? '0');
|
||||||
$visitDate = (string) ($source['visit_date'] ?? date('Y-m-d'));
|
$visitDate = (string) ($source['visit_date'] ?? date('Y-m-d'));
|
||||||
|
$bundleIdentifier = trim((string) ($source['bundle_identifier'] ?? ''));
|
||||||
|
if ($bundleIdentifier === '') {
|
||||||
|
$bundleIdentifier = 'discharge-' . $recordId . '-' . strtotime($timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
$careContextReference = 'DISCHARGE-' . $recordId . '-S' . $sessionId . '-' . $visitDate;
|
$careContextReference = 'DISCHARGE-' . $recordId . '-S' . $sessionId . '-' . $visitDate;
|
||||||
$careContextDisplay = 'Discharge Summary ' . $visitDate;
|
$careContextDisplay = 'Discharge Summary ' . $visitDate;
|
||||||
|
|
||||||
$builder = new \App\Libraries\Abdm\Fhir\FhirDocumentBuilder();
|
$builder = new \App\Libraries\Abdm\Fhir\FhirDocumentBuilder();
|
||||||
$builder
|
$builder
|
||||||
->buildBundleMeta('discharge-' . $recordId . '-' . strtotime($timestamp), $timestamp)
|
->buildBundleMeta($bundleIdentifier, $timestamp)
|
||||||
->buildComposition($this->buildBaseComposition($source, 'Discharge Summary', '18842-5', 'Discharge summary'))
|
->updateBundleMeta(['meta' => ['profile' => [
|
||||||
|
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle',
|
||||||
|
]]])
|
||||||
|
->buildComposition($this->buildBaseComposition($source, 'IPD Discharge Summary', '18842-5', 'Discharge summary'))
|
||||||
|
->updateComposition(['meta' => ['profile' => [
|
||||||
|
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DischargeSummaryRecord',
|
||||||
|
]]])
|
||||||
->addPatient($this->buildBasePatient($source));
|
->addPatient($this->buildBasePatient($source));
|
||||||
|
|
||||||
$encounter = $this->buildEncounter($source);
|
$encounter = $this->buildEncounter($source);
|
||||||
|
|
@ -52,6 +62,26 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
$builder->addOrganization($organization);
|
$builder->addOrganization($organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$compositionUpdate = [];
|
||||||
|
if (is_array($practitioner)) {
|
||||||
|
$compositionUpdate['author'] = [[
|
||||||
|
'reference' => 'urn:uuid:' . (string) $practitioner['id'],
|
||||||
|
'display' => (string) ($practitioner['name'][0]['text'] ?? ''),
|
||||||
|
]];
|
||||||
|
} elseif (is_array($organization)) {
|
||||||
|
$compositionUpdate['author'] = [[
|
||||||
|
'reference' => 'urn:uuid:' . (string) $organization['id'],
|
||||||
|
'display' => (string) ($organization['name'] ?? ''),
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
if (is_array($organization)) {
|
||||||
|
$compositionUpdate['custodian'] = [
|
||||||
|
'reference' => 'urn:uuid:' . (string) $organization['id'],
|
||||||
|
'display' => (string) ($organization['name'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$builder->updateComposition($compositionUpdate);
|
||||||
|
|
||||||
$patientRef = 'urn:uuid:patient-' . $patientId;
|
$patientRef = 'urn:uuid:patient-' . $patientId;
|
||||||
$encounterRef = is_array($encounter) ? 'urn:uuid:' . (string) ($encounter['id'] ?? '') : null;
|
$encounterRef = is_array($encounter) ? 'urn:uuid:' . (string) ($encounter['id'] ?? '') : null;
|
||||||
|
|
||||||
|
|
@ -62,11 +92,12 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
}
|
}
|
||||||
|
|
||||||
$resolved = $this->codingResolver->resolveSnomedForDiagnosisOrFinding((string) ($cond['code'] ?? ''), $text);
|
$resolved = $this->codingResolver->resolveSnomedForDiagnosisOrFinding((string) ($cond['code'] ?? ''), $text);
|
||||||
|
$coding = $this->withoutPlaceholderCoding((array) ($resolved['coding'] ?? []));
|
||||||
$builder->addCondition([
|
$builder->addCondition([
|
||||||
'resourceType' => 'Condition',
|
'resourceType' => 'Condition',
|
||||||
'id' => 'discharge-condition-' . $recordId . '-' . $idx,
|
'id' => 'discharge-condition-' . $recordId . '-' . $idx,
|
||||||
'code' => [
|
'code' => [
|
||||||
'coding' => $resolved['coding'] ?? [],
|
'coding' => $coding,
|
||||||
'text' => $text,
|
'text' => $text,
|
||||||
],
|
],
|
||||||
'subject' => ['reference' => $patientRef],
|
'subject' => ['reference' => $patientRef],
|
||||||
|
|
@ -81,12 +112,13 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
}
|
}
|
||||||
|
|
||||||
$resolved = $this->codingResolver->resolveSnomedForDiagnosisOrFinding((string) ($proc['code'] ?? ''), $text);
|
$resolved = $this->codingResolver->resolveSnomedForDiagnosisOrFinding((string) ($proc['code'] ?? ''), $text);
|
||||||
|
$coding = $this->withoutPlaceholderCoding((array) ($resolved['coding'] ?? []));
|
||||||
$builder->addProcedure([
|
$builder->addProcedure([
|
||||||
'resourceType' => 'Procedure',
|
'resourceType' => 'Procedure',
|
||||||
'id' => 'discharge-procedure-' . $recordId . '-' . $idx,
|
'id' => 'discharge-procedure-' . $recordId . '-' . $idx,
|
||||||
'status' => 'completed',
|
'status' => 'completed',
|
||||||
'code' => [
|
'code' => [
|
||||||
'coding' => $resolved['coding'] ?? [],
|
'coding' => $coding,
|
||||||
'text' => $text,
|
'text' => $text,
|
||||||
],
|
],
|
||||||
'subject' => ['reference' => $patientRef],
|
'subject' => ['reference' => $patientRef],
|
||||||
|
|
@ -240,6 +272,85 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ((array) ($source['documents'] ?? []) as $idx => $document) {
|
||||||
|
$data = trim((string) ($document['data'] ?? ''));
|
||||||
|
if ($data === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder->addDocumentReference([
|
||||||
|
'resourceType' => 'DocumentReference',
|
||||||
|
'id' => 'discharge-document-' . $recordId . '-' . $idx,
|
||||||
|
'status' => 'current',
|
||||||
|
'type' => ['coding' => [[
|
||||||
|
'system' => 'http://loinc.org',
|
||||||
|
'code' => (string) ($document['loinc_code'] ?? '55107-7'),
|
||||||
|
'display' => (string) ($document['title'] ?? 'Clinical document'),
|
||||||
|
]]],
|
||||||
|
'subject' => ['reference' => $patientRef],
|
||||||
|
'date' => (string) ($document['created_at'] ?? $timestamp),
|
||||||
|
'author' => is_array($practitioner) ? [[
|
||||||
|
'reference' => 'urn:uuid:' . (string) $practitioner['id'],
|
||||||
|
]] : null,
|
||||||
|
'custodian' => is_array($organization) ? [
|
||||||
|
'reference' => 'urn:uuid:' . (string) $organization['id'],
|
||||||
|
] : null,
|
||||||
|
'description' => (string) ($document['title'] ?? 'Clinical document'),
|
||||||
|
'content' => [[
|
||||||
|
'attachment' => [
|
||||||
|
'contentType' => (string) ($document['content_type'] ?? 'application/pdf'),
|
||||||
|
'language' => 'en-IN',
|
||||||
|
'data' => $data,
|
||||||
|
'title' => (string) ($document['title'] ?? 'Clinical document'),
|
||||||
|
'creation' => (string) ($document['created_at'] ?? $timestamp),
|
||||||
|
],
|
||||||
|
]],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sectionDefinitions = [
|
||||||
|
['title' => 'Diagnosis', 'code' => '11450-4', 'prefix' => 'discharge-condition-', 'items' => (array) ($source['conditions'] ?? [])],
|
||||||
|
['title' => 'Procedures', 'code' => '47519-4', 'prefix' => 'discharge-procedure-', 'items' => (array) ($source['procedures'] ?? [])],
|
||||||
|
['title' => 'Discharge medications', 'code' => '10183-2', 'prefix' => 'discharge-medication-', 'items' => (array) ($source['medications'] ?? [])],
|
||||||
|
['title' => 'Observations', 'code' => '8716-3', 'prefix' => 'discharge-observation-', 'items' => (array) ($source['observations'] ?? [])],
|
||||||
|
['title' => 'Investigations', 'code' => '18776-5', 'prefix' => 'discharge-investigation-', 'items' => (array) ($source['investigations'] ?? [])],
|
||||||
|
['title' => 'Allergies', 'code' => '48765-2', 'prefix' => 'discharge-allergy-', 'items' => (array) ($source['allergies'] ?? [])],
|
||||||
|
['title' => 'Discharge advice', 'code' => '8653-8', 'prefix' => 'discharge-careplan-', 'items' => (array) ($source['care_plans'] ?? [])],
|
||||||
|
['title' => 'Attached documents', 'code' => '55107-7', 'prefix' => 'discharge-document-', 'items' => (array) ($source['documents'] ?? [])],
|
||||||
|
];
|
||||||
|
$sections = [];
|
||||||
|
foreach ($sectionDefinitions as $definition) {
|
||||||
|
$references = [];
|
||||||
|
$narrativeParts = [];
|
||||||
|
foreach ($definition['items'] as $idx => $item) {
|
||||||
|
$references[] = ['reference' => 'urn:uuid:' . $definition['prefix'] . $recordId . '-' . $idx];
|
||||||
|
$narrative = $this->sectionItemNarrative((array) $item);
|
||||||
|
if ($narrative !== '') {
|
||||||
|
$narrativeParts[] = $narrative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($references === []) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sections[] = [
|
||||||
|
'title' => $definition['title'],
|
||||||
|
'code' => ['coding' => [[
|
||||||
|
'system' => 'http://loinc.org',
|
||||||
|
'code' => $definition['code'],
|
||||||
|
]]],
|
||||||
|
'entry' => $references,
|
||||||
|
'text' => $narrativeParts !== [] ? [
|
||||||
|
'status' => 'generated',
|
||||||
|
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><ul><li>'
|
||||||
|
. implode('</li><li>', array_map('htmlspecialchars', $narrativeParts))
|
||||||
|
. '</li></ul></div>',
|
||||||
|
] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($sections !== []) {
|
||||||
|
$builder->updateComposition(['section' => $sections]);
|
||||||
|
}
|
||||||
|
|
||||||
$bundle = $builder->toBundle();
|
$bundle = $builder->toBundle();
|
||||||
$validation = $this->validator->validate($bundle, 'discharge', ['resolved' => 1, 'unresolved' => 0, 'fallback_used' => 0]);
|
$validation = $this->validator->validate($bundle, 'discharge', ['resolved' => 1, 'unresolved' => 0, 'fallback_used' => 0]);
|
||||||
|
|
||||||
|
|
@ -251,4 +362,33 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
'validation' => $validation->toArray(),
|
'validation' => $validation->toArray(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int,array<string,mixed>> $coding
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
private function withoutPlaceholderCoding(array $coding): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter($coding, static function (array $item): bool {
|
||||||
|
$code = strtoupper(trim((string) ($item['code'] ?? '')));
|
||||||
|
return $code !== '' && $code !== 'UNMAPPED';
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $item */
|
||||||
|
private function sectionItemNarrative(array $item): string
|
||||||
|
{
|
||||||
|
foreach (['text', 'description', 'title', 'name', 'value'] as $field) {
|
||||||
|
$value = trim((string) ($item[$field] ?? ''));
|
||||||
|
if ($value !== '') {
|
||||||
|
if ($field === 'name') {
|
||||||
|
$dosage = trim((string) ($item['dosage'] ?? ''));
|
||||||
|
return trim($value . ($dosage !== '' ? ' - ' . $dosage : ''));
|
||||||
|
}
|
||||||
|
return trim(strip_tags($value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,10 @@ class FhirBundleValidator
|
||||||
$resourceRefs[$rtype . '/' . $rid] = true;
|
$resourceRefs[$rtype . '/' . $rid] = true;
|
||||||
$resourceRefs['urn:uuid:' . $rid] = true;
|
$resourceRefs['urn:uuid:' . $rid] = true;
|
||||||
}
|
}
|
||||||
|
$fullUrl = trim((string) ($entry['fullUrl'] ?? ''));
|
||||||
|
if ($fullUrl !== '') {
|
||||||
|
$resourceRefs[$fullUrl] = true;
|
||||||
|
}
|
||||||
|
|
||||||
if ($rtype === 'DiagnosticReport') {
|
if ($rtype === 'DiagnosticReport') {
|
||||||
$results = $resource['result'] ?? [];
|
$results = $resource['result'] ?? [];
|
||||||
|
|
|
||||||
|
|
@ -146,14 +146,20 @@ class AbdmSyncWorkerService
|
||||||
|
|
||||||
$result = $this->gatewayClient->pushRecord($recordPayload, $idempotencyKey);
|
$result = $this->gatewayClient->pushRecord($recordPayload, $idempotencyKey);
|
||||||
|
|
||||||
$this->recordModel->update($syncRecordId, [
|
$recordUpdate = [
|
||||||
'sync_status' => (bool) ($result['ok'] ?? false) ? 'done' : ((bool) ($result['retryable'] ?? false) ? 'failed' : 'dead'),
|
'sync_status' => (bool) ($result['ok'] ?? false) ? 'done' : ((bool) ($result['retryable'] ?? false) ? 'failed' : 'dead'),
|
||||||
'gateway_record_id' => (int) ($result['gateway_record_id'] ?? 0) ?: null,
|
'gateway_record_id' => (int) ($result['gateway_record_id'] ?? 0) ?: null,
|
||||||
'gateway_queue_id' => (string) ($result['gateway_queue_id'] ?? '') ?: null,
|
'gateway_queue_id' => (string) ($result['gateway_queue_id'] ?? '') ?: null,
|
||||||
'last_synced_at' => date('Y-m-d H:i:s'),
|
'last_synced_at' => date('Y-m-d H:i:s'),
|
||||||
'last_error' => (bool) ($result['ok'] ?? false) ? null : (string) ($result['message'] ?? 'Sync failed'),
|
'last_error' => (bool) ($result['ok'] ?? false) ? null : (string) ($result['message'] ?? 'Sync failed'),
|
||||||
'retry_count' => (int) ($record['retry_count'] ?? 0) + ((bool) ($result['ok'] ?? false) ? 0 : 1),
|
'retry_count' => (int) ($record['retry_count'] ?? 0) + ((bool) ($result['ok'] ?? false) ? 0 : 1),
|
||||||
]);
|
];
|
||||||
|
if ($this->db->fieldExists('first_pushed_at', 'abdm_sync_record')) {
|
||||||
|
$recordUpdate['first_pushed_at'] = (string) ($result['first_pushed_at'] ?? '') ?: null;
|
||||||
|
$this->db->table('abdm_sync_record')->where('id', $syncRecordId)->update($recordUpdate);
|
||||||
|
} else {
|
||||||
|
$this->recordModel->update($syncRecordId, $recordUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -938,6 +938,9 @@ class FhirR4Builder
|
||||||
$pdfDocRefRef = 'urn:uuid:' . $pdfDocRefUuid;
|
$pdfDocRefRef = 'urn:uuid:' . $pdfDocRefUuid;
|
||||||
$pdfContentType = trim((string) ($attachment['content_type'] ?? 'application/pdf')) ?: 'application/pdf';
|
$pdfContentType = trim((string) ($attachment['content_type'] ?? 'application/pdf')) ?: 'application/pdf';
|
||||||
$pdfTitle = trim((string) ($attachment['title'] ?? 'Lab Report PDF')) ?: 'Lab Report PDF';
|
$pdfTitle = trim((string) ($attachment['title'] ?? 'Lab Report PDF')) ?: 'Lab Report PDF';
|
||||||
|
$pdfBytes = base64_decode($pdfData, true);
|
||||||
|
$pdfSize = (int) ($attachment['size'] ?? (is_string($pdfBytes) ? strlen($pdfBytes) : 0));
|
||||||
|
$pdfHash = trim((string) ($attachment['hash'] ?? (is_string($pdfBytes) ? base64_encode(sha1($pdfBytes, true)) : '')));
|
||||||
|
|
||||||
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $pdfBinaryUuid, 'resource' => [
|
$resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $pdfBinaryUuid, 'resource' => [
|
||||||
'resourceType' => 'Binary',
|
'resourceType' => 'Binary',
|
||||||
|
|
@ -955,12 +958,14 @@ class FhirR4Builder
|
||||||
'type' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]],
|
'type' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]],
|
||||||
'subject' => ['reference' => $patientRef],
|
'subject' => ['reference' => $patientRef],
|
||||||
'content' => [[
|
'content' => [[
|
||||||
'attachment' => [
|
'attachment' => array_filter([
|
||||||
'contentType' => $pdfContentType,
|
'contentType' => $pdfContentType,
|
||||||
'url' => 'urn:uuid:' . $pdfBinaryUuid,
|
'url' => 'urn:uuid:' . $pdfBinaryUuid,
|
||||||
'data' => $pdfData,
|
'data' => $pdfData,
|
||||||
'title' => $pdfTitle,
|
'title' => $pdfTitle,
|
||||||
],
|
'size' => $pdfSize > 0 ? $pdfSize : null,
|
||||||
|
'hash' => $pdfHash !== '' ? $pdfHash : null,
|
||||||
|
], static fn ($value): bool => $value !== null),
|
||||||
]],
|
]],
|
||||||
]];
|
]];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-wrap gap-2 mb-3" id="taskFilters">
|
<div class="d-flex flex-wrap gap-2 mb-3" id="taskFilters">
|
||||||
<button class="btn btn-sm btn-primary filter-btn" data-filter="dashboard">Dashboard</button>
|
<button type="button" class="btn btn-sm btn-primary filter-btn" data-filter="dashboard">Dashboard</button>
|
||||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="opd_book">OPD Book</button>
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="opd_book">OPD Book</button>
|
||||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="opd_consult_publish">OPD Consult Publish</button>
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="opd_consult_publish">OPD Consult Publish</button>
|
||||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="lab_report_publish">Lab Reports</button>
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="lab_report_publish">Lab Reports</button>
|
||||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="radiology_report_publish">Radiology Reports</button>
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="radiology_report_publish">Radiology Reports</button>
|
||||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="immunization_record_publish">Immunization Record</button>
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="immunization_record_publish">Immunization Record</button>
|
||||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="invoice">Invoice</button>
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="invoice">Invoice</button>
|
||||||
<button class="btn btn-sm btn-outline-primary filter-btn" data-filter="ipd_discharge_publish">IPD Discharge</button>
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="ipd_discharge_publish">IPD Discharge</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php $dashboard = $dashboard_metrics ?? []; ?>
|
<?php $dashboard = $dashboard_metrics ?? []; ?>
|
||||||
|
|
@ -165,25 +165,25 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex gap-1 flex-wrap">
|
<div class="d-flex gap-1 flex-wrap">
|
||||||
<?php if ($type === 'patient_abha_create' || $type === 'patient_abha_link'): ?>
|
<?php if ($type === 'patient_abha_create' || $type === 'patient_abha_link'): ?>
|
||||||
<button class="btn btn-sm btn-outline-success action-btn" data-action="create_abha">Create ABHA</button>
|
<button type="button" class="btn btn-sm btn-outline-success action-btn" data-action="create_abha">Create ABHA</button>
|
||||||
<?php elseif ($type === 'patient_abha_update'): ?>
|
<?php elseif ($type === 'patient_abha_update'): ?>
|
||||||
<button class="btn btn-sm btn-outline-primary action-btn" data-action="update_abha">Update ABHA</button>
|
<button type="button" class="btn btn-sm btn-outline-primary action-btn" data-action="update_abha">Update ABHA</button>
|
||||||
<?php elseif ($type === 'immunization_record_publish'): ?>
|
<?php elseif ($type === 'immunization_record_publish'): ?>
|
||||||
<?php if ($bridgeSubmitted): ?>
|
<?php if ($bridgeSubmitted): ?>
|
||||||
<button class="btn btn-sm btn-outline-success" disabled title="Submitted to ABDM Bridge<?= $bridgeCareContext !== '' ? ': ' . esc($bridgeCareContext) : '' ?>">Submitted to Bridge</button>
|
<button type="button" class="btn btn-sm btn-outline-success" disabled title="Submitted to ABDM Bridge<?= $bridgeCareContext !== '' ? ': ' . esc($bridgeCareContext) : '' ?>">Submitted to Bridge</button>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<button class="btn btn-sm btn-outline-success immunization-bridge-btn" title="Push this ImmunizationRecord FHIR bundle to ABDM Bridge.">Push to Bridge</button>
|
<button type="button" class="btn btn-sm btn-outline-success immunization-bridge-btn" title="Push this ImmunizationRecord FHIR bundle to ABDM Bridge.">Push to Bridge</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<button class="btn btn-sm btn-outline-warning action-btn" data-action="submit">Submit</button>
|
<button type="button" class="btn btn-sm btn-outline-warning action-btn" data-action="submit">Submit</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($showSandbox): ?>
|
<?php if ($showSandbox): ?>
|
||||||
<button class="btn btn-sm btn-outline-info sandbox-btn">Sandbox</button>
|
<button type="button" class="btn btn-sm btn-outline-info sandbox-btn">Sandbox</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($showPreview): ?>
|
<?php if ($showPreview): ?>
|
||||||
<button class="btn btn-sm btn-outline-primary preview-fhir-btn">Preview FHIR</button>
|
<button type="button" class="btn btn-sm btn-outline-primary preview-fhir-btn">Preview FHIR</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button class="btn btn-sm btn-outline-dark close-btn">Close</button>
|
<button type="button" class="btn btn-sm btn-outline-dark close-btn">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -757,6 +757,19 @@
|
||||||
statusBox.style.color = isError ? '#dc3545' : '#6c757d';
|
statusBox.style.color = isError ? '#dc3545' : '#6c757d';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSubmissionStatus(msg, queueId) {
|
||||||
|
setStatus(msg, false);
|
||||||
|
if (!queueId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
statusBox.appendChild(document.createTextNode(' '));
|
||||||
|
var logLink = document.createElement('a');
|
||||||
|
logLink.href = '<?= base_url('AbdmBridgeLog') ?>' + '?search=' + encodeURIComponent(queueId);
|
||||||
|
logLink.textContent = 'View Bridge Log';
|
||||||
|
logLink.className = 'ms-1';
|
||||||
|
statusBox.appendChild(logLink);
|
||||||
|
}
|
||||||
|
|
||||||
function post(url, payload, cb) {
|
function post(url, payload, cb) {
|
||||||
payload[csrfName] = csrfHash;
|
payload[csrfName] = csrfHash;
|
||||||
var body = Object.keys(payload).map(function (key) {
|
var body = Object.keys(payload).map(function (key) {
|
||||||
|
|
@ -1665,6 +1678,7 @@
|
||||||
var _fhirInvoiceSource = '';
|
var _fhirInvoiceSource = '';
|
||||||
var _fhirInvoiceBillId = 0;
|
var _fhirInvoiceBillId = 0;
|
||||||
var _fhirInvoicePatientId = 0;
|
var _fhirInvoicePatientId = 0;
|
||||||
|
var _fhirPreviewUrl = '';
|
||||||
|
|
||||||
// ── Inline complaint SNOMED edit (event delegation on fhirFormView) ─
|
// ── Inline complaint SNOMED edit (event delegation on fhirFormView) ─
|
||||||
var _editTimer = null;
|
var _editTimer = null;
|
||||||
|
|
@ -2535,6 +2549,7 @@
|
||||||
|
|
||||||
function openFhirModal(url, title, options) {
|
function openFhirModal(url, title, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
_fhirPreviewUrl = url;
|
||||||
_fhirSubmitMode = options.submitMode || 'opd';
|
_fhirSubmitMode = options.submitMode || 'opd';
|
||||||
_fhirSubmitTaskRow = options.taskRow || null;
|
_fhirSubmitTaskRow = options.taskRow || null;
|
||||||
_fhirSubmitAbha = (options.abhaId || '').trim();
|
_fhirSubmitAbha = (options.abhaId || '').trim();
|
||||||
|
|
@ -2588,14 +2603,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (btnRegenerateFhirModal) {
|
if (btnRegenerateFhirModal) {
|
||||||
|
var taskType = _fhirSubmitTaskRow ? (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() : '';
|
||||||
|
var canRebuildDiagnostic = _fhirSubmitMode === 'task'
|
||||||
|
&& (taskType === 'lab_report_publish' || taskType === 'radiology_report_publish');
|
||||||
btnRegenerateFhirModal.disabled = false;
|
btnRegenerateFhirModal.disabled = false;
|
||||||
btnRegenerateFhirModal.style.display = _fhirSubmitMode === 'task' ? 'none' : '';
|
btnRegenerateFhirModal.style.display = _fhirSubmitMode === 'task' && !canRebuildDiagnostic ? 'none' : '';
|
||||||
btnRegenerateFhirModal.innerHTML = _fhirSubmitMode === 'invoice'
|
btnRegenerateFhirModal.innerHTML = _fhirSubmitMode === 'invoice' || canRebuildDiagnostic
|
||||||
? '<i class="bi bi-arrow-clockwise"></i> Rebuild Bundle'
|
? '<i class="bi bi-arrow-clockwise"></i> Rebuild Digital Record'
|
||||||
: '<i class="bi bi-arrow-clockwise"></i> Regenerate';
|
: '<i class="bi bi-arrow-clockwise"></i> Regenerate';
|
||||||
btnRegenerateFhirModal.title = _fhirSubmitMode === 'invoice'
|
btnRegenerateFhirModal.title = canRebuildDiagnostic
|
||||||
? 'Rebuild FHIR JSON from current billing data'
|
? 'Rebuild FHIR and Digital Share PDF from the latest report data'
|
||||||
: 'Rebuild FHIR bundle from current prescription data';
|
: (_fhirSubmitMode === 'invoice'
|
||||||
|
? 'Rebuild FHIR and Digital Share PDF from current billing data'
|
||||||
|
: 'Rebuild FHIR bundle from current prescription data');
|
||||||
}
|
}
|
||||||
|
|
||||||
fhirPreviewModal.show();
|
fhirPreviewModal.show();
|
||||||
|
|
@ -2632,6 +2652,20 @@
|
||||||
// Regenerate FHIR bundle in modal
|
// Regenerate FHIR bundle in modal
|
||||||
btnRegenerateFhirModal.addEventListener('click', function () {
|
btnRegenerateFhirModal.addEventListener('click', function () {
|
||||||
var btn = this;
|
var btn = this;
|
||||||
|
var taskType = _fhirSubmitTaskRow ? (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() : '';
|
||||||
|
if (_fhirSubmitMode === 'task' && (taskType === 'lab_report_publish' || taskType === 'radiology_report_publish')) {
|
||||||
|
if (_fhirPreviewUrl === '') {
|
||||||
|
alert('Diagnostic preview URL is unavailable.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm('Rebuild this FHIR record and Digital Share PDF from the latest report data?')) return;
|
||||||
|
openFhirModal(_fhirPreviewUrl, fhirModalTitle ? fhirModalTitle.textContent : 'Diagnostic FHIR Preview', {
|
||||||
|
submitMode: 'task',
|
||||||
|
taskRow: _fhirSubmitTaskRow,
|
||||||
|
abhaId: _fhirSubmitAbha
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (_fhirSubmitMode === 'invoice') {
|
if (_fhirSubmitMode === 'invoice') {
|
||||||
if (!_fhirInvoiceSource || _fhirInvoiceBillId <= 0 || _fhirInvoicePatientId <= 0) {
|
if (!_fhirInvoiceSource || _fhirInvoiceBillId <= 0 || _fhirInvoicePatientId <= 0) {
|
||||||
alert('Invoice source data is incomplete.');
|
alert('Invoice source data is incomplete.');
|
||||||
|
|
@ -2812,17 +2846,13 @@
|
||||||
|
|
||||||
var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : '';
|
var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : '';
|
||||||
var warningText = res.warning ? (' Warning: ' + res.warning) : '';
|
var warningText = res.warning ? (' Warning: ' + res.warning) : '';
|
||||||
setStatus('Submitted to ABDM gateway.' + queueText + warningText);
|
setSubmissionStatus('Submitted to ABDM gateway.' + queueText + warningText, res.queue_id || '');
|
||||||
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
||||||
if (badge) {
|
if (badge) {
|
||||||
badge.textContent = 'IN_PROGRESS';
|
badge.textContent = 'IN_PROGRESS';
|
||||||
badge.className = 'badge bg-info status-pill';
|
badge.className = 'badge bg-info status-pill';
|
||||||
}
|
}
|
||||||
alert('Submitted to ABDM gateway.' + queueText + warningText);
|
alert('Submitted to ABDM gateway.' + queueText + warningText);
|
||||||
if (res.queue_id) {
|
|
||||||
var logUrl = '<?= base_url('AbdmBridgeLog') ?>' + '?search=' + encodeURIComponent(res.queue_id);
|
|
||||||
window.open(logUrl, '_blank');
|
|
||||||
}
|
|
||||||
fhirPreviewModal.hide();
|
fhirPreviewModal.hide();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -2858,17 +2888,13 @@
|
||||||
|
|
||||||
var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : '';
|
var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : '';
|
||||||
var warningText = res.warning ? (' Warning: ' + res.warning) : '';
|
var warningText = res.warning ? (' Warning: ' + res.warning) : '';
|
||||||
setStatus('Submitted to ABDM gateway.' + queueText + warningText);
|
setSubmissionStatus('Submitted to ABDM gateway.' + queueText + warningText, res.queue_id || '');
|
||||||
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
||||||
if (badge) {
|
if (badge) {
|
||||||
badge.textContent = 'IN_PROGRESS';
|
badge.textContent = 'IN_PROGRESS';
|
||||||
badge.className = 'badge bg-info status-pill';
|
badge.className = 'badge bg-info status-pill';
|
||||||
}
|
}
|
||||||
alert('Submitted to ABDM gateway.' + queueText + warningText);
|
alert('Submitted to ABDM gateway.' + queueText + warningText);
|
||||||
if (res.queue_id) {
|
|
||||||
var logUrl = '<?= base_url('AbdmBridgeLog') ?>' + '?search=' + encodeURIComponent(res.queue_id);
|
|
||||||
window.open(logUrl, '_blank');
|
|
||||||
}
|
|
||||||
fhirPreviewModal.hide();
|
fhirPreviewModal.hide();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,7 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
||||||
$this->assertIsString($pdfBytes);
|
$this->assertIsString($pdfBytes);
|
||||||
$this->assertSame('application/pdf', $attachment['contentType'] ?? null);
|
$this->assertSame('application/pdf', $attachment['contentType'] ?? null);
|
||||||
$this->assertStringStartsWith('%PDF-', $pdfBytes);
|
$this->assertStringStartsWith('%PDF-', $pdfBytes);
|
||||||
|
$this->assertStringNotContainsString('table.items{', $pdfBytes);
|
||||||
$this->assertSame(strlen($pdfBytes), $attachment['size'] ?? null);
|
$this->assertSame(strlen($pdfBytes), $attachment['size'] ?? null);
|
||||||
$this->assertSame(base64_encode(sha1($pdfBytes, true)), $attachment['hash'] ?? null);
|
$this->assertSame(base64_encode(sha1($pdfBytes, true)), $attachment['hash'] ?? null);
|
||||||
$this->assertSame(
|
$this->assertSame(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user