diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php index 2b17d2f..23d40ab 100644 --- a/app/Controllers/Patient.php +++ b/app/Controllers/Patient.php @@ -1168,12 +1168,17 @@ class Patient extends BaseController } $q = trim((string) ($this->request->getGet('q') ?? '')); + $filterConsentRequestId = trim((string) ($this->request->getGet('consent_request_id') ?? '')); + $includeSummary = ((int) ($this->request->getGet('include_summary') ?? 0)) === 1; $docFields = $this->db->getFieldNames('abdm_hiu_documents') ?? []; $selectCols = ['d.id', 'd.patient_id', 'd.abha_address', 'd.document_title', 'd.document_date', 'd.care_context_reference', 'd.practitioner_name', 'd.organization_name', 'd.bundle_type', 'd.created_at']; if (in_array('consent_request_id', $docFields, true)) { $selectCols[] = 'd.consent_request_id'; } + if ($includeSummary) { + $selectCols[] = 'd.summary_json'; + } $builder = $this->db->table('abdm_hiu_documents d') ->select(implode(', ', $selectCols)) @@ -1188,6 +1193,10 @@ class Patient extends BaseController } $builder->groupEnd(); + if ($filterConsentRequestId !== '' && in_array('consent_request_id', $docFields, true)) { + $builder->where('d.consent_request_id', $filterConsentRequestId); + } + if ($q !== '') { $builder->groupStart() ->like('d.document_title', $q) @@ -1199,6 +1208,22 @@ class Patient extends BaseController $rows = $builder->get()->getResultArray(); + if ($includeSummary) { + foreach ($rows as &$rowRef) { + $summary = json_decode((string) ($rowRef['summary_json'] ?? ''), true); + if (! is_array($summary)) { + $summary = []; + } + // Attachments carry base64 image/PDF data — strip them from this + // list payload to keep it light; use abdm_document_detail for the + // full attachment content of a single document. + unset($summary['attachments']); + $rowRef['summary'] = $summary; + unset($rowRef['summary_json']); + } + unset($rowRef); + } + return $this->response->setJSON([ 'ok' => 1, 'count' => count($rows), @@ -1208,6 +1233,7 @@ class Patient extends BaseController ]); } + public function abdm_document_detail(int $pno, int $docId) { if ($docId <= 0 || ! $this->db->tableExists('abdm_hiu_documents')) { @@ -4027,6 +4053,17 @@ class Patient extends BaseController } } + // A short, human-friendly identifier for this session (the "Request ID" + // guid is too long to display in a table column) -- use the underlying + // abdm_hiu_workflows row id of this session's own CONSENT_REQUEST row + // (falling back to whichever row scored best) since it's already a + // small, unique, DB-backed integer. + $displayId = (int) (is_array($consentRequestRow) ? ($consentRequestRow['id'] ?? 0) : 0); + if ($displayId <= 0) { + $displayId = (int) ($best['id'] ?? 0); + } + + $requestedHiTypes = []; $requestedOn = ''; $purpose = ''; @@ -4129,6 +4166,7 @@ class Patient extends BaseController return [ 'ok' => 1, 'consent' => [ + 'id' => $displayId, 'consent_id' => $consentId, 'consent_request_id' => $consentRequestId, 'abha_address' => $abhaAddress, diff --git a/app/Libraries/Abdm/M3HiuDocumentRepository.php b/app/Libraries/Abdm/M3HiuDocumentRepository.php index 47ea0fa..c62437a 100644 --- a/app/Libraries/Abdm/M3HiuDocumentRepository.php +++ b/app/Libraries/Abdm/M3HiuDocumentRepository.php @@ -270,6 +270,10 @@ class M3HiuDocumentRepository $conditions = []; $vitals = []; $medications = []; + // Indexed by "ResourceType/id" and by fullUrl, so Composition.section[].entry + // references (e.g. {"reference": "Condition/xyz"}) can be resolved back to + // their actual resource below. + $resourcesByRef = []; foreach ($entries as $entry) { $resource = is_array($entry['resource'] ?? null) ? (array) $entry['resource'] : []; @@ -278,6 +282,15 @@ class M3HiuDocumentRepository continue; } + $resourceId = trim((string) ($resource['id'] ?? '')); + if ($resourceId !== '') { + $resourcesByRef[$type . '/' . $resourceId] = $resource; + } + $fullUrl = trim((string) ($entry['fullUrl'] ?? '')); + if ($fullUrl !== '') { + $resourcesByRef[$fullUrl] = $resource; + } + if ($type === 'Patient' && empty($patient)) { $patient = $resource; } elseif ($type === 'Composition' && empty($composition)) { @@ -336,9 +349,121 @@ class M3HiuDocumentRepository 'vitals' => $vitals, 'medications' => $medications, 'attachments' => $this->extractAttachments($bundle), + // Mirrors the Composition's own section breakdown (Chief Complaints, + // Physical Examination, Allergies, Investigations, Medications, + // Procedures, Medical History, Care Plan, Documents, etc.) so the UI + // can render an accordion per record exactly like the source document + // is organized, instead of a fixed hard-coded set of categories. + 'sections' => $this->buildCompositionSections($composition, $resourcesByRef), ]; } + /** + * Resolves each Composition.section[].entry[] reference to its underlying + * resource and produces a short display label for it, falling back to the + * section's own narrative text (section.text.div) when there are no + * resolvable entries (some HIPs only populate the narrative). + * + * @return array, narrative: string}> + */ + private function buildCompositionSections(array $composition, array $resourcesByRef): array + { + $sections = []; + + foreach ((array) ($composition['section'] ?? []) as $section) { + if (! is_array($section)) { + continue; + } + + $title = trim((string) (($section['title'] ?? '') ?: ($section['code']['text'] ?? ''))); + if ($title === '') { + continue; + } + + $items = []; + foreach ((array) ($section['entry'] ?? []) as $ref) { + if (! is_array($ref)) { + continue; + } + $refStr = trim((string) ($ref['reference'] ?? '')); + if ($refStr === '' || ! isset($resourcesByRef[$refStr])) { + continue; + } + $label = $this->describeResourceForSection((array) $resourcesByRef[$refStr]); + if ($label !== '') { + $items[] = $label; + } + } + + $narrative = ''; + if ($items === []) { + $div = trim((string) ($section['text']['div'] ?? '')); + if ($div !== '') { + $div = preg_replace('/]*>/i', "\n- ", $div) ?? $div; + $div = preg_replace('//i', "\n", $div) ?? $div; + $narrative = trim(html_entity_decode(strip_tags($div), ENT_QUOTES | ENT_HTML5)); + } + } + + $sections[] = [ + 'title' => $title, + 'items' => $items, + 'narrative' => $narrative, + ]; + } + + return $sections; + } + + /** + * Produces a short, human-readable one-line label for a FHIR resource, for + * display inside a Composition section's accordion body. + */ + private function describeResourceForSection(array $resource): string + { + $type = trim((string) ($resource['resourceType'] ?? '')); + + switch ($type) { + case 'Condition': + case 'AllergyIntolerance': + case 'Procedure': + case 'ServiceRequest': + return trim((string) (($resource['code']['text'] ?? '') ?: ($resource['code']['coding'][0]['display'] ?? ''))); + + case 'Observation': + $name = trim((string) (($resource['code']['text'] ?? '') ?: ($resource['code']['coding'][0]['display'] ?? ''))); + $val = ''; + if (isset($resource['valueQuantity']) && is_array($resource['valueQuantity'])) { + $val = trim((string) (($resource['valueQuantity']['value'] ?? '') . ' ' . ($resource['valueQuantity']['unit'] ?? ''))); + } elseif (isset($resource['valueString'])) { + $val = trim((string) $resource['valueString']); + } elseif (isset($resource['valueCodeableConcept'])) { + $val = trim((string) (($resource['valueCodeableConcept']['text'] ?? '') ?: ($resource['valueCodeableConcept']['coding'][0]['display'] ?? ''))); + } + return $val !== '' ? ($name . ': ' . $val) : $name; + + case 'MedicationRequest': + case 'MedicationStatement': + $med = trim((string) (($resource['medicationCodeableConcept']['text'] ?? '') ?: ($resource['medicationCodeableConcept']['coding'][0]['display'] ?? ''))); + $dose = trim((string) ($resource['dosageInstruction'][0]['text'] ?? ($resource['dosage'][0]['text'] ?? ''))); + return $dose !== '' ? ($med . ' — ' . $dose) : $med; + + case 'DiagnosticReport': + $name = trim((string) (($resource['code']['text'] ?? '') ?: ($resource['code']['coding'][0]['display'] ?? ''))); + $conclusion = trim((string) ($resource['conclusion'] ?? '')); + return $conclusion !== '' ? ($name . ' — ' . $conclusion) : $name; + + case 'CarePlan': + return trim((string) (($resource['description'] ?? '') ?: ($resource['title'] ?? ''))); + + case 'DocumentReference': + return trim((string) (($resource['description'] ?? '') ?: ($resource['type']['text'] ?? '') ?: ($resource['type']['coding'][0]['display'] ?? ''))); + + default: + return ''; + } + } + /** * Extracts attached scanned images/PDFs/generic documents (DocumentReference * resources, whose content may carry the base64 attachment data inline or diff --git a/app/Views/billing/Patient_Profile_Opd_V.php b/app/Views/billing/Patient_Profile_Opd_V.php index cd86b34..2b9fe5b 100644 --- a/app/Views/billing/Patient_Profile_Opd_V.php +++ b/app/Views/billing/Patient_Profile_Opd_V.php @@ -347,13 +347,13 @@ if ($patientPhotoPath === '') { + - - - - - - + + + + + @@ -446,6 +446,20 @@ if ($patientPhotoPath === '') { + + ' + + '' + '' - + '' - + '' - + '' - + '' + + '' + + '' + '' + + '' + '' + ''; }); @@ -736,6 +747,8 @@ $(function() { fetchOnlyRunningIdx = idx; applyNewRequestButtonState(); setAbdmStatus('Fetching latest records using existing granted consent...', false); + openAbdmFetchResultModal(); + $('#abdmFetchResultModalBody').html('
Fetching latest records...
'); var url = abdmFetchOnlyUrl + '?consent_id=' + encodeURIComponent(consent.consent_id || '') + '&consent_request_id=' + encodeURIComponent(consent.consent_request_id || ''); fetch(url, { credentials: 'same-origin' }) @@ -749,14 +762,95 @@ $(function() { applyNewRequestButtonState(); loadAbdmDocs(); loadAbdmConsentRequests(); + loadAbdmFetchResultModalData(consent); }) .catch(function(err) { fetchOnlyRunningIdx = -1; applyNewRequestButtonState(); setAbdmStatus('Fetch failed: ' + (err.message || err), true); + // Even if the fetch call itself failed, still show whatever records + // are already saved locally for this consent request. + loadAbdmFetchResultModalData(consent); }); } + function openAbdmFetchResultModal() { + var bsModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('abdmFetchResultModal')); + bsModal.show(); + } + + function loadAbdmFetchResultModalData(consent) { + var url = abdmDocumentsUrl + '?limit=200&include_summary=1'; + var reqId = (consent && consent.consent_request_id) ? consent.consent_request_id : ''; + if (reqId !== '') { + url += '&consent_request_id=' + encodeURIComponent(reqId); + } + + fetch(url, { credentials: 'same-origin' }) + .then(function(resp) { return resp.json(); }) + .then(function(data) { + if (!data || data.ok !== 1) { + throw new Error((data && data.error) || 'Unable to load fetched records.'); + } + renderAbdmFetchResultModal(Array.isArray(data.items) ? data.items : []); + }) + .catch(function(err) { + $('#abdmFetchResultModalBody').html('
' + escHtml('Failed to load records: ' + (err.message || err)) + '
'); + }); + } + + function renderAbdmFetchResultModal(docs) { + if (!docs || !docs.length) { + $('#abdmFetchResultModalBody').html('
No fetched records found for this consent request yet.
'); + return; + } + + var html = ''; + docs.forEach(function(doc, di) { + var summary = doc.summary || {}; + var sections = Array.isArray(summary.sections) ? summary.sections : []; + var accId = 'abdmFetchAcc' + di; + + html += '
' + + '
' + + '
' + escHtml(doc.document_title || 'ABDM Document') + '
' + + '
' + + escHtml(doc.organization_name || '-') + '  |  ' + + escHtml(doc.practitioner_name || '-') + '  |  ' + + 'Date: ' + escHtml(fmtDateTime(doc.document_date || doc.created_at)) + + '
'; + + if (sections.length) { + html += '
'; + sections.forEach(function(sec, si) { + var itemId = accId + '_' + si; + var body = ''; + if (sec.items && sec.items.length) { + body = '
    ' + sec.items.map(function(it) { return '
  • ' + escHtml(it) + '
  • '; }).join('') + '
'; + } else if (sec.narrative) { + body = '
' + escHtml(sec.narrative) + '
'; + } else { + body = '
No details recorded.
'; + } + html += '
' + + '

' + + '' + + '

' + + '
' + + '
' + body + '
' + + '
' + + '
'; + }); + html += '
'; + } else { + html += '
No structured sections available for this record.
'; + } + + html += '
'; + }); + $('#abdmFetchResultModalBody').html(html); + } + var zoom = 1; var minZoom = 0.5; var maxZoom = 3;
ID StatusRequest IDHI TypesRequested ByRequested OnExpiryActionsExpiry DateDate FromDate ToViewShow Data
' + escHtml(rowId) + '' + escHtml(status || 'UNKNOWN') + '' + escHtml(requestId) + '' + escHtml(hiTypesText) + '' + escHtml(consent.requested_by || 'HMS') + '' + fmtConsentTs(consent.requested_on) + '' + fmtConsentTs(consent.erase_at) + '' + fmtConsentTs(consent.valid_from) + '' + fmtConsentTs(consent.valid_to) + '' - + '' - + (canFetch ? ('') : '') + + (canFetch ? ('') : '-') + '