Feat: left sidebar nav, tab persistence, admin models fix, hospital/chart save

- Replace horizontal tab-nav with collapsible left sidebar (sticky desktop, slide-in mobile with hamburger toggle)
- Fix tab persistence: use localStorage instead of URL hash; restore last tab after login in enterApp()
- Fix admin CMS models loading: move autoDeleteDays read inside .then() callback (was ReferenceError), switch admin loading from click-only to tabChanged event
- Add Save/Load bars to Hospital Course and Chart Review tabs with popover support
- Add wv (well visit) save handler in encounters.js
- Register load handlers for hospital and chart tabs
- Expand popover prefix list to include hosp and chart
This commit is contained in:
Daniel Onyejesi 2026-03-22 17:17:45 -04:00
parent 97f8009d38
commit bc47be371a
8 changed files with 241 additions and 98 deletions

View file

@ -50,15 +50,25 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
.btn-header{background:rgba(255,255,255,0.15);border:none;color:white;width:36px;height:36px;border-radius:8px;cursor:pointer;font-size:14px;transition:background 0.2s;}
.btn-header:hover{background:rgba(255,255,255,0.3);}
/* TABS */
.tab-nav{display:flex;background:white;border-bottom:2px solid var(--g200);max-width:1200px;margin:0 auto;overflow-x:auto;}
.tab-btn{display:flex;align-items:center;gap:6px;padding:12px 18px;border:none;border-bottom:3px solid transparent;background:none;color:var(--g500);font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap;font-family:inherit;transition:all 0.2s;}
.tab-btn:hover{color:var(--g700);background:var(--g50);}
.tab-btn.active{color:var(--blue);border-bottom-color:var(--blue);}
.tab-btn i{font-size:15px;}
/* APP BODY LAYOUT */
.app-body{display:flex;min-height:calc(100vh - 66px);}
/* SIDEBAR */
.sidebar{width:210px;flex-shrink:0;background:white;border-right:1.5px solid var(--g200);display:flex;flex-direction:column;position:sticky;top:0;height:calc(100vh - 66px);overflow-y:auto;z-index:100;}
.sidebar-header{display:none;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--g200);}
.sidebar-title{font-size:13px;font-weight:700;color:var(--g700);}
.sidebar-nav{display:flex;flex-direction:column;padding:8px 0;flex:1;}
.sidebar-section-label{font-size:10px;font-weight:700;color:var(--g400);text-transform:uppercase;letter-spacing:0.7px;padding:10px 16px 4px;}
.tab-btn{display:flex;align-items:center;gap:10px;padding:10px 16px;border:none;border-left:3px solid transparent;background:none;color:var(--g600);font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap;font-family:inherit;transition:all 0.15s;width:100%;text-align:left;}
.tab-btn:hover{background:var(--g50);color:var(--g900);}
.tab-btn.active{background:var(--blue-light);color:var(--blue);border-left-color:var(--blue);font-weight:600;}
.tab-btn i{width:16px;text-align:center;flex-shrink:0;font-size:14px;}
.tab-btn span{overflow:hidden;text-overflow:ellipsis;}
.sidebar-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:199;}
.btn-menu-toggle{display:none;}
/* MAIN */
.main-content{max-width:1200px;margin:0 auto;padding:20px 16px;}
.main-content{flex:1;min-width:0;padding:20px 20px;}
.tab-content{display:none;}
.tab-content.active{display:block;animation:fadeIn 0.25s ease;}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}}
@ -223,13 +233,18 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
/* RESPONSIVE */
@media(max-width:768px){
.header-top{flex-direction:column;align-items:flex-start;}
.header-right{width:100%;flex-wrap:wrap;}
.header-top{flex-direction:row;align-items:center;}
.header-right{flex-wrap:wrap;}
.model-selector{flex:1;}
.model-selector select{max-width:100%;flex:1;}
.tab-btn span{display:none;}
.tab-btn{flex:1;justify-content:center;padding:10px;}
.tab-btn i{font-size:16px;}
.model-selector select{max-width:160px;flex:1;}
.btn-menu-toggle{display:flex;align-items:center;justify-content:center;}
/* Sidebar: hidden off-screen on mobile, slides in */
.app-body{display:block;}
.sidebar{position:fixed;left:-220px;top:0;width:220px;height:100vh;z-index:200;transition:left 0.28s ease;box-shadow:none;}
.sidebar.open{left:0;box-shadow:4px 0 20px rgba(0,0,0,0.18);}
.sidebar.open~.sidebar-overlay{display:block;}
.sidebar-header{display:flex;}
.main-content{padding:12px;}
.demographics-bar{flex-direction:column;}
.demo-field input,.demo-field select{min-width:100%;}
.note-meta{flex-direction:column;}

View file

@ -102,11 +102,16 @@
<header class="app-header">
<div class="header-content">
<div class="header-top">
<div class="logo">
<i class="fas fa-stethoscope"></i>
<div>
<h1>Pediatric AI Scribe</h1>
<p class="tagline">Welcome, <span id="user-name">Doctor</span></p>
<div style="display:flex;align-items:center;gap:10px;">
<button id="btn-menu-toggle" class="btn-header btn-menu-toggle" title="Menu" aria-label="Toggle menu">
<i class="fas fa-bars"></i>
</button>
<div class="logo">
<i class="fas fa-stethoscope"></i>
<div>
<h1>Pediatric AI Scribe</h1>
<p class="tagline">Welcome, <span id="user-name">Doctor</span></p>
</div>
</div>
</div>
<div class="header-right">
@ -135,50 +140,66 @@
<button class="announcement-close" onclick="this.parentElement.classList.add('hidden')" aria-label="Dismiss"><i class="fas fa-times"></i></button>
</div>
<!-- TAB NAVIGATION -->
<nav class="tab-nav">
<button class="tab-btn active" data-tab="encounter">
<i class="fas fa-comments"></i>
<span>Encounter HPI</span>
</button>
<button class="tab-btn" data-tab="dictation">
<i class="fas fa-microphone"></i>
<span>Dictation HPI</span>
</button>
<button class="tab-btn" data-tab="hospital">
<i class="fas fa-hospital"></i>
<span>Hospital Course</span>
</button>
<button class="tab-btn" data-tab="chart">
<i class="fas fa-clipboard-list"></i>
<span>Chart Review</span>
</button>
<button class="tab-btn" data-tab="soap">
<i class="fas fa-file-waveform"></i>
<span>SOAP Note</span>
</button>
<button class="tab-btn" data-tab="milestones">
<i class="fas fa-baby"></i>
<span>Milestones</span>
</button>
<button class="tab-btn" data-tab="wellvisit">
<i class="fas fa-calendar-check"></i>
<span>Well Visit</span>
</button>
<button class="tab-btn" data-tab="sickvisit">
<i class="fas fa-thermometer-half"></i>
<span>Sick Visit</span>
</button>
<button class="tab-btn" data-tab="settings">
<i class="fas fa-cog"></i>
<span>Settings</span>
</button>
<button class="tab-btn hidden" data-tab="admin" id="admin-tab-btn">
<i class="fas fa-user-shield"></i>
<span>Admin</span>
</button>
<div class="app-body">
<!-- SIDEBAR NAVIGATION -->
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<span class="sidebar-title"><i class="fas fa-stethoscope"></i> Menu</span>
<button id="btn-sidebar-close" class="btn-sm btn-ghost" title="Close menu" style="padding:4px 8px;">
<i class="fas fa-times"></i>
</button>
</div>
<div class="sidebar-nav">
<span class="sidebar-section-label">Encounters</span>
<button class="tab-btn active" data-tab="encounter">
<i class="fas fa-comments"></i>
<span>Encounter HPI</span>
</button>
<button class="tab-btn" data-tab="dictation">
<i class="fas fa-microphone"></i>
<span>Dictation HPI</span>
</button>
<span class="sidebar-section-label">Notes</span>
<button class="tab-btn" data-tab="hospital">
<i class="fas fa-hospital"></i>
<span>Hospital Course</span>
</button>
<button class="tab-btn" data-tab="chart">
<i class="fas fa-clipboard-list"></i>
<span>Chart Review</span>
</button>
<button class="tab-btn" data-tab="soap">
<i class="fas fa-file-waveform"></i>
<span>SOAP Note</span>
</button>
<span class="sidebar-section-label">Pediatric</span>
<button class="tab-btn" data-tab="milestones">
<i class="fas fa-baby"></i>
<span>Milestones</span>
</button>
<button class="tab-btn" data-tab="wellvisit">
<i class="fas fa-calendar-check"></i>
<span>Well Visit</span>
</button>
<button class="tab-btn" data-tab="sickvisit">
<i class="fas fa-thermometer-half"></i>
<span>Sick Visit</span>
</button>
<span class="sidebar-section-label">Account</span>
<button class="tab-btn" data-tab="settings">
<i class="fas fa-cog"></i>
<span>Settings</span>
</button>
<button class="tab-btn hidden" data-tab="admin" id="admin-tab-btn">
<i class="fas fa-user-shield"></i>
<span>Admin</span>
</button>
</div>
</nav>
<div class="sidebar-overlay" id="sidebar-overlay"></div>
<main class="main-content">
<!-- ===== TAB: ENCOUNTER HPI ===== -->
@ -386,6 +407,25 @@
</div>
</div>
<!-- Save/Load bar -->
<div class="save-bar-wrap">
<div class="save-bar" id="hosp-save-bar">
<div style="display:flex;align-items:center;gap:8px;flex:1;">
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
<input type="text" id="hosp-label" class="save-label-input" placeholder="Patient label (e.g. John D, Visit #42)">
</div>
<button id="btn-hosp-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
<button id="btn-hosp-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
</div>
<div id="hosp-load-popover" class="enc-load-popover hidden">
<div class="enc-load-popover-inner">
<input type="text" id="hosp-load-search" class="enc-load-search" placeholder="Search saved encounters...">
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
</div>
<div id="hosp-pop-list" class="enc-pop-list"></div>
</div>
</div>
<!-- ED Note -->
<div class="card">
<div class="card-header"><h3><i class="fas fa-truck-medical"></i> ED Note (before admission)</h3></div>
@ -513,6 +553,25 @@
</div>
</div>
<!-- Save/Load bar -->
<div class="save-bar-wrap">
<div class="save-bar" id="chart-save-bar">
<div style="display:flex;align-items:center;gap:8px;flex:1;">
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
<input type="text" id="chart-label" class="save-label-input" placeholder="Patient label (e.g. John D, Visit #42)">
</div>
<button id="btn-chart-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
<button id="btn-chart-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
</div>
<div id="chart-load-popover" class="enc-load-popover hidden">
<div class="enc-load-popover-inner">
<input type="text" id="chart-load-search" class="enc-load-search" placeholder="Search saved encounters...">
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
</div>
<div id="chart-pop-list" class="enc-pop-list"></div>
</div>
</div>
<!-- Visits -->
<div id="cr-visits-container">
<div class="card visit-card">
@ -1285,7 +1344,7 @@
</section>
<!-- ===== TAB: SETTINGS ===== -->
<section id="settings-tab" class="tab-content hidden">
<section id="settings-tab" class="tab-content">
<div class="module-header">
<h2><i class="fas fa-cog"></i> Settings</h2>
<p>Account security, integrations, and personal templates</p>
@ -1388,6 +1447,8 @@
</main>
</div><!-- /.app-body -->
<!-- dummy settings-modal kept for any stale JS references -->
<div id="settings-modal" class="hidden" style="display:none !important;"></div>

View file

@ -6,12 +6,14 @@
var loaded = false;
// Load admin panel when tab is clicked
document.addEventListener('click', function(e) {
var btn = e.target.closest('.tab-btn');
if (btn && btn.getAttribute('data-tab') === 'admin') {
// Load admin panel when admin tab is activated
document.addEventListener('tabChanged', function(e) {
if (e.detail && e.detail.tab === 'admin') {
if (!loaded) { loadAdmin(); loaded = true; }
}
});
document.addEventListener('click', function(e) {
// Refresh button
if (e.target.closest('#btn-refresh-users')) {
loadUsers();
@ -237,12 +239,14 @@
var cmsLoaded = false;
// Load CMS when admin tab is opened
document.addEventListener('click', function(e) {
var btn = e.target.closest('.tab-btn');
if (btn && btn.getAttribute('data-tab') === 'admin') {
// Load CMS when admin tab is opened (via tabChanged event or click)
document.addEventListener('tabChanged', function(e) {
if (e.detail && e.detail.tab === 'admin') {
if (!cmsLoaded) { loadCms(); cmsLoaded = true; }
}
});
document.addEventListener('click', function(e) {
// Save buttons
if (e.target.closest('#btn-save-announcement')) saveAnnouncement();
@ -288,6 +292,13 @@
if (flagReadAloud) flagReadAloud.value = cfg['feature.read_aloud'] !== undefined ? cfg['feature.read_aloud'] : 'true';
if (flagNextcloud) flagNextcloud.value = cfg['feature.nextcloud'] !== undefined ? cfg['feature.nextcloud'] : 'true';
// Auto-delete setting
var autoDeleteDays = cfg['site.auto_delete_days'] || '7';
var el = document.getElementById('cms-auto-delete-days');
if (el) el.value = autoDeleteDays;
el = document.getElementById('admin-auto-delete-days');
if (el) el.textContent = autoDeleteDays;
// Email — load verify template by default
window._adminEmailConfig = cfg;
loadEmailFields('verify');
@ -297,13 +308,6 @@
loadPromptList();
loadSmtp();
loadModels();
// Auto-delete setting
var autoDeleteDays = cfg['site.auto_delete_days'] || '7';
var el = document.getElementById('cms-auto-delete-days');
if (el) el.value = autoDeleteDays;
el = document.getElementById('admin-auto-delete-days');
if (el) el.textContent = autoDeleteDays;
}
// ---- ANNOUNCEMENT ----

View file

@ -35,21 +35,41 @@ document.addEventListener('DOMContentLoaded', function() {
btn.classList.add('active');
var tabEl = document.getElementById(tabName + '-tab');
if (tabEl) tabEl.classList.add('active');
window.location.hash = tabName;
// Persist last tab in localStorage (restored after login in auth.js)
try { localStorage.setItem('ped_last_tab', tabName); } catch(e) {}
document.dispatchEvent(new CustomEvent('tabChanged', { detail: { tab: tabName } }));
// Close sidebar on mobile after tab click
var sidebar = document.getElementById('sidebar');
if (sidebar && window.innerWidth <= 768) {
sidebar.classList.remove('open');
}
return true;
}
// Expose activateTab globally so auth.js can call it after login
window.activateTab = activateTab;
document.querySelectorAll('.tab-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
activateTab(btn.getAttribute('data-tab'));
});
});
// Restore tab from URL hash on load
var hash = window.location.hash.replace('#', '');
if (!hash || !activateTab(hash)) {
activateTab('encounter'); // default
// --- SIDEBAR TOGGLE ---
var sidebar = document.getElementById('sidebar');
var sidebarOverlay = document.getElementById('sidebar-overlay');
document.addEventListener('click', function(e) {
if (e.target.closest('#btn-menu-toggle')) {
if (sidebar) sidebar.classList.toggle('open');
}
if (e.target.closest('#btn-sidebar-close') || e.target.closest('#sidebar-overlay')) {
if (sidebar) sidebar.classList.remove('open');
}
});
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', function() {
if (sidebar) sidebar.classList.remove('open');
});
}
// Load settings data when settings tab is activated

View file

@ -94,6 +94,15 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('[Auth] ✅ Entered app as:', user.email, 'Role:', user.role);
// Restore last visited tab (saved by activateTab)
var lastTab = null;
try { lastTab = localStorage.getItem('ped_last_tab'); } catch(e) {}
if (typeof window.activateTab === 'function') {
if (!lastTab || !window.activateTab(lastTab)) {
window.activateTab('encounter');
}
}
// Load announcement banner if function is available
if (typeof loadAnnouncement === 'function') loadAnnouncement();
}

View file

@ -113,5 +113,15 @@
document.getElementById('cr-refine-btn').addEventListener('click', function() { refineDocument('cr-review-text', 'cr-refine-input'); });
document.getElementById('cr-shorten-btn').addEventListener('click', function() { shortenDocument('cr-review-text'); });
// Register load handler for saved encounters
window.registerEncounterLoadHandler('chart', function(enc) {
if (enc.generated_note) {
var reviewText = document.getElementById('cr-review-text');
var outputCard = document.getElementById('cr-output');
if (reviewText) reviewText.textContent = enc.generated_note;
if (outputCard) outputCard.classList.remove('hidden');
}
});
console.log('✅ Chart Review module loaded');
})();

View file

@ -100,7 +100,7 @@
_savedEncounters = data.encounters || [];
renderSavedList();
// Refresh any open popovers
['enc', 'dict', 'wv', 'sick'].forEach(function(pfx) {
['enc', 'dict', 'wv', 'sick', 'hosp', 'chart'].forEach(function(pfx) {
var pop = document.getElementById(pfx + '-load-popover');
if (pop && !pop.classList.contains('hidden')) {
var search = pop.querySelector('.enc-load-search');
@ -190,7 +190,7 @@
if (!popover) return;
var isHidden = popover.classList.contains('hidden');
// Close all popovers first
['enc', 'dict', 'wv', 'sick'].forEach(function(p) {
['enc', 'dict', 'wv', 'sick', 'hosp', 'chart'].forEach(function(p) {
var pop = document.getElementById(p + '-load-popover');
if (pop) pop.classList.add('hidden');
});
@ -228,15 +228,16 @@
if (!data.success) { showToast(data.error || 'Failed', 'error'); return; }
var enc = data.encounter;
// Close all popovers
['enc', 'dict', 'wv', 'sick'].forEach(function(p) {
['enc', 'dict', 'wv', 'sick', 'hosp', 'chart'].forEach(function(p) {
var pop = document.getElementById(p + '-load-popover');
if (pop) pop.classList.add('hidden');
});
// Navigate to correct tab
var tabMap = {
encounter: 'encounter', dictation: 'dictation', hospital: 'hospital',
chart: 'chart', soap: 'soap', milestones: 'milestones',
encounter: 'encounter', dictation: 'dictation',
hospital: 'hospital', chart: 'chart',
soap: 'soap', milestones: 'milestones',
wellvisit: 'wellvisit', sickvisit: 'sickvisit'
};
var tabName = tabMap[enc.enc_type] || 'encounter';
@ -294,17 +295,23 @@
loadSavedEncountersList();
}
// Save buttons per tab
if (e.target.closest('#btn-enc-save')) saveFromTab('encounter', 'enc');
if (e.target.closest('#btn-dict-save')) saveFromTab('dictation', 'dict');
if (e.target.closest('#btn-enc-save')) saveFromTab('encounter', 'enc');
if (e.target.closest('#btn-dict-save')) saveFromTab('dictation', 'dict');
if (e.target.closest('#btn-hosp-save')) saveFromTab('hospital', 'hosp');
if (e.target.closest('#btn-chart-save')) saveFromTab('chart', 'chart');
if (e.target.closest('#btn-wv-save')) saveFromTab('wellvisit', 'wv');
// Load buttons — open inline popovers
if (e.target.closest('#btn-enc-load')) openLoadPopover('enc');
if (e.target.closest('#btn-dict-load')) openLoadPopover('dict');
if (e.target.closest('#btn-wv-load')) openLoadPopover('wv');
if (e.target.closest('#btn-sick-load')) openLoadPopover('sick');
// Close popovers when clicking outside (popover stops its own propagation so this only fires for outside clicks)
if (!e.target.closest('#btn-enc-load') && !e.target.closest('#btn-dict-load') &&
!e.target.closest('#btn-wv-load') && !e.target.closest('#btn-sick-load')) {
['enc', 'dict', 'wv', 'sick'].forEach(function(p) {
if (e.target.closest('#btn-enc-load')) openLoadPopover('enc');
if (e.target.closest('#btn-dict-load')) openLoadPopover('dict');
if (e.target.closest('#btn-wv-load')) openLoadPopover('wv');
if (e.target.closest('#btn-sick-load')) openLoadPopover('sick');
if (e.target.closest('#btn-hosp-load')) openLoadPopover('hosp');
if (e.target.closest('#btn-chart-load')) openLoadPopover('chart');
// Close popovers when clicking outside
var loadBtnIds = ['#btn-enc-load','#btn-dict-load','#btn-wv-load','#btn-sick-load','#btn-hosp-load','#btn-chart-load'];
var clickedLoadBtn = loadBtnIds.some(function(id) { return e.target.closest(id); });
if (!clickedLoadBtn) {
['enc', 'dict', 'wv', 'sick', 'hosp', 'chart'].forEach(function(p) {
var pop = document.getElementById(p + '-load-popover');
if (pop) pop.classList.add('hidden');
});
@ -313,13 +320,20 @@
function saveFromTab(type, prefix) {
var labelEl = document.getElementById(prefix + '-label');
var transcriptEl = document.getElementById(prefix + '-transcript');
var noteEl = document.getElementById(prefix + '-hpi-text');
var label = labelEl ? labelEl.value.trim() : '';
if (!label) { showToast('Enter a patient label before saving', 'error'); return; }
var savedId = window['_savedEncId_' + type] || null;
var transcriptEl = document.getElementById(prefix + '-transcript');
// Each tab stores its generated note in a different element
var noteIdMap = {
encounter: 'enc-hpi-text', dictation: 'dict-hpi-text',
hospital: 'hc-course-text', chart: 'cr-review-text',
wellvisit: 'wv-note-text'
};
var noteEl = document.getElementById(noteIdMap[type] || (prefix + '-hpi-text'));
window.saveEncounter({
id: savedId,
label: label,

View file

@ -191,5 +191,15 @@
.catch(function(err) { hideLoading(); showToast(err.message, 'error'); });
});
// Register load handler for saved encounters
window.registerEncounterLoadHandler('hospital', function(enc) {
if (enc.generated_note) {
var courseText = document.getElementById('hc-course-text');
var outputCard = document.getElementById('hc-output');
if (courseText) courseText.textContent = enc.generated_note;
if (outputCard) outputCard.classList.remove('hidden');
}
});
console.log('✅ Hospital Course module loaded');
})();