ABDM: enrich discharge FHIR with observations advice and risks

This commit is contained in:
DevSoftTech Server 2026-07-17 17:59:54 +05:30
parent 2335006c30
commit d7fe05e6b0
4 changed files with 324 additions and 3 deletions

View File

@ -2186,6 +2186,119 @@ class AbdmGateway extends BaseController
}
}
$observations = [];
foreach ($this->ipdRows('ipd_discharge_1_b', ['short_head', 'rdata'], $ipdId, 'ipd_d_id') as $row) {
$label = trim((string) ($row['short_head'] ?? ''));
$value = trim((string) ($row['rdata'] ?? ''));
if ($label === '' || $value === '') {
continue;
}
$observations[] = [
'text' => $label,
'value' => $value,
'category' => 'vital-signs',
'category_code' => 'vital-signs',
'effective_at' => $this->toIsoDateTimeOrNow($admissionRaw),
];
}
foreach ($this->ipdRows('ipd_discharge_1_b_final', ['short_head', 'rdata'], $ipdId, 'ipd_d_id') as $row) {
$label = trim((string) ($row['short_head'] ?? ''));
$value = trim((string) ($row['rdata'] ?? ''));
if ($label === '' || $value === '') {
continue;
}
$observations[] = [
'text' => 'Discharge ' . $label,
'value' => $value,
'category' => 'vital-signs',
'category_code' => 'vital-signs',
'effective_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
];
}
$investigations = [];
foreach ($this->ipdRows('ipd_discharge_1_d', ['short_head', 'rdata'], $ipdId, 'ipd_d_id') as $row) {
$label = trim((string) ($row['short_head'] ?? ''));
$value = trim((string) ($row['rdata'] ?? ''));
if ($label === '') {
continue;
}
$investigations[] = [
'text' => $value !== '' ? ($label . ': ' . $value) : $label,
'authored_on' => $this->toIsoDateTimeOrNow($admissionRaw),
];
}
foreach ($this->ipdRows('ipd_discharge_1_e', ['short_head', 'rdata'], $ipdId, 'ipd_d_id') as $row) {
$label = trim((string) ($row['short_head'] ?? ''));
$value = trim((string) ($row['rdata'] ?? ''));
if ($label === '') {
continue;
}
$investigations[] = [
'text' => $value !== '' ? ($label . ': ' . $value) : $label,
'authored_on' => $this->toIsoDateTimeOrNow($admissionRaw),
];
}
$allergies = [];
$carePlans = [];
$instructionRows = $this->ipdRows('ipd_discharge_instructions', ['comp_report', 'comp_remark', 'review_after'], $ipdId);
if (! empty($instructionRows)) {
$instructionRow = $instructionRows[0];
$instructionMeta = $this->parseJsonArray((string) ($instructionRow['comp_report'] ?? ''));
$nabhMeta = is_array($instructionMeta['nabh'] ?? null) ? $instructionMeta['nabh'] : [];
$allergyStatus = strtolower(trim((string) ($nabhMeta['drug_allergy_status'] ?? '')));
$allergyDetails = trim((string) ($nabhMeta['drug_allergy_details'] ?? ''));
if ($allergyStatus !== '' || $allergyDetails !== '') {
$allergies[] = [
'text' => $allergyDetails !== '' ? $allergyDetails : ('Drug allergy status: ' . $allergyStatus),
'note' => $allergyStatus,
'criticality' => $allergyStatus === 'yes' ? 'high' : 'low',
'recorded_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
];
}
$dietAdvice = trim((string) ($instructionRow['comp_remark'] ?? ''));
if ($dietAdvice !== '') {
$carePlans[] = [
'title' => 'Dietary Advice',
'description' => $dietAdvice,
'created_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
];
}
$otherAdvice = trim((string) ($instructionMeta['other_text'] ?? ''));
if ($otherAdvice !== '') {
$carePlans[] = [
'title' => 'Other Advice',
'description' => $otherAdvice,
'created_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
];
}
$reviewAfter = trim((string) ($instructionRow['review_after'] ?? ''));
if ($reviewAfter !== '') {
$carePlans[] = [
'title' => 'Follow Up',
'description' => 'Review After: ' . $reviewAfter,
'created_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
];
}
}
$courseRows = $this->ipdRows('ipd_discharge_course_remark', ['comp_remark'], $ipdId);
if (! empty($courseRows)) {
$courseText = trim((string) ($courseRows[0]['comp_remark'] ?? ''));
if ($courseText !== '') {
$carePlans[] = [
'title' => 'Course In Hospital',
'description' => $courseText,
'created_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
];
}
}
$hospital = $this->getHospitalProfileForFhir();
$hfrId = trim((string) ($hospital['hfr_id'] ?? ''));
@ -2212,6 +2325,10 @@ class AbdmGateway extends BaseController
'conditions' => $conditions,
'procedures' => $procedures,
'medications' => $medications,
'observations' => $observations,
'investigations' => $investigations,
'allergies' => $allergies,
'care_plans' => $carePlans,
];
$factory = new FhirGeneratorFactory();
@ -2233,9 +2350,9 @@ class AbdmGateway extends BaseController
* @param array<int,string> $columns
* @return array<int,array<string,mixed>>
*/
private function ipdRows(string $table, array $columns, int $ipdId): array
private function ipdRows(string $table, array $columns, int $ipdId, string $ipdKey = 'ipd_id'): array
{
if ($ipdId <= 0 || ! $this->db->tableExists($table) || ! $this->db->fieldExists('ipd_id', $table)) {
if ($ipdId <= 0 || ! $this->db->tableExists($table) || ! $this->db->fieldExists($ipdKey, $table)) {
return [];
}
@ -2254,7 +2371,7 @@ class AbdmGateway extends BaseController
$builder = $this->db->table($table)
->select(implode(',', $available))
->where('ipd_id', $ipdId);
->where($ipdKey, $ipdId);
if ($this->db->fieldExists('id', $table)) {
$builder->orderBy('id', 'ASC');
@ -2263,6 +2380,20 @@ class AbdmGateway extends BaseController
return $builder->get()->getResultArray();
}
/**
* @return array<string,mixed>
*/
private function parseJsonArray(string $json): array
{
$json = trim($json);
if ($json === '') {
return [];
}
$decoded = json_decode($json, true);
return is_array($decoded) ? $decoded : [];
}
private function normalizeFhirGender(string $gender): string
{
$value = strtolower(trim($gender));

View File

@ -78,6 +78,24 @@ class FhirDocumentBuilder
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addServiceRequest(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addAllergyIntolerance(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addCarePlan(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addDiagnosticReport(array $resource): self
{

View File

@ -93,6 +93,131 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
]);
}
foreach ((array) ($source['observations'] ?? []) as $idx => $obs) {
$text = trim((string) ($obs['text'] ?? ''));
$value = trim((string) ($obs['value'] ?? ''));
if ($text === '' || $value === '') {
continue;
}
$coding = [];
$loincCode = trim((string) ($obs['loinc_code'] ?? ''));
if ($loincCode !== '') {
$coding[] = [
'system' => 'http://loinc.org',
'code' => $loincCode,
'display' => trim((string) ($obs['loinc_display'] ?? '')),
];
}
$builder->addObservation([
'resourceType' => 'Observation',
'id' => 'discharge-observation-' . $recordId . '-' . $idx,
'status' => 'final',
'category' => [[
'text' => trim((string) ($obs['category'] ?? 'vital-signs')),
'coding' => [[
'system' => 'http://terminology.hl7.org/CodeSystem/observation-category',
'code' => trim((string) ($obs['category_code'] ?? 'vital-signs')),
]],
]],
'code' => [
'coding' => $coding,
'text' => $text,
],
'subject' => ['reference' => $patientRef],
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
'effectiveDateTime' => (string) ($obs['effective_at'] ?? $timestamp),
'valueString' => $value,
]);
}
foreach ((array) ($source['investigations'] ?? []) as $idx => $inv) {
$text = trim((string) ($inv['text'] ?? ''));
if ($text === '') {
continue;
}
$coding = [];
$loincCode = trim((string) ($inv['loinc_code'] ?? ''));
if ($loincCode !== '') {
$coding[] = [
'system' => 'http://loinc.org',
'code' => $loincCode,
'display' => trim((string) ($inv['loinc_display'] ?? '')),
];
}
$builder->addServiceRequest([
'resourceType' => 'ServiceRequest',
'id' => 'discharge-investigation-' . $recordId . '-' . $idx,
'status' => 'active',
'intent' => 'order',
'code' => [
'coding' => $coding,
'text' => $text,
],
'subject' => ['reference' => $patientRef],
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
'authoredOn' => (string) ($inv['authored_on'] ?? $timestamp),
]);
}
foreach ((array) ($source['allergies'] ?? []) as $idx => $alg) {
$text = trim((string) ($alg['text'] ?? ''));
if ($text === '') {
continue;
}
$builder->addAllergyIntolerance([
'resourceType' => 'AllergyIntolerance',
'id' => 'discharge-allergy-' . $recordId . '-' . $idx,
'clinicalStatus' => [
'coding' => [[
'system' => 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical',
'code' => 'active',
]],
],
'verificationStatus' => [
'coding' => [[
'system' => 'http://terminology.hl7.org/CodeSystem/allergyintolerance-verification',
'code' => 'unconfirmed',
]],
],
'type' => 'allergy',
'category' => ['medication'],
'criticality' => trim((string) ($alg['criticality'] ?? 'low')),
'code' => [
'text' => $text,
],
'patient' => ['reference' => $patientRef],
'recordedDate' => (string) ($alg['recorded_at'] ?? $timestamp),
'note' => isset($alg['note']) && trim((string) $alg['note']) !== ''
? [['text' => trim((string) $alg['note'])]]
: null,
]);
}
foreach ((array) ($source['care_plans'] ?? []) as $idx => $cp) {
$title = trim((string) ($cp['title'] ?? 'Discharge Advice'));
$description = trim((string) ($cp['description'] ?? ''));
if ($description === '') {
continue;
}
$builder->addCarePlan([
'resourceType' => 'CarePlan',
'id' => 'discharge-careplan-' . $recordId . '-' . $idx,
'status' => 'active',
'intent' => 'plan',
'title' => $title,
'description' => $description,
'subject' => ['reference' => $patientRef],
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
'created' => (string) ($cp['created_at'] ?? $timestamp),
]);
}
$bundle = $builder->toBundle();
$validation = $this->validator->validate($bundle, 'discharge', ['resolved' => 1, 'unresolved' => 0, 'fallback_used' => 0]);

View File

@ -1711,7 +1711,10 @@
var allConditions = byType('Condition');
var observations = byType('Observation');
var medications = byType('MedicationRequest');
var procedures = byType('Procedure');
var serviceReqs = byType('ServiceRequest');
var allergies = byType('AllergyIntolerance');
var carePlans = byType('CarePlan');
var complaints = allConditions.filter(function(c) {
return (c.category || []).some(function(cat) { return (cat.text || '').toLowerCase().includes('complaint'); });
@ -1923,6 +1926,20 @@
html += '</ol></div></div>';
}
// ── Procedures / Surgeries
if (procedures.length) {
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Procedures</small></div><div class="card-body py-2">';
html += '<ul class="mb-0 ps-3">';
procedures.forEach(function(p) {
var text = (p.code || {}).text || ((((p.code || {}).coding) || [])[0] || {}).display || '';
var at = fmtDate(p.performedDateTime || '');
html += '<li class="mb-1"><strong>' + hesc(text || 'Procedure') + '</strong>';
if (at) html += ' <span class="text-muted small">(' + hesc(at) + ')</span>';
html += '</li>';
});
html += '</ul></div></div>';
}
// ── Investigations
if (serviceReqs.length) {
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Investigations</small></div><div class="card-body py-2">';
@ -1937,6 +1954,36 @@
html += '</ul></div></div>';
}
// ── Allergy & Risk Profile
if (allergies.length) {
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Allergies / Risk Profile</small></div><div class="card-body py-2">';
html += '<ul class="mb-0 ps-3">';
allergies.forEach(function(a) {
var text = (a.code || {}).text || '';
var note = (((a.note || [])[0]) || {}).text || '';
var criticality = (a.criticality || '').toUpperCase();
html += '<li class="mb-1"><strong>' + hesc(text || 'Allergy') + '</strong>';
if (criticality) html += ' <span class="badge bg-warning text-dark" style="font-size:.7rem;">' + hesc(criticality) + '</span>';
if (note) html += ' <small class="text-muted d-block">' + hesc(note) + '</small>';
html += '</li>';
});
html += '</ul></div></div>';
}
// ── Discharge Advice / Follow-up
if (carePlans.length) {
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Discharge Advice & Follow-up</small></div><div class="card-body py-2">';
html += '<ul class="mb-0 ps-3">';
carePlans.forEach(function(cp) {
var title = cp.title || 'Care Plan';
var desc = cp.description || '';
html += '<li class="mb-2"><strong>' + hesc(title) + '</strong>';
if (desc) html += '<div class="text-muted small">' + hesc(desc) + '</div>';
html += '</li>';
});
html += '</ul></div></div>';
}
// ── Practitioner
if (practitioner) {
var pName = ((practitioner.name || [{}])[0].text || '').trim();