R EA ource page updated

This commit is contained in:
nisha.meshram 2026-08-27 13:40:13 +00:00
parent 543afd3d6c
commit b9ba92e484

View File

@ -0,0 +1,350 @@
// Generic engine for a "browse one resource type across all patients" page
// (Conditions, Observations, Allergies, Medications, Encounters). Each page
// just defines window.RESOURCE_PAGE_CONFIG and includes this script — no
// per-page JS needed. See conditions.html for the config shape.
let rpPatientsCache = [];
let rpSearchTerm = '';
let rpAddModal, rpEditingRowId = null, rpEditingPatientId = null;
function rpEscapeHtml(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function rpFmtDate(value) {
if (!value) return '—';
const d = new Date(value);
if (isNaN(d)) return '—';
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
}
function rpShowToast(message, variant = 'success') {
const container = document.getElementById('toastContainer');
if (!container) { alert(message); return; }
const el = document.createElement('div');
el.className = `toast align-items-center text-bg-${variant === 'danger' ? 'danger' : 'success'} border-0 show mb-2`;
el.innerHTML = `<div class="d-flex"><div class="toast-body">${rpEscapeHtml(message)}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" onclick="this.closest('.toast').remove()"></button></div>`;
container.appendChild(el);
setTimeout(() => el.remove(), 5000);
}
function rpFieldInputHtml(field, value) {
const id = `rp_field_${field.key}`;
const val = value !== undefined && value !== null ? value : (field.default || '');
if (field.type === 'select') {
return `<div class="col-12">
<label class="form-label small">${rpEscapeHtml(field.label)}</label>
<select class="form-select" id="${id}">
${field.options.map(o => `<option value="${o}" ${o === val ? 'selected' : ''}>${o}</option>`).join('')}
</select>
</div>`;
}
if (field.type === 'checkbox') {
const checked = value === true || value === 'true';
return `<div class="col-12 form-check mt-1">
<input type="checkbox" class="form-check-input" id="${id}" ${checked ? 'checked' : ''}>
<label class="form-check-label small" for="${id}">${rpEscapeHtml(field.label)}</label>
</div>`;
}
if (field.type === 'textarea') {
return `<div class="col-12">
<label class="form-label small">${rpEscapeHtml(field.label)}</label>
<textarea ${field.required ? 'required' : ''} class="form-control" id="${id}" rows="2">${rpEscapeHtml(val)}</textarea>
</div>`;
}
const inputVal = field.type === 'date' && val ? String(val).slice(0, 10)
: field.type === 'datetime-local' && val ? String(val).slice(0, 16)
: val;
return `<div class="col-12">
<label class="form-label small">${rpEscapeHtml(field.label)}</label>
<input type="${field.type}" ${field.required ? 'required' : ''} class="form-control" id="${id}" value="${rpEscapeHtml(inputVal)}">
</div>`;
}
async function rpLoadPatientsForPicker() {
try {
rpPatientsCache = await Api.getPatients();
const select = document.getElementById('rp_patientSelect');
if (select) {
select.innerHTML = rpPatientsCache.map(p => `<option value="${p.id}">${rpEscapeHtml(p.fullName)} (${rpEscapeHtml(p.medicalRecordNumber)})</option>`).join('');
}
} catch { /* patient picker just won't populate; add flow will fail loudly instead */ }
}
function rpOpenAddModal() {
rpEditingRowId = null;
rpEditingPatientId = null;
const config = window.RESOURCE_PAGE_CONFIG;
document.getElementById('rp_modalTitle').textContent = `Add ${config.title.replace(/s$/, '')}`;
document.getElementById('rp_patientPickerRow').classList.remove('d-none');
document.getElementById('rp_fieldsBody').innerHTML = config.fields.map(f => rpFieldInputHtml(f, null)).join('');
rpAddModal.show();
}
function rpOpenEditModal(row) {
rpEditingRowId = row.id;
rpEditingPatientId = row.patientId;
const config = window.RESOURCE_PAGE_CONFIG;
document.getElementById('rp_modalTitle').textContent = `Edit ${config.title.replace(/s$/, '')}${row.patientName}`;
document.getElementById('rp_patientPickerRow').classList.add('d-none');
document.getElementById('rp_fieldsBody').innerHTML = config.fields.map(f => rpFieldInputHtml(f, row[f.key])).join('');
rpAddModal.show();
}
async function rpSubmitModal(e) {
e.preventDefault();
const config = window.RESOURCE_PAGE_CONFIG;
const payload = {};
for (const f of config.fields) {
const el = document.getElementById(`rp_field_${f.key}`);
let v;
if (f.type === 'checkbox') {
v = el.checked;
} else {
v = el.value;
if (f.type === 'number') v = v === '' ? null : parseFloat(v);
v = v === '' ? null : v;
}
payload[f.key] = v;
}
try {
if (rpEditingRowId) {
await config.update(rpEditingPatientId, rpEditingRowId, payload);
rpShowToast(`${config.title} updated.`);
} else {
const patientId = document.getElementById('rp_patientSelect').value;
if (!patientId) { rpShowToast('Select a patient first.', 'danger'); return; }
await config.add(patientId, payload);
rpShowToast(`${config.title} added.`);
}
rpAddModal.hide();
await rpLoadGrid();
} catch (err) {
rpShowToast(err.message, 'danger');
}
}
async function rpDeleteRow(row) {
const config = window.RESOURCE_PAGE_CONFIG;
if (!confirm(`Delete this ${config.title.replace(/s$/, '').toLowerCase()} for ${row.patientName}?`)) return;
try {
await config.remove(row.patientId, row.id);
rpShowToast('Deleted.');
await rpLoadGrid();
// The row's Delete button lives inside the popup modal — refresh (or
// close, if that was their last record) so it doesn't show stale data.
const container = document.getElementById('rp_gridBody');
const stillHasRows = (container._rpRows || []).some(r => r.patientId === row.patientId);
if (stillHasRows) rpOpenViewModal(row.patientId);
else if (rpViewModalInstance) rpViewModalInstance.hide();
} catch (err) {
rpShowToast(err.message, 'danger');
}
}
// Bootstrap "bg-*" classes cycled per tile so patients are visually easy to
// tell apart at a glance — not tied to any data meaning, just variety.
const RP_TILE_COLORS = ['primary', 'success', 'info', 'warning', 'danger', 'secondary'];
// The pages ship a <table><thead>...<tbody id="rp_gridBody"> wrapper (from
// before this was a tile grid). Swap that whole table out for a plain grid
// div, once, so none of the 7 HTML pages that use this engine need editing.
function rpEnsureTileContainer() {
const existing = document.getElementById('rp_gridBody');
if (existing.tagName.toLowerCase() === 'div') return existing;
const table = existing.closest('table');
const div = document.createElement('div');
div.id = 'rp_gridBody';
div.className = 'rp-tile-grid';
table.replaceWith(div);
return div;
}
let rpViewModalInstance = null;
// The pages don't ship a "view details" modal in their HTML — build it once
// and reuse it, same approach as rpEnsureTileContainer above. Scrollable so
// patients with a lot of records don't blow out the page.
function rpEnsureViewModal() {
let el = document.getElementById('rp_viewModal');
if (el) return el;
el = document.createElement('div');
el.className = 'modal fade';
el.id = 'rp_viewModal';
el.tabIndex = -1;
el.innerHTML = `
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="rp_viewModalTitle"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0" id="rp_viewModalBody"></div>
</div>
</div>`;
document.body.appendChild(el);
rpViewModalInstance = new bootstrap.Modal(el);
return el;
}
function rpOpenViewModal(patientId) {
const config = window.RESOURCE_PAGE_CONFIG;
const container = document.getElementById('rp_gridBody');
const rows = (container._rpRows || []).filter(r => r.patientId === patientId);
if (rows.length === 0) return;
rpEnsureViewModal();
document.getElementById('rp_viewModalTitle').textContent = rows[0].patientName;
const detailColumns = config.columns.filter(c => c.key !== 'patientName');
const detailRows = rows.map(row => {
const cells = detailColumns.map(c => {
if (c.render) return `<td>${c.render(row[c.key], row)}</td>`;
let v = row[c.key];
v = c.type === 'date' ? rpFmtDate(v) : rpEscapeHtml(v ?? '—');
return `<td>${v}</td>`;
}).join('');
const actions = config.readOnly ? '' : `
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary me-1" data-edit-row="${row.id}" data-edit-patient="${row.patientId}">Edit</button>
<button class="btn btn-sm btn-outline-danger" data-delete-row="${row.id}" data-delete-patient="${row.patientId}">Delete</button>
</td>`;
return `<tr data-row-id="${row.id}" data-row-patient="${row.patientId}">${cells}${actions}</tr>`;
}).join('');
document.getElementById('rp_viewModalBody').innerHTML = `
<div style="max-height:60vh;overflow-y:auto">
<table class="table table-sm resource-grid-table align-middle mb-0">
<thead><tr>${detailColumns.map(c => `<th>${rpEscapeHtml(c.label)}</th>`).join('')}${config.readOnly ? '' : '<th></th>'}</tr></thead>
<tbody>${detailRows}</tbody>
</table>
</div>`;
rpViewModalInstance.show();
}
async function rpLoadGrid() {
const config = window.RESOURCE_PAGE_CONFIG;
const container = rpEnsureTileContainer();
const empty = document.getElementById('rp_gridEmpty');
const countLabel = document.getElementById('rp_countLabel');
try {
const rows = await config.listAll(rpSearchTerm.trim());
const patientCount = new Set(rows.map(r => r.patientId)).size;
if (countLabel) countLabel.textContent = `${rows.length} record${rows.length === 1 ? '' : 's'} · ${patientCount} patient${patientCount === 1 ? '' : 's'}`;
if (rows.length === 0) {
container.innerHTML = '';
empty.classList.remove('d-none');
return;
}
empty.classList.add('d-none');
const groups = new Map();
rows.forEach(r => {
if (!groups.has(r.patientId)) groups.set(r.patientId, { patientId: r.patientId, patientName: r.patientName, items: [] });
groups.get(r.patientId).items.push(r);
});
container.innerHTML = [...groups.values()].map((g, i) => {
const color = RP_TILE_COLORS[i % RP_TILE_COLORS.length];
const initials = g.patientName.split(' ').map(n => n[0]).slice(0, 2).join('').toUpperCase();
return `
<div class="card shadow-sm border-0" style="cursor:pointer" data-view-patient="${g.patientId}">
<div class="card-body text-center py-3">
<div class="rounded-circle bg-${color} text-white d-inline-flex align-items-center justify-content-center mb-2" style="width:44px;height:44px;font-size:14px" aria-hidden="true">${rpEscapeHtml(initials)}</div>
<div class="fw-semibold small">${rpEscapeHtml(g.patientName)}</div>
<span class="badge bg-${color}-subtle text-${color}-emphasis rounded-pill mt-1">${g.items.length} record${g.items.length === 1 ? '' : 's'}</span>
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-${color}" data-view-patient="${g.patientId}">View</button>
</div>
</div>
</div>`;
}).join('');
// Stash rows for the View/Edit/Delete handlers (avoids a second fetch per click).
container._rpRows = rows;
// Lets a page compute something from the freshly-loaded rows (e.g. an
// "X% actioned" stat) without the generic engine needing to know about
// page-specific fields like "Actioned".
if (typeof config.afterLoad === 'function') config.afterLoad(rows);
} catch (err) {
rpShowToast(err.message, 'danger');
}
}
function rpInit() {
const config = window.RESOURCE_PAGE_CONFIG;
if (!config) return;
if (!sessionStorage.getItem('cip_token')) {
window.location.href = 'login.html';
return;
}
renderSidebar(config.key);
document.getElementById('rp_pageIcon').textContent = config.icon;
document.getElementById('rp_pageTitle').textContent = config.title;
document.title = `${config.title} — Clinical Insight Pro`;
document.getElementById('clinicianLabel').textContent = `👤 ${sessionStorage.getItem('cip_clinician') || 'Clinician'}`;
document.getElementById('logoutBtn').addEventListener('click', () => {
sessionStorage.clear();
window.location.href = 'login.html';
});
if (config.readOnly) {
document.getElementById('rp_addBtn').classList.add('d-none');
}
// "View" opens a scrollable popup with that patient's records, instead of
// expanding inline (which used to push the rest of the grid down). Works
// on every page, including read-only ones (Risk Assessment, Care Plans).
rpEnsureTileContainer().addEventListener('click', (e) => {
const viewEl = e.target.closest('[data-view-patient]');
if (!viewEl) return;
rpOpenViewModal(viewEl.dataset.viewPatient);
});
if (!config.readOnly) {
rpAddModal = new bootstrap.Modal(document.getElementById('rp_modal'));
document.getElementById('rp_addBtn').addEventListener('click', rpOpenAddModal);
document.getElementById('rp_form').addEventListener('submit', rpSubmitModal);
rpLoadPatientsForPicker();
rpEnsureViewModal().addEventListener('click', (e) => {
const editBtn = e.target.closest('[data-edit-row]');
const delBtn = e.target.closest('[data-delete-row]');
const tbody = document.getElementById('rp_gridBody');
if (editBtn) {
const row = (tbody._rpRows || []).find(r => String(r.id) === editBtn.dataset.editRow && r.patientId === editBtn.dataset.editPatient);
if (row) { rpViewModalInstance.hide(); rpOpenEditModal(row); }
} else if (delBtn) {
const row = (tbody._rpRows || []).find(r => String(r.id) === delBtn.dataset.deleteRow && r.patientId === delBtn.dataset.deletePatient);
if (row) rpDeleteRow(row);
}
});
}
document.getElementById('rp_searchInput').addEventListener('input', (e) => {
rpSearchTerm = e.target.value;
rpLoadGrid();
});
rpLoadGrid();
}
document.addEventListener('DOMContentLoaded', rpInit);