Redesign Consent Request History table columns and add Fetch/Show Data modal with FHIR section accordion
This commit is contained in:
parent
7129f6e17b
commit
37dba1948e
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<int, array{title: string, items: array<int, string>, 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('/<li[^>]*>/i', "\n- ", $div) ?? $div;
|
||||
$div = preg_replace('/<br\s*\/?>/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
|
||||
|
|
|
|||
|
|
@ -347,13 +347,13 @@ if ($patientPhotoPath === '') {
|
|||
<table class="table table-sm table-hover mb-0" id="abdmRequestsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th>Request ID</th>
|
||||
<th>HI Types</th>
|
||||
<th>Requested By</th>
|
||||
<th>Requested On</th>
|
||||
<th>Expiry</th>
|
||||
<th class="text-end">Actions</th>
|
||||
<th>Expiry Date</th>
|
||||
<th>Date From</th>
|
||||
<th>Date To</th>
|
||||
<th>View</th>
|
||||
<th class="text-end">Show Data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -446,6 +446,20 @@ if ($patientPhotoPath === '') {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="abdmFetchResultModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Fetched Health Records</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="abdmFetchResultModalBody">
|
||||
<div class="text-muted small text-center">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="abdmCustomConsentModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
|
|
@ -687,21 +701,18 @@ $(function() {
|
|||
var html = '';
|
||||
requests.forEach(function(consent, idx) {
|
||||
var status = (consent.status || '').toString().toUpperCase();
|
||||
var hiTypes = (consent.requested_hi_types && consent.requested_hi_types.length) ? consent.requested_hi_types : (consent.granted_hi_types || []);
|
||||
var hiTypesText = hiTypes.length ? hiTypes.join(', ') : '-';
|
||||
var canFetch = (status === 'GRANTED' || status === 'COMPLETED');
|
||||
var requestId = consent.consent_request_id || '-';
|
||||
var rowId = consent.id || (idx + 1);
|
||||
|
||||
html += '<tr>'
|
||||
+ '<td class="small">' + escHtml(rowId) + '</td>'
|
||||
+ '<td><span class="badge ' + consentStatusBadgeClass(status) + '">' + escHtml(status || 'UNKNOWN') + '</span></td>'
|
||||
+ '<td class="small text-truncate" style="max-width:160px;" title="' + escHtml(requestId) + '">' + escHtml(requestId) + '</td>'
|
||||
+ '<td class="small">' + escHtml(hiTypesText) + '</td>'
|
||||
+ '<td class="small">' + escHtml(consent.requested_by || 'HMS') + '</td>'
|
||||
+ '<td class="small">' + fmtConsentTs(consent.requested_on) + '</td>'
|
||||
+ '<td class="small">' + fmtConsentTs(consent.erase_at) + '</td>'
|
||||
+ '<td class="small">' + fmtConsentTs(consent.valid_from) + '</td>'
|
||||
+ '<td class="small">' + fmtConsentTs(consent.valid_to) + '</td>'
|
||||
+ '<td><button type="button" class="btn btn-link btn-sm p-0 abdm-view-request-btn" data-idx="' + idx + '">View</button></td>'
|
||||
+ '<td class="text-end">'
|
||||
+ '<button type="button" class="btn btn-link btn-sm p-0 me-2 abdm-view-request-btn" data-idx="' + idx + '">View</button>'
|
||||
+ (canFetch ? ('<button type="button" class="btn btn-link btn-sm p-0 abdm-fetch-request-btn" data-idx="' + idx + '">Fetch Records</button>') : '')
|
||||
+ (canFetch ? ('<button type="button" class="btn btn-link btn-sm p-0 abdm-fetch-request-btn" data-idx="' + idx + '">Show Data</button>') : '<span class="text-muted small">-</span>')
|
||||
+ '</td>'
|
||||
+ '</tr>';
|
||||
});
|
||||
|
|
@ -736,6 +747,8 @@ $(function() {
|
|||
fetchOnlyRunningIdx = idx;
|
||||
applyNewRequestButtonState();
|
||||
setAbdmStatus('Fetching latest records using existing granted consent...', false);
|
||||
openAbdmFetchResultModal();
|
||||
$('#abdmFetchResultModalBody').html('<div class="text-muted small text-center">Fetching latest records...</div>');
|
||||
|
||||
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('<div class="text-danger small text-center">' + escHtml('Failed to load records: ' + (err.message || err)) + '</div>');
|
||||
});
|
||||
}
|
||||
|
||||
function renderAbdmFetchResultModal(docs) {
|
||||
if (!docs || !docs.length) {
|
||||
$('#abdmFetchResultModalBody').html('<div class="text-muted small text-center">No fetched records found for this consent request yet.</div>');
|
||||
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 += '<div class="card mb-3">'
|
||||
+ '<div class="card-body">'
|
||||
+ '<div class="fw-semibold">' + escHtml(doc.document_title || 'ABDM Document') + '</div>'
|
||||
+ '<div class="small text-muted mb-2">'
|
||||
+ escHtml(doc.organization_name || '-') + ' | '
|
||||
+ escHtml(doc.practitioner_name || '-') + ' | '
|
||||
+ 'Date: ' + escHtml(fmtDateTime(doc.document_date || doc.created_at))
|
||||
+ '</div>';
|
||||
|
||||
if (sections.length) {
|
||||
html += '<div class="accordion" id="' + accId + '">';
|
||||
sections.forEach(function(sec, si) {
|
||||
var itemId = accId + '_' + si;
|
||||
var body = '';
|
||||
if (sec.items && sec.items.length) {
|
||||
body = '<ul class="mb-0 small">' + sec.items.map(function(it) { return '<li>' + escHtml(it) + '</li>'; }).join('') + '</ul>';
|
||||
} else if (sec.narrative) {
|
||||
body = '<div class="small" style="white-space:pre-line;">' + escHtml(sec.narrative) + '</div>';
|
||||
} else {
|
||||
body = '<div class="small text-muted">No details recorded.</div>';
|
||||
}
|
||||
html += '<div class="accordion-item">'
|
||||
+ '<h2 class="accordion-header">'
|
||||
+ '<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#' + itemId + '">' + escHtml(sec.title || 'Section') + '</button>'
|
||||
+ '</h2>'
|
||||
+ '<div id="' + itemId + '" class="accordion-collapse collapse" data-bs-parent="#' + accId + '">'
|
||||
+ '<div class="accordion-body">' + body + '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '<div class="small text-muted">No structured sections available for this record.</div>';
|
||||
}
|
||||
|
||||
html += '</div></div>';
|
||||
});
|
||||
$('#abdmFetchResultModalBody').html(html);
|
||||
}
|
||||
|
||||
var zoom = 1;
|
||||
var minZoom = 0.5;
|
||||
var maxZoom = 3;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user