Security: - Add session management: users can view/revoke active sessions in Settings - Add password change in Settings (requires current password, HIBP check) - Force logout all sessions on password reset - Fix logout to destroy server-side session (was only clearing cookie) - Add trust proxy for correct client IP in rate limiting and audit logs - Add CORS support for multiple domains (CORS_ORIGINS env var) - Add HIBP breach check endpoint and inline warnings on password fields Audit logging: - Add audit logging to all 24 PHI-handling endpoints across 13 route files - Covers: generation, transcription, TTS, refine, encounters, documents, Nextcloud - All fire-and-forget (no response delay) AI improvements: - Refine now includes original source material (transcript, notes, labs) so AI can reference the full input when modifying output - Add correction tracking (trackAIOutput) to sick visit and well visit tabs - Fix sickvisit missing from encounter save noteIdMap UI fixes: - Non-blocking busy bar for transcription and AI generation (replaces full-screen overlay) - Fix encounter recording: hide record button during recording (was showing two stop buttons) - Fix ROS/PE "All WNL" stacking duplicate event handlers; add Clear buttons - Enlarge AI instructions textarea in Learning Hub CMS Domain: - Primary domain now app.pedshub.com, with scribe.pedshub.com and peds.danvics.com as CORS origins
240 lines
9.9 KiB
JavaScript
240 lines
9.9 KiB
JavaScript
(function() {
|
|
var _inited = false;
|
|
var MILESTONES_DATA = {}; // Will be loaded from API
|
|
|
|
document.addEventListener('tabChanged', function(e) {
|
|
if (e.detail.tab !== 'wellvisit' || _inited) return;
|
|
_inited = true;
|
|
initMilestones();
|
|
});
|
|
|
|
function initMilestones() {
|
|
var ageSelect = document.getElementById('ms-age-group');
|
|
var checklist = document.getElementById('milestone-checklist');
|
|
var actionsBar = document.getElementById('milestone-actions');
|
|
var allYesBtn = document.getElementById('ms-all-yes');
|
|
var allClearBtn = document.getElementById('ms-all-clear');
|
|
var generateBtn = document.getElementById('ms-generate-btn');
|
|
var outputCard = document.getElementById('ms-output');
|
|
var summaryBar = document.getElementById('ms-summary-bar');
|
|
var narrativeText = document.getElementById('ms-narrative-text');
|
|
var modelTag = document.getElementById('ms-model-tag');
|
|
|
|
var state = {};
|
|
|
|
// Load milestones from API (database), fallback to static data if empty
|
|
fetch('/api/milestones-data', {
|
|
headers: getAuthHeaders()
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.success && data.milestones && Object.keys(data.milestones).length > 0) {
|
|
// Database has milestones - use them
|
|
MILESTONES_DATA = data.milestones;
|
|
populateAgeGroups();
|
|
} else {
|
|
// Database empty - fallback to static data
|
|
console.log('[Milestones] Database empty, using static data');
|
|
if (typeof window.MILESTONES_DATA_STATIC !== 'undefined') {
|
|
MILESTONES_DATA = window.MILESTONES_DATA_STATIC;
|
|
populateAgeGroups();
|
|
} else {
|
|
showToast('Milestones data not available', 'error');
|
|
}
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
console.error('[Milestones] Load error, falling back to static:', err);
|
|
// Fallback to static data on API error
|
|
if (typeof window.MILESTONES_DATA_STATIC !== 'undefined') {
|
|
MILESTONES_DATA = window.MILESTONES_DATA_STATIC;
|
|
populateAgeGroups();
|
|
} else {
|
|
showToast('Failed to load milestones: ' + err.message, 'error');
|
|
}
|
|
});
|
|
|
|
function populateAgeGroups() {
|
|
// Populate age group dropdown
|
|
if (ageSelect && Object.keys(MILESTONES_DATA).length > 0) {
|
|
ageSelect.innerHTML = '<option value="">Select age group</option>';
|
|
Object.keys(MILESTONES_DATA).forEach(function(age) {
|
|
var opt = document.createElement('option');
|
|
opt.value = age;
|
|
opt.textContent = age;
|
|
ageSelect.appendChild(opt);
|
|
});
|
|
}
|
|
}
|
|
|
|
ageSelect.addEventListener('change', function() {
|
|
var age = ageSelect.value;
|
|
state = {};
|
|
outputCard.classList.add('hidden');
|
|
if (!age || !MILESTONES_DATA[age]) {
|
|
checklist.innerHTML = '<p style="text-align:center;color:#9ca3af;padding:40px;">Select an age group.</p>';
|
|
actionsBar.style.display = 'none';
|
|
return;
|
|
}
|
|
renderChecklist(MILESTONES_DATA[age]);
|
|
actionsBar.style.display = 'flex';
|
|
});
|
|
|
|
function safeId(s) { return s.replace(/[^a-zA-Z0-9]/g, '_'); }
|
|
|
|
function renderChecklist(data) {
|
|
checklist.innerHTML = '';
|
|
Object.keys(data).forEach(function(domain) {
|
|
var items = data[domain];
|
|
var config = DOMAIN_CONFIG[domain] || { icon: '📋', css: '' };
|
|
var section = document.createElement('div');
|
|
section.className = 'domain-section ' + config.css;
|
|
|
|
var html = '<div class="domain-header"><span>' + config.icon + '</span><span>' + domain +
|
|
'</span><span class="badge" id="badge-' + safeId(domain) + '">' + items.length + ' items</span></div><div class="ms-items">';
|
|
|
|
items.forEach(function(m, idx) {
|
|
var id = domain + '-' + idx;
|
|
state[id] = { domain: domain, milestone: m, status: null };
|
|
html += '<div class="ms-item" id="item-' + safeId(id) + '">' +
|
|
'<div class="ms-toggle">' +
|
|
'<button class="toggle-btn" data-id="' + id + '" data-action="yes">✓</button>' +
|
|
'<button class="toggle-btn" data-id="' + id + '" data-action="no">✗</button>' +
|
|
'</div><span class="ms-text">' + m + '</span></div>';
|
|
});
|
|
|
|
section.innerHTML = html + '</div>';
|
|
checklist.appendChild(section);
|
|
});
|
|
|
|
checklist.querySelectorAll('.toggle-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() { handleToggle(btn.getAttribute('data-id'), btn.getAttribute('data-action')); });
|
|
});
|
|
}
|
|
|
|
function handleToggle(id, action) {
|
|
var sid = safeId(id);
|
|
var item = document.getElementById('item-' + sid);
|
|
var yesBtn = item.querySelector('[data-action="yes"]');
|
|
var noBtn = item.querySelector('[data-action="no"]');
|
|
|
|
if (action === 'yes') {
|
|
if (state[id].status === 'yes') { state[id].status = null; yesBtn.classList.remove('yes-on'); item.classList.remove('is-yes','is-no'); }
|
|
else { state[id].status = 'yes'; yesBtn.classList.add('yes-on'); noBtn.classList.remove('no-on'); item.classList.add('is-yes'); item.classList.remove('is-no'); }
|
|
} else {
|
|
if (state[id].status === 'no') { state[id].status = null; noBtn.classList.remove('no-on'); item.classList.remove('is-yes','is-no'); }
|
|
else { state[id].status = 'no'; noBtn.classList.add('no-on'); yesBtn.classList.remove('yes-on'); item.classList.remove('is-yes'); item.classList.add('is-no'); }
|
|
}
|
|
updateBadges();
|
|
}
|
|
|
|
function updateBadges() {
|
|
var counts = {};
|
|
Object.keys(state).forEach(function(id) {
|
|
var d = state[id].domain;
|
|
if (!counts[d]) counts[d] = { total: 0, yes: 0, no: 0 };
|
|
counts[d].total++;
|
|
if (state[id].status === 'yes') counts[d].yes++;
|
|
if (state[id].status === 'no') counts[d].no++;
|
|
});
|
|
Object.keys(counts).forEach(function(d) {
|
|
var b = document.getElementById('badge-' + safeId(d));
|
|
if (b) { var c = counts[d]; b.textContent = (c.yes + c.no) > 0 ? c.yes + '✓ ' + c.no + '✗ / ' + c.total : c.total + ' items'; }
|
|
});
|
|
}
|
|
|
|
allYesBtn.addEventListener('click', function() {
|
|
Object.keys(state).forEach(function(id) {
|
|
state[id].status = 'yes';
|
|
var item = document.getElementById('item-' + safeId(id));
|
|
if (item) { item.querySelector('[data-action="yes"]').classList.add('yes-on'); item.querySelector('[data-action="no"]').classList.remove('no-on'); item.classList.add('is-yes'); item.classList.remove('is-no'); }
|
|
});
|
|
updateBadges();
|
|
});
|
|
|
|
allClearBtn.addEventListener('click', function() {
|
|
Object.keys(state).forEach(function(id) {
|
|
state[id].status = null;
|
|
var item = document.getElementById('item-' + safeId(id));
|
|
if (item) { item.querySelector('[data-action="yes"]').classList.remove('yes-on'); item.querySelector('[data-action="no"]').classList.remove('no-on'); item.classList.remove('is-yes','is-no'); }
|
|
});
|
|
updateBadges();
|
|
outputCard.classList.add('hidden');
|
|
});
|
|
|
|
generateBtn.addEventListener('click', function() {
|
|
if (!ageSelect.value) { showToast('Select age group', 'error'); return; }
|
|
var assessed = Object.values(state).filter(function(m) { return m.status !== null; });
|
|
if (assessed.length === 0) { showToast('Assess at least one milestone', 'error'); return; }
|
|
|
|
showBusy('Generating...');
|
|
|
|
fetch('/api/generate-milestone-narrative', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
milestones: Object.values(state),
|
|
ageGroup: ageSelect.value,
|
|
patientAge: document.getElementById('ms-age').value,
|
|
patientGender: document.getElementById('ms-gender').value,
|
|
format: document.getElementById('ms-format').value,
|
|
model: getSelectedModel()
|
|
})
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
hideBusy();
|
|
if (data.success) {
|
|
setOutputText(narrativeText, data.narrative);
|
|
modelTag.textContent = (data.model || '').split('/').pop();
|
|
if (data.summary) {
|
|
summaryBar.innerHTML = '<div class="stat"><span class="dot dot-green"></span> Achieved: ' + data.summary.achieved + '</div>' +
|
|
'<div class="stat"><span class="dot dot-red"></span> Not Yet: ' + data.summary.notAchieved + '</div>' +
|
|
'<div class="stat"><span class="dot dot-gray"></span> Not Assessed: ' + data.summary.notAssessed + ' (omitted)</div>';
|
|
summaryBar.classList.remove('hidden');
|
|
}
|
|
outputCard.classList.remove('hidden');
|
|
document.getElementById('ms-quick-summary').classList.add('hidden');
|
|
outputCard.scrollIntoView({ behavior: 'smooth' });
|
|
showToast('Generated!', 'success');
|
|
} else showToast(data.error || 'Failed', 'error');
|
|
})
|
|
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
|
});
|
|
|
|
// 3-sentence summary
|
|
document.getElementById('ms-quick-summary-btn').addEventListener('click', function() {
|
|
var narrative = narrativeText.innerText.trim();
|
|
if (!narrative) { showToast('Generate narrative first', 'error'); return; }
|
|
|
|
var btn = document.getElementById('ms-quick-summary-btn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Summarizing...';
|
|
|
|
fetch('/api/generate-milestone-summary', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
narrative: narrative,
|
|
ageGroup: ageSelect.value,
|
|
patientAge: document.getElementById('ms-age').value,
|
|
patientGender: document.getElementById('ms-gender').value,
|
|
model: getSelectedModel()
|
|
})
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-compress"></i> Regenerate Summary';
|
|
if (data.success) {
|
|
setOutputText('ms-summary-text', data.summary);
|
|
document.getElementById('ms-quick-summary').classList.remove('hidden');
|
|
showToast('Summary generated!', 'success');
|
|
}
|
|
})
|
|
.catch(function(err) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-compress"></i> 3-Sentence Summary'; showToast(err.message, 'error'); });
|
|
});
|
|
|
|
console.log('✅ Milestones module loaded');
|
|
} // end initMilestones
|
|
})();
|