diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index 41b1ea2..ac6e7ce 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -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|null $labReqRow + * @return array|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. */ diff --git a/app/Libraries/FhirR4Builder.php b/app/Libraries/FhirR4Builder.php index 8b0f976..e42df43 100644 --- a/app/Libraries/FhirR4Builder.php +++ b/app/Libraries/FhirR4Builder.php @@ -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 $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.