diff --git a/.Net Capstone Project/resource-page updated.js b/.Net Capstone Project/resource-page updated.js new file mode 100644 index 0000000..31a779f --- /dev/null +++ b/.Net Capstone Project/resource-page updated.js @@ -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, '&').replace(//g, '>') + .replace(/"/g, '"').replace(/'/g, '''); +} + +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 = `
${rpEscapeHtml(message)}
+
`; + 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 `
+ + +
`; + } + + if (field.type === 'checkbox') { + const checked = value === true || value === 'true'; + return `
+ + +
`; + } + + if (field.type === 'textarea') { + return `
+ + +
`; + } + + const inputVal = field.type === 'date' && val ? String(val).slice(0, 10) + : field.type === 'datetime-local' && val ? String(val).slice(0, 16) + : val; + return `
+ + +
`; +} + +async function rpLoadPatientsForPicker() { + try { + rpPatientsCache = await Api.getPatients(); + const select = document.getElementById('rp_patientSelect'); + if (select) { + select.innerHTML = rpPatientsCache.map(p => ``).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 ... 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 = ` + `; + 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 ``; + let v = row[c.key]; + v = c.type === 'date' ? rpFmtDate(v) : rpEscapeHtml(v ?? '—'); + return ``; + }).join(''); + const actions = config.readOnly ? '' : ` + `; + return `${cells}${actions}`; + }).join(''); + + document.getElementById('rp_viewModalBody').innerHTML = ` +
+
${c.render(row[c.key], row)}${v} + + +
+ ${detailColumns.map(c => ``).join('')}${config.readOnly ? '' : ''} + ${detailRows} +
${rpEscapeHtml(c.label)}
+ `; + + 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 ` +
+
+ +
${rpEscapeHtml(g.patientName)}
+ ${g.items.length} record${g.items.length === 1 ? '' : 's'} +
+ +
+
+
`; + }).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);