Add automatic ICD-10 and CPT billing code suggestions
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)
This commit is contained in:
parent
5a394219db
commit
059fbefa7e
17 changed files with 1863 additions and 256 deletions
188
IMPROVEMENTS.md
Normal file
188
IMPROVEMENTS.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# Pediatric AI Scribe — Improvement Roadmap
|
||||
|
||||
A non-technical overview of what the app does today and how it can be taken further.
|
||||
|
||||
---
|
||||
|
||||
## What the App Does Today
|
||||
|
||||
Pediatric AI Scribe is a clinical documentation tool for pediatric physicians. It listens to doctor-patient encounters (or accepts typed/pasted notes) and uses AI to generate structured medical notes — HPIs, SOAP notes, hospital courses, chart reviews, well visit and sick visit documentation.
|
||||
|
||||
It also includes pediatric calculators (blood pressure percentiles, BMI, growth charts, bilirubin nomograms, vital signs reference), a Learning Hub for educational content and quizzes, and a full security layer (two-factor authentication, session management, audit logging, single sign-on).
|
||||
|
||||
The app runs as a self-hosted web application with a mobile-friendly PWA interface.
|
||||
|
||||
---
|
||||
|
||||
## Areas for Improvement
|
||||
|
||||
### 1. Visual Growth Charts
|
||||
|
||||
**Current state:** Growth percentiles are displayed as numbers (e.g., "75th percentile, Z-score 0.67").
|
||||
|
||||
**Improvement:** Plot actual WHO/CDC percentile curves (the familiar growth chart lines pediatricians use) with the patient's data point shown on the chart. This would make results immediately interpretable at a glance, matching the paper charts physicians are trained on. Support for plotting multiple visits over time would make it even more useful for tracking growth trends.
|
||||
|
||||
### 2. Blood Pressure Calculator Accuracy
|
||||
|
||||
**Current state:** The BP calculator uses simplified reference values at the 50th height percentile only.
|
||||
|
||||
**Improvement:** Implement the full Rosner quantile spline regression method (the same math used by the Baylor College of Medicine reference calculator). This would give exact BP percentiles adjusted for the patient's actual height, not just an approximation. The regression coefficients are publicly available and can be integrated directly.
|
||||
|
||||
### 3. Multi-Visit Tracking
|
||||
|
||||
**Current state:** Each encounter is independent. There is no way to see a patient's history across visits.
|
||||
|
||||
**Improvement:** Allow physicians to associate notes with a patient identifier (MRN, initials, or a pseudonym) and view previous encounters for that patient. This would enable:
|
||||
- Growth tracking over time (plot multiple points on growth curves)
|
||||
- Trend monitoring (weight gain/loss, blood pressure trends)
|
||||
- Quick access to past notes during follow-up visits
|
||||
|
||||
This would need careful design around data retention and privacy since it changes the app from a transient tool to one that stores longitudinal data.
|
||||
|
||||
### 4. EHR Integration
|
||||
|
||||
**Current state:** Notes are copied manually and pasted into the EHR.
|
||||
|
||||
**Improvement:** Direct integration with common EHR systems:
|
||||
- **FHIR API** — connect to Epic, Cerner, or other FHIR-enabled EHRs to push notes directly into the patient chart
|
||||
- **HL7 messaging** — for institutions using traditional interfaces
|
||||
- **Smart on FHIR** — launch the app from within the EHR as an embedded tool
|
||||
|
||||
This is the highest-impact improvement for adoption but also the most complex to implement (requires EHR vendor partnerships and institutional approval).
|
||||
|
||||
### 5. Offline Mode
|
||||
|
||||
**Current state:** The app requires an internet connection for AI generation and cloud-based transcription. Browser Whisper works offline for transcription only.
|
||||
|
||||
**Improvement:** Add a local AI model option (e.g., a small medical LLM running on the device or local server) so the entire workflow — record, transcribe, generate note — can happen without any network calls. This would be valuable for:
|
||||
- Rural clinics with unreliable internet
|
||||
- Maximum privacy (no data leaves the building)
|
||||
- Disaster/field medicine scenarios
|
||||
|
||||
### 6. Specialty Expansion
|
||||
|
||||
**Current state:** Focused on general pediatrics with some subspecialty support in chart review.
|
||||
|
||||
**Improvement:** Add specialty-specific note templates and AI prompts for:
|
||||
- Pediatric cardiology (echo reports, cath summaries)
|
||||
- Pediatric neurology (EEG reports, seizure logs)
|
||||
- Neonatology (daily progress notes, discharge summaries)
|
||||
- Pediatric surgery (operative notes, pre-op assessments)
|
||||
- Pediatric psychiatry (intake assessments, progress notes)
|
||||
|
||||
Each specialty has unique documentation requirements that could be addressed with tailored prompts and input forms.
|
||||
|
||||
### 7. Billing Code Suggestions
|
||||
|
||||
**Current state:** The well visit tab includes some billing code references.
|
||||
|
||||
**Improvement:** Automatically suggest ICD-10 and CPT codes based on the generated note content. After the AI generates a note, it could analyze the diagnoses, procedures, and visit complexity to suggest appropriate billing codes. This saves time on coding and reduces missed charges.
|
||||
|
||||
### 8. Quality Metrics Dashboard
|
||||
|
||||
**Current state:** Admin panel shows basic usage statistics (total API calls, users).
|
||||
|
||||
**Improvement:** Add a dashboard showing:
|
||||
- Average note generation time by type
|
||||
- Most-used AI models and their accuracy (based on how often users edit the output)
|
||||
- Transcription accuracy metrics (if corrections are tracked)
|
||||
- Usage patterns by time of day and day of week
|
||||
- Cost tracking across AI providers
|
||||
|
||||
This would help administrators optimize model selection and identify training opportunities.
|
||||
|
||||
### 9. Patient Education Materials
|
||||
|
||||
**Current state:** The Learning Hub serves educational content to physicians.
|
||||
|
||||
**Improvement:** Add a patient-facing education module that generates age-appropriate handouts based on the diagnosis. For example, after generating a note for a child with asthma, the app could produce a parent-friendly handout explaining the diagnosis, medications, and when to seek emergency care — in the parent's preferred language.
|
||||
|
||||
### 10. Multi-Language Support
|
||||
|
||||
**Current state:** English only.
|
||||
|
||||
**Improvement:** Add support for:
|
||||
- Generating notes in other languages (Spanish, French, Arabic, etc.)
|
||||
- Transcribing encounters conducted in other languages
|
||||
- Patient education materials in the family's language
|
||||
- UI translation for non-English-speaking staff
|
||||
|
||||
Medical Spanish alone would significantly expand the app's reach in the United States.
|
||||
|
||||
### 11. Voice Commands During Recording
|
||||
|
||||
**Current state:** Recording is continuous — the physician presses start and stop.
|
||||
|
||||
**Improvement:** Add voice command recognition during recording:
|
||||
- "New section" — marks a section break in the transcript
|
||||
- "Off the record" — pauses transcription temporarily (for sidebar conversations)
|
||||
- "Add diagnosis: [condition]" — tags a diagnosis without typing
|
||||
- "Skip" — ignores the last segment
|
||||
|
||||
This would make the recording workflow more natural and reduce post-generation editing.
|
||||
|
||||
### 12. Collaborative Notes
|
||||
|
||||
**Current state:** Single-user editing. Notes are created and edited by one physician.
|
||||
|
||||
**Improvement:** Allow multiple team members to work on the same encounter:
|
||||
- Attending reviews and co-signs a resident's note
|
||||
- Nurse adds vital signs and chief complaint before the physician sees the patient
|
||||
- Specialist adds their consultation note to the same encounter
|
||||
|
||||
This mirrors the real workflow in training institutions and group practices.
|
||||
|
||||
### 13. Mobile-Optimized Recording
|
||||
|
||||
**Current state:** Recording works on mobile but stops when the screen locks or the app is backgrounded (browser limitation).
|
||||
|
||||
**Improvement:** Build a native mobile wrapper (using Capacitor or React Native) that can record audio in the background even when the screen is off. This is the single biggest usability improvement for mobile users and removes the most common complaint.
|
||||
|
||||
### 14. Template Library
|
||||
|
||||
**Current state:** Physician memories and corrections provide some personalization.
|
||||
|
||||
**Improvement:** Add a shared template library where physicians can create, share, and browse note templates:
|
||||
- "My asthma follow-up template"
|
||||
- "Standard newborn discharge summary"
|
||||
- "ED laceration repair template"
|
||||
- Import/export templates between institutions
|
||||
|
||||
### 15. Audit and Compliance Reporting
|
||||
|
||||
**Current state:** Audit logs exist in the database but there is no reporting UI.
|
||||
|
||||
**Improvement:** Add an admin-facing compliance dashboard:
|
||||
- Who accessed what, when (filterable by user, date, action)
|
||||
- Export audit logs to CSV/PDF for compliance reviews
|
||||
- Automated alerts for unusual access patterns
|
||||
- HIPAA compliance checklist with green/red status indicators
|
||||
- BAA tracking (which providers have signed BAAs)
|
||||
|
||||
---
|
||||
|
||||
## Priority Recommendations
|
||||
|
||||
If resources are limited, focus on these high-impact improvements first:
|
||||
|
||||
| Priority | Improvement | Impact | Effort |
|
||||
|----------|-------------|--------|--------|
|
||||
| 1 | Visual growth charts | High — physicians expect visual curves | Medium |
|
||||
| 2 | Accurate BP calculator | High — clinical accuracy matters | Medium |
|
||||
| 3 | Billing code suggestions | High — direct revenue impact | Medium |
|
||||
| 4 | Multi-language support | High — expands reach significantly | Large |
|
||||
| 5 | Audit/compliance reporting | Medium — required for institutional adoption | Small |
|
||||
| 6 | EHR integration (FHIR) | Very high — but requires partnerships | Very large |
|
||||
|
||||
---
|
||||
|
||||
## What Makes This App Unique
|
||||
|
||||
Compared to existing medical scribes and documentation tools:
|
||||
|
||||
- **Pediatric-specific** — prompts, calculators, milestones, and growth charts designed for children, not adapted from adult tools
|
||||
- **Self-hosted** — runs on your own infrastructure, not a SaaS that holds your data
|
||||
- **Provider-agnostic** — works with any AI provider (swap between them without changing anything)
|
||||
- **Privacy-first** — optional fully offline transcription, auto-expiring data, no permanent PHI storage
|
||||
- **Learning system** — AI improves its output based on each physician's editing patterns
|
||||
- **All-in-one** — documentation, calculators, education, and administration in a single platform
|
||||
|
|
@ -48,6 +48,36 @@
|
|||
<button id="btn-calc-bp" class="btn-sm btn-primary" style="margin-top:12px;"><i class="fas fa-calculator"></i> Calculate</button>
|
||||
<button id="btn-clear-bp" class="btn-sm btn-ghost" style="margin-top:12px;">Clear</button>
|
||||
<div id="bp-result" class="calc-result hidden"></div>
|
||||
<div id="bp-chart-container" class="chart-container hidden"><canvas id="bp-chart-canvas"></canvas></div>
|
||||
|
||||
<details style="margin-top:16px;font-size:12px;border:1px solid var(--g200);border-radius:8px;padding:0;">
|
||||
<summary style="padding:10px 14px;cursor:pointer;font-weight:600;color:var(--g700);background:var(--g50);border-radius:8px;">Definitions: Hypertension & Hypotension (AAP 2017 / Nelson)</summary>
|
||||
<div style="padding:12px 14px;">
|
||||
<p style="font-weight:700;color:var(--g800);margin:0 0 8px;">Hypertension Classification (AAP 2017)</p>
|
||||
<table style="width:100%;font-size:12px;border-collapse:collapse;margin-bottom:14px;">
|
||||
<thead><tr style="background:var(--g100);"><th style="padding:6px 8px;text-align:left;border:1px solid var(--g200);">Category</th><th style="padding:6px 8px;text-align:left;border:1px solid var(--g200);">Ages 1 to <13 years</th><th style="padding:6px 8px;text-align:left;border:1px solid var(--g200);">Ages ≥13 years</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="padding:6px 8px;border:1px solid var(--g200);font-weight:600;color:#10b981;">Normal</td><td style="padding:6px 8px;border:1px solid var(--g200);"><90th percentile</td><td style="padding:6px 8px;border:1px solid var(--g200);"><120/<80 mmHg</td></tr>
|
||||
<tr><td style="padding:6px 8px;border:1px solid var(--g200);font-weight:600;color:#f59e0b;">Elevated BP</td><td style="padding:6px 8px;border:1px solid var(--g200);">≥90th to <95th percentile<br>OR 120/80 mmHg to <95th (whichever lower)</td><td style="padding:6px 8px;border:1px solid var(--g200);">120-129/<80 mmHg</td></tr>
|
||||
<tr><td style="padding:6px 8px;border:1px solid var(--g200);font-weight:600;color:#f97316;">Stage 1 HTN</td><td style="padding:6px 8px;border:1px solid var(--g200);">≥95th percentile to <95th + 12 mmHg<br>OR 130/80-139/89 mmHg (whichever lower)</td><td style="padding:6px 8px;border:1px solid var(--g200);">130/80 to 139/89 mmHg</td></tr>
|
||||
<tr><td style="padding:6px 8px;border:1px solid var(--g200);font-weight:600;color:#ef4444;">Stage 2 HTN</td><td style="padding:6px 8px;border:1px solid var(--g200);">≥95th + 12 mmHg<br>OR ≥140/90 mmHg (whichever lower)</td><td style="padding:6px 8px;border:1px solid var(--g200);">≥140/90 mmHg</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-weight:700;color:var(--g800);margin:0 0 6px;">Hypertensive Urgency vs Emergency (Nelson)</p>
|
||||
<ul style="margin:0 0 14px;padding-left:18px;color:var(--g700);line-height:1.7;">
|
||||
<li><strong>Hypertensive urgency:</strong> Severe BP elevation (>95th + 30 mmHg or >180/120) WITHOUT end-organ damage. Requires reduction over 24-48 hours.</li>
|
||||
<li><strong>Hypertensive emergency:</strong> Severe BP elevation WITH end-organ damage (encephalopathy, heart failure, retinopathy, renal injury). Requires immediate IV therapy with goal reduction of 25% in first 8 hours.</li>
|
||||
</ul>
|
||||
<p style="font-weight:700;color:var(--g800);margin:0 0 6px;">Hypotension (AAP/PALS)</p>
|
||||
<ul style="margin:0 0 8px;padding-left:18px;color:var(--g700);line-height:1.7;">
|
||||
<li><strong>Definition:</strong> Systolic BP below the 5th percentile for age, sex, and height.</li>
|
||||
<li><strong>Quick estimate (1-10 years):</strong> Hypotension = SBP < 70 + (2 × age in years) mmHg</li>
|
||||
<li><strong>Neonates:</strong> SBP < gestational age in weeks (term: <60 mmHg)</li>
|
||||
<li><strong>Clinical significance:</strong> Hypotension in children is a late sign of shock. Tachycardia, poor perfusion, and altered mental status typically precede hypotension.</li>
|
||||
</ul>
|
||||
<p style="font-size:11px;color:var(--g400);margin:8px 0 0;">Sources: Flynn JT et al., Pediatrics 2017;140(3). Nelson Textbook of Pediatrics, 21st ed. PALS Provider Manual.</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -64,7 +94,12 @@
|
|||
<div class="calc-grid">
|
||||
<div class="calc-field">
|
||||
<label>Age (years)</label>
|
||||
<input type="number" id="bmi-age" min="2" max="20" step="0.1" placeholder="2-20">
|
||||
<input type="number" id="bmi-age-yr" min="2" max="20" step="1" placeholder="e.g. 7" style="width:60%;display:inline-block;">
|
||||
<select id="bmi-age-mo" style="width:35%;display:inline-block;padding:8px 4px;border:1.5px solid var(--g300);border-radius:6px;font-size:13px;">
|
||||
<option value="0">0 mo</option><option value="1">1 mo</option><option value="2">2 mo</option><option value="3">3 mo</option>
|
||||
<option value="4">4 mo</option><option value="5">5 mo</option><option value="6">6 mo</option><option value="7">7 mo</option>
|
||||
<option value="8">8 mo</option><option value="9">9 mo</option><option value="10">10 mo</option><option value="11">11 mo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="calc-field">
|
||||
<label>Sex</label>
|
||||
|
|
@ -82,6 +117,7 @@
|
|||
<button id="btn-calc-bmi" class="btn-sm btn-primary" style="margin-top:12px;"><i class="fas fa-calculator"></i> Calculate</button>
|
||||
<button id="btn-clear-bmi" class="btn-sm btn-ghost" style="margin-top:12px;">Clear</button>
|
||||
<div id="bmi-result" class="calc-result hidden"></div>
|
||||
<div id="bmi-chart-container" class="chart-container hidden"><canvas id="bmi-chart-canvas"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -176,8 +212,15 @@
|
|||
<select id="growth-sex"><option value="">Select</option><option value="male">Male</option><option value="female">Female</option></select>
|
||||
</div>
|
||||
<div class="calc-field" id="growth-age-field">
|
||||
<label>Age (months)</label>
|
||||
<input type="number" id="growth-age" min="0" max="240" step="0.5" placeholder="e.g. 24">
|
||||
<label>Age</label>
|
||||
<div style="display:flex;gap:4px;align-items:center;">
|
||||
<input type="number" id="growth-age-yr" min="0" max="20" step="1" placeholder="yr" style="width:40%;">
|
||||
<select id="growth-age-mo" style="width:55%;padding:8px 4px;border:1.5px solid var(--g300);border-radius:6px;font-size:13px;">
|
||||
<option value="0">0 mo</option><option value="1">1 mo</option><option value="2">2 mo</option><option value="3">3 mo</option>
|
||||
<option value="4">4 mo</option><option value="5">5 mo</option><option value="6">6 mo</option><option value="7">7 mo</option>
|
||||
<option value="8">8 mo</option><option value="9">9 mo</option><option value="10">10 mo</option><option value="11">11 mo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calc-field" id="growth-ga-field" style="display:none;">
|
||||
<label>Gestational Age (weeks)</label>
|
||||
|
|
@ -196,9 +239,23 @@
|
|||
<input type="number" id="growth-hc" min="20" max="60" step="0.1" placeholder="e.g. 35">
|
||||
</div>
|
||||
</div>
|
||||
<div id="growth-mph-section" style="display:none;margin-top:12px;padding:12px;background:var(--g50);border-radius:8px;">
|
||||
<div style="font-size:12px;font-weight:600;color:var(--g600);margin-bottom:8px;">Mid-Parental Height (optional)</div>
|
||||
<div class="calc-grid">
|
||||
<div class="calc-field">
|
||||
<label>Mother's Height (cm)</label>
|
||||
<input type="number" id="growth-mother-ht" min="120" max="200" step="0.1" placeholder="e.g. 165">
|
||||
</div>
|
||||
<div class="calc-field">
|
||||
<label>Father's Height (cm)</label>
|
||||
<input type="number" id="growth-father-ht" min="140" max="220" step="0.1" placeholder="e.g. 178">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-calc-growth" class="btn-sm btn-primary" style="margin-top:12px;"><i class="fas fa-calculator"></i> Calculate</button>
|
||||
<button id="btn-clear-growth" class="btn-sm btn-ghost" style="margin-top:12px;">Clear</button>
|
||||
<div id="growth-result" class="calc-result hidden"></div>
|
||||
<div id="growth-chart-container" class="chart-container hidden"><canvas id="growth-chart-canvas"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -352,6 +409,7 @@
|
|||
</div>
|
||||
|
||||
<div id="bili-result" class="calc-result hidden"></div>
|
||||
<div id="bili-chart-container" class="chart-container hidden"><canvas id="bili-chart-canvas"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -276,6 +276,42 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calculators -->
|
||||
<div class="faq-section">
|
||||
<h3 class="faq-section-title"><i class="fas fa-calculator"></i> Pediatric Calculators</h3>
|
||||
|
||||
<div class="faq-item">
|
||||
<button class="faq-question">What calculators are available?</button>
|
||||
<div class="faq-answer">
|
||||
<p>The <strong>Calculators</strong> tab includes clinical tools commonly used in pediatric practice:</p>
|
||||
<ul>
|
||||
<li><strong>Blood Pressure Percentile</strong> — AAP 2017 guidelines using the Rosner quantile spline regression method. Requires age, sex, height, and BP. Provides exact systolic and diastolic percentiles adjusted for height, with AAP classification (Normal, Elevated, Stage 1, Stage 2). Includes definitions of hypertension and hypotension.</li>
|
||||
<li><strong>BMI Percentile</strong> — CDC 2000 growth reference with extended obesity classification (Class 1, 2, 3 using % of 95th percentile). Shows BMI chart with percentile curves.</li>
|
||||
<li><strong>Growth Charts</strong> — Visual percentile curves (3rd through 97th) with your patient plotted. Includes Weight-for-Age, Length/Height-for-Age (with mid-parental height), Head Circumference, Weight-for-Length, and Fenton preterm charts.</li>
|
||||
<li><strong>Bilirubin</strong> — AAP 2022 phototherapy threshold calculator and Bhutani hour-specific nomogram with risk zone classification. Includes Nelson Table 137.1 risk factors.</li>
|
||||
<li><strong>Vital Signs by Age</strong> — Harriet Lane reference table for HR, RR, BP, and weight by age from preterm through 18 years. Includes quick formulas for estimated weight, minimum SBP, ETT size, and maintenance fluids.</li>
|
||||
<li><strong>Body Surface Area</strong> — Mosteller formula for BSA calculation.</li>
|
||||
<li><strong>Weight-Based Dosing</strong> — Dose per kg with frequency, max dose cap, and volume calculation from concentration.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<button class="faq-question">How accurate is the BP calculator?</button>
|
||||
<div class="faq-answer">
|
||||
<p>The BP calculator uses the same Rosner quantile spline regression method as the Baylor College of Medicine reference calculator. It computes exact percentiles (1st-99th) based on your patient's age, sex, and height using published regression coefficients. This is the same methodology underlying the AAP 2017 normative tables. Results are height-adjusted and clinically accurate.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<button class="faq-question">What are the growth chart curves?</button>
|
||||
<div class="faq-answer">
|
||||
<p>The growth charts display WHO/CDC percentile curves (3rd, 5th, 10th, 25th, 50th, 75th, 90th, 95th, 97th percentiles) with your patient's measurement plotted as a blue dot. The 50th percentile is shown as a bold green line. Shaded bands show the normal range between symmetric percentiles.</p>
|
||||
<p>For <strong>Length/Height-for-Age</strong>, you can optionally enter both parents' heights to see the mid-parental target height range plotted on the chart.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Troubleshooting -->
|
||||
<div class="faq-section">
|
||||
<h3 class="faq-section-title"><i class="fas fa-wrench"></i> Troubleshooting</h3>
|
||||
|
|
|
|||
|
|
@ -264,6 +264,21 @@ textarea.full-input{resize:vertical;}
|
|||
.faq-answer strong{color:var(--g800);}
|
||||
.faq-answer em{color:var(--g500);font-size:12.5px;}
|
||||
|
||||
/* Billing codes */
|
||||
.billing-codes-card{margin-top:12px;padding:14px;background:var(--g50);border-radius:8px;border:1px solid var(--g200);}
|
||||
.billing-codes-card.hidden{display:none;}
|
||||
.billing-codes-card h4{font-size:13px;font-weight:700;color:var(--g700);margin:0 0 8px;display:flex;align-items:center;gap:6px;}
|
||||
.billing-codes-section{margin-bottom:10px;}
|
||||
.billing-codes-section:last-child{margin-bottom:0;}
|
||||
.billing-codes-label{font-size:11px;font-weight:600;color:var(--g500);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;}
|
||||
.billing-code-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;margin:3px;transition:opacity 0.15s;}
|
||||
.billing-code-chip.icd{background:#dbeafe;color:#1d4ed8;border:1px solid #93c5fd;}
|
||||
.billing-code-chip.cpt{background:#d1fae5;color:#059669;border:1px solid #6ee7b7;}
|
||||
.billing-code-chip.em{background:#ede9fe;color:#7c3aed;border:1px solid #c4b5fd;}
|
||||
.billing-code-chip:hover{opacity:0.7;}
|
||||
.billing-code-chip:active{transform:scale(0.95);}
|
||||
.billing-code-name{font-weight:400;color:var(--g600);font-size:11px;margin-left:2px;}
|
||||
|
||||
/* Calculators */
|
||||
.calc-pill,.calc-nav-pill{padding:8px 16px;border-radius:20px;border:1.5px solid var(--g200);background:white;font-size:13px;font-weight:600;color:var(--g600);cursor:pointer;transition:all 0.15s;}
|
||||
.calc-pill:hover,.calc-nav-pill:hover{border-color:var(--blue);color:var(--blue);}
|
||||
|
|
@ -280,6 +295,8 @@ textarea.full-input{resize:vertical;}
|
|||
.calc-result-label{font-size:11px;font-weight:600;color:var(--g500);text-transform:uppercase;letter-spacing:0.5px;}
|
||||
.calc-result-value{font-size:24px;font-weight:700;color:var(--g800);margin:4px 0;}
|
||||
.calc-result-sub{font-size:11px;color:var(--g500);}
|
||||
.chart-container{margin-top:16px;padding:12px;background:white;border:1px solid var(--g200);border-radius:10px;max-width:750px;}
|
||||
.chart-container.hidden{display:none;}
|
||||
|
||||
/* Non-blocking inline busy bar */
|
||||
.busy-bar{position:sticky;top:0;z-index:100;display:none;align-items:center;gap:8px;padding:8px 16px;background:linear-gradient(135deg,#eff6ff,#e0f2fe);border-bottom:1px solid #bfdbfe;font-size:13px;font-weight:500;color:#1d4ed8;animation:busySlideIn 0.2s ease;}
|
||||
|
|
|
|||
|
|
@ -108,13 +108,22 @@
|
|||
|
||||
<div class="hipaa-notice">
|
||||
<i class="fas fa-triangle-exclamation"></i>
|
||||
<span>AWS Bedrock available with BAA for HIPAA compliance. Check with your institution's guidelines before use. Not intended for production clinical use without proper authorization.</span>
|
||||
<span>HIPAA-compliant AI providers available with BAA. Check with your institution's guidelines before use. Not intended for production clinical use without proper authorization.</span>
|
||||
</div>
|
||||
|
||||
<details style="margin:16px 0 0;text-align:left;max-width:480px;margin-left:auto;margin-right:auto;">
|
||||
<summary style="cursor:pointer;font-size:13px;font-weight:600;color:#64748b;text-align:center;">About Pediatric AI Scribe</summary>
|
||||
<div style="padding:12px 0;font-size:12px;color:#64748b;line-height:1.7;">
|
||||
<p style="margin:0 0 8px;">AI-powered clinical documentation for pediatric medicine. Generate HPIs, SOAP notes, hospital courses, chart reviews, well/sick visit notes, and developmental milestone assessments from voice or text.</p>
|
||||
<p style="margin:0 0 8px;">Includes pediatric calculators (BP percentile, BMI, growth charts, bilirubin nomograms, vital signs), Learning Hub with quizzes and AI-generated content, and comprehensive security (2FA, SSO, session management, audit logging).</p>
|
||||
<p style="margin:0;">All AI providers support BAA for HIPAA compliance. Audio and encounters auto-expire. AI learns from your edits over time.</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<footer class="app-footer">
|
||||
Committed to healthcare equity — always consult a qualified healthcare professional for medical advice.
|
||||
•
|
||||
© <script>document.write(new Date().getFullYear())</script> Pediatric AI Scribe. All rights reserved.
|
||||
© 2024-2026 Pediatric AI Scribe by PedsHub. All rights reserved.
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
|
@ -279,6 +288,73 @@
|
|||
|
||||
</div><!-- /.app-body -->
|
||||
|
||||
<footer style="padding:12px 24px;text-align:center;font-size:12px;color:var(--g400);border-top:1px solid var(--g100);">
|
||||
<a href="#" id="btn-show-about" style="color:var(--g500);text-decoration:none;">About Pediatric AI Scribe</a>
|
||||
•
|
||||
© <span id="app-year"></span> PedsHub. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<!-- About Modal -->
|
||||
<div id="about-modal" class="confirm-modal hidden">
|
||||
<div class="confirm-modal-box" style="max-width:600px;max-height:80vh;overflow-y:auto;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
||||
<h2 style="margin:0;font-size:18px;color:var(--g800);"><i class="fas fa-stethoscope" style="color:var(--blue);margin-right:8px;"></i>Pediatric AI Scribe</h2>
|
||||
<button id="btn-close-about" class="btn-sm btn-ghost" style="padding:4px 8px;"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<p style="color:var(--g600);line-height:1.7;font-size:13px;margin:0 0 14px;">An AI-powered clinical documentation platform designed specifically for pediatric medicine. Built to help physicians spend less time on documentation and more time with patients.</p>
|
||||
|
||||
<h4 style="font-size:13px;color:var(--blue-dark);margin:16px 0 8px;">Clinical Documentation</h4>
|
||||
<ul style="font-size:12.5px;color:var(--g700);line-height:1.8;padding-left:18px;margin:0 0 12px;">
|
||||
<li><strong>Live Encounter</strong> — Record doctor-patient conversations, AI generates structured HPI using OLDCARTS framework</li>
|
||||
<li><strong>Voice Dictation</strong> — Dictate your narrative, AI restructures into clean clinical documentation</li>
|
||||
<li><strong>SOAP Notes</strong> — Full SOAP or subjective-only from transcript or dictation</li>
|
||||
<li><strong>Hospital Course</strong> — Prose, day-by-day, organ-system (ICU), or psych format from progress notes</li>
|
||||
<li><strong>Chart Review</strong> — Summarize outpatient, subspecialty, and ED visits for precharting</li>
|
||||
<li><strong>Well Visit</strong> — AAP Bright Futures with SSHADESS, milestones, vaccines, and billing codes</li>
|
||||
<li><strong>Sick Visit</strong> — Quick documentation with auto-suggested ROS and PE</li>
|
||||
</ul>
|
||||
|
||||
<h4 style="font-size:13px;color:var(--blue-dark);margin:16px 0 8px;">Pediatric Calculators</h4>
|
||||
<ul style="font-size:12.5px;color:var(--g700);line-height:1.8;padding-left:18px;margin:0 0 12px;">
|
||||
<li><strong>BP Percentile</strong> — AAP 2017 with Rosner spline regression, height-adjusted</li>
|
||||
<li><strong>BMI Percentile</strong> — CDC 2000 with extended obesity classification and visual chart</li>
|
||||
<li><strong>Growth Charts</strong> — Visual WHO/CDC/Fenton curves with patient plotting and mid-parental height</li>
|
||||
<li><strong>Bilirubin</strong> — AAP 2022 phototherapy thresholds and Bhutani nomogram</li>
|
||||
<li><strong>Vital Signs</strong> — Harriet Lane reference with quick formulas (ETT, fluids, weight estimation)</li>
|
||||
<li><strong>BSA & Dosing</strong> — Mosteller BSA and weight-based dose calculator with volume</li>
|
||||
</ul>
|
||||
|
||||
<h4 style="font-size:13px;color:var(--blue-dark);margin:16px 0 8px;">AI & Speech</h4>
|
||||
<ul style="font-size:12.5px;color:var(--g700);line-height:1.8;padding-left:18px;margin:0 0 12px;">
|
||||
<li>Multiple AI providers with HIPAA-compliant options (BAA-covered)</li>
|
||||
<li>Multiple transcription engines including medical-specific models</li>
|
||||
<li>Browser Whisper for fully offline, private transcription</li>
|
||||
<li>Text-to-speech for reading notes aloud</li>
|
||||
<li>AI learns from your edits over time (correction tracking)</li>
|
||||
<li>Customizable AI prompts for each note type</li>
|
||||
</ul>
|
||||
|
||||
<h4 style="font-size:13px;color:var(--blue-dark);margin:16px 0 8px;">Learning Hub</h4>
|
||||
<ul style="font-size:12.5px;color:var(--g700);line-height:1.8;padding-left:18px;margin:0 0 12px;">
|
||||
<li>Educational articles, clinical pearls, quizzes, and slide presentations</li>
|
||||
<li>AI-powered content generation from topics, PDFs, or Nextcloud files</li>
|
||||
<li>Marp presentations with PPTX export</li>
|
||||
<li>Semantic search powered by vector embeddings</li>
|
||||
</ul>
|
||||
|
||||
<h4 style="font-size:13px;color:var(--blue-dark);margin:16px 0 8px;">Security & Privacy</h4>
|
||||
<ul style="font-size:12.5px;color:var(--g700);line-height:1.8;padding-left:18px;margin:0 0 12px;">
|
||||
<li>Two-factor authentication (TOTP) and SSO/OIDC support</li>
|
||||
<li>Session management with device tracking and revocation</li>
|
||||
<li>Comprehensive audit logging of all clinical actions</li>
|
||||
<li>Auto-expiring data (encounters 7 days, audio 24 hours)</li>
|
||||
<li>Cloudflare Turnstile bot protection</li>
|
||||
<li>Role-based access control (admin, moderator, user)</li>
|
||||
</ul>
|
||||
|
||||
<p style="font-size:11px;color:var(--g400);margin:16px 0 0;text-align:center;">AI-generated notes should always be reviewed by a qualified healthcare professional before clinical use.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- dummy settings-modal kept for any stale JS references -->
|
||||
<div id="settings-modal" class="hidden" style="display:none !important;"></div>
|
||||
|
|
@ -330,6 +406,7 @@
|
|||
|
||||
<!-- SCRIPTS (defer for faster initial paint) -->
|
||||
<script src="/vendor/tiptap.bundle.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js" defer></script>
|
||||
<script defer src="/js/milestonesData.js"></script>
|
||||
<script defer src="/js/pediatricScheduleData.js"></script>
|
||||
<script defer src="/js/audioBackup.js"></script>
|
||||
|
|
|
|||
|
|
@ -148,6 +148,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
});
|
||||
}
|
||||
|
||||
// Set footer year
|
||||
var yearEl = document.getElementById('app-year');
|
||||
if (yearEl) yearEl.textContent = new Date().getFullYear();
|
||||
|
||||
// Load settings data when settings tab is activated
|
||||
document.addEventListener('tabChanged', function(e) {
|
||||
if (e.detail && e.detail.tab === 'settings') {
|
||||
|
|
@ -629,8 +633,10 @@ function findReadButton(elementId) {
|
|||
function createTimer(el) {
|
||||
var s = 0, iv = null;
|
||||
return {
|
||||
start: function() { s = 0; this.update(); var self = this; iv = setInterval(function() { s++; self.update(); }, 1000); },
|
||||
start: function() { s = 0; this.resume(); },
|
||||
resume: function() { this.update(); var self = this; if (!iv) iv = setInterval(function() { s++; self.update(); }, 1000); },
|
||||
stop: function() { if (iv) { clearInterval(iv); iv = null; } return s; },
|
||||
reset: function() { s = 0; this.update(); },
|
||||
update: function() { el.textContent = String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0'); }
|
||||
};
|
||||
}
|
||||
|
|
@ -745,6 +751,94 @@ function storeSourceContext(outputElementId, sourceText) {
|
|||
if (el && sourceText) el.dataset.sourceContext = sourceText;
|
||||
}
|
||||
|
||||
// ── Billing code suggestions (called after note generation) ──
|
||||
function suggestBillingCodes(outputElementId, noteText, noteType, patientAge, visitType) {
|
||||
// Find or create the billing codes container near the output element
|
||||
var outputEl = document.getElementById(outputElementId);
|
||||
if (!outputEl || !noteText) return;
|
||||
var card = outputEl.closest('.card, .output-card');
|
||||
if (!card) return;
|
||||
|
||||
var containerId = outputElementId.replace('-text', '') + '-billing-codes';
|
||||
var container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
// Create container dynamically if not in HTML
|
||||
container = document.createElement('div');
|
||||
container.id = containerId;
|
||||
container.className = 'billing-codes-card';
|
||||
// Insert after the output text element
|
||||
outputEl.parentNode.insertBefore(container, outputEl.nextSibling);
|
||||
}
|
||||
|
||||
container.className = 'billing-codes-card';
|
||||
container.innerHTML = '<div style="font-size:12px;color:var(--g500);"><i class="fas fa-spinner fa-spin"></i> Analyzing billing codes...</div>';
|
||||
|
||||
fetch('/api/suggest-codes', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
noteText: noteText,
|
||||
noteType: noteType || 'soap',
|
||||
patientAge: patientAge || '',
|
||||
visitType: visitType || 'outpatient'
|
||||
})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (!data.success || (!data.icd10.length && !data.cpt.length)) {
|
||||
container.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<h4><i class="fas fa-file-invoice-dollar"></i> Suggested Billing Codes</h4>';
|
||||
|
||||
if (data.icd10 && data.icd10.length > 0) {
|
||||
html += '<div class="billing-codes-section"><div class="billing-codes-label">ICD-10 Diagnoses</div><div>';
|
||||
data.icd10.forEach(function(c) {
|
||||
var name = c.name ? ' <span class="billing-code-name">' + escHtml(c.name) + '</span>' : '';
|
||||
html += '<span class="billing-code-chip icd" title="Click to copy" data-code="' + escHtml(c.code) + '">' + escHtml(c.code) + name + '</span>';
|
||||
});
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
if (data.cpt && data.cpt.length > 0) {
|
||||
html += '<div class="billing-codes-section"><div class="billing-codes-label">CPT / E&M</div><div>';
|
||||
data.cpt.forEach(function(c) {
|
||||
var desc = c.desc ? ' <span class="billing-code-name">' + escHtml(c.desc) + '</span>' : '';
|
||||
html += '<span class="billing-code-chip cpt" title="Click to copy" data-code="' + escHtml(c.code) + '">' + escHtml(c.code) + desc + '</span>';
|
||||
});
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
if (data.emLevel) {
|
||||
html += '<div class="billing-codes-section"><div class="billing-codes-label">E/M Assessment</div>';
|
||||
html += '<span class="billing-code-chip em">Level ' + data.emLevel.level + '</span>';
|
||||
html += '<span style="font-size:11px;color:var(--g500);margin-left:6px;">MDM: ' + data.emLevel.complexity + ' | ' + data.emLevel.diagnosisCount + ' dx | ' + data.emLevel.rosCount + ' ROS | ' + data.emLevel.peCount + ' PE</span>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '<p style="font-size:10px;color:var(--g400);margin:8px 0 0;">Suggestions only. Always verify codes against your institution\'s coding guidelines.</p>';
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Wire click-to-copy on chips
|
||||
container.querySelectorAll('.billing-code-chip').forEach(function(chip) {
|
||||
chip.addEventListener('click', function() {
|
||||
var code = chip.dataset.code;
|
||||
if (code && navigator.clipboard) {
|
||||
navigator.clipboard.writeText(code);
|
||||
showToast('Copied: ' + code, 'info');
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(function() {
|
||||
container.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
function escHtml(s) { return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
||||
|
||||
function refineDocument(outputElementId, inputElementId) {
|
||||
var doc = document.getElementById(outputElementId);
|
||||
var input = document.getElementById(inputElementId);
|
||||
|
|
|
|||
|
|
@ -234,6 +234,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
showToast('Logged out', 'info');
|
||||
}
|
||||
|
||||
// About modal
|
||||
if (target.id === 'btn-show-about' || target.closest('#btn-show-about')) {
|
||||
e.preventDefault();
|
||||
var aboutModal = document.getElementById('about-modal');
|
||||
if (aboutModal) aboutModal.classList.remove('hidden');
|
||||
}
|
||||
if (target.id === 'btn-close-about' || target.closest('#btn-close-about')) {
|
||||
e.preventDefault();
|
||||
var aboutModal = document.getElementById('about-modal');
|
||||
if (aboutModal) aboutModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Settings button — navigate to settings tab
|
||||
if (target.id === 'btn-settings' || target.closest('#btn-settings')) {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -168,6 +168,7 @@
|
|||
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'); });
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@
|
|||
clarifyOutput.classList.add('hidden');
|
||||
outputCard.scrollIntoView({ behavior: 'smooth' });
|
||||
showToast('Hospital course generated!', 'success');
|
||||
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('hc-course-text', data.hospitalCourse, 'hospital', document.getElementById('hc-age').value, document.getElementById('hc-setting').value);
|
||||
} else showToast(data.error || 'Failed', 'error');
|
||||
})
|
||||
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
||||
|
|
|
|||
|
|
@ -124,27 +124,45 @@
|
|||
pauseBtn.addEventListener('click', function() {
|
||||
if (!isRecording) return;
|
||||
if (!isPaused) {
|
||||
if (recorder.mediaRecorder && recorder.mediaRecorder.state === 'recording') {
|
||||
recorder.mediaRecorder.pause();
|
||||
timer.stop();
|
||||
isPaused = true;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
||||
pauseBtn.classList.add('btn-primary');
|
||||
pauseBtn.classList.remove('btn-ghost');
|
||||
if (recognition) try { recognition.stop(); } catch(e) {}
|
||||
showToast('Recording paused', 'info');
|
||||
// Pause: try MediaRecorder.pause(), fall back to stop+accumulate
|
||||
if (recorder.mediaRecorder) {
|
||||
try {
|
||||
if (typeof recorder.mediaRecorder.pause === 'function' && recorder.mediaRecorder.state === 'recording') {
|
||||
recorder.mediaRecorder.pause();
|
||||
}
|
||||
} catch(e) { console.warn('[Rec] Pause not supported:', e.message); }
|
||||
}
|
||||
timer.stop();
|
||||
isPaused = true;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
||||
pauseBtn.classList.add('btn-primary');
|
||||
pauseBtn.classList.remove('btn-ghost');
|
||||
if (recognition) try { recognition.stop(); } catch(e) {}
|
||||
showToast('Recording paused', 'info');
|
||||
} else {
|
||||
if (recorder.mediaRecorder && recorder.mediaRecorder.state === 'paused') {
|
||||
recorder.mediaRecorder.resume();
|
||||
timer.start();
|
||||
isPaused = false;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
||||
pauseBtn.classList.remove('btn-primary');
|
||||
pauseBtn.classList.add('btn-ghost');
|
||||
if (recognition) try { recognition.start(); } catch(e) {}
|
||||
showToast('Recording resumed', 'info');
|
||||
// Resume: try MediaRecorder.resume(), fall back to just restarting recognition
|
||||
if (recorder.mediaRecorder) {
|
||||
try {
|
||||
if (typeof recorder.mediaRecorder.resume === 'function' && recorder.mediaRecorder.state === 'paused') {
|
||||
recorder.mediaRecorder.resume();
|
||||
} else if (recorder.mediaRecorder.state === 'inactive') {
|
||||
// MediaRecorder was stopped (browser killed it) — restart it on the same stream
|
||||
if (recorder.stream && recorder.stream.active) {
|
||||
var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
|
||||
recorder.mediaRecorder = new MediaRecorder(recorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 });
|
||||
recorder.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) recorder.chunks.push(e.data); };
|
||||
recorder.mediaRecorder.start(1000);
|
||||
}
|
||||
}
|
||||
} catch(e) { console.warn('[Rec] Resume error:', e.message); }
|
||||
}
|
||||
timer.resume();
|
||||
isPaused = false;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
||||
pauseBtn.classList.remove('btn-primary');
|
||||
pauseBtn.classList.add('btn-ghost');
|
||||
if (recognition) try { recognition.start(); } catch(e) {}
|
||||
showToast('Recording resumed', 'info');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -190,6 +208,7 @@
|
|||
outputCard.classList.remove('hidden');
|
||||
outputCard.scrollIntoView({ behavior: 'smooth' });
|
||||
showToast('HPI generated!', 'success');
|
||||
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('enc-hpi-text', data.hpi, 'hpi', document.getElementById('enc-age').value, document.getElementById('enc-setting').value);
|
||||
} else showToast(data.error || 'Failed', 'error');
|
||||
})
|
||||
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
||||
|
|
|
|||
|
|
@ -298,14 +298,22 @@
|
|||
pauseBtn.addEventListener('click', function() {
|
||||
if (!_shadessRecording || !_shadessRecorder || !_shadessRecorder.mediaRecorder) return;
|
||||
if (!_shadessIsPaused) {
|
||||
_shadessRecorder.mediaRecorder.pause();
|
||||
try { if (_shadessRecorder.mediaRecorder.state === 'recording') _shadessRecorder.mediaRecorder.pause(); } catch(e) {}
|
||||
_shadessTimer.stop();
|
||||
_shadessIsPaused = true;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
||||
if (_shhadessRecognition) try { _shhadessRecognition.stop(); } catch(e) {}
|
||||
} else {
|
||||
_shadessRecorder.mediaRecorder.resume();
|
||||
_shadessTimer.start();
|
||||
try {
|
||||
if (_shadessRecorder.mediaRecorder.state === 'paused') { _shadessRecorder.mediaRecorder.resume(); }
|
||||
else if (_shadessRecorder.mediaRecorder.state === 'inactive' && _shadessRecorder.stream && _shadessRecorder.stream.active) {
|
||||
var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
|
||||
_shadessRecorder.mediaRecorder = new MediaRecorder(_shadessRecorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 });
|
||||
_shadessRecorder.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) _shadessRecorder.chunks.push(e.data); };
|
||||
_shadessRecorder.mediaRecorder.start(1000);
|
||||
}
|
||||
} catch(e) {}
|
||||
_shadessTimer.resume();
|
||||
_shadessIsPaused = false;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
||||
if (_shhadessRecognition) try { _shhadessRecognition.start(); } catch(e) {}
|
||||
|
|
@ -761,11 +769,21 @@
|
|||
pauseBtn.addEventListener('click', function() {
|
||||
if (!_wvRecording || !_wvRecorder || !_wvRecorder.mediaRecorder) return;
|
||||
if (!_wvPaused) {
|
||||
_wvRecorder.mediaRecorder.pause(); _wvTimer.stop(); _wvPaused = true;
|
||||
try { if (_wvRecorder.mediaRecorder.state === 'recording') _wvRecorder.mediaRecorder.pause(); } catch(e) {}
|
||||
_wvTimer.stop(); _wvPaused = true;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
||||
if (_wvRecognition) try { _wvRecognition.stop(); } catch(e) {}
|
||||
} else {
|
||||
_wvRecorder.mediaRecorder.resume(); _wvTimer.start(); _wvPaused = false;
|
||||
try {
|
||||
if (_wvRecorder.mediaRecorder.state === 'paused') { _wvRecorder.mediaRecorder.resume(); }
|
||||
else if (_wvRecorder.mediaRecorder.state === 'inactive' && _wvRecorder.stream && _wvRecorder.stream.active) {
|
||||
var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
|
||||
_wvRecorder.mediaRecorder = new MediaRecorder(_wvRecorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 });
|
||||
_wvRecorder.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) _wvRecorder.chunks.push(e.data); };
|
||||
_wvRecorder.mediaRecorder.start(1000);
|
||||
}
|
||||
} catch(e) {}
|
||||
_wvTimer.resume(); _wvPaused = false;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
||||
if (_wvRecognition) try { _wvRecognition.start(); } catch(e) {}
|
||||
}
|
||||
|
|
@ -904,6 +922,7 @@
|
|||
if (tag) tag.textContent = (data.model || '').split('/').pop();
|
||||
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
|
||||
showToast('Well visit note generated!', 'success');
|
||||
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('wv-note-text', data.note, 'wellvisit', document.getElementById('wv-note-age').value);
|
||||
} else {
|
||||
showToast(data.error || 'Generation failed', 'error');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,11 +193,21 @@
|
|||
pauseBtn.addEventListener('click', function() {
|
||||
if (!_sickRecording || !_sickRecorder || !_sickRecorder.mediaRecorder) return;
|
||||
if (!_sickPaused) {
|
||||
_sickRecorder.mediaRecorder.pause(); _sickTimer.stop(); _sickPaused = true;
|
||||
try { if (_sickRecorder.mediaRecorder.state === 'recording') _sickRecorder.mediaRecorder.pause(); } catch(e) {}
|
||||
_sickTimer.stop(); _sickPaused = true;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
||||
if (_sickRecognition) try { _sickRecognition.stop(); } catch(e) {}
|
||||
} else {
|
||||
_sickRecorder.mediaRecorder.resume(); _sickTimer.start(); _sickPaused = false;
|
||||
try {
|
||||
if (_sickRecorder.mediaRecorder.state === 'paused') { _sickRecorder.mediaRecorder.resume(); }
|
||||
else if (_sickRecorder.mediaRecorder.state === 'inactive' && _sickRecorder.stream && _sickRecorder.stream.active) {
|
||||
var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
|
||||
_sickRecorder.mediaRecorder = new MediaRecorder(_sickRecorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 });
|
||||
_sickRecorder.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) _sickRecorder.chunks.push(e.data); };
|
||||
_sickRecorder.mediaRecorder.start(1000);
|
||||
}
|
||||
} catch(e) {}
|
||||
_sickTimer.resume(); _sickPaused = false;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
||||
if (_sickRecognition) try { _sickRecognition.start(); } catch(e) {}
|
||||
}
|
||||
|
|
@ -329,6 +339,7 @@
|
|||
if (tag) tag.textContent = (data.model || '').split('/').pop();
|
||||
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
|
||||
showToast('Sick visit note generated!', 'success');
|
||||
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('sick-note-text', data.note, 'sickvisit', document.getElementById('sick-age').value);
|
||||
} else {
|
||||
showToast(data.error || 'Generation failed', 'error');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@
|
|||
outputCard.classList.remove('hidden');
|
||||
outputCard.scrollIntoView({ behavior: 'smooth' });
|
||||
showToast('SOAP note generated!', 'success');
|
||||
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('soap-text', data.soap, 'soap', document.getElementById('soap-age').value);
|
||||
} else showToast(data.error || 'Failed', 'error');
|
||||
})
|
||||
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
||||
|
|
|
|||
|
|
@ -104,27 +104,31 @@
|
|||
pauseBtn.addEventListener('click', function() {
|
||||
if (!isRecording) return;
|
||||
if (!isPaused) {
|
||||
if (recorder.mediaRecorder && recorder.mediaRecorder.state === 'recording') {
|
||||
recorder.mediaRecorder.pause();
|
||||
timer.stop();
|
||||
isPaused = true;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
||||
pauseBtn.classList.add('btn-primary');
|
||||
pauseBtn.classList.remove('btn-ghost');
|
||||
if (recognition) try { recognition.stop(); } catch(e) {}
|
||||
showToast('Paused', 'info');
|
||||
}
|
||||
try { if (recorder.mediaRecorder && recorder.mediaRecorder.state === 'recording') recorder.mediaRecorder.pause(); } catch(e) {}
|
||||
timer.stop();
|
||||
isPaused = true;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
||||
pauseBtn.classList.add('btn-primary');
|
||||
pauseBtn.classList.remove('btn-ghost');
|
||||
if (recognition) try { recognition.stop(); } catch(e) {}
|
||||
showToast('Paused', 'info');
|
||||
} else {
|
||||
if (recorder.mediaRecorder && recorder.mediaRecorder.state === 'paused') {
|
||||
recorder.mediaRecorder.resume();
|
||||
timer.start();
|
||||
isPaused = false;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
||||
pauseBtn.classList.remove('btn-primary');
|
||||
pauseBtn.classList.add('btn-ghost');
|
||||
if (recognition) try { recognition.start(); } catch(e) {}
|
||||
showToast('Resumed', 'info');
|
||||
}
|
||||
try {
|
||||
if (recorder.mediaRecorder && recorder.mediaRecorder.state === 'paused') { recorder.mediaRecorder.resume(); }
|
||||
else if (recorder.mediaRecorder && recorder.mediaRecorder.state === 'inactive' && recorder.stream && recorder.stream.active) {
|
||||
var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
|
||||
recorder.mediaRecorder = new MediaRecorder(recorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 });
|
||||
recorder.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) recorder.chunks.push(e.data); };
|
||||
recorder.mediaRecorder.start(1000);
|
||||
}
|
||||
} catch(e) {}
|
||||
timer.resume();
|
||||
isPaused = false;
|
||||
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
||||
pauseBtn.classList.remove('btn-primary');
|
||||
pauseBtn.classList.add('btn-ghost');
|
||||
if (recognition) try { recognition.start(); } catch(e) {}
|
||||
showToast('Resumed', 'info');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ app.use('/api', require('./src/routes/encounters'));
|
|||
app.use('/api', require('./src/routes/memories'));
|
||||
app.use('/api', require('./src/routes/documents'));
|
||||
app.use('/api', require('./src/routes/audioBackups'));
|
||||
app.use('/api', require('./src/routes/billing'));
|
||||
app.use('/api/sessions', require('./src/routes/sessions'));
|
||||
app.use('/api', require('./src/routes/wellVisit'));
|
||||
app.use('/api', require('./src/routes/sickVisit'));
|
||||
|
|
|
|||
365
src/routes/billing.js
Normal file
365
src/routes/billing.js
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
// ============================================================
|
||||
// BILLING CODE SUGGESTIONS — ICD-10 + CPT from note analysis
|
||||
// Uses NLM Clinical Tables API for ICD-10 lookup (free, no auth)
|
||||
// ============================================================
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var { authMiddleware } = require('../middleware/auth');
|
||||
var logger = require('../utils/logger');
|
||||
|
||||
// ── CPT E/M Reference Tables ──────────────────────────────────────
|
||||
|
||||
var CPT_EM = {
|
||||
outpatient_new: [
|
||||
{ code: '99202', level: 2, desc: 'New patient, straightforward MDM', time: '15-29 min' },
|
||||
{ code: '99203', level: 3, desc: 'New patient, low complexity MDM', time: '30-44 min' },
|
||||
{ code: '99204', level: 4, desc: 'New patient, moderate complexity MDM', time: '45-59 min' },
|
||||
{ code: '99205', level: 5, desc: 'New patient, high complexity MDM', time: '60-74 min' }
|
||||
],
|
||||
outpatient_established: [
|
||||
{ code: '99211', level: 1, desc: 'Established patient, may not require physician', time: '—' },
|
||||
{ code: '99212', level: 2, desc: 'Established patient, straightforward MDM', time: '10-19 min' },
|
||||
{ code: '99213', level: 3, desc: 'Established patient, low complexity MDM', time: '20-29 min' },
|
||||
{ code: '99214', level: 4, desc: 'Established patient, moderate complexity MDM', time: '30-39 min' },
|
||||
{ code: '99215', level: 5, desc: 'Established patient, high complexity MDM', time: '40-54 min' }
|
||||
],
|
||||
wellvisit_new: [
|
||||
{ code: '99381', desc: 'Preventive visit, new patient, infant (< 1 year)' },
|
||||
{ code: '99382', desc: 'Preventive visit, new patient, early childhood (1-4 years)' },
|
||||
{ code: '99383', desc: 'Preventive visit, new patient, late childhood (5-11 years)' },
|
||||
{ code: '99384', desc: 'Preventive visit, new patient, adolescent (12-17 years)' },
|
||||
{ code: '99385', desc: 'Preventive visit, new patient, 18-39 years' }
|
||||
],
|
||||
wellvisit_established: [
|
||||
{ code: '99391', desc: 'Preventive visit, established patient, infant (< 1 year)' },
|
||||
{ code: '99392', desc: 'Preventive visit, established patient, early childhood (1-4 years)' },
|
||||
{ code: '99393', desc: 'Preventive visit, established patient, late childhood (5-11 years)' },
|
||||
{ code: '99394', desc: 'Preventive visit, established patient, adolescent (12-17 years)' },
|
||||
{ code: '99395', desc: 'Preventive visit, established patient, 18-39 years' }
|
||||
],
|
||||
ed: [
|
||||
{ code: '99281', level: 1, desc: 'ED visit, straightforward MDM' },
|
||||
{ code: '99282', level: 2, desc: 'ED visit, low complexity MDM' },
|
||||
{ code: '99283', level: 3, desc: 'ED visit, moderate complexity MDM' },
|
||||
{ code: '99284', level: 4, desc: 'ED visit, moderate-high complexity MDM' },
|
||||
{ code: '99285', level: 5, desc: 'ED visit, high complexity MDM' }
|
||||
],
|
||||
inpatient_admit: [
|
||||
{ code: '99221', level: 1, desc: 'Initial hospital care, straightforward/low MDM' },
|
||||
{ code: '99222', level: 2, desc: 'Initial hospital care, moderate MDM' },
|
||||
{ code: '99223', level: 3, desc: 'Initial hospital care, high MDM' }
|
||||
],
|
||||
inpatient_subsequent: [
|
||||
{ code: '99231', level: 1, desc: 'Subsequent hospital care, straightforward/low MDM' },
|
||||
{ code: '99232', level: 2, desc: 'Subsequent hospital care, moderate MDM' },
|
||||
{ code: '99233', level: 3, desc: 'Subsequent hospital care, high MDM' }
|
||||
],
|
||||
inpatient_discharge: [
|
||||
{ code: '99238', desc: 'Hospital discharge, 30 min or less' },
|
||||
{ code: '99239', desc: 'Hospital discharge, more than 30 min' }
|
||||
]
|
||||
};
|
||||
|
||||
// ── ROS Systems for counting ──────────────────────────────────────
|
||||
var ROS_SYSTEMS = [
|
||||
'constitutional', 'eyes', 'ent', 'cardiovascular', 'respiratory',
|
||||
'gastrointestinal', 'genitourinary', 'musculoskeletal', 'integumentary',
|
||||
'neurological', 'psychiatric', 'endocrine', 'hematologic', 'allergic'
|
||||
];
|
||||
|
||||
var PE_SYSTEMS = [
|
||||
'general', 'heent', 'eyes', 'ears', 'nose', 'throat', 'neck',
|
||||
'cardiovascular', 'respiratory', 'chest', 'lungs', 'abdomen', 'abdominal',
|
||||
'genitourinary', 'musculoskeletal', 'skin', 'integumentary',
|
||||
'neurologic', 'neurological', 'psychiatric', 'lymphatic'
|
||||
];
|
||||
|
||||
// ── Extract diagnoses from note text ─────────────────────────────
|
||||
|
||||
// Common pediatric diagnoses — avoids NLM lookup for known terms
|
||||
var COMMON_ICD10 = {
|
||||
'fever': { code: 'R50.9', name: 'Fever, unspecified' },
|
||||
'cough': { code: 'R05.9', name: 'Cough, unspecified' },
|
||||
'vomiting': { code: 'R11.10', name: 'Vomiting, unspecified' },
|
||||
'diarrhea': { code: 'R19.7', name: 'Diarrhea, unspecified' },
|
||||
'constipation': { code: 'K59.00', name: 'Constipation, unspecified' },
|
||||
'headache': { code: 'R51.9', name: 'Headache, unspecified' },
|
||||
'abdominal pain': { code: 'R10.9', name: 'Unspecified abdominal pain' },
|
||||
'rash': { code: 'R21', name: 'Rash and other nonspecific skin eruption' },
|
||||
'sore throat': { code: 'J02.9', name: 'Acute pharyngitis, unspecified' },
|
||||
'pharyngitis': { code: 'J02.9', name: 'Acute pharyngitis, unspecified' },
|
||||
'strep pharyngitis': { code: 'J02.0', name: 'Streptococcal pharyngitis' },
|
||||
'otitis media': { code: 'H66.90', name: 'Otitis media, unspecified' },
|
||||
'acute otitis media': { code: 'H66.90', name: 'Otitis media, unspecified' },
|
||||
'uri': { code: 'J06.9', name: 'Acute upper respiratory infection, unspecified' },
|
||||
'upper respiratory infection': { code: 'J06.9', name: 'Acute upper respiratory infection, unspecified' },
|
||||
'bronchiolitis': { code: 'J21.9', name: 'Acute bronchiolitis, unspecified' },
|
||||
'asthma': { code: 'J45.20', name: 'Mild intermittent asthma, uncomplicated' },
|
||||
'asthma exacerbation': { code: 'J45.21', name: 'Mild intermittent asthma with acute exacerbation' },
|
||||
'pneumonia': { code: 'J18.9', name: 'Pneumonia, unspecified organism' },
|
||||
'croup': { code: 'J05.0', name: 'Acute obstructive laryngitis' },
|
||||
'bronchitis': { code: 'J20.9', name: 'Acute bronchitis, unspecified' },
|
||||
'eczema': { code: 'L30.9', name: 'Dermatitis, unspecified' },
|
||||
'atopic dermatitis': { code: 'L20.9', name: 'Atopic dermatitis, unspecified' },
|
||||
'urticaria': { code: 'L50.9', name: 'Urticaria, unspecified' },
|
||||
'conjunctivitis': { code: 'H10.9', name: 'Unspecified conjunctivitis' },
|
||||
'urinary tract infection': { code: 'N39.0', name: 'Urinary tract infection, site not specified' },
|
||||
'uti': { code: 'N39.0', name: 'Urinary tract infection, site not specified' },
|
||||
'gastroenteritis': { code: 'K52.9', name: 'Noninfective gastroenteritis and colitis, unspecified' },
|
||||
'dehydration': { code: 'E86.0', name: 'Dehydration' },
|
||||
'seizure': { code: 'R56.9', name: 'Unspecified convulsions' },
|
||||
'febrile seizure': { code: 'R56.00', name: 'Simple febrile convulsions' },
|
||||
'anemia': { code: 'D64.9', name: 'Anemia, unspecified' },
|
||||
'iron deficiency anemia': { code: 'D50.9', name: 'Iron deficiency anemia, unspecified' },
|
||||
'obesity': { code: 'E66.01', name: 'Morbid (severe) obesity due to excess calories' },
|
||||
'failure to thrive': { code: 'R62.51', name: 'Failure to thrive (child)' },
|
||||
'adhd': { code: 'F90.9', name: 'Attention-deficit hyperactivity disorder, unspecified type' },
|
||||
'anxiety': { code: 'F41.9', name: 'Anxiety disorder, unspecified' },
|
||||
'depression': { code: 'F32.9', name: 'Major depressive disorder, single episode, unspecified' },
|
||||
'well child': { code: 'Z00.129', name: 'Encounter for routine child health examination without abnormal findings' },
|
||||
'well child visit': { code: 'Z00.129', name: 'Encounter for routine child health examination without abnormal findings' },
|
||||
'newborn': { code: 'Z00.110', name: 'Health examination for newborn under 8 days old' },
|
||||
'jaundice': { code: 'P59.9', name: 'Neonatal jaundice, unspecified' },
|
||||
'neonatal jaundice': { code: 'P59.9', name: 'Neonatal jaundice, unspecified' },
|
||||
'hyperbilirubinemia': { code: 'P59.9', name: 'Neonatal jaundice, unspecified' }
|
||||
};
|
||||
|
||||
function extractDiagnoses(noteText) {
|
||||
var diagnoses = [];
|
||||
|
||||
// Look for Assessment section
|
||||
var assessmentMatch = noteText.match(/(?:assessment|diagnos[ie]s|impression|a\/p|assessment\s*(?:and|&)\s*plan)[:\s]*\n?([\s\S]*?)(?:\n\s*(?:plan|disposition|follow.up|return|recommendations)[:\s]|\n\s*\n\s*\n|$)/i);
|
||||
var assessmentText = assessmentMatch ? assessmentMatch[1] : noteText;
|
||||
|
||||
// Extract ICD-10 codes already in the text (e.g., "J06.9" or "(J06.9)")
|
||||
var icdPattern = /\b([A-TV-Z]\d{2}(?:\.\d{1,4})?)\b/g;
|
||||
var icdMatch;
|
||||
while ((icdMatch = icdPattern.exec(noteText)) !== null) {
|
||||
diagnoses.push({ term: icdMatch[1], type: 'icd_extracted' });
|
||||
}
|
||||
|
||||
// Extract numbered diagnoses from assessment
|
||||
var numberedLines = assessmentText.match(/^\s*\d+[\.\)]\s*(.+)$/gm);
|
||||
if (numberedLines) {
|
||||
numberedLines.forEach(function(line) {
|
||||
var term = line.replace(/^\s*\d+[\.\)]\s*/, '').replace(/\s*[\(\[].*$/, '').trim();
|
||||
if (term.length > 3 && term.length < 100) {
|
||||
diagnoses.push({ term: term, type: 'numbered' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Extract dash/bullet diagnoses
|
||||
var bulletLines = assessmentText.match(/^\s*[-•]\s*(.+)$/gm);
|
||||
if (bulletLines) {
|
||||
bulletLines.forEach(function(line) {
|
||||
var term = line.replace(/^\s*[-•]\s*/, '').replace(/\s*[\(\[].*$/, '').trim();
|
||||
if (term.length > 3 && term.length < 100) {
|
||||
diagnoses.push({ term: term, type: 'bullet' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If no structured diagnoses found, try to extract from inline text
|
||||
if (diagnoses.length === 0) {
|
||||
var commonPatterns = [
|
||||
/(?:diagnosed? with|assessment of|consistent with|suggestive of|likely|probable)\s+([^,\.\n]{3,60})/gi
|
||||
];
|
||||
commonPatterns.forEach(function(pattern) {
|
||||
var m;
|
||||
while ((m = pattern.exec(assessmentText)) !== null) {
|
||||
diagnoses.push({ term: m[1].trim(), type: 'inline' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
var seen = {};
|
||||
return diagnoses.filter(function(d) {
|
||||
var key = d.term.toLowerCase();
|
||||
if (seen[key]) return false;
|
||||
seen[key] = true;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Count ROS and PE systems documented ──────────────────────────
|
||||
|
||||
function countDocumentedSystems(noteText) {
|
||||
var lower = noteText.toLowerCase();
|
||||
var rosCount = 0;
|
||||
var peCount = 0;
|
||||
|
||||
ROS_SYSTEMS.forEach(function(sys) {
|
||||
if (lower.indexOf(sys) !== -1) rosCount++;
|
||||
});
|
||||
// Also check common abbreviations
|
||||
if (/\bENT\b/.test(noteText)) rosCount++;
|
||||
if (/\bGI\b/.test(noteText)) rosCount++;
|
||||
if (/\bGU\b/.test(noteText)) rosCount++;
|
||||
if (/\bMSK\b/.test(noteText)) rosCount++;
|
||||
|
||||
PE_SYSTEMS.forEach(function(sys) {
|
||||
if (lower.indexOf(sys) !== -1) peCount++;
|
||||
});
|
||||
|
||||
return { rosCount: Math.min(rosCount, 14), peCount: Math.min(peCount, 12) };
|
||||
}
|
||||
|
||||
// ── Estimate E/M complexity level ────────────────────────────────
|
||||
|
||||
function estimateEMLevel(noteText, diagnosisCount) {
|
||||
var systems = countDocumentedSystems(noteText);
|
||||
var noteLength = noteText.length;
|
||||
|
||||
// MDM complexity estimation (simplified 2021 E/M guidelines)
|
||||
// Based on: number of diagnoses, data reviewed, risk
|
||||
var mdmLevel = 2; // default straightforward
|
||||
|
||||
if (diagnosisCount >= 4 || noteLength > 3000) mdmLevel = 4;
|
||||
else if (diagnosisCount >= 2 || noteLength > 1500) mdmLevel = 3;
|
||||
|
||||
// Check for high-risk indicators
|
||||
var highRisk = /(?:admit|hospitali[sz]|intubat|seizure|sepsis|meningitis|fracture|surgery|ICU|PICU|NICU|emergent|critical|unstable)/i.test(noteText);
|
||||
if (highRisk) mdmLevel = Math.max(mdmLevel, 4);
|
||||
|
||||
var moderateRisk = /(?:dehydrat|IV fluid|antibiotic|x-ray|CT scan|MRI|ultrasound|lab|CBC|CMP|urinalysis|blood culture|lumbar puncture)/i.test(noteText);
|
||||
if (moderateRisk) mdmLevel = Math.max(mdmLevel, 3);
|
||||
|
||||
return {
|
||||
level: Math.min(mdmLevel, 5),
|
||||
rosCount: systems.rosCount,
|
||||
peCount: systems.peCount,
|
||||
diagnosisCount: diagnosisCount,
|
||||
complexity: mdmLevel >= 4 ? 'high' : mdmLevel >= 3 ? 'moderate' : 'low'
|
||||
};
|
||||
}
|
||||
|
||||
// ── Get age category for well visit CPT ──────────────────────────
|
||||
|
||||
function getWellVisitCPT(patientAge) {
|
||||
var ageStr = (patientAge || '').toLowerCase();
|
||||
var ageYears = 0;
|
||||
var monthMatch = ageStr.match(/(\d+)\s*(?:month|mo)/);
|
||||
var yearMatch = ageStr.match(/(\d+)\s*(?:year|yr|y\b)/);
|
||||
var dayMatch = ageStr.match(/(\d+)\s*(?:day|d\b)/);
|
||||
var weekMatch = ageStr.match(/(\d+)\s*(?:week|wk)/);
|
||||
|
||||
if (yearMatch) ageYears = parseInt(yearMatch[1]);
|
||||
else if (monthMatch) ageYears = parseInt(monthMatch[1]) / 12;
|
||||
else if (weekMatch) ageYears = parseInt(weekMatch[1]) / 52;
|
||||
else if (dayMatch) ageYears = parseInt(dayMatch[1]) / 365;
|
||||
|
||||
// Established patient codes (most common)
|
||||
if (ageYears < 1) return CPT_EM.wellvisit_established[0];
|
||||
if (ageYears < 5) return CPT_EM.wellvisit_established[1];
|
||||
if (ageYears < 12) return CPT_EM.wellvisit_established[2];
|
||||
if (ageYears < 18) return CPT_EM.wellvisit_established[3];
|
||||
return CPT_EM.wellvisit_established[4];
|
||||
}
|
||||
|
||||
// ── NLM ICD-10 lookup ────────────────────────────────────────────
|
||||
|
||||
async function lookupICD10(term) {
|
||||
try {
|
||||
// Use maxList=1 for short/generic terms, 2 for longer specific terms
|
||||
var maxList = term.split(/\s+/).length >= 3 ? 2 : 1;
|
||||
var url = 'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?sf=code,name&terms=' + encodeURIComponent(term) + '&maxList=' + maxList;
|
||||
var resp = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
||||
if (!resp.ok) return [];
|
||||
var data = await resp.json();
|
||||
// NLM response format: [totalCount, codeArray, extraDataArray, displayArray]
|
||||
if (!data || !data[1] || !data[3]) return [];
|
||||
return data[3].map(function(item) {
|
||||
return { code: item[0], name: item[1] };
|
||||
});
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main endpoint ────────────────────────────────────────────────
|
||||
|
||||
router.post('/suggest-codes', authMiddleware, async function(req, res) {
|
||||
try {
|
||||
var { noteText, noteType, patientAge, visitType } = req.body;
|
||||
if (!noteText) return res.status(400).json({ error: 'No note text provided' });
|
||||
|
||||
// 1. Extract diagnoses from note text
|
||||
var diagnoses = extractDiagnoses(noteText);
|
||||
|
||||
// 2. Look up ICD-10 codes via NLM for each diagnosis
|
||||
var icd10Results = [];
|
||||
var seen = {};
|
||||
|
||||
// First add any ICD-10 codes already extracted from the text
|
||||
diagnoses.filter(function(d) { return d.type === 'icd_extracted'; }).forEach(function(d) {
|
||||
if (!seen[d.term]) { icd10Results.push({ code: d.term, name: '', source: 'extracted' }); seen[d.term] = true; }
|
||||
});
|
||||
|
||||
// Then look up non-ICD terms — check local common map first, then NLM
|
||||
var lookupTerms = diagnoses.filter(function(d) { return d.type !== 'icd_extracted'; }).slice(0, 8);
|
||||
for (var i = 0; i < lookupTerms.length; i++) {
|
||||
var termLower = lookupTerms[i].term.toLowerCase().trim();
|
||||
var localMatch = COMMON_ICD10[termLower];
|
||||
if (localMatch && !seen[localMatch.code]) {
|
||||
icd10Results.push({ code: localMatch.code, name: localMatch.name, source: 'local' });
|
||||
seen[localMatch.code] = true;
|
||||
} else if (!localMatch) {
|
||||
var results = await lookupICD10(lookupTerms[i].term);
|
||||
results.forEach(function(r) {
|
||||
if (!seen[r.code]) { icd10Results.push({ code: r.code, name: r.name, source: 'nlm' }); seen[r.code] = true; }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Suggest CPT codes based on note type and complexity
|
||||
var cptResults = [];
|
||||
var emLevel = estimateEMLevel(noteText, diagnoses.length);
|
||||
|
||||
if (noteType === 'wellvisit') {
|
||||
var wvCode = getWellVisitCPT(patientAge);
|
||||
if (wvCode) cptResults.push(wvCode);
|
||||
// Well visits may also have a problem-oriented code if issues found
|
||||
if (diagnoses.length > 1) {
|
||||
cptResults.push({ code: '99213', desc: 'May also bill problem-oriented visit if significant issue addressed', note: 'modifier -25' });
|
||||
}
|
||||
} else if (noteType === 'hospital' || visitType === 'inpatient') {
|
||||
var isAdmission = /(?:admit|admission|initial hospital|H&P)/i.test(noteText);
|
||||
var isDischarge = /(?:discharge|d\/c summary)/i.test(noteText);
|
||||
if (isDischarge) {
|
||||
cptResults.push(CPT_EM.inpatient_discharge[noteText.length > 2000 ? 1 : 0]);
|
||||
} else if (isAdmission) {
|
||||
var admitIdx = Math.min(emLevel.level - 2, 2);
|
||||
if (admitIdx >= 0) cptResults.push(CPT_EM.inpatient_admit[admitIdx]);
|
||||
} else {
|
||||
var subIdx = Math.min(emLevel.level - 2, 2);
|
||||
if (subIdx >= 0) cptResults.push(CPT_EM.inpatient_subsequent[subIdx]);
|
||||
}
|
||||
} else if (visitType === 'ed') {
|
||||
var edIdx = Math.min(emLevel.level - 1, 4);
|
||||
cptResults.push(CPT_EM.ed[edIdx]);
|
||||
} else {
|
||||
// Outpatient — determine new vs established
|
||||
var isNew = /\bnew patient\b/i.test(noteText);
|
||||
var table = isNew ? CPT_EM.outpatient_new : CPT_EM.outpatient_established;
|
||||
var idx = Math.min(emLevel.level - 1, table.length - 1);
|
||||
cptResults.push(table[idx]);
|
||||
}
|
||||
|
||||
logger.audit(req.user.id, 'suggest_billing_codes', 'Suggested ' + icd10Results.length + ' ICD-10 + ' + cptResults.length + ' CPT codes', req, { category: 'clinical' });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
icd10: icd10Results.slice(0, 10),
|
||||
cpt: cptResults,
|
||||
emLevel: emLevel
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Billing] Error:', err.message);
|
||||
res.status(500).json({ error: 'Code suggestion failed' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Loading…
Reference in a new issue