Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions apps/web/public/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1014,9 +1014,8 @@ async function viewCaseload(q) {

async function viewSuperbill(q) {
const month = q.get('month') ?? thisMonthISO();
const rule = q.get('rule') === 'midpoint' ? 'midpoint' : 'full';
const sb = await get(`/orgs/${state.orgId}/superbill?month=${month}&rule=${rule}`);
const link = (m, r = rule) => `/app/superbill?month=${m}${r === 'midpoint' ? '&rule=midpoint' : ''}`;
const sb = await get(`/orgs/${state.orgId}/superbill?month=${month}`);
const link = (m) => `/app/superbill?month=${m}`;
const totals = Object.entries(sb.totals);
const html = `
<div class="head">
Expand All @@ -1025,8 +1024,7 @@ async function viewSuperbill(q) {
<a class="btn sm" href="${link(shiftMonth(month, -1))}" aria-label="Previous month">‹</a>
<a class="btn sm" href="${link(thisMonthISO())}">This month</a>
<a class="btn sm" href="${link(shiftMonth(month, 1))}" aria-label="Next month">›</a>
<select id="rule" class="inline" aria-label="Time rule"><option value="full" ${rule === 'full' ? 'selected' : ''}>Full time per unit</option><option value="midpoint" ${rule === 'midpoint' ? 'selected' : ''}>Midpoint rule</option></select>
<a class="btn primary sm" href="/api/v1/orgs/${state.orgId}/superbill?month=${month}&rule=${rule}&format=csv" download>Download CSV</a>
<a class="btn primary sm" href="/api/v1/orgs/${state.orgId}/superbill?month=${month}&format=csv" download>Download CSV</a>
</div>
</div>
<div class="cols">
Expand All @@ -1050,16 +1048,13 @@ async function viewSuperbill(q) {
}</section>
<section class="card"><div class="card-b dim small stack">
<span>One line per code, per patient, per month, for your biller or clearinghouse. Prices vary by locality, so none are shown.</span>
<span><b>Full time</b> counts a unit only when its whole time is met. <b>Midpoint</b> applies the CPT rule (G0023 at 31 minutes); check that your payers accept it before using it. CCM always needs its full 20 minutes.</span>
<span>A code counts only when its full time is met: 60 minutes for G0023 (and G0019, G0140), 30 more for each add-on, 20 for CCM. Medicare does not allow rounding up from 31 minutes on these codes.</span>
<span>On hold means a claim would be missing something: consent (renewed yearly for PIN and CHI), the initiating visit, the billing practitioner's NPI, or for CCM a care plan.</span>
</div></section>
</div>
</div>`;
return {
html,
bind() {
document.getElementById('rule').onchange = (e) => go(link(month, e.target.value));
},
};
}

Expand Down
73 changes: 72 additions & 1 deletion apps/web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>tleehealth: the practice that runs itself</title>
<meta name="description" content="Telehealth and practice management with an AI agent that calls every patient. Scheduling, prescriptions, labs and health-record import. $10 per seat or $199 for up to 1,000.">
<meta name="description" content="Telehealth and practice management with an AI agent that calls every patient. Scheduling, prescriptions, labs, care plans and Medicare navigation billing. $10 per seat or $199 for up to 1,000.">
<link rel="canonical" href="https://tleehealth.com/">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
Expand All @@ -29,6 +29,7 @@
</a>
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 28px; font-size: 15px; color: #A3ACA6">
<a href="#product" style="color: #A3ACA6; text-decoration: none">Product</a>
<a href="#navigation" style="color: #A3ACA6; text-decoration: none">Get paid</a>
<a href="#agents" style="color: #A3ACA6; text-decoration: none">Agents</a>
<a href="#import" style="color: #A3ACA6; text-decoration: none">Import</a>
<a href="#developers" style="color: #A3ACA6; text-decoration: none">Developers</a>
Expand Down Expand Up @@ -140,6 +141,74 @@ <h3 style="margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.01em
</div>
</section>

<!-- Navigation billing -->
<section id="navigation" style="border-top: 1px solid #1E2522; background: #0C100F">
<div style="max-width: 1200px; margin: 0 auto; padding: 96px 24px; display: flex; flex-wrap: wrap; gap: 56px; align-items: center">
<div style="flex: 1 1 460px; min-width: 0; display: flex; flex-direction: column; gap: 22px">
<div style="font-family: 'Geist Mono', monospace; font-size: 13px; letter-spacing: 0.08em; color: #E6E39A">GET PAID FOR COORDINATION</div>
<h2 style="margin: 0; font-size: clamp(32px, 4vw, 52px); letter-spacing: -0.035em; line-height: 1.02; font-weight: 600">Medicare pays for the help your staff already gives.</h2>
<p style="margin: 0; color: #A3ACA6; font-size: 17px; line-height: 1.55">Chasing a prior authorization. Booking the oncologist. Finding a ride to infusion. Since 2024 Medicare pays practices for this work, roughly $75 to $100 a month per patient for the first hour, depending on where you practice. Most practices never bill it, because nobody counts the minutes.</p>
<ol style="margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 14px; font-size: 15px; line-height: 1.5">
<li style="display: flex; gap: 14px; align-items: baseline"><span style="font-family: 'Geist Mono', monospace; color: #E6E39A">01</span><span><b style="font-weight: 600">Your advocate logs the time.</b> <span style="color: #A3ACA6">A staff member helps a patient, picks what they did and how long it took. The care plan keeps the goals and who owes what.</span></span></li>
<li style="display: flex; gap: 14px; align-items: baseline"><span style="font-family: 'Geist Mono', monospace; color: #E6E39A">02</span><span><b style="font-weight: 600">The month adds itself up.</b> <span style="color: #A3ACA6">Minutes turn into billing codes per patient, and the caseload shows how close each one is to the next.</span></span></li>
<li style="display: flex; gap: 14px; align-items: baseline"><span style="font-family: 'Geist Mono', monospace; color: #E6E39A">03</span><span><b style="font-weight: 600">Hand your biller a superbill.</b> <span style="color: #A3ACA6">One spreadsheet of what to claim, with anything that would get a claim rejected (missing consent, no qualifying visit, no NPI) held back and named.</span></span></li>
</ol>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px">
<div style="border: 1px solid #2E3122; border-radius: 12px; padding: 14px 16px; display: flex; flex-direction: column; gap: 4px">
<div style="font-size: 15px; font-weight: 600">Illness navigation</div>
<div style="font-size: 13px; color: #8A948E; line-height: 1.45">One serious condition, like cancer or heart failure</div>
<div style="font-family: 'Geist Mono', monospace; font-size: 12px; color: #E6E39A">G0023 · G0024</div>
</div>
<div style="border: 1px solid #2E3122; border-radius: 12px; padding: 14px 16px; display: flex; flex-direction: column; gap: 4px">
<div style="font-size: 15px; font-weight: 600">Community health</div>
<div style="font-size: 13px; color: #8A948E; line-height: 1.45">Housing, food or transport in the way of treatment</div>
<div style="font-family: 'Geist Mono', monospace; font-size: 12px; color: #E6E39A">G0019 · G0022</div>
</div>
<div style="border: 1px solid #2E3122; border-radius: 12px; padding: 14px 16px; display: flex; flex-direction: column; gap: 4px">
<div style="font-size: 15px; font-weight: 600">Chronic care</div>
<div style="font-size: 13px; color: #8A948E; line-height: 1.45">Two or more long-term conditions, with a care plan</div>
<div style="font-family: 'Geist Mono', monospace; font-size: 12px; color: #E6E39A">99490 · 99439</div>
</div>
</div>
<p style="margin: 0; color: #6E7872; font-size: 13px; line-height: 1.5">Your practice submits the claims; tleehealth keeps the record and does the math. Rates vary by locality and payer.</p>
</div>

<!-- Superbill preview -->
<div style="flex: 1 1 420px; min-width: 0; background: #0F1412; border: 1px solid #222A27; border-radius: 18px; overflow: hidden; box-shadow: 0 40px 80px rgba(0,0,0,0.45)">
<div style="display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid #1E2522">
<div style="font-size: 14px; font-weight: 600">Superbill · October</div>
<div style="font-family: 'Geist Mono', monospace; font-size: 12px; color: #8A948E">2 ready · 1 on hold</div>
</div>
<ul style="list-style: none; margin: 0; padding: 8px 0; font-size: 14px">
<li style="display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 12px; padding: 14px 18px; align-items: center">
<span>Rosa D.<br><span style="color: #8A948E; font-size: 13px">Illness navigation · 105 min</span></span>
<span style="font-family: 'Geist Mono', monospace; font-size: 13px">G0023 + G0024</span>
<span style="font-size: 11px; padding: 3px 8px; border-radius: 99px; background: #15302A; color: #9AF5C4">ready</span>
</li>
<li style="display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 12px; padding: 14px 18px; align-items: center">
<span>Marcus O.<br><span style="color: #8A948E; font-size: 13px">Chronic care · 42 min</span></span>
<span style="font-family: 'Geist Mono', monospace; font-size: 13px">99490 + 99439</span>
<span style="font-size: 11px; padding: 3px 8px; border-radius: 99px; background: #15302A; color: #9AF5C4">ready</span>
</li>
<li style="display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 12px; padding: 14px 18px; align-items: center; background: #17180F">
<span>Walt K.<br><span style="color: #8A948E; font-size: 13px">Community health · 64 min</span></span>
<span style="font-family: 'Geist Mono', monospace; font-size: 13px">G0019</span>
<span style="font-size: 11px; padding: 3px 8px; border-radius: 99px; background: #26291A; color: #E6E39A">hold</span>
</li>
<li style="padding: 0 18px 12px; color: #E6E39A; font-size: 13px">Walt K.: consent is over a year old; renew it</li>
</ul>
<div style="padding: 14px 18px 18px; border-top: 1px solid #1E2522; display: flex; flex-direction: column; gap: 10px">
<div style="display: flex; justify-content: space-between; font-family: 'Geist Mono', monospace; font-size: 12px; color: #8A948E"><span>Rosa D. · progress to the next add-on</span><span>15 min to go</span></div>
<div style="height: 6px; border-radius: 99px; background: #1C2220; overflow: hidden"><div style="width: 50%; height: 100%; background: #7CF2B0; border-radius: 99px"></div></div>
<div style="display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 4px">
<span style="font-size: 13px; color: #A3ACA6">Ready to bill: G0023 ×1 · G0024 ×1 · 99490 ×1 · 99439 ×1</span>
<span style="font-size: 13px; padding: 8px 12px; border-radius: 9px; background: #7CF2B0; color: #0A0D0C; font-weight: 600; white-space: nowrap">Download CSV</span>
</div>
</div>
</div>
</div>
</section>

<!-- Import -->
<section id="import" style="border-top: 1px solid #1E2522; border-bottom: 1px solid #1E2522; background: #0C100F">
<div style="max-width: 1200px; margin: 0 auto; padding: 96px 24px; display: flex; flex-wrap: wrap; gap: 56px; align-items: center">
Expand Down Expand Up @@ -197,6 +266,7 @@ <h2 style="margin: 0; font-size: clamp(32px, 4vw, 52px); letter-spacing: -0.035e
<span style="padding: 8px 14px; border-radius: 99px; background: #15302A; color: #9AF5C4">Org manager</span>
<span style="padding: 8px 14px; border-radius: 99px; background: #15302A; color: #9AF5C4">Provider</span>
<span style="padding: 8px 14px; border-radius: 99px; background: #15302A; color: #9AF5C4">Staff</span>
<span style="padding: 8px 14px; border-radius: 99px; background: #15302A; color: #9AF5C4">Advocate</span>
<span style="color: #8A948E; margin: 0 6px 0 16px">Always free:</span>
<span style="padding: 8px 14px; border-radius: 99px; border: 1px solid #2C3531; color: #ECEEE6">Patients</span>
<span style="padding: 8px 14px; border-radius: 99px; border: 1px solid #2C3531; color: #ECEEE6">Leads</span>
Expand Down Expand Up @@ -236,6 +306,7 @@ <h2 style="margin: 0; font-size: clamp(32px, 4vw, 52px); letter-spacing: -0.035e
<pre style="margin: 0; padding: 18px 16px; font-family: 'Geist Mono', monospace; font-size: 13px; line-height: 1.7; color: #C9D0CA; overflow-x: auto"><span style="color: #7CF2B0">$</span> tleehealth login
<span style="color: #7CF2B0">$</span> tleehealth schedule today --location mission-st
<span style="color: #7CF2B0">$</span> tleehealth invite dr.lin@example.com --type provider
<span style="color: #7CF2B0">$</span> tleehealth superbill --month 2026-10 --csv
<span style="color: #7CF2B0">$</span> tleehealth tui</pre>
</div>
</div>
Expand Down
12 changes: 4 additions & 8 deletions apps/web/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ async function carePlan(patientId) {
}

/** Enrollments with their minutes in a month, decorated with codes and blockers. */
async function programRows(orgId, { start, end, month }, filter, rule = 'full') {
async function programRows(orgId, { start, end, month }, filter) {
const sql = db();
const rows = await sql`
select cp.*, p.name as patient, p.dob, bp.name as billing_provider, bp.npi as billing_npi, nv.name as navigator,
Expand All @@ -726,7 +726,7 @@ async function programRows(orgId, { start, end, month }, filter, rule = 'full')
where cp.org_id = ${orgId} ${filter(sql)}
order by p.name nulls last, cp.program`;
return rows.map((r) => ({
...nav.superbillRow(r, r.minutes, month, rule),
...nav.superbillRow(r, r.minutes, month),
status: r.status,
navigator_id: r.navigator_id,
billing_provider_id: r.billing_provider_id,
Expand All @@ -736,7 +736,7 @@ async function programRows(orgId, { start, end, month }, filter, rule = 'full')
}));
}

api.get('/navigation/programs', (c) => c.json({ programs: nav.PROGRAMS, activities: nav.ACTIVITIES, time_rules: nav.TIME_RULES }));
api.get('/navigation/programs', (c) => c.json({ programs: nav.PROGRAMS, activities: nav.ACTIVITIES }));

api.get('/orgs/:org/patients/:id/care-plan', async (c) => {
const { me, user } = await member(c, c.req.param('org'));
Expand Down Expand Up @@ -945,19 +945,16 @@ api.get('/orgs/:org/caseload', async (c) => {
return c.json({ month: m.month, mine, caseload: rows });
});

/** Everything billable in a month. ?rule=full|midpoint, ?format=csv. */
/** Everything billable in a month. ?format=csv. Units need their full time (see navigation.js). */
api.get('/orgs/:org/superbill', async (c) => {
const { me, user } = await member(c, c.req.param('org'), BILLERS);
const m = monthOf(c);
const rule = c.req.query('rule') ?? 'full';
if (!nav.TIME_RULES.includes(rule)) fail(400, 'rule must be full or midpoint');
// Every enrollment that overlapped the month and has time in it.
const rows = (
await programRows(
me.org_id,
m,
(sql) => sql`and cp.started_on < ${m.end} and (cp.ended_on is null or cp.ended_on >= ${m.start})`,
rule,
)
).filter((r) => r.minutes > 0);
rows.sort((a, b) => Number(b.ready) - Number(a.ready));
Expand All @@ -972,7 +969,6 @@ api.get('/orgs/:org/superbill', async (c) => {
for (const r of rows) if (r.ready) for (const l of r.lines) totals[l.code] = (totals[l.code] ?? 0) + l.units;
return c.json({
month: m.month,
rule,
rows,
totals,
ready: rows.filter((r) => r.ready).length,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ Authenticate with a session cookie or \`Authorization: Bearer th_live_...\` (cre
- PATCH /orgs/:org/programs/:id consent (renews it), billing practitioner, end
- POST /orgs/:org/programs/:id/time log navigation minutes ({"minutes", "activity", "note", "performed_on"})
- GET /orgs/:org/caseload?month=YYYY-MM enrollments, minutes toward G0023/G0024, G0019/G0022, 99490/99439, open tasks
- GET /orgs/:org/superbill?month=YYYY-MM&rule=full|midpoint&format=csv
- GET /orgs/:org/superbill?month=YYYY-MM&format=csv
the month's billable codes per patient, ready or on hold with the reason

## Tools
Expand Down
Loading
Loading