New feature: after generating any clinical note, the app automatically suggests relevant billing codes displayed as clickable chips below the output. Backend (src/routes/billing.js): - POST /api/suggest-codes endpoint analyzes note text - Extracts diagnoses from Assessment section via regex - Looks up ICD-10 codes: local common pediatric map (40+ conditions) first, then NLM Clinical Tables API for unknown terms - Suggests CPT E/M codes based on note type, visit complexity, ROS/PE system counts, and MDM level estimation - Supports: outpatient (new/established), well visit (age-based), ED, inpatient (admit/subsequent/discharge) Frontend (public/js/app.js): - suggestBillingCodes() renders collapsible card with ICD-10 and CPT chips - Click any chip to copy the code to clipboard - Shows E/M level assessment (diagnosis count, ROS, PE, MDM complexity) - Disclaimer: "Suggestions only. Always verify codes." Integration: called after note generation in all 6 tabs (encounter, SOAP, sick visit, well visit, hospital course, chart review)
192 lines
9.2 KiB
JavaScript
192 lines
9.2 KiB
JavaScript
(function() {
|
|
var _inited = false;
|
|
document.addEventListener('tabChanged', function(e) {
|
|
if (e.detail.tab !== 'chart' || _inited) return;
|
|
_inited = true;
|
|
var visitsContainer = document.getElementById('cr-visits-container');
|
|
var addVisitBtn = document.getElementById('cr-add-visit');
|
|
var labsContainer = document.getElementById('cr-labs-container');
|
|
var addLabBtn = document.getElementById('cr-add-lab');
|
|
var generateBtn = document.getElementById('cr-generate-btn');
|
|
var outputCard = document.getElementById('cr-output');
|
|
var reviewText = document.getElementById('cr-review-text');
|
|
var modelTag = document.getElementById('cr-model-tag');
|
|
|
|
var visitIndex = 1;
|
|
|
|
function resetChartReview() {
|
|
// Clear demographics and fields
|
|
['cr-age', 'cr-pmh', 'cr-instructions', 'chart-label'].forEach(function(id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.value = '';
|
|
});
|
|
var gender = document.getElementById('cr-gender');
|
|
if (gender) gender.value = '';
|
|
var type = document.getElementById('cr-type');
|
|
if (type) type.value = 'outpatient';
|
|
|
|
// Reset visit index and restore single empty visit card
|
|
visitIndex = 1;
|
|
visitsContainer.innerHTML = '<div class="card visit-card">' +
|
|
'<div class="card-header"><h3><i class="fas fa-calendar"></i> Visit / Note #1</h3>' +
|
|
'<button class="btn-sm btn-ghost remove-visit-btn"><i class="fas fa-trash"></i></button></div>' +
|
|
'<div class="note-entry"><div class="note-meta">' +
|
|
'<input type="date" class="visit-date">' +
|
|
'<select class="visit-type"><option value="outpatient">Outpatient Visit</option><option value="subspecialty">Subspecialty Note</option><option value="ed">ED Visit</option></select>' +
|
|
'<input type="text" class="visit-specialist" placeholder="Specialist name (if subspecialty)">' +
|
|
'<input type="text" class="visit-specialty" placeholder="Specialty (e.g., Endocrinology)"></div>' +
|
|
'<div class="editable-box visit-content" contenteditable="true" data-placeholder="Paste note here..."></div>' +
|
|
'<textarea class="visit-labs" placeholder="Labs from this visit (optional)" rows="3"></textarea></div></div>';
|
|
visitsContainer.querySelector('.remove-visit-btn').addEventListener('click', function() {
|
|
visitsContainer.querySelector('.visit-card').remove();
|
|
});
|
|
|
|
// Clear labs container - restore single empty entry
|
|
labsContainer.innerHTML = '<div class="lab-entry">' +
|
|
'<input type="date" class="lab-date">' +
|
|
'<textarea class="lab-values" placeholder="Lab results..." rows="3"></textarea>' +
|
|
'</div>';
|
|
|
|
// Clear output
|
|
reviewText.textContent = '';
|
|
outputCard.classList.add('hidden');
|
|
}
|
|
|
|
// Expose for encounters.js clearTab
|
|
window.resetChartReview = resetChartReview;
|
|
|
|
addVisitBtn.addEventListener('click', function() {
|
|
visitIndex++;
|
|
var card = document.createElement('div');
|
|
card.className = 'card visit-card';
|
|
card.innerHTML = '<div class="card-header"><h3><i class="fas fa-calendar"></i> Visit / Note #' + visitIndex + '</h3>' +
|
|
'<button class="btn-sm btn-ghost remove-visit-btn"><i class="fas fa-trash"></i></button></div>' +
|
|
'<div class="note-entry"><div class="note-meta">' +
|
|
'<input type="date" class="visit-date">' +
|
|
'<select class="visit-type"><option value="outpatient">Outpatient Visit</option><option value="subspecialty">Subspecialty Note</option><option value="ed">ED Visit</option></select>' +
|
|
'<input type="text" class="visit-specialist" placeholder="Specialist name">' +
|
|
'<input type="text" class="visit-specialty" placeholder="Specialty (e.g., Endocrinology)"></div>' +
|
|
'<div class="editable-box visit-content" contenteditable="true" data-placeholder="Paste note here..."></div>' +
|
|
'<textarea class="visit-labs" placeholder="Labs from this visit (optional)" rows="3"></textarea></div>';
|
|
visitsContainer.appendChild(card);
|
|
card.querySelector('.remove-visit-btn').addEventListener('click', function() { card.remove(); });
|
|
});
|
|
|
|
document.querySelectorAll('.remove-visit-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() { btn.closest('.visit-card').remove(); });
|
|
});
|
|
|
|
addLabBtn.addEventListener('click', function() {
|
|
var entry = document.createElement('div');
|
|
entry.className = 'lab-entry';
|
|
entry.innerHTML = '<input type="date" class="lab-date"><textarea class="lab-values" placeholder="Lab results..." rows="3"></textarea>' +
|
|
'<button class="btn-sm btn-ghost" onclick="this.parentElement.remove()"><i class="fas fa-trash"></i></button>';
|
|
labsContainer.appendChild(entry);
|
|
});
|
|
|
|
generateBtn.addEventListener('click', function() {
|
|
var type = document.getElementById('cr-type').value;
|
|
var visits = [];
|
|
var subspecialty = [];
|
|
var edVisits = [];
|
|
|
|
visitsContainer.querySelectorAll('.visit-card').forEach(function(card) {
|
|
var content = card.querySelector('.visit-content').innerText.trim();
|
|
if (!content) return;
|
|
var vType = card.querySelector('.visit-type').value;
|
|
var entry = {
|
|
date: card.querySelector('.visit-date').value || '',
|
|
content: content,
|
|
labs: card.querySelector('.visit-labs').value || ''
|
|
};
|
|
|
|
if (vType === 'subspecialty') {
|
|
entry.specialistName = card.querySelector('.visit-specialist').value || '';
|
|
entry.specialty = card.querySelector('.visit-specialty').value || '';
|
|
subspecialty.push(entry);
|
|
} else if (vType === 'ed') {
|
|
edVisits.push(entry);
|
|
} else {
|
|
entry.type = 'outpatient';
|
|
visits.push(entry);
|
|
}
|
|
});
|
|
|
|
var labs = [];
|
|
labsContainer.querySelectorAll('.lab-entry').forEach(function(entry) {
|
|
var vals = entry.querySelector('.lab-values').value.trim();
|
|
if (vals) labs.push({ date: entry.querySelector('.lab-date').value || '', values: vals });
|
|
});
|
|
|
|
if (visits.length === 0 && subspecialty.length === 0 && edVisits.length === 0) {
|
|
showToast('Add at least one visit/note', 'error');
|
|
return;
|
|
}
|
|
|
|
// Warn if payload is very large (>8MB = approaching 10MB server limit)
|
|
var payloadEstimate = JSON.stringify({ visits: visits, subspecialty: subspecialty, edVisits: edVisits, labs: labs }).length;
|
|
if (payloadEstimate > 8 * 1024 * 1024) {
|
|
showToast('Notes are very large (' + Math.round(payloadEstimate / 1024 / 1024) + 'MB). Consider trimming some notes.', 'warning');
|
|
return;
|
|
}
|
|
var totalNotes = visits.length + subspecialty.length + edVisits.length;
|
|
if (totalNotes > 30) {
|
|
showToast('Processing ' + totalNotes + ' notes — this may take a moment...', 'info');
|
|
}
|
|
|
|
showBusy('Generating chart review...');
|
|
|
|
fetch('/api/generate-chart-review', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
type: type,
|
|
visits: visits,
|
|
subspecialty: subspecialty,
|
|
edVisits: edVisits,
|
|
labs: labs,
|
|
patientAge: document.getElementById('cr-age').value,
|
|
patientGender: document.getElementById('cr-gender').value,
|
|
pmh: document.getElementById('cr-pmh').value,
|
|
additionalInstructions: document.getElementById('cr-instructions').value,
|
|
model: getSelectedModel()
|
|
})
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
hideBusy();
|
|
if (data.success) {
|
|
setOutputText(reviewText, data.review);
|
|
// Store source material for refine
|
|
var sourceItems = [];
|
|
visits.forEach(function(v) { sourceItems.push('VISIT (' + v.date + '): ' + v.content + (v.labs ? '\nLabs: ' + v.labs : '')); });
|
|
subspecialty.forEach(function(v) { sourceItems.push('SUBSPECIALTY (' + v.date + ', ' + v.specialty + '): ' + v.content + (v.labs ? '\nLabs: ' + v.labs : '')); });
|
|
edVisits.forEach(function(v) { sourceItems.push('ED VISIT (' + v.date + '): ' + v.content + (v.labs ? '\nLabs: ' + v.labs : '')); });
|
|
labs.forEach(function(l) { sourceItems.push('LAB (' + l.date + '): ' + l.values); });
|
|
storeSourceContext('cr-review-text', sourceItems.join('\n\n'));
|
|
modelTag.textContent = (data.model || '').split('/').pop();
|
|
outputCard.classList.remove('hidden');
|
|
outputCard.scrollIntoView({ behavior: 'smooth' });
|
|
showToast('Chart review generated!', 'success');
|
|
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('cr-review-text', data.review, 'chart', document.getElementById('cr-age').value);
|
|
} else showToast(data.error || 'Failed', 'error');
|
|
})
|
|
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
|
});
|
|
|
|
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');
|
|
});
|
|
})();
|