Fix FHIR DiagnosticReportRecord datetime and practitioner quality

This commit is contained in:
DevSoftTech Server 2026-06-01 06:01:24 +05:30
parent 7595f9fd29
commit e1d9e85e64
2 changed files with 223 additions and 13 deletions

View File

@ -966,6 +966,7 @@ class AbdmGateway extends BaseController
$organization = $hospitalProfile['name'] !== ''
? ['name' => $hospitalProfile['name'], 'hfr_id' => $hospitalProfile['hfr_id']]
: null;
$practitioner = $this->resolveLabPractitionerForFhir($labReqId, $labReq);
$encounter = [
'id' => 'LAB-' . $labReqId,
@ -988,7 +989,7 @@ class AbdmGateway extends BaseController
]);
}
$bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, null, $organization, $encounter, $pdfAttachment);
$bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, $practitioner, $organization, $encounter, $pdfAttachment);
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// ── Store health_record ───────────────────────────────────────────────
@ -1138,6 +1139,7 @@ class AbdmGateway extends BaseController
$organization = $hospitalProfile['name'] !== ''
? ['name' => $hospitalProfile['name'], 'hfr_id' => $hospitalProfile['hfr_id']]
: null;
$practitioner = $this->resolveLabPractitionerForFhir($labReqId, $labReq);
$encounter = [
'id' => 'LAB-' . $labReqId,
@ -1169,7 +1171,7 @@ class AbdmGateway extends BaseController
]);
}
$bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, null, $organization, $encounter, $pdfAttachment);
$bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, $practitioner, $organization, $encounter, $pdfAttachment);
return $this->response->setJSON([
'status' => 'ok',
@ -2472,6 +2474,167 @@ class AbdmGateway extends BaseController
return ['name' => $name, 'hfr_id' => $hfrId];
}
/**
* Resolve practitioner details for lab/radiology DiagnosticReportRecord.
*
* Best-effort strategy:
* 1) read doctor id/name/registration from lab_request (when present)
* 2) enrich from doctor_master using doctor id
*
* @param int $labReqId
* @param object|array<string,mixed>|null $labReqRow
* @return array<string,string>|null
*/
private function resolveLabPractitionerForFhir(int $labReqId, $labReqRow = null): ?array
{
if ($labReqId <= 0 || ! $this->db->tableExists('lab_request')) {
return null;
}
$doctorId = 0;
$doctorName = '';
$doctorRegNo = '';
$candidateIdCols = ['doctor_id', 'dr_id', 'doc_id', 'r_doc_id', 'consultant_id', 'reported_by_doctor_id'];
$candidateNameCols = ['doctor_name', 'dr_name', 'doc_name', 'r_doc_name', 'consultant_name', 'reported_by_name', 'reported_by'];
$candidateRegCols = ['doctor_reg_no', 'registration_no', 'reg_no', 'doctor_registration_no'];
// Try immediate row first (if caller passed a richer row object/array).
if (is_object($labReqRow)) {
foreach ($candidateIdCols as $c) {
if (isset($labReqRow->{$c}) && (int) $labReqRow->{$c} > 0) {
$doctorId = (int) $labReqRow->{$c};
break;
}
}
foreach ($candidateNameCols as $c) {
$v = isset($labReqRow->{$c}) ? trim((string) $labReqRow->{$c}) : '';
if ($v !== '') {
$doctorName = $v;
break;
}
}
foreach ($candidateRegCols as $c) {
$v = isset($labReqRow->{$c}) ? trim((string) $labReqRow->{$c}) : '';
if ($v !== '') {
$doctorRegNo = $v;
break;
}
}
} elseif (is_array($labReqRow)) {
foreach ($candidateIdCols as $c) {
if (isset($labReqRow[$c]) && (int) $labReqRow[$c] > 0) {
$doctorId = (int) $labReqRow[$c];
break;
}
}
foreach ($candidateNameCols as $c) {
$v = isset($labReqRow[$c]) ? trim((string) $labReqRow[$c]) : '';
if ($v !== '') {
$doctorName = $v;
break;
}
}
foreach ($candidateRegCols as $c) {
$v = isset($labReqRow[$c]) ? trim((string) $labReqRow[$c]) : '';
if ($v !== '') {
$doctorRegNo = $v;
break;
}
}
}
try {
$labFields = $this->db->getFieldNames('lab_request') ?? [];
$selectCols = ['id'];
foreach (array_merge($candidateIdCols, $candidateNameCols, $candidateRegCols) as $c) {
if (in_array($c, $labFields, true)) {
$selectCols[] = $c;
}
}
if (count($selectCols) > 1) {
$labDbRow = $this->db->table('lab_request')
->select(implode(',', array_unique($selectCols)))
->where('id', $labReqId)
->get(1)
->getRowArray() ?? [];
if (! empty($labDbRow)) {
if ($doctorId <= 0) {
foreach ($candidateIdCols as $c) {
if (isset($labDbRow[$c]) && (int) $labDbRow[$c] > 0) {
$doctorId = (int) $labDbRow[$c];
break;
}
}
}
if ($doctorName === '') {
foreach ($candidateNameCols as $c) {
$v = isset($labDbRow[$c]) ? trim((string) $labDbRow[$c]) : '';
if ($v !== '') {
$doctorName = $v;
break;
}
}
}
if ($doctorRegNo === '') {
foreach ($candidateRegCols as $c) {
$v = isset($labDbRow[$c]) ? trim((string) $labDbRow[$c]) : '';
if ($v !== '') {
$doctorRegNo = $v;
break;
}
}
}
}
}
} catch (\Throwable $e) {
// fail-open; continue with what we already have
}
if ($doctorId > 0 && $this->db->tableExists('doctor_master')) {
try {
$dFields = $this->db->getFieldNames('doctor_master') ?? [];
$dSelect = ['id'];
foreach (['p_fname', 'p_lname', 'doctor_reg_no', 'registration_no', 'reg_no'] as $f) {
if (in_array($f, $dFields, true)) {
$dSelect[] = $f;
}
}
$dRow = $this->db->table('doctor_master')
->select(implode(',', array_unique($dSelect)))
->where('id', $doctorId)
->get(1)
->getRowArray() ?? [];
if ($doctorName === '' && ! empty($dRow)) {
$doctorName = trim(trim((string) ($dRow['p_fname'] ?? '')) . ' ' . trim((string) ($dRow['p_lname'] ?? '')));
}
if ($doctorRegNo === '' && ! empty($dRow)) {
foreach (['doctor_reg_no', 'registration_no', 'reg_no'] as $f) {
if (! empty($dRow[$f])) {
$doctorRegNo = trim((string) $dRow[$f]);
break;
}
}
}
} catch (\Throwable $e) {
// fail-open
}
}
if ($doctorName === '') {
return null;
}
return [
'name' => $doctorName,
'registration_number' => $doctorRegNo,
];
}
/**
* Map numeric lab_type code to a human-readable title.
*/

View File

@ -132,7 +132,7 @@ class FhirR4Builder
'display' => 'Ambulatory',
],
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
'period' => ['start' => trim((string) ($encounter['period_start'] ?? '')) !== '' ? (string) $encounter['period_start'] : $issuedAt],
'period' => ['start' => $this->normalizeIsoDateTime((string) ($encounter['period_start'] ?? ''), $issuedAt)],
];
if ($practitionerRef !== '') {
$encounterResource['participant'] = [['individual' => ['reference' => $practitionerRef]]];
@ -657,7 +657,7 @@ class FhirR4Builder
$patientRef = 'urn:uuid:' . $patientUuid;
$reportRef = 'urn:uuid:' . $reportUuid;
$reportedAt = trim((string) ($diagnosticReport['reported_at'] ?? '')) ?: $issuedAt;
$reportedAt = $this->normalizeIsoDateTime((string) ($diagnosticReport['reported_at'] ?? ''), $issuedAt);
$reportTitle = trim((string) ($diagnosticReport['title'] ?? 'Diagnostic Report'));
$reportStatus = trim((string) ($diagnosticReport['status'] ?? 'final'));
$isImaging = (bool) ($diagnosticReport['is_imaging'] ?? false)
@ -734,12 +734,15 @@ class FhirR4Builder
],
'subject' => ['reference' => $patientRef, 'display' => 'Patient'],
];
$pStart = trim((string) ($encounter['period_start'] ?? ''));
if ($pStart !== '') {
$encounterRes['period'] = ['start' => $pStart];
$pEnd = trim((string) ($encounter['period_end'] ?? ''));
if ($pEnd !== '') {
$encounterRes['period']['end'] = $pEnd;
$pStartRaw = trim((string) ($encounter['period_start'] ?? ''));
if ($pStartRaw !== '') {
$encounterRes['period'] = ['start' => $this->normalizeIsoDateTime($pStartRaw, $issuedAt)];
$pEndRaw = trim((string) ($encounter['period_end'] ?? ''));
if ($pEndRaw !== '') {
$pEndIso = $this->normalizeIsoDateTime($pEndRaw);
if ($pEndIso !== '') {
$encounterRes['period']['end'] = $pEndIso;
}
}
}
if ($practitionerRef !== '') {
@ -809,7 +812,9 @@ class FhirR4Builder
if ($refHigh !== null && is_numeric($refHigh)) {
$rr['high'] = ['value' => (float) $refHigh, 'unit' => $unit ?: $ucum, 'system' => 'http://unitsofmeasure.org', 'code' => $ucum];
}
$obsResource['referenceRange'] = [$rr];
if (! empty($rr)) {
$obsResource['referenceRange'] = [$rr];
}
}
// Interpretation (H/L/N)
$interp = trim((string) ($obs['interpretation'] ?? ''));
@ -1400,6 +1405,28 @@ class FhirR4Builder
return (new \DateTime('now', new \DateTimeZone('Asia/Kolkata')))->format('Y-m-d\TH:i:sP');
}
/**
* Normalize any datetime-like string into ISO 8601 with IST offset.
*/
private function normalizeIsoDateTime(string $value, string $fallback = ''): string
{
$raw = trim($value);
if ($raw === '') {
return trim($fallback);
}
try {
$hasTimezone = preg_match('/([+-]\d{2}:\d{2}|Z)$/', $raw) === 1;
$dt = $hasTimezone && strpos($raw, 'T') !== false
? new \DateTime($raw)
: new \DateTime($raw, new \DateTimeZone('Asia/Kolkata'));
return $dt->setTimezone(new \DateTimeZone('Asia/Kolkata'))->format('Y-m-d\TH:i:sP');
} catch (\Throwable $e) {
return trim($fallback);
}
}
/**
* @param array<string, mixed> $patient
*
@ -1435,8 +1462,9 @@ class FhirR4Builder
'name' => [$nameEntry],
];
if (! empty($patient['gender'])) {
$resource['gender'] = (string) $patient['gender'];
$gender = $this->normalizeAdministrativeGender($patient['gender'] ?? null);
if ($gender !== '') {
$resource['gender'] = $gender;
}
if (! empty($patient['birthDate'])) {
@ -1471,6 +1499,25 @@ class FhirR4Builder
return $resource;
}
/**
* Convert legacy/local gender values to FHIR AdministrativeGender.
*/
private function normalizeAdministrativeGender($value): string
{
$raw = strtolower(trim((string) $value));
if ($raw === '') {
return '';
}
return match ($raw) {
'm', 'male', '1' => 'male',
'f', 'female', '2' => 'female',
'o', 'other', '3', 'transgender', 'trans' => 'other',
'u', 'unknown', '0', 'na', 'n/a', 'not known' => 'unknown',
default => in_array($raw, ['male', 'female', 'other', 'unknown'], true) ? $raw : 'unknown',
};
}
/**
* Map common route-of-administration text to SNOMED CT codes.
* Returns empty string when no mapping is found.