Feat: milestones under well visit, standalone schedule tabs, remove admin models

- Move Milestones from standalone tab to well visit subtab (wv-panel-milestones)
  with 'Copy to Note' button that fills Developmental Assessment card in Visit Note
- Sidebar Milestones button now navigates to Well Visit → Milestones subtab
- Move Full Vaccine Schedule → standalone sidebar tab (vaxschedule-tab)
- Move Catch-Up Schedule → standalone sidebar tab (catchup-tab)
- Add Developmental Assessment textarea (wv-milestones-text) to Visit Note panel
- Remove AI Models management from admin panel (was failing to load)
- switchSubtab scoped to wellvisit-tab so standalone schedule panels unaffected
- Expose window.wvSwitchSubtab for cross-module subtab switching
This commit is contained in:
Daniel Onyejesi 2026-03-22 18:52:11 -04:00
parent bfa832713d
commit 90c7bc35af
5 changed files with 136 additions and 197 deletions

View file

@ -174,7 +174,7 @@
<span>SOAP Note</span>
</button>
<span class="sidebar-section-label">Pediatric</span>
<button class="tab-btn" data-tab="milestones">
<button class="tab-btn" data-tab="wellvisit" data-subtab="milestones">
<i class="fas fa-baby"></i>
<span>Milestones</span>
</button>
@ -182,6 +182,14 @@
<i class="fas fa-calendar-check"></i>
<span>Well Visit</span>
</button>
<button class="tab-btn" data-tab="vaxschedule">
<i class="fas fa-table"></i>
<span>Vaccine Schedule</span>
</button>
<button class="tab-btn" data-tab="catchup">
<i class="fas fa-rotate"></i>
<span>Catch-Up Schedule</span>
</button>
<button class="tab-btn" data-tab="sickvisit">
<i class="fas fa-thermometer-half"></i>
<span>Sick Visit</span>
@ -708,87 +716,25 @@
</div>
</section>
<!-- ===== TAB: MILESTONES ===== -->
<section id="milestones-tab" class="tab-content">
<!-- ===== TAB: VACCINE SCHEDULE ===== -->
<section id="vaxschedule-tab" class="tab-content">
<div class="module-header">
<h2><i class="fas fa-baby"></i> Developmental Milestones</h2>
<p>AAP & Nelson — Click to assess. Blank = not assessed (omitted).</p>
<h2><i class="fas fa-table"></i> Full Vaccine Schedule</h2>
<p>AAP/ACIP 2025 — Complete immunization schedule for 018 years</p>
</div>
<div class="demographics-bar">
<div class="demo-field"><label>Patient Age</label><input type="text" id="ms-age" placeholder="e.g., 9 months"></div>
<div class="demo-field"><label>Gender</label><select id="ms-gender"><option value="">Select</option><option>Male</option><option>Female</option></select></div>
<div class="demo-field">
<label>Age Group</label>
<select id="ms-age-group">
<option value="">-- Select --</option>
<option value="2 months">2 Months</option>
<option value="4 months">4 Months</option>
<option value="6 months">6 Months</option>
<option value="9 months">9 Months</option>
<option value="12 months">12 Months</option>
<option value="15 months">15 Months</option>
<option value="18 months">18 Months</option>
<option value="24 months">2 Years</option>
<option value="30 months">2.5 Years</option>
<option value="36 months">3 Years</option>
<option value="48 months">4 Years</option>
<option value="60 months">5 Years</option>
</select>
</div>
<div class="demo-field">
<label>Output Format</label>
<select id="ms-format">
<option value="narrative">Narrative (paragraphs)</option>
<option value="list">Structured List</option>
</select>
</div>
<div class="demo-field demo-field-model">
<label><i class="fas fa-robot"></i> Model</label>
<select class="tab-model-select"></select>
</div>
<div id="wv-panel-schedule">
<p class="wv-loading">Loading schedule...</p>
</div>
</section>
<div class="legend">
<div class="legend-item"><span class="legend-icon legend-yes"></span> Achieved</div>
<div class="legend-item"><span class="legend-icon legend-no"></span> Not Achieved</div>
<div class="legend-item"><span class="legend-icon legend-blank"></span> Not Assessed (omitted)</div>
<!-- ===== TAB: CATCH-UP SCHEDULE ===== -->
<section id="catchup-tab" class="tab-content">
<div class="module-header">
<h2><i class="fas fa-rotate"></i> Catch-Up Schedule</h2>
<p>ACIP catch-up immunization schedule for persons aged 4 months18 years</p>
</div>
<div id="milestone-checklist"></div>
<div class="milestone-actions" id="milestone-actions" style="display:none">
<button id="ms-all-yes" class="btn-sm btn-success"><i class="fas fa-check-double"></i> All Yes</button>
<button id="ms-all-clear" class="btn-sm btn-ghost"><i class="fas fa-eraser"></i> Clear All</button>
<button id="ms-generate-btn" class="btn-generate btn-generate-teal"><i class="fas fa-wand-magic-sparkles"></i> Generate Narrative</button>
</div>
<div id="ms-output" class="card output-card hidden">
<div class="card-header output-header">
<h3><i class="fas fa-file-medical"></i> Developmental Assessment</h3>
<div class="output-actions">
<span id="ms-model-tag" class="model-tag"></span>
<button class="btn-sm btn-primary" onclick="copyText('ms-narrative-text')"><i class="fas fa-copy"></i> Copy</button>
<button class="btn-sm btn-ghost" onclick="speakText('ms-narrative-text')"><i class="fas fa-volume-high"></i> Read</button>
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('ms-narrative-text','milestones')"><i class="fas fa-cloud-arrow-up"></i></button>
</div>
</div>
<div id="ms-summary-bar" class="summary-bar hidden"></div>
<div id="ms-narrative-text" class="output-text" contenteditable="true"></div>
<div class="summary-section">
<div class="summary-section-header">
<button id="ms-quick-summary-btn" class="btn-generate-summary"><i class="fas fa-compress"></i> 3-Sentence Summary</button>
</div>
<div id="ms-quick-summary" class="hidden">
<div class="card-header summary-output-header">
<h3><i class="fas fa-clipboard-list"></i> Quick Summary</h3>
<div class="output-actions">
<button class="btn-sm btn-primary" onclick="copyText('ms-summary-text')"><i class="fas fa-copy"></i> Copy</button>
</div>
</div>
<div id="ms-summary-text" class="summary-output-text" contenteditable="true"></div>
</div>
</div>
<div id="wv-panel-catchup">
<p class="wv-loading">Loading catch-up schedule...</p>
</div>
</section>
@ -977,19 +923,6 @@
</div>
</div>
<!-- ── CMS: Models Management ─────────────────────────────────── -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-microchip"></i> AI Models</h3>
<button id="btn-add-custom-model" class="btn-sm btn-ghost"><i class="fas fa-plus"></i> Add Custom</button>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:10px;">
<div id="cms-models-list" style="display:flex;flex-direction:column;gap:6px;">
<p style="color:var(--g400);font-size:13px;">Loading models...</p>
</div>
</div>
</div>
<!-- ── CMS: Saved Encounters (Admin View) ─────────────────────── -->
<div class="card">
<div class="card-header">
@ -1036,11 +969,8 @@
<button class="wv-subtab-btn active" data-subtab="byvisit">
<i class="fas fa-child"></i> By Visit Age
</button>
<button class="wv-subtab-btn" data-subtab="schedule">
<i class="fas fa-table"></i> Full Vaccine Schedule
</button>
<button class="wv-subtab-btn" data-subtab="catchup">
<i class="fas fa-rotate"></i> Catch-Up Schedule
<button class="wv-subtab-btn" data-subtab="milestones">
<i class="fas fa-baby"></i> Milestones
</button>
<button class="wv-subtab-btn" data-subtab="shadess" style="display:none;">
<i class="fas fa-brain"></i> SSHADESS (12+)
@ -1061,14 +991,86 @@
</div>
</div>
<!-- Full Vaccine Schedule sub-panel -->
<div id="wv-panel-schedule" class="wv-subpanel hidden">
<p class="wv-loading">Loading schedule...</p>
</div>
<!-- Milestones sub-panel -->
<div id="wv-panel-milestones" class="wv-subpanel hidden">
<div class="demographics-bar">
<div class="demo-field"><label>Patient Age</label><input type="text" id="ms-age" placeholder="e.g., 9 months"></div>
<div class="demo-field"><label>Gender</label><select id="ms-gender"><option value="">Select</option><option>Male</option><option>Female</option></select></div>
<div class="demo-field">
<label>Age Group</label>
<select id="ms-age-group">
<option value="">-- Select --</option>
<option value="2 months">2 Months</option>
<option value="4 months">4 Months</option>
<option value="6 months">6 Months</option>
<option value="9 months">9 Months</option>
<option value="12 months">12 Months</option>
<option value="15 months">15 Months</option>
<option value="18 months">18 Months</option>
<option value="24 months">2 Years</option>
<option value="30 months">2.5 Years</option>
<option value="36 months">3 Years</option>
<option value="48 months">4 Years</option>
<option value="60 months">5 Years</option>
</select>
</div>
<div class="demo-field">
<label>Output Format</label>
<select id="ms-format">
<option value="narrative">Narrative (paragraphs)</option>
<option value="list">Structured List</option>
</select>
</div>
<div class="demo-field demo-field-model">
<label><i class="fas fa-robot"></i> Model</label>
<select class="tab-model-select"></select>
</div>
</div>
<div class="legend">
<div class="legend-item"><span class="legend-icon legend-yes"></span> Achieved</div>
<div class="legend-item"><span class="legend-icon legend-no"></span> Not Achieved</div>
<div class="legend-item"><span class="legend-icon legend-blank"></span> Not Assessed (omitted)</div>
</div>
<div id="milestone-checklist"></div>
<div class="milestone-actions" id="milestone-actions" style="display:none">
<button id="ms-all-yes" class="btn-sm btn-success"><i class="fas fa-check-double"></i> All Yes</button>
<button id="ms-all-clear" class="btn-sm btn-ghost"><i class="fas fa-eraser"></i> Clear All</button>
<button id="ms-generate-btn" class="btn-generate btn-generate-teal"><i class="fas fa-wand-magic-sparkles"></i> Generate Narrative</button>
</div>
<div id="ms-output" class="card output-card hidden">
<div class="card-header output-header">
<h3><i class="fas fa-file-medical"></i> Developmental Assessment</h3>
<div class="output-actions">
<span id="ms-model-tag" class="model-tag"></span>
<button class="btn-sm btn-primary" onclick="copyText('ms-narrative-text')"><i class="fas fa-copy"></i> Copy</button>
<button class="btn-sm btn-success" id="btn-ms-copy-to-note"><i class="fas fa-clipboard-check"></i> Copy to Note</button>
<button class="btn-sm btn-ghost" onclick="speakText('ms-narrative-text')"><i class="fas fa-volume-high"></i> Read</button>
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('ms-narrative-text','milestones')"><i class="fas fa-cloud-arrow-up"></i></button>
</div>
</div>
<div id="ms-summary-bar" class="summary-bar hidden"></div>
<div id="ms-narrative-text" class="output-text" contenteditable="true"></div>
<div class="summary-section">
<div class="summary-section-header">
<button id="ms-quick-summary-btn" class="btn-generate-summary"><i class="fas fa-compress"></i> 3-Sentence Summary</button>
</div>
<div id="ms-quick-summary" class="hidden">
<div class="card-header summary-output-header">
<h3><i class="fas fa-clipboard-list"></i> Quick Summary</h3>
<div class="output-actions">
<button class="btn-sm btn-primary" onclick="copyText('ms-summary-text')"><i class="fas fa-copy"></i> Copy</button>
</div>
</div>
<div id="ms-summary-text" class="summary-output-text" contenteditable="true"></div>
</div>
</div>
</div>
<!-- Catch-Up Schedule sub-panel -->
<div id="wv-panel-catchup" class="wv-subpanel hidden">
<p class="wv-loading">Loading catch-up schedule...</p>
</div>
<!-- SSHADESS sub-panel (age 12+) -->
@ -1199,6 +1201,17 @@
</div>
</div>
<!-- Milestones carry-over -->
<div class="card" style="margin-bottom:10px;" id="wv-milestones-card">
<div class="card-header">
<h3><i class="fas fa-baby"></i> Developmental Assessment</h3>
<span style="font-size:12px;color:var(--g500);">From Milestones tab</span>
</div>
<div style="padding:8px 16px;">
<textarea id="wv-milestones-text" rows="3" style="width:100%;font-size:12px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="Developmental assessment auto-fills when you click 'Copy to Note' on Milestones tab..."></textarea>
</div>
</div>
<!-- ROS Card -->
<div class="card" style="margin-bottom:10px;">
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">

