diff --git a/.Net Capstone Project/sidebarvv.js b/.Net Capstone Project/sidebarvv.js new file mode 100644 index 0000000..eb1cb90 --- /dev/null +++ b/.Net Capstone Project/sidebarvv.js @@ -0,0 +1,300 @@ +// Single source of truth for the left navigation on EVERY page. +// +// Two problems this file solves: +// +// 1. Pages that never call renderSidebar() β€” it self-initialises on +// DOMContentLoaded, so just including the script is enough. +// +// 2. Pages with their own custom script that renders a DIFFERENT (shorter) +// menu β€” a MutationObserver watches the container and re-asserts the full +// list whenever something replaces it. This is why CDS Alerts and +// Observations were showing only 10 items while the Dashboard showed all +// of them: those pages build their own nav after this script has run. +// +// Adding a nav item? Add it to SIDEBAR_ITEMS and it appears everywhere. + +const SIDEBAR_ITEMS = [ + { key: 'dashboard', href: 'dashboard.html', icon: '🏠', label: 'Dashboard' }, + { key: 'patients', href: 'patients.html', icon: 'πŸ§‘\u200dβš•οΈ', label: 'Patients' }, + { key: 'conditions', href: 'conditions.html', icon: '🩺', label: 'Conditions' }, + { key: 'observations', href: 'observations.html', icon: 'πŸ“ˆ', label: 'Observations' }, + { key: 'allergies', href: 'allergies.html', icon: '⚠️', label: 'Allergies' }, + { key: 'medications', href: 'medications.html', icon: 'πŸ’Š', label: 'Medications' }, + { key: 'encounters', href: 'encounters.html', icon: 'πŸ“…', label: 'Encounters' }, + { key: 'organizations', href: 'organizations.html', icon: 'πŸ₯', label: 'Organizations' }, + { key: 'locations', href: 'locations.html', icon: 'πŸ“', label: 'Locations' }, + { key: 'practitioners', href: 'practitioners.html', icon: 'πŸ‘¨β€βš•οΈ', label: 'Practitioners' }, + { key: 'practitionerroles', href: 'practitionerroles.html', icon: '🩹', label: 'Practitioner Roles' }, + { key: 'servicerequests', href: 'servicerequests.html', icon: 'πŸ“„', label: 'Service Requests' }, + { key: 'riskassessments', href: 'riskassessments.html', icon: 'πŸ“Š', label: 'Risk Assessment' }, + { key: 'cdsalerts', href: 'cdsalerts.html', icon: 'πŸ””', label: 'CDS Alerts' }, + { key: 'careplans', href: 'careplans.html', icon: 'πŸ“‹', label: 'Care Plans' }, + { key: 'cdsrules', href: 'cdsrules.html', icon: 'βš™οΈ', label: 'CDS Rules' }, + { key: 'users', href: 'users.html', icon: 'πŸ‘€', label: 'Users' } +]; + +// The collapse + scroll + search styles ship with this script rather than +// css/styles.css. That external file wasn't being picked up on most pages, +// which is why the toggle only ever worked on the dashboard. +const SIDEBAR_STYLES = ` +.app-sidebar { + display: flex; + flex-direction: column; + transition: width .16s ease; + /* Without a bounded height the nav grows instead of scrolling, which is + why the scrollbar never appeared. */ + max-height: 100vh; + position: sticky; + top: 0; + overflow: hidden; +} +.sidebar-toggle { + background: rgba(255,255,255,.14); border: none; color: #fff; + font-size: 1.1rem; line-height: 1; width: 40px; height: 40px; + border-radius: 10px; margin: 0 .2rem .6rem; cursor: pointer; flex-shrink: 0; +} +.sidebar-toggle:hover { background: rgba(255,255,255,.26); } + +/* Hamburger in the top bar, left of the brand. */ +.navbar-sidebar-toggle { + background: rgba(255,255,255,.16); border: none; color: #fff; + font-size: 1.15rem; line-height: 1; width: 38px; height: 38px; + border-radius: 9px; margin-right: .75rem; cursor: pointer; flex-shrink: 0; +} +.navbar-sidebar-toggle:hover { background: rgba(255,255,255,.3); } + +/* Welcome line at the top of the rail. */ +.sidebar-welcome { + padding: 0 .55rem .55rem; flex-shrink: 0; + border-bottom: 1px solid rgba(255,255,255,.12); margin-bottom: .55rem; +} +.sidebar-welcome-hi { display: block; font-size: .72rem; color: rgba(255,255,255,.6); } +.sidebar-welcome-name { + display: block; font-weight: 600; color: #fff; font-size: .95rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} + +/* --- Search --- */ +.sidebar-search { padding: 0 .35rem .6rem; flex-shrink: 0; } +.sidebar-search input { + width: 100%; border-radius: 8px; border: 1px solid rgba(255,255,255,.2); + background: rgba(255,255,255,.12); color: #fff; + padding: .35rem .6rem; font-size: .85rem; +} +.sidebar-search input::placeholder { color: rgba(255,255,255,.6); } +.sidebar-search input:focus { + outline: none; border-color: rgba(255,255,255,.5); + background: rgba(255,255,255,.18); +} +.sidebar-empty { + color: rgba(255,255,255,.55); font-size: .82rem; + padding: .5rem .6rem; +} + +/* --- Scrollable nav so long menus don't get cut off --- */ +.sidebar-nav { + flex: 1 1 auto; + overflow-y: auto; + min-height: 0; + padding-right: .15rem; + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,.45) rgba(255,255,255,.08); +} +.sidebar-nav::-webkit-scrollbar { width: 10px; } +.sidebar-nav::-webkit-scrollbar-track { background: rgba(0,0,0,.22); border-radius: 5px; } +.sidebar-nav::-webkit-scrollbar-thumb { + background: rgba(255,255,255,.65); border-radius: 5px; + border: 2px solid transparent; background-clip: content-box; +} +.sidebar-nav::-webkit-scrollbar-thumb:hover { background: #fff; background-clip: content-box; } + +/* --- Collapsed rail --- */ +/* !important because css/styles.css sets .app-sidebar { width: ... } and, + depending on load order, could otherwise win this. */ +body.sidebar-collapsed .app-sidebar, +body.sidebar-collapsed #appSidebar { + width: 68px !important; + min-width: 68px !important; + padding-left: .4rem; + padding-right: .4rem; +} +body.sidebar-collapsed .sidebar-text { display: none; } +body.sidebar-collapsed .sidebar-search, +body.sidebar-collapsed .sidebar-welcome { display: none; } +body.sidebar-collapsed .sidebar-link { justify-content: center; padding-left: .4rem; padding-right: .4rem; } +`; + +function injectSidebarStyles() { + if (document.getElementById('cip-sidebar-styles')) return; + const style = document.createElement('style'); + style.id = 'cip-sidebar-styles'; + style.textContent = SIDEBAR_STYLES; + document.head.appendChild(style); +} + +// Pages that deliberately have no sidebar (public / auth screens). +const SIDEBAR_EXCLUDED_PAGES = ['index.html', 'login.html', 'register.html', 'callback.html']; + +// Set while this script is writing to the container, so the observer below +// doesn't react to its own changes and loop forever. +let sidebarWriting = false; + +function sidebarContainer() { + return document.getElementById('appSidebar') || document.querySelector('.app-sidebar'); +} + +// Works out which item to highlight from the URL, so a page doesn't have to +// pass its key in. An explicit key still wins when one is supplied. +function sidebarKeyFromUrl() { + const file = (window.location.pathname.split('/').pop() || 'dashboard.html').toLowerCase(); + const match = SIDEBAR_ITEMS.find(i => i.href.toLowerCase() === file); + return match ? match.key : ''; +} + +// Puts the hamburger in the top bar, immediately left of the brand β€” the +// spot people reach for first. Returns false if the page has no navbar, in +// which case the button stays inside the sidebar as before. +function mountNavbarToggle() { + const brand = document.querySelector('.navbar-brand'); + if (!brand) return false; + + if (!document.getElementById('sidebarToggle')) { + const btn = document.createElement('button'); + btn.id = 'sidebarToggle'; + btn.type = 'button'; + btn.className = 'navbar-sidebar-toggle'; + btn.title = 'Collapse / expand menu'; + btn.setAttribute('aria-label', 'Toggle navigation'); + btn.textContent = '☰'; + brand.parentNode.insertBefore(btn, brand); + } + return true; +} + +// "Welcome, " using whoever signed in. auth.js stores the display name +// under cip_clinician at sign-in. +function currentUserName() { + try { + return sessionStorage.getItem('cip_clinician') + || localStorage.getItem('cip_clinician') + || ''; + } catch { return ''; } +} + +function renderSidebar(activeKey) { + injectSidebarStyles(); + const container = sidebarContainer(); + if (!container) return; + + const key = activeKey || sidebarKeyFromUrl(); + const collapsed = localStorage.getItem('cip_sidebar_collapsed') === '1'; + + sidebarWriting = true; + + const navbarHasToggle = mountNavbarToggle(); + + container.innerHTML = ` + ${navbarHasToggle ? '' : ` + `} + + + + `; + + const name = currentUserName(); + const welcome = document.getElementById('sidebarWelcome'); + if (welcome) { + welcome.innerHTML = name + ? `Welcome, + ${name}` + : ''; + welcome.classList.toggle('d-none', !name); + } + + applySidebarState(collapsed); + + // Filter the menu as you type. Expands the rail first if it's collapsed, + // since the search box is hidden in that state. + const search = document.getElementById('sidebarSearch'); + if (search) search.oninput = () => { + const term = search.value.trim().toLowerCase(); + let shown = 0; + + container.querySelectorAll('.sidebar-link').forEach(a => { + const match = !term || a.dataset.label.includes(term); + a.classList.toggle('d-none', !match); + if (match) shown++; + }); + + document.getElementById('sidebarNoMatch').classList.toggle('d-none', shown > 0); + }; + + // Assigned, not added. The navbar button is created once and survives + // every re-render, so addEventListener stacked a new handler each time β€” + // an even number of handlers flipped the state twice per click, which + // looked exactly like a dead button. + const toggleBtn = document.getElementById('sidebarToggle'); + if (toggleBtn) { + toggleBtn.onclick = () => { + const nowCollapsed = !document.body.classList.contains('sidebar-collapsed'); + applySidebarState(nowCollapsed); + try { + localStorage.setItem('cip_sidebar_collapsed', nowCollapsed ? '1' : '0'); + } catch {} + }; + } + + // Release on the next tick so the observer ignores this whole write. + setTimeout(() => { sidebarWriting = false; }, 0); +} + +function applySidebarState(collapsed) { + document.body.classList.toggle('sidebar-collapsed', collapsed); +} + +// Re-render if the menu ever ends up with the wrong number of links β€” which +// is what happens when a page's own script overwrites the container after +// this one has run. +function guardSidebar() { + const container = sidebarContainer(); + if (!container) return; + + new MutationObserver(() => { + if (sidebarWriting) return; + const links = container.querySelectorAll('.sidebar-link').length; + if (links !== SIDEBAR_ITEMS.length) renderSidebar(); + }).observe(container, { childList: true, subtree: true }); +} + +function initSidebar() { + const file = (window.location.pathname.split('/').pop() || '').toLowerCase(); + if (SIDEBAR_EXCLUDED_PAGES.includes(file)) return; + + renderSidebar(); + guardSidebar(); + + // Belt and braces for pages whose own script runs late (after images and + // stylesheets finish loading). + window.addEventListener('load', () => { + const container = sidebarContainer(); + if (container && container.querySelectorAll('.sidebar-link').length !== SIDEBAR_ITEMS.length) { + renderSidebar(); + } + }); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initSidebar); +} else { + initSidebar(); +}