Dark/light mode, dynamic stat badges, Supply Lapsed / Renewal Due / Resupply Ready / Active flags, priority scoring, PHI footer. Deployed live at signal.sttilsolutions.com. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1005 lines
32 KiB
HTML
1005 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Signal — Outreach Worklist</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@600;700&family=Inter:wght@400;500&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
/* ── DARK MODE TOKENS (default) ── */
|
|
:root {
|
|
--bg-page: #041A1A;
|
|
--bg-card: #072E2E;
|
|
--bg-elevated: #0A4444;
|
|
--bg-sidebar: #072E2E;
|
|
--sidebar-border: 1px solid #0F5E5E;
|
|
--text-heading: #FFFAF6;
|
|
--text-primary: #F0F4F4;
|
|
--text-secondary: #5BBBBB;
|
|
--text-muted: #5A8080;
|
|
--border: #0F5E5E;
|
|
--border-subtle: rgba(15, 94, 94, 0.45);
|
|
--brand: #2EA3A3;
|
|
--brand-hover: #1A8A8A;
|
|
--copper: #F07840;
|
|
--copper-text: #FFB070;
|
|
--copper-glow: 0 0 24px rgba(240,120,64,0.30), 0 2px 10px rgba(4,26,26,0.7);
|
|
--priority-row: rgba(224,104,48,0.09);
|
|
--card-shadow: 0 2px 10px rgba(4,26,26,0.6);
|
|
--row-hover: rgba(46,163,163,0.06);
|
|
--font-heading: 'Plus Jakarta Sans', sans-serif;
|
|
--font-body: 'Inter', sans-serif;
|
|
--font-mono: 'JetBrains Mono', monospace;
|
|
--sidebar-width: 240px;
|
|
}
|
|
|
|
/* ── LIGHT MODE OVERRIDES ── */
|
|
body.light {
|
|
--bg-page: #F4F9F9;
|
|
--bg-card: #FFFFFF;
|
|
--bg-elevated: #E5EEEE;
|
|
--bg-sidebar: #072E2E;
|
|
--text-heading: #0A3030;
|
|
--text-primary: #1C2E2E;
|
|
--text-secondary: #5A7E7E;
|
|
--text-muted: #7A9E9E;
|
|
--border: #C8D8D8;
|
|
--border-subtle: rgba(200,216,216,0.7);
|
|
--brand: #147A7A;
|
|
--brand-hover: #0F5E5E;
|
|
--copper: #E06028;
|
|
--copper-text: #E06028;
|
|
--copper-glow: 0 0 16px rgba(224,96,40,0.20), 0 2px 8px rgba(0,0,0,0.08);
|
|
--priority-row: rgba(224,96,40,0.05);
|
|
--card-shadow: 0 2px 8px rgba(7,46,46,0.09);
|
|
--row-hover: rgba(20,122,122,0.04);
|
|
}
|
|
|
|
body {
|
|
font-family: var(--font-body);
|
|
background: var(--bg-page);
|
|
color: var(--text-primary);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
transition: background 0.25s, color 0.25s;
|
|
}
|
|
|
|
/* ══════════════════════════════
|
|
LAYOUT
|
|
══════════════════════════════ */
|
|
.app { display: flex; width: 100%; min-height: 100vh; }
|
|
|
|
/* ── Sidebar ── */
|
|
.sidebar {
|
|
width: var(--sidebar-width);
|
|
background: var(--bg-sidebar);
|
|
border-right: var(--sidebar-border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-shrink: 0;
|
|
position: fixed;
|
|
top: 0; left: 0; bottom: 0;
|
|
z-index: 20;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 22px 20px 18px;
|
|
border-bottom: 1px solid rgba(15,94,94,0.5);
|
|
}
|
|
|
|
.brand-mark {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.brand-icon {
|
|
width: 33px; height: 33px;
|
|
background: linear-gradient(135deg, #1A8A8A, #147A7A);
|
|
border-radius: 8px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-family: var(--font-heading);
|
|
font-weight: 700;
|
|
font-size: 16px;
|
|
color: #FFFAF6;
|
|
flex-shrink: 0;
|
|
box-shadow: 0 2px 8px rgba(20,122,122,0.35);
|
|
}
|
|
|
|
.brand-name {
|
|
font-family: var(--font-heading);
|
|
font-weight: 700;
|
|
font-size: 17px;
|
|
color: #FFFAF6;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.brand-sub {
|
|
font-size: 11px;
|
|
color: #5BBBBB;
|
|
margin-left: 43px;
|
|
letter-spacing: 0.01em;
|
|
}
|
|
|
|
/* ── Nav ── */
|
|
.nav { padding: 10px 0; flex: 1; overflow-y: auto; }
|
|
|
|
.nav-label {
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
letter-spacing: 0.09em;
|
|
text-transform: uppercase;
|
|
color: #426060;
|
|
padding: 10px 20px 4px;
|
|
}
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 9px 20px;
|
|
font-size: 13.5px;
|
|
color: #7A9E9E;
|
|
cursor: pointer;
|
|
border-left: 3px solid transparent;
|
|
text-decoration: none;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.nav-item:hover { color: #2EA3A3; background: rgba(46,163,163,0.07); }
|
|
|
|
.nav-item.active {
|
|
color: #FFFAF6;
|
|
font-weight: 500;
|
|
border-left-color: #D97B35;
|
|
background: rgba(10,68,68,0.55);
|
|
}
|
|
|
|
.nav-icon { width: 18px; text-align: center; font-size: 14px; flex-shrink: 0; }
|
|
|
|
.nav-badge {
|
|
margin-left: auto;
|
|
background: rgba(200,48,48,0.22);
|
|
color: #FF8080;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 1px 7px;
|
|
border-radius: 20px;
|
|
font-family: var(--font-mono);
|
|
}
|
|
.nav-badge.warn { background: rgba(217,123,53,0.22); color: #E49655; }
|
|
|
|
.sidebar-footer {
|
|
padding: 14px 20px;
|
|
border-top: 1px solid rgba(15,94,94,0.5);
|
|
}
|
|
|
|
.user-row { display: flex; align-items: center; gap: 10px; }
|
|
|
|
.user-avatar {
|
|
width: 30px; height: 30px;
|
|
background: rgba(46,163,163,0.18);
|
|
border: 1px solid rgba(46,163,163,0.3);
|
|
border-radius: 50%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 11px; color: #2EA3A3; font-weight: 600;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.user-name { font-size: 12px; font-weight: 500; color: #FFFAF6; }
|
|
.user-role { font-size: 10px; color: #5BBBBB; margin-top: 1px; }
|
|
|
|
/* ── Main ── */
|
|
.main {
|
|
margin-left: var(--sidebar-width);
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* ── Topbar ── */
|
|
.topbar {
|
|
background: var(--bg-card);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0 28px;
|
|
height: 60px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
position: sticky;
|
|
top: 0; z-index: 10;
|
|
transition: background 0.25s, border-color 0.25s;
|
|
}
|
|
|
|
.topbar-left { display: flex; align-items: center; gap: 16px; }
|
|
|
|
.page-title {
|
|
font-family: var(--font-heading);
|
|
font-weight: 700;
|
|
font-size: 18px;
|
|
color: var(--text-heading);
|
|
}
|
|
|
|
.last-updated {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
font-family: var(--font-mono);
|
|
padding: 3px 8px;
|
|
background: var(--bg-elevated);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.topbar-right { display: flex; align-items: center; gap: 10px; }
|
|
|
|
.mode-toggle {
|
|
background: var(--bg-elevated);
|
|
border: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
padding: 6px 13px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-family: var(--font-body);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
.mode-toggle:hover { border-color: var(--brand); color: var(--brand); }
|
|
|
|
.export-btn {
|
|
background: var(--brand);
|
|
border: none;
|
|
color: #FFFAF6;
|
|
padding: 7px 16px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
font-family: var(--font-body);
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
}
|
|
.export-btn:hover { background: var(--brand-hover); }
|
|
|
|
/* ══════════════════════════════
|
|
CONTENT
|
|
══════════════════════════════ */
|
|
.content { padding: 28px; }
|
|
|
|
/* ── Stat cards ── */
|
|
.stats-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 20px 22px;
|
|
box-shadow: var(--card-shadow);
|
|
transition: all 0.25s;
|
|
}
|
|
|
|
.stat-card.priority {
|
|
border-color: var(--copper);
|
|
box-shadow: var(--copper-glow);
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-family: var(--font-mono);
|
|
font-size: 34px;
|
|
font-weight: 500;
|
|
color: var(--text-heading);
|
|
letter-spacing: -0.02em;
|
|
margin-bottom: 2px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.stat-value.copper-val { color: var(--copper-text); }
|
|
|
|
.stat-sub { font-size: 12px; color: var(--text-secondary); margin-top: 6px; }
|
|
|
|
.stat-delta {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
padding: 2px 8px;
|
|
border-radius: 20px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.delta-good { color: #4AE899; background: rgba(26,122,78,0.14); }
|
|
.delta-warn { color: var(--copper-text); background: rgba(240,120,64,0.14); }
|
|
|
|
body.light .delta-good { color: #146040; background: #C8EDD8; }
|
|
body.light .delta-warn { color: #903A14; background: #FFE4CC; }
|
|
|
|
/* ══════════════════════════════
|
|
WORKLIST TABLE
|
|
══════════════════════════════ */
|
|
.table-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
box-shadow: var(--card-shadow);
|
|
overflow: hidden;
|
|
transition: background 0.25s, border-color 0.25s;
|
|
}
|
|
|
|
.table-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 22px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-elevated);
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.table-title {
|
|
font-family: var(--font-heading);
|
|
font-weight: 700;
|
|
font-size: 15px;
|
|
color: var(--text-heading);
|
|
}
|
|
|
|
.table-meta { font-size: 11px; color: var(--text-muted); font-family: var(--font-mono); margin-top: 2px; }
|
|
|
|
.filters { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
|
|
.chip {
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
font-size: 11.5px;
|
|
cursor: pointer;
|
|
font-family: var(--font-body);
|
|
transition: all 0.15s;
|
|
}
|
|
.chip:hover { border-color: var(--brand); color: var(--brand); }
|
|
.chip.on { background: var(--brand); color: #fff; border-color: var(--brand); }
|
|
|
|
table { width: 100%; border-collapse: collapse; }
|
|
|
|
thead th {
|
|
padding: 10px 22px;
|
|
text-align: left;
|
|
font-size: 10.5px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
background: var(--bg-elevated);
|
|
border-bottom: 1px solid var(--border);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
tbody tr {
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
transition: background 0.1s;
|
|
}
|
|
tbody tr:last-child { border-bottom: none; }
|
|
tbody tr:hover { background: var(--row-hover); }
|
|
|
|
tbody tr.p-row { background: var(--priority-row); }
|
|
tbody tr.p-row:hover { background: rgba(203,107,32,0.14); }
|
|
|
|
td {
|
|
padding: 13px 22px;
|
|
font-size: 13.5px;
|
|
color: var(--text-primary);
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.pid {
|
|
font-family: var(--font-mono);
|
|
font-size: 12.5px;
|
|
color: var(--text-secondary);
|
|
font-weight: 700;
|
|
}
|
|
.p-row .pid { color: var(--copper-text); }
|
|
|
|
.top-tag {
|
|
font-size: 9.5px;
|
|
font-family: var(--font-mono);
|
|
font-weight: 600;
|
|
letter-spacing: 0.06em;
|
|
color: var(--copper-text);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.device { font-weight: 500; }
|
|
.payer { color: var(--text-secondary); font-size: 13px; }
|
|
|
|
.days { font-family: var(--font-mono); font-weight: 500; }
|
|
.days-critical { color: #FF7070; }
|
|
.days-warn { color: var(--copper-text); }
|
|
.days-ok { color: var(--text-primary); }
|
|
.days-expired { color: #FF7070; font-weight: 600; }
|
|
|
|
.score {
|
|
font-family: var(--font-mono);
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
}
|
|
.score-hi { color: var(--copper-text); }
|
|
.score-mid { color: var(--text-secondary); }
|
|
.score-lo { color: var(--text-muted); }
|
|
|
|
/* ── Badges ── */
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 3px 9px;
|
|
border-radius: 20px;
|
|
font-size: 11.5px;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
}
|
|
.b-active { background: rgba(26,122,78,0.16); color: #4AE899; }
|
|
.b-expiring { background: rgba(168,90,24,0.16); color: #F0B464; }
|
|
.b-critical { background: rgba(200,48,48,0.16); color: #FF7070; }
|
|
.b-pa-req { background: rgba(240,120,64,0.18); color: #FFB070; border: 1px solid rgba(240,120,64,0.3); }
|
|
.b-pa-pend { background: rgba(122,94,160,0.16); color: #C0A8E0; }
|
|
.b-denied { background: rgba(200,48,48,0.22); color: #FF7070; }
|
|
|
|
body.light .b-active { background: #C8EDD8; color: #146040; }
|
|
body.light .b-expiring { background: #FDECD5; color: #A85A18; }
|
|
body.light .b-critical { background: #F8CCCC; color: #A02020; }
|
|
body.light .b-pa-req { background: #FFE4CC; color: #903A14; border-color: rgba(144,58,20,0.2); }
|
|
body.light .b-pa-pend { background: #E2D8F0; color: #7A5EA0; }
|
|
body.light .b-denied { background: #F8CCCC; color: #A02020; }
|
|
|
|
/* ── Action buttons ── */
|
|
.act {
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
padding: 5px 13px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
font-family: var(--font-body);
|
|
white-space: nowrap;
|
|
transition: all 0.15s;
|
|
}
|
|
.act:hover { border-color: var(--brand); color: var(--brand); }
|
|
|
|
.act-copper {
|
|
border-color: var(--copper);
|
|
color: var(--copper-text);
|
|
}
|
|
.act-copper:hover { background: rgba(203,107,32,0.1); }
|
|
|
|
/* ── Footer note ── */
|
|
.footer-note {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 22px;
|
|
border-top: 1px solid var(--border-subtle);
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
background: var(--bg-elevated);
|
|
}
|
|
.phi-note {
|
|
display: flex; align-items: center; gap: 6px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* ── Import toast ── */
|
|
.import-toast {
|
|
display: none;
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
background: var(--bg-elevated);
|
|
border: 1px solid var(--brand);
|
|
border-radius: 8px;
|
|
padding: 12px 18px;
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
z-index: 100;
|
|
box-shadow: var(--card-shadow);
|
|
animation: slideIn 0.2s ease;
|
|
}
|
|
.import-toast.show { display: block; }
|
|
@keyframes slideIn {
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app">
|
|
|
|
<!-- hidden file input for CSV import -->
|
|
<input type="file" id="csv-input" accept=".csv" style="display:none" onchange="handleCSVImport(event)" />
|
|
|
|
<!-- ══ SIDEBAR ══ -->
|
|
<aside class="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="brand-mark">
|
|
<div class="brand-icon">S</div>
|
|
<span class="brand-name">Signal</span>
|
|
</div>
|
|
<div class="brand-sub">by STTIL Solutions</div>
|
|
</div>
|
|
|
|
<nav class="nav">
|
|
<div class="nav-label">Worklist</div>
|
|
<a class="nav-item active" href="#">
|
|
<span class="nav-icon">≡</span> All Patients
|
|
</a>
|
|
<a class="nav-item" href="#">
|
|
<span class="nav-icon">⚠</span> Resupply Ready
|
|
<span class="nav-badge warn" id="badge-expiring">0</span>
|
|
</a>
|
|
<a class="nav-item" href="#">
|
|
<span class="nav-icon">⚡</span> Renewal Due
|
|
<span class="nav-badge" id="badge-visit">0</span>
|
|
</a>
|
|
<a class="nav-item" href="#">
|
|
<span class="nav-icon">✕</span> Supply Lapsed
|
|
<span class="nav-badge" id="badge-ooc">0</span>
|
|
</a>
|
|
<div class="nav-label" style="margin-top:4px;">System</div>
|
|
<a class="nav-item" href="#" onclick="document.getElementById('csv-input').click(); return false;">
|
|
<span class="nav-icon">↑</span> Import CSV
|
|
</a>
|
|
<a class="nav-item" href="#">
|
|
<span class="nav-icon">⚙</span> Settings
|
|
</a>
|
|
</nav>
|
|
|
|
<div class="sidebar-footer">
|
|
<div class="user-row">
|
|
<div class="user-avatar">JS</div>
|
|
<div>
|
|
<div class="user-name">J. Sullivan</div>
|
|
<div class="user-role">Billing Manager</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ══ MAIN ══ -->
|
|
<main class="main">
|
|
|
|
<header class="topbar">
|
|
<div class="topbar-left">
|
|
<div class="page-title">Outreach Worklist</div>
|
|
<span class="last-updated" id="import-label">Demo data · Apr 23, 2026</span>
|
|
</div>
|
|
<div class="topbar-right">
|
|
<button class="mode-toggle" onclick="document.body.classList.toggle('light')">
|
|
◐ Toggle Light/Dark
|
|
</button>
|
|
<button class="export-btn">↓ Export CSV</button>
|
|
<a href="mailto:kisa@sttilsolutions.com" class="export-btn" style="text-decoration:none; background:#D97B35;">Request Access →</a>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content">
|
|
|
|
<!-- ── Stats ── -->
|
|
<div class="stats-row">
|
|
<div class="stat-card priority" id="stat-card-urgent">
|
|
<div class="stat-label" id="stat-urgent-label">⚡ Prescriber Action</div>
|
|
<div class="stat-value copper-val" id="stat-urgent-value">6</div>
|
|
<div class="stat-sub" id="stat-urgent-sub">supply lapsed or renewal due — prescriber contact required</div>
|
|
<div class="stat-delta delta-warn" id="stat-urgent-delta">▲ action required today</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">⚠ Resupply Ready</div>
|
|
<div class="stat-value" id="stat-refill-value">14</div>
|
|
<div class="stat-sub">patients within resupply window — initiate now</div>
|
|
<div class="stat-delta delta-good" id="stat-refill-delta">initiate resupply now</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">✓ Active</div>
|
|
<div class="stat-value" id="stat-ok-value">142</div>
|
|
<div class="stat-sub">patients with supply on track — no action needed</div>
|
|
<div class="stat-delta delta-good">no action needed</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Worklist Table ── -->
|
|
<div class="table-card">
|
|
<div class="table-header">
|
|
<div>
|
|
<div class="table-title">Outreach Worklist</div>
|
|
<div class="table-meta" id="table-meta">162 patients · sorted by priority score · Apr 23, 2026</div>
|
|
</div>
|
|
<div class="filters">
|
|
<button class="chip on">All</button>
|
|
<button class="chip">Supply Lapsed</button>
|
|
<button class="chip">Renewal Due</button>
|
|
<button class="chip">Resupply Ready</button>
|
|
<button class="chip">Active</button>
|
|
</div>
|
|
</div>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Patient ID</th>
|
|
<th>Device</th>
|
|
<th>Payer</th>
|
|
<th>Days Left</th>
|
|
<th>Status</th>
|
|
<th>Priority</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="worklist-tbody">
|
|
|
|
<!-- Static demo rows — replaced on CSV import -->
|
|
<tr class="p-row">
|
|
<td>
|
|
<div class="pid">PT-00142</div>
|
|
<div class="top-tag">★ TOP PRIORITY</div>
|
|
</td>
|
|
<td><span class="device">Dexcom G7</span></td>
|
|
<td><span class="payer">Medicare Part B</span></td>
|
|
<td><span class="days days-critical">4 days</span></td>
|
|
<td><span class="badge b-pa-req"><span>⚡</span> Renewal Due</span></td>
|
|
<td><span class="score score-hi">98</span></td>
|
|
<td><button class="act act-copper">Request Renewal →</button></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td><div class="pid">PT-00387</div></td>
|
|
<td><span class="device">FreeStyle Libre 3</span></td>
|
|
<td><span class="payer">Medicaid — GA</span></td>
|
|
<td><span class="days days-critical">11 days</span></td>
|
|
<td><span class="badge b-critical"><span>⚠</span> Supply Lapsed</span></td>
|
|
<td><span class="score score-hi">87</span></td>
|
|
<td><button class="act act-copper">Contact Prescriber</button></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td><div class="pid">PT-00291</div></td>
|
|
<td><span class="device">Dexcom G7</span></td>
|
|
<td><span class="payer">Medicare Part B</span></td>
|
|
<td><span class="days days-critical">18 days</span></td>
|
|
<td><span class="badge b-expiring"><span>⚠</span> Resupply Ready</span></td>
|
|
<td><span class="score score-hi">74</span></td>
|
|
<td><button class="act">Initiate Resupply</button></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td><div class="pid">PT-00556</div></td>
|
|
<td><span class="device">Omnipod 5</span></td>
|
|
<td><span class="payer">BCBS — FL</span></td>
|
|
<td><span class="days days-warn">22 days</span></td>
|
|
<td><span class="badge b-expiring"><span>⚠</span> Resupply Ready</span></td>
|
|
<td><span class="score score-mid">68</span></td>
|
|
<td><button class="act">Initiate Resupply</button></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td><div class="pid">PT-00103</div></td>
|
|
<td><span class="device">FreeStyle Libre 3</span></td>
|
|
<td><span class="payer">Medicare Part B</span></td>
|
|
<td><span class="days days-warn">31 days</span></td>
|
|
<td><span class="badge b-expiring"><span>⚠</span> Resupply Ready</span></td>
|
|
<td><span class="score score-mid">52</span></td>
|
|
<td><button class="act">Initiate Resupply</button></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td><div class="pid">PT-00478</div></td>
|
|
<td><span class="device">Dexcom G7</span></td>
|
|
<td><span class="payer">Aetna</span></td>
|
|
<td><span class="days days-ok">45 days</span></td>
|
|
<td><span class="badge b-active"><span>✓</span> Active</span></td>
|
|
<td><span class="score score-mid">41</span></td>
|
|
<td><button class="act">View</button></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td><div class="pid">PT-00612</div></td>
|
|
<td><span class="device">FreeStyle Libre 3</span></td>
|
|
<td><span class="payer">UnitedHealth</span></td>
|
|
<td><span class="days days-ok">62 days</span></td>
|
|
<td><span class="badge b-active"><span>✓</span> Active</span></td>
|
|
<td><span class="score score-lo">28</span></td>
|
|
<td><button class="act">View</button></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td><div class="pid">PT-00089</div></td>
|
|
<td><span class="device">Dexcom G7</span></td>
|
|
<td><span class="payer">Medicare Part B</span></td>
|
|
<td><span class="days days-expired">Expired</span></td>
|
|
<td><span class="badge b-denied"><span>✕</span> Supply Lapsed</span></td>
|
|
<td><span class="score score-lo">—</span></td>
|
|
<td><button class="act act-copper">Contact Prescriber</button></td>
|
|
</tr>
|
|
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="footer-note">
|
|
<span class="phi-note">
|
|
🔒 PHI-safe — patient names and DOBs never stored. Crosswalk: patient_id only.
|
|
</span>
|
|
<span id="footer-count">Showing 8 of 162 · Load more →</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /content -->
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Import toast notification -->
|
|
<div class="import-toast" id="import-toast"></div>
|
|
|
|
<script>
|
|
// ── Coverage calculation rules (mirrored from payer_rules.json) ──
|
|
const DEVICE_RULES = {
|
|
dexcom_g7: { sensor: 10 },
|
|
dexcom_g6: { sensor: 10, transmitter: 90 },
|
|
freestyle_libre_2: { sensor: 14 },
|
|
freestyle_libre_3: { sensor: 14 },
|
|
omnipod_5: { pod: 3, sensor: 14 }
|
|
};
|
|
|
|
const DEVICE_DISPLAY = {
|
|
dexcom_g7: 'Dexcom G7',
|
|
dexcom_g6: 'Dexcom G6',
|
|
freestyle_libre_2: 'FreeStyle Libre 2',
|
|
freestyle_libre_3: 'FreeStyle Libre 3',
|
|
omnipod_5: 'Omnipod 5'
|
|
};
|
|
|
|
function getPayerConfig(payer) {
|
|
const p = payer.toLowerCase();
|
|
if (p.includes('medicare')) {
|
|
return { visit_renewal_days: 180, refill_window_days: 30 };
|
|
}
|
|
if (p.includes('medicaid')) {
|
|
return { visit_renewal_days: null, refill_window_days: 30 };
|
|
}
|
|
return { visit_renewal_days: null, refill_window_days: 30 };
|
|
}
|
|
|
|
function getWearDays(device_type, component) {
|
|
const device = DEVICE_RULES[device_type];
|
|
if (!device) return null;
|
|
return device[component] || null;
|
|
}
|
|
|
|
function addDays(dateStr, days) {
|
|
const d = new Date(dateStr + 'T00:00:00');
|
|
d.setDate(d.getDate() + days);
|
|
return d;
|
|
}
|
|
|
|
function today() {
|
|
const d = new Date();
|
|
d.setHours(0, 0, 0, 0);
|
|
return d;
|
|
}
|
|
|
|
function daysDiff(a, b) {
|
|
return Math.round((a - b) / 86400000);
|
|
}
|
|
|
|
function computePriority(flag, daysUntilEnd) {
|
|
if (flag === 'OUT_OF_COVERAGE') return 1000 + Math.abs(daysUntilEnd);
|
|
if (flag === 'VISIT_DUE') return 500 + Math.max(0, 90 - daysUntilEnd);
|
|
if (flag === 'REFILL_WINDOW') return 200 + Math.max(0, 30 - daysUntilEnd);
|
|
return 0;
|
|
}
|
|
|
|
function calculateCoverage(record) {
|
|
const wearDays = getWearDays(record.device_type, record.component);
|
|
if (wearDays === null) return null;
|
|
|
|
const now = today();
|
|
const totalWear = wearDays * parseInt(record.quantity, 10);
|
|
const coverageEnd = addDays(record.shipment_date, totalWear);
|
|
const daysUntilEnd = daysDiff(coverageEnd, now);
|
|
|
|
const payerCfg = getPayerConfig(record.payer);
|
|
const refillWindow = payerCfg.refill_window_days;
|
|
|
|
let daysUntilVisit = null;
|
|
if (payerCfg.visit_renewal_days) {
|
|
const visitDue = addDays(record.shipment_date, payerCfg.visit_renewal_days);
|
|
daysUntilVisit = daysDiff(visitDue, now);
|
|
}
|
|
|
|
let flag;
|
|
if (daysUntilEnd < 0) {
|
|
flag = 'OUT_OF_COVERAGE';
|
|
} else if (daysUntilVisit !== null && daysUntilVisit <= 30) {
|
|
flag = 'VISIT_DUE';
|
|
} else if (daysUntilEnd <= refillWindow) {
|
|
flag = 'REFILL_WINDOW';
|
|
} else {
|
|
flag = 'OK';
|
|
}
|
|
|
|
return {
|
|
...record,
|
|
flag,
|
|
daysUntilEnd,
|
|
daysUntilVisit,
|
|
priority: computePriority(flag, daysUntilEnd)
|
|
};
|
|
}
|
|
|
|
// ── CSV parser ──
|
|
function parseCSV(text) {
|
|
const lines = text.trim().split(/\r?\n/);
|
|
if (lines.length < 2) return [];
|
|
const headers = lines[0].split(',').map(h => h.trim().toLowerCase());
|
|
return lines.slice(1).filter(l => l.trim()).map(line => {
|
|
const cols = line.split(',').map(c => c.trim());
|
|
const row = {};
|
|
headers.forEach((h, i) => row[h] = cols[i] || '');
|
|
return row;
|
|
});
|
|
}
|
|
|
|
// ── Render helpers ──
|
|
function daysLabel(days) {
|
|
if (days < 0) return `<span class="days days-expired">Expired ${Math.abs(days)}d ago</span>`;
|
|
if (days <= 7) return `<span class="days days-critical">${days} days</span>`;
|
|
if (days <= 30) return `<span class="days days-warn">${days} days</span>`;
|
|
return `<span class="days days-ok">${days} days</span>`;
|
|
}
|
|
|
|
function flagBadge(flag) {
|
|
switch (flag) {
|
|
case 'OUT_OF_COVERAGE':
|
|
return `<span class="badge b-denied"><span>✕</span> Supply Lapsed</span>`;
|
|
case 'VISIT_DUE':
|
|
return `<span class="badge b-pa-req"><span>⚡</span> Renewal Due</span>`;
|
|
case 'REFILL_WINDOW':
|
|
return `<span class="badge b-expiring"><span>⚠</span> Resupply Ready</span>`;
|
|
default:
|
|
return `<span class="badge b-active"><span>✓</span> Active</span>`;
|
|
}
|
|
}
|
|
|
|
function actionBtn(flag) {
|
|
switch (flag) {
|
|
case 'OUT_OF_COVERAGE':
|
|
return `<button class="act act-copper">Contact Prescriber</button>`;
|
|
case 'VISIT_DUE':
|
|
return `<button class="act act-copper">Request Renewal →</button>`;
|
|
case 'REFILL_WINDOW':
|
|
return `<button class="act">Initiate Resupply</button>`;
|
|
default:
|
|
return `<button class="act">View</button>`;
|
|
}
|
|
}
|
|
|
|
function scoreClass(priority) {
|
|
if (priority >= 500) return 'score-hi';
|
|
if (priority >= 200) return 'score-mid';
|
|
return 'score-lo';
|
|
}
|
|
|
|
function isHighPriority(flag) {
|
|
return flag === 'OUT_OF_COVERAGE' || flag === 'VISIT_DUE';
|
|
}
|
|
|
|
function renderRows(results) {
|
|
const tbody = document.getElementById('worklist-tbody');
|
|
tbody.innerHTML = results.map((r, i) => {
|
|
const priorityRow = isHighPriority(r.flag) ? 'p-row' : '';
|
|
const topTag = i === 0 ? '<div class="top-tag">★ TOP PRIORITY</div>' : '';
|
|
const device = DEVICE_DISPLAY[r.device_type] || r.device_type;
|
|
return `
|
|
<tr class="${priorityRow}">
|
|
<td>
|
|
<div class="pid">${r.patient_id}</div>
|
|
${topTag}
|
|
</td>
|
|
<td><span class="device">${device}</span></td>
|
|
<td><span class="payer">${r.payer}</span></td>
|
|
<td>${daysLabel(r.daysUntilEnd)}</td>
|
|
<td>${flagBadge(r.flag)}</td>
|
|
<td><span class="score ${scoreClass(r.priority)}">${r.priority}</span></td>
|
|
<td>${actionBtn(r.flag)}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
function updateStats(results) {
|
|
const ooc = results.filter(r => r.flag === 'OUT_OF_COVERAGE').length;
|
|
const visitDue = results.filter(r => r.flag === 'VISIT_DUE').length;
|
|
const refill = results.filter(r => r.flag === 'REFILL_WINDOW').length;
|
|
const ok = results.filter(r => r.flag === 'OK').length;
|
|
const urgent = ooc + visitDue;
|
|
|
|
document.getElementById('stat-urgent-value').textContent = urgent;
|
|
document.getElementById('stat-urgent-sub').textContent =
|
|
`${ooc} out of coverage · ${visitDue} visit${visitDue !== 1 ? 's' : ''} due`;
|
|
document.getElementById('stat-refill-value').textContent = refill;
|
|
document.getElementById('stat-ok-value').textContent = ok;
|
|
|
|
document.getElementById('badge-ooc').textContent = ooc;
|
|
document.getElementById('badge-visit').textContent = visitDue;
|
|
document.getElementById('badge-expiring').textContent = refill;
|
|
|
|
const today = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
document.getElementById('table-meta').textContent =
|
|
`${results.length} patients · sorted by priority score · ${today}`;
|
|
document.getElementById('footer-count').textContent =
|
|
`${results.length} patients · all results shown`;
|
|
}
|
|
|
|
function showToast(msg) {
|
|
const t = document.getElementById('import-toast');
|
|
t.textContent = msg;
|
|
t.classList.add('show');
|
|
setTimeout(() => t.classList.remove('show'), 3500);
|
|
}
|
|
|
|
// ── Main import handler ──
|
|
function handleCSVImport(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
const rows = parseCSV(e.target.result);
|
|
const results = [];
|
|
const skipped = [];
|
|
|
|
for (const row of rows) {
|
|
const result = calculateCoverage(row);
|
|
if (result) {
|
|
results.push(result);
|
|
} else {
|
|
skipped.push(row.patient_id || '?');
|
|
}
|
|
}
|
|
|
|
results.sort((a, b) => b.priority - a.priority);
|
|
|
|
renderRows(results);
|
|
updateStats(results);
|
|
|
|
const label = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
document.getElementById('import-label').textContent = `Import: ${file.name} · ${label}`;
|
|
|
|
let msg = `Loaded ${results.length} patient${results.length !== 1 ? 's' : ''} from ${file.name}`;
|
|
if (skipped.length) msg += ` · ${skipped.length} skipped (unknown device)`;
|
|
showToast(msg);
|
|
};
|
|
|
|
reader.readAsText(file);
|
|
event.target.value = '';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|