View file

@ -257,7 +257,6 @@
if (e.target.closest('#btn-reset-prompt')) resetPrompt();
if (e.target.closest('#btn-save-smtp')) saveSmtp();
if (e.target.closest('#btn-clear-smtp')) clearSmtp();
if (e.target.closest('#btn-add-custom-model')) addCustomModel();
if (e.target.closest('#btn-save-auto-delete')) saveAutoDelete();
if (e.target.closest('#btn-reset-all-defaults')) resetAllDefaults();
});
@ -307,7 +306,6 @@
loadPromptList();
loadSmtp();
loadModels();
}
// ---- ANNOUNCEMENT ----
@ -512,98 +510,6 @@
.catch(function() { showToast('Request failed', 'error'); });
}
// ---- MODELS ----
function loadModels() {
var container = document.getElementById('cms-models-list');
fetch('/api/admin/config/models', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) {
if (container) container.innerHTML = '<p style="color:var(--red);font-size:13px;">Failed to load models: ' + (data.error || 'Unknown error') + '</p>';
return;
}
renderModelsList(data.models || [], data.custom || []);
})
.catch(function(e) {
console.error('[Admin] Models load failed:', e);
if (container) container.innerHTML = '<p style="color:var(--red);font-size:13px;"><i class="fas fa-circle-xmark"></i> Failed to load models. <button id="btn-retry-models" style="color:var(--blue);background:none;border:none;cursor:pointer;text-decoration:underline;font-size:13px;">Retry</button></p>';
var retryBtn = document.getElementById('btn-retry-models');
if (retryBtn) retryBtn.addEventListener('click', loadModels);
});
}
function renderModelsList(models, custom) {
var container = document.getElementById('cms-models-list');
if (!container) return;
var rows = models.map(function(m) {
return '<div class="model-row">' +
'<div class="model-row-name">' + esc(m.name) +
' <span class="model-row-tag">' + esc(m.tag || '') + '</span>' +
'</div>' +
'<span class="model-row-cost">' + esc(m.cost || '') + '</span>' +
'<label style="display:flex;align-items:center;gap:4px;font-size:12px;">' +
'<input type="checkbox" class="model-toggle" data-id="' + esc(m.id) + '" ' + (m.enabled !== false ? 'checked' : '') + '> Enabled' +
'</label>' +
'</div>';
});
if (custom.length > 0) {
rows.push('<div style="font-size:11px;font-weight:700;color:var(--g500);text-transform:uppercase;letter-spacing:0.5px;margin-top:8px;padding-top:8px;border-top:1px solid var(--g200);">Custom Models</div>');
custom.forEach(function(m) {
rows.push('<div class="model-row">' +
'<div class="model-row-name">' + esc(m.name) + ' <span class="model-row-tag" style="background:var(--blue-light);color:var(--blue);">CUSTOM</span></div>' +
'<span class="model-row-cost" style="font-family:monospace;font-size:11px;">' + esc(m.id) + '</span>' +
'<button class="btn-sm btn-ghost model-delete-custom" data-id="' + esc(m.id) + '" style="color:var(--red);font-size:11px;"><i class="fas fa-trash"></i></button>' +
'</div>');
});
}
container.innerHTML = rows.join('');
container.querySelectorAll('.model-toggle').forEach(function(cb) {
cb.addEventListener('change', function() {
fetch('/api/admin/config/models/toggle', {
method: 'PUT', headers: getAuthHeaders(),
body: JSON.stringify({ modelId: cb.dataset.id, enabled: cb.checked })
})
.then(function(r) { return r.json(); })
.then(function(d) { if (!d.success) showToast(d.error || 'Failed', 'error'); })
.catch(function() { showToast('Request failed', 'error'); });
});
});
container.querySelectorAll('.model-delete-custom').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm('Remove custom model "' + btn.dataset.id + '"?')) return;
fetch('/api/admin/config/models/custom/' + encodeURIComponent(btn.dataset.id), { method: 'DELETE', headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(d) { if (d.success) { showToast('Removed', 'info'); loadModels(); } })
.catch(function() {});
});
});
}
function addCustomModel() {
var id = prompt('OpenRouter Model ID (e.g. google/gemini-2.5-pro):');
if (!id || !id.trim()) return;
var name = prompt('Display name:', id.split('/').pop());
if (!name) return;
var cost = prompt('Cost estimate (e.g. ~$0.01):', '~$0.01');
fetch('/api/admin/config/models/custom', {
method: 'POST', headers: getAuthHeaders(),
body: JSON.stringify({ id: id.trim(), name: name.trim(), cost: cost || '?' })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) { showToast('Custom model added', 'success'); loadModels(); }
else showToast(data.error || 'Failed', 'error');
})
.catch(function() { showToast('Request failed', 'error'); });
}
// ---- AUTO-DELETE / RESET ----
function saveAutoDelete() {

View file

@ -52,6 +52,10 @@ document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.tab-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
activateTab(btn.getAttribute('data-tab'));
var subtab = btn.getAttribute('data-subtab');
if (subtab && typeof window.wvSwitchSubtab === 'function') {
setTimeout(function() { window.wvSwitchSubtab(subtab); }, 0);
}
});
});

View file

@ -320,6 +320,16 @@
window.loadSavedEncountersList = loadSavedEncountersList;
document.addEventListener('click', function(e) {
// Copy milestones narrative to visit note
if (e.target.closest('#btn-ms-copy-to-note')) {
var msText = document.getElementById('ms-narrative-text');
var wvMsEl = document.getElementById('wv-milestones-text');
if (msText && wvMsEl) {
var content = (msText.innerText || msText.textContent || '').trim();
if (content) { wvMsEl.value = content; showToast('Milestones copied to Visit Note', 'success'); }
else showToast('Generate a milestone assessment first', 'info');
}
}
// Settings button — refresh saved encounters list
if (e.target.closest('#btn-settings')) {
loadSavedEncountersList();

View file

@ -191,16 +191,22 @@
switchSubtab('byvisit');
}
}
// Milestones subtab is always shown (not age-gated)
var milestonesBtn = document.querySelector('.wv-subtab-btn[data-subtab="milestones"]');
if (milestonesBtn) {
milestonesBtn.style.display = '';
}
}
function switchSubtab(name) {
document.querySelectorAll('.wv-subtab-btn').forEach(function (b) {
b.classList.toggle('active', b.dataset.subtab === name);
});
document.querySelectorAll('.wv-subpanel').forEach(function (p) {
(document.getElementById('wellvisit-tab') || document).querySelectorAll('.wv-subpanel').forEach(function (p) {
p.classList.toggle('hidden', p.id !== 'wv-panel-' + name);
});
}
window.wvSwitchSubtab = switchSubtab;
// ─── BY VISIT panel ───────────────────────────────────────────────────────
function renderVisitPanel(visitId) {