rdcm-rule-checker / RBS_Rule_Checker_UI.html
AhsanIkram231's picture
Upload 8 files
d6913a4 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>RDCM β€” Rule Checker</title>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@400;600;800&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
/* ═══════════════════════════════════════════
TOKENS
═══════════════════════════════════════════ */
:root {
--bg:#0d0f14; --surface:#13161e; --surface2:#1a1e28; --border:#272c3a;
--accent:#00e5b4; --accent2:#3b82f6; --danger:#ff4f6a;
--success:#00e5b4; --warning:#f59e0b; --text:#e8eaf0; --muted:#6b7280;
--mono:'IBM Plex Mono',monospace; --display:'Syne',sans-serif; --body:'DM Sans',sans-serif;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
html,body{width:100%;max-width:100%;overflow-x:hidden;background:var(--bg);color:var(--text);font-family:var(--body);min-height:100vh;}
/* ═══════════════════════════════════════════
GRID BACKGROUND
═══════════════════════════════════════════ */
body::before{
content:'';position:fixed;inset:0;z-index:0;
background-image:linear-gradient(rgba(0,229,180,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,229,180,.03) 1px,transparent 1px);
background-size:40px 40px;pointer-events:none;
}
/* ═══════════════════════════════════════════
PAGE SYSTEM
═══════════════════════════════════════════ */
.page{position:relative;z-index:1;min-height:100vh;display:none;}
.page.active{display:block;animation:pageIn .45s cubic-bezier(.22,1,.36,1);}
@keyframes pageIn{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}
/* ═══════════════════════════════════════════
β–‘β–‘β–‘ PAGE 0 β€” WELCOME β–‘β–‘β–‘
═══════════════════════════════════════════ */
#welcomePage{
display:flex;flex-direction:column;align-items:center;justify-content:center;
padding:40px 20px;gap:0;
}
#welcomePage.active{display:flex;}
/* animated orb */
.orb-wrap{position:relative;width:160px;height:160px;margin-bottom:36px;}
.orb{
width:160px;height:160px;border-radius:50%;
background:radial-gradient(circle at 38% 38%,#00e5b4 0%,#3b82f6 55%,#0d0f14 100%);
box-shadow:0 0 60px rgba(0,229,180,.35),0 0 120px rgba(59,130,246,.2);
animation:orbFloat 5s ease-in-out infinite;
}
@keyframes orbFloat{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-12px) scale(1.04)}}
.orb-ring{
position:absolute;inset:-16px;border-radius:50%;
border:1.5px solid rgba(0,229,180,.2);
animation:ringPulse 3s ease-in-out infinite;
}
.orb-ring2{
position:absolute;inset:-34px;border-radius:50%;
border:1px solid rgba(59,130,246,.12);
animation:ringPulse 3s ease-in-out infinite .6s;
}
@keyframes ringPulse{0%,100%{transform:scale(1);opacity:.7}50%{transform:scale(1.06);opacity:1}}
/* floating particles */
.particles{position:fixed;inset:0;pointer-events:none;z-index:0;overflow:hidden;}
.particle{
position:absolute;border-radius:50%;
animation:particleDrift linear infinite;
opacity:0;
}
@keyframes particleDrift{
0%{transform:translateY(100vh) scale(0);opacity:0}
10%{opacity:1}
90%{opacity:.4}
100%{transform:translateY(-10vh) scale(1.5);opacity:0}
}
/* welcome text */
.welcome-tag{
font-family:var(--mono);font-size:11px;letter-spacing:2px;text-transform:uppercase;
color:var(--accent);background:rgba(0,229,180,.08);border:1px solid rgba(0,229,180,.2);
padding:5px 16px;border-radius:100px;margin-bottom:22px;
animation:fadeUp .6s .1s both;
}
.welcome-h1{
font-family:var(--display);font-size:clamp(28px,5vw,52px);font-weight:800;
letter-spacing:-1.5px;text-align:center;line-height:1.15;
animation:fadeUp .6s .2s both;
}
.welcome-h1 span{
background:linear-gradient(120deg,var(--accent),var(--accent2));
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
}
.welcome-sub{
font-size:15px;color:var(--muted);text-align:center;max-width:480px;line-height:1.7;
margin-top:14px;margin-bottom:48px;
animation:fadeUp .6s .3s both;
}
@keyframes fadeUp{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}
/* mode cards */
.mode-cards{
display:grid;grid-template-columns:1fr 1fr;gap:20px;width:100%;max-width:680px;
animation:fadeUp .6s .4s both;
}
@media(max-width:560px){.mode-cards{grid-template-columns:1fr;max-width:340px;}}
.mode-card{
background:var(--surface);border:1.5px solid var(--border);border-radius:20px;
padding:32px 28px;cursor:pointer;transition:all .3s cubic-bezier(.22,1,.36,1);
display:flex;flex-direction:column;align-items:flex-start;gap:14px;
position:relative;overflow:hidden;
}
.mode-card::before{
content:'';position:absolute;inset:0;border-radius:20px;opacity:0;transition:opacity .3s;
}
.mode-card.single::before{background:radial-gradient(circle at 30% 30%,rgba(0,229,180,.08),transparent 70%);}
.mode-card.multi::before{background:radial-gradient(circle at 30% 30%,rgba(59,130,246,.08),transparent 70%);}
.mode-card:hover{transform:translateY(-5px);}
.mode-card.single:hover{border-color:rgba(0,229,180,.5);box-shadow:0 12px 40px rgba(0,229,180,.15);}
.mode-card.multi:hover{border-color:rgba(59,130,246,.5);box-shadow:0 12px 40px rgba(59,130,246,.15);}
.mode-card:hover::before{opacity:1;}
.mode-icon{
width:52px;height:52px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:24px;
}
.mode-card.single .mode-icon{background:rgba(0,229,180,.1);border:1px solid rgba(0,229,180,.2);}
.mode-card.multi .mode-icon{background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.2);}
.mode-title{font-family:var(--display);font-size:17px;font-weight:700;letter-spacing:-.3px;}
.mode-card.single .mode-title{color:var(--accent);}
.mode-card.multi .mode-title{color:var(--accent2);}
.mode-desc{font-size:12px;color:var(--muted);line-height:1.6;}
.mode-arrow{
margin-top:6px;font-size:18px;transition:transform .25s;
align-self:flex-end;
}
.mode-card:hover .mode-arrow{transform:translateX(5px);}
/* stats strip */
.stats-strip{
display:flex;gap:32px;flex-wrap:wrap;justify-content:center;
margin-top:52px;padding-top:28px;border-top:1px solid var(--border);width:100%;max-width:680px;
animation:fadeUp .6s .55s both;
}
.stat-item{text-align:center;}
.stat-item .num{font-family:var(--mono);font-size:22px;font-weight:700;color:var(--accent);}
.stat-item .lbl{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;margin-top:3px;}
/* ═══════════════════════════════════════════
SHARED HEADER (pages 1 & 2)
═══════════════════════════════════════════ */
.app{width:100%;max-width:1400px;margin:0 auto;padding:0 20px 60px;}
header{
display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;
padding:22px 0 24px;border-bottom:1px solid var(--border);margin-bottom:28px;
}
.logo{display:flex;align-items:center;gap:12px;}
.logo-icon{
width:72px;height:36px;flex-shrink:0;
background:linear-gradient(135deg,var(--accent),var(--accent2));
border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;color:#0d0f14;font-family:var(--mono);letter-spacing:.5px;
}
.logo-text h1{font-family:var(--display);font-size:17px;font-weight:800;letter-spacing:-.3px;}
.header-right{display:flex;align-items:center;gap:10px;}
.header-badge{
display:flex;align-items:center;gap:8px;
background:rgba(0,229,180,.07);border:1px solid rgba(0,229,180,.2);
padding:6px 13px;border-radius:100px;font-family:var(--mono);font-size:11px;color:var(--accent);white-space:nowrap;
}
.pulse{width:6px;height:6px;border-radius:50%;background:var(--accent);animation:pulse 2s infinite;flex-shrink:0;}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.8)}}
.btn-back{
background:var(--surface2);border:1px solid var(--border);color:var(--muted);
padding:7px 14px;border-radius:8px;cursor:pointer;font-family:var(--mono);font-size:11px;
transition:all .2s;display:flex;align-items:center;gap:6px;
}
.btn-back:hover{border-color:var(--accent);color:var(--accent);}
/* ═══════════════════════════════════════════
SHARED COMPONENTS
═══════════════════════════════════════════ */
.panel{background:var(--surface);border:1px solid var(--border);border-radius:14px;overflow:hidden;width:100%;min-width:0;}
.panel-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:14px 20px;border-bottom:1px solid var(--border);background:var(--surface2);}
.panel-title{display:flex;align-items:center;gap:8px;font-family:var(--display);font-size:13px;font-weight:600;letter-spacing:.3px;}
.panel-title .icon{font-size:15px;flex-shrink:0;}
.panel-badge{font-family:var(--mono);font-size:11px;color:var(--muted);white-space:nowrap;}
.panel-body{padding:20px;}
.btn-primary{
padding:11px 24px;background:linear-gradient(135deg,var(--accent),#00c49a);
border:none;border-radius:8px;color:#0d0f14;font-family:var(--display);font-size:13px;font-weight:700;cursor:pointer;
letter-spacing:.3px;transition:all .2s;display:inline-flex;align-items:center;gap:8px;white-space:nowrap;
}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 24px rgba(0,229,180,.25);}
.btn-primary:active{transform:translateY(0);}
.btn-primary:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none;}
.btn-secondary{
background:var(--surface2);border:1px solid var(--border);color:var(--muted);
padding:11px 18px;border-radius:8px;cursor:pointer;font-size:12px;font-family:var(--mono);
transition:all .2s;display:inline-flex;align-items:center;gap:6px;white-space:nowrap;
}
.btn-secondary:hover{border-color:var(--accent2);color:var(--text);}
.alert-box{padding:12px 14px;border-radius:8px;background:rgba(245,158,11,.07);border:1px solid rgba(245,158,11,.3);color:var(--warning);font-size:12px;display:flex;gap:8px;align-items:flex-start;}
.alert-success{background:rgba(0,229,180,.07);border-color:rgba(0,229,180,.3);color:var(--success);}
.alert-danger{background:rgba(255,79,106,.07);border-color:rgba(255,79,106,.3);color:var(--danger);}
/* loading */
.loading-banner{display:flex;align-items:center;gap:12px;padding:16px;border-radius:10px;background:rgba(59,130,246,.05);border:1px solid rgba(59,130,246,.2);margin-bottom:16px;}
.loading-dots span{display:inline-block;width:7px;height:7px;background:var(--accent2);border-radius:50%;margin:0 2px;animation:bounce 1.2s infinite;}
.loading-dots span:nth-child(2){animation-delay:.2s;}
.loading-dots span:nth-child(3){animation-delay:.4s;}
@keyframes bounce{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-6px)}}
/* spinner */
.spinner-wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:50px;gap:14px;}
.spinner{width:32px;height:32px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite;}
@keyframes spin{to{transform:rotate(360deg)}}
.spinner-wrap p{font-size:12px;color:var(--muted);font-family:var(--mono);}
.empty-state{text-align:center;padding:50px 20px;color:var(--muted);}
.empty-state .big{font-size:42px;margin-bottom:12px;}
.empty-state p{font-size:12px;}
/* drop zone */
.drop-zone{border:2px dashed var(--border);border-radius:12px;padding:36px 20px;text-align:center;cursor:pointer;transition:all .2s;background:rgba(0,229,180,.02);position:relative;}
.drop-zone:hover,.drop-zone.drag-over{border-color:var(--accent);background:rgba(0,229,180,.05);}
.drop-zone input{position:absolute;inset:0;opacity:0;cursor:pointer;}
.drop-icon{font-size:32px;margin-bottom:10px;}
.drop-title{font-family:var(--display);font-size:14px;font-weight:600;margin-bottom:5px;}
.drop-sub{font-size:11px;color:var(--muted);}
.drop-badge{display:inline-block;margin-top:10px;background:var(--surface2);border:1px solid var(--border);padding:3px 10px;border-radius:6px;font-family:var(--mono);font-size:10px;color:var(--muted);}
/* scrollbars */
::-webkit-scrollbar{width:5px;height:5px;}
::-webkit-scrollbar-track{background:var(--bg);}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px;}
/* ═══════════════════════════════════════════
β–‘β–‘β–‘ PAGE 1 β€” SINGLE CLAIM β–‘β–‘β–‘
═══════════════════════════════════════════ */
.single-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;align-items:start;}
@media(max-width:860px){.single-grid{grid-template-columns:1fr;}}
/* JSON editor */
.json-editor-wrap{position:relative;}
.json-textarea{
width:100%;min-height:420px;background:var(--surface2);border:1px solid var(--border);
border-radius:10px;color:var(--accent);font-family:var(--mono);font-size:11.5px;
padding:16px;resize:vertical;outline:none;line-height:1.7;transition:border .2s;
caret-color:var(--text);
}
.json-textarea:focus{border-color:var(--accent2);}
.json-textarea.error{border-color:var(--danger);}
.json-error{color:var(--danger);font-family:var(--mono);font-size:10.5px;margin-top:6px;min-height:16px;}
.json-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:10px;flex-wrap:wrap;}
.json-label{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);flex:1;}
.btn-tiny{
background:var(--surface2);border:1px solid var(--border);color:var(--muted);
padding:5px 11px;border-radius:6px;cursor:pointer;font-family:var(--mono);font-size:10px;
transition:all .15s;
}
.btn-tiny:hover{border-color:var(--accent2);color:var(--text);}
/* claim field pills */
.field-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;}
.field-item{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:8px 11px;}
.field-key{font-family:var(--mono);font-size:9.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;}
.field-val{font-size:12px;color:var(--text);margin-top:2px;font-weight:500;word-break:break-all;}
/* verdict */
.verdict-card{border-radius:12px;padding:18px;margin-bottom:16px;display:flex;align-items:center;gap:14px;animation:fadeIn .35s ease;}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.verdict-card.accepted{background:rgba(0,229,180,.08);border:1px solid rgba(0,229,180,.3);}
.verdict-card.rejected{background:rgba(255,79,106,.08);border:1px solid rgba(255,79,106,.3);}
.verdict-icon{font-size:32px;flex-shrink:0;}
.verdict-text h2{font-family:var(--display);font-size:18px;font-weight:800;}
.verdict-text p{font-size:11px;color:var(--muted);margin-top:3px;}
.verdict-card.accepted .verdict-text h2{color:var(--success);}
.verdict-card.rejected .verdict-text h2{color:var(--danger);}
.rules-list{display:flex;flex-direction:column;gap:7px;}
.rule-item{background:var(--surface2);border:1px solid var(--border);border-left:3px solid var(--danger);border-radius:8px;padding:11px 13px;animation:slideIn .3s ease forwards;opacity:0;}
@keyframes slideIn{to{opacity:1;transform:translateX(0)}from{opacity:0;transform:translateX(-10px)}}
.rule-name{font-family:var(--mono);font-size:10.5px;color:var(--danger);margin-bottom:4px;display:flex;align-items:center;gap:5px;}
.rule-name::before{content:'';width:4px;height:4px;border-radius:50%;background:var(--danger);flex-shrink:0;}
.rule-msg{font-size:11.5px;color:var(--text);line-height:1.5;}
.mini-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:14px;}
.mini-stat{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:10px 12px;text-align:center;}
.mini-stat .n{font-family:var(--mono);font-size:18px;font-weight:700;}
.mini-stat .l{font-size:9.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;margin-top:2px;}
/* ═══════════════════════════════════════════
β–‘β–‘β–‘ PAGE 2 β€” MULTI CLAIM β–‘β–‘β–‘
═══════════════════════════════════════════ */
.controls-row{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:20px;}
.file-badge{display:flex;align-items:center;gap:10px;background:var(--surface);border:1px solid var(--border);padding:10px 16px;border-radius:10px;min-width:0;}
.file-badge .file-icon{font-size:20px;flex-shrink:0;}
.file-badge .file-info{min-width:0;}
.file-badge .file-name{font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:260px;}
.file-badge .file-meta{font-size:11px;color:var(--muted);font-family:var(--mono);margin-top:2px;}
.auto-loaded-badge{background:rgba(0,229,180,.12);border:1px solid rgba(0,229,180,.3);color:var(--accent);font-family:var(--mono);font-size:10px;padding:3px 10px;border-radius:100px;text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;flex-shrink:0;}
.controls-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.btn-upload{background:var(--surface2);border:1px solid var(--border);color:var(--muted);padding:10px 16px;border-radius:8px;cursor:pointer;font-size:12px;font-family:var(--mono);transition:all .2s;position:relative;overflow:hidden;display:inline-flex;align-items:center;gap:6px;}
.btn-upload:hover{border-color:var(--accent2);color:var(--text);}
.btn-upload input{position:absolute;inset:0;opacity:0;cursor:pointer;}
/* preview */
.preview-panel{margin-bottom:20px;}
.preview-wrap{overflow-x:auto;overflow-y:auto;max-height:240px;}
.preview-wrap::-webkit-scrollbar{width:5px;height:5px;}
.preview-wrap::-webkit-scrollbar-track{background:var(--surface2);}
.preview-wrap::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px;}
.preview-wrap table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:11px;}
.preview-wrap thead th{background:var(--surface2);padding:8px 12px;text-align:left;color:var(--accent);font-size:10px;text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;border-bottom:1px solid var(--border);position:sticky;top:0;z-index:2;}
.preview-wrap tbody tr{border-bottom:1px solid rgba(255,255,255,.04);}
.preview-wrap tbody tr:hover{background:rgba(255,255,255,.03);}
.preview-wrap tbody td{padding:7px 12px;color:var(--muted);white-space:nowrap;max-width:140px;overflow:hidden;text-overflow:ellipsis;}
.preview-wrap tbody td.primary{color:var(--text);}
/* progress */
.progress-wrap{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:20px;margin-bottom:20px;}
.progress-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;}
.progress-label{font-family:var(--mono);font-size:12px;color:var(--accent);}
.progress-pct{font-family:var(--mono);font-size:12px;color:var(--muted);}
.progress-track{width:100%;height:6px;background:var(--border);border-radius:3px;overflow:hidden;}
.progress-bar{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));border-radius:3px;transition:width .3s ease;width:0%;}
.progress-stats{display:flex;gap:20px;margin-top:10px;flex-wrap:wrap;}
.progress-stat{font-family:var(--mono);font-size:11px;color:var(--muted);}
.progress-stat span{color:var(--text);font-weight:600;}
/* summary */
.summary-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px;}
@media(max-width:700px){.summary-grid{grid-template-columns:repeat(2,1fr);}}
.sum-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px 16px;}
.sum-card.accepted-card{border-color:rgba(0,229,180,.3);background:rgba(0,229,180,.05);}
.sum-card.rejected-card{border-color:rgba(255,79,106,.3);background:rgba(255,79,106,.05);}
.sum-num{font-family:var(--mono);font-size:24px;font-weight:700;}
.sum-num.green{color:var(--success);}
.sum-num.red{color:var(--danger);}
.sum-num.blue{color:var(--accent2);}
.sum-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;margin-top:3px;font-family:var(--mono);}
/* filter bar */
.filter-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:12px;}
.filter-btn{padding:6px 13px;border-radius:7px;background:var(--surface2);border:1px solid var(--border);font-family:var(--mono);font-size:11px;cursor:pointer;transition:all .15s;color:var(--muted);}
.filter-btn:hover{border-color:var(--accent2);color:var(--text);}
.filter-btn.active{background:rgba(0,229,180,.1);border-color:var(--accent);color:var(--accent);}
.filter-btn.danger.active{background:rgba(255,79,106,.1);border-color:var(--danger);color:var(--danger);}
.search-input{background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:6px 12px;border-radius:7px;font-family:var(--mono);font-size:11px;outline:none;width:180px;transition:border .2s;}
.search-input:focus{border-color:var(--accent2);}
.search-input::placeholder{color:var(--muted);}
/* results table */
.results-wrap{overflow-x:auto;overflow-y:auto;max-height:500px;border-radius:8px;border:1px solid var(--border);}
.results-wrap::-webkit-scrollbar{width:5px;height:5px;}
.results-wrap::-webkit-scrollbar-track{background:var(--surface2);}
.results-wrap::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px;}
table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:11px;}
thead th{background:var(--surface2);padding:9px 12px;text-align:left;color:var(--accent);font-size:10px;text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;border-bottom:1px solid var(--border);position:sticky;top:0;z-index:2;}
tbody tr{border-bottom:1px solid rgba(255,255,255,.04);transition:background .1s;cursor:pointer;}
tbody tr:hover{background:rgba(255,255,255,.04);}
tbody td{padding:9px 12px;color:var(--muted);white-space:nowrap;max-width:160px;overflow:hidden;text-overflow:ellipsis;}
tbody td.primary{color:var(--text);font-weight:500;}
.status-pill{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:100px;font-size:10px;font-weight:700;letter-spacing:.5px;}
.status-pill.accepted{background:rgba(0,229,180,.12);color:var(--success);border:1px solid rgba(0,229,180,.25);}
.status-pill.rejected{background:rgba(255,79,106,.12);color:var(--danger);border:1px solid rgba(255,79,106,.25);}
/* drawer */
tr.drawer-row td{padding:0;}
.drawer-inner{padding:14px 20px;background:rgba(255,79,106,.03);border-bottom:1px solid rgba(255,79,106,.15);animation:fadeIn .2s ease;}
.drawer-rules{display:flex;flex-direction:column;gap:6px;}
.drawer-rule{background:var(--surface2);border:1px solid var(--border);border-left:3px solid var(--danger);border-radius:6px;padding:9px 12px;}
.drawer-rule-name{font-family:var(--mono);font-size:10px;color:var(--danger);margin-bottom:3px;display:flex;align-items:center;gap:5px;}
.drawer-rule-name::before{content:'';width:4px;height:4px;border-radius:50%;background:var(--danger);flex-shrink:0;}
.drawer-rule-msg{font-size:11px;color:var(--text);line-height:1.5;white-space:normal;}
/* ── Form builder ── */
.form-section{border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-bottom:10px;}
.sec-hdr{display:flex;justify-content:space-between;align-items:center;padding:9px 14px;background:var(--surface2);cursor:pointer;font-family:var(--mono);font-size:11px;color:var(--muted);user-select:none;}
.sec-hdr:hover{color:var(--text);}
.sec-body{padding:12px;}
.sec-body.collapsed{display:none;}
.chev{font-size:9px;transition:transform .2s;}
.chev.up{transform:rotate(180deg);}
.fgrid{display:grid;grid-template-columns:1fr 1fr;gap:7px;}
.ff{display:flex;flex-direction:column;gap:2px;}
.ff.wide{grid-column:span 2;}
.ff label{font-family:var(--mono);font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.7px;}
.ff input,.ff select,.ff textarea{background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:5px 8px;font-size:11.5px;font-family:var(--mono);outline:none;transition:border .15s;width:100%;}
.ff input:focus,.ff select:focus{border-color:var(--accent2);}
.ff select option{background:var(--surface2);}
/* DX rows */
.dx-row{display:flex;gap:6px;align-items:center;margin-bottom:6px;}
.dx-idx{font-family:var(--mono);font-size:10px;color:var(--accent);background:rgba(0,229,180,.1);border:1px solid rgba(0,229,180,.2);border-radius:4px;padding:2px 7px;flex-shrink:0;min-width:30px;text-align:center;}
.dx-inp{flex:1;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:5px 8px;font-size:11.5px;font-family:var(--mono);outline:none;transition:border .15s;}
.dx-inp:focus{border-color:var(--accent2);}
.btn-x{background:var(--surface2);border:1px solid var(--border);color:var(--muted);width:24px;height:24px;border-radius:6px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:11px;flex-shrink:0;transition:all .15s;}
.btn-x:hover{border-color:var(--danger);color:var(--danger);}
/* CPT lines */
.cpt-line{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:11px;margin-bottom:8px;}
.cpt-lhdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:9px;}
.cpt-lnum{font-family:var(--mono);font-size:10px;color:var(--accent2);background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.2);border-radius:4px;padding:2px 8px;}
.cpt-lg{display:grid;grid-template-columns:2fr 1fr 1fr 1fr 1.2fr;gap:6px;margin-bottom:8px;}
.cpt-dg{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;}
.cf{display:flex;flex-direction:column;gap:2px;}
.cf label{font-family:var(--mono);font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;}
.cf input{background:var(--bg);border:1px solid var(--border);border-radius:5px;color:var(--text);padding:5px 7px;font-size:11px;font-family:var(--mono);outline:none;transition:border .15s;width:100%;}
.cf input:focus{border-color:var(--accent2);}
.dx-ptr-lbl{font-family:var(--mono);font-size:9px;color:var(--muted);margin-bottom:5px;letter-spacing:.4px;text-transform:uppercase;}
</style>
</head>
<body>
<!-- ════════════════════════════════════
PARTICLES
════════════════════════════════════ -->
<div class="particles" id="particles"></div>
<!-- ════════════════════════════════════
PAGE 0 β€” WELCOME
════════════════════════════════════ -->
<div id="welcomePage" class="page active">
<div class="orb-wrap">
<div class="orb"></div>
<div class="orb-ring"></div>
<div class="orb-ring2"></div>
</div>
<h1 class="welcome-h1">Rejected / Denied<br><span>Claim Model</span></h1>
<p class="welcome-sub">
Validate insurance claims.<br>
Choose your workflow below to get started.
</p>
<div class="mode-cards">
<div class="mode-card single" onclick="goTo('singlePage')">
<div class="mode-icon">πŸ“‹</div>
<div>
<div class="mode-title">Single Claim</div>
<div class="mode-desc">Paste or edit a JSON claim and instantly get a detailed rule-by-rule breakdown.</div>
</div>
<div class="mode-arrow">β†’</div>
</div>
<div class="mode-card multi" onclick="goTo('multiPage')">
<div class="mode-icon">πŸ“Š</div>
<div>
<div class="mode-title">Multiple Claims</div>
<div class="mode-desc">Upload an Excel file and batch-process every claim in parallel with a live progress tracker.</div>
</div>
<div class="mode-arrow">β†’</div>
</div>
</div>
<div class="stats-strip">
<div class="stat-item"><div class="num">274+</div><div class="lbl">Rules can handle</div></div>
<div class="stat-item"><div class="num">4Γ—</div><div class="lbl">Parallel Speed</div></div>
<div class="stat-item"><div class="num">Real-time</div><div class="lbl">Results</div></div>
</div>
</div>
<!-- ════════════════════════════════════
PAGE 1 β€” SINGLE CLAIM
════════════════════════════════════ -->
<div id="singlePage" class="page">
<div class="app">
<header>
<div class="logo">
<div class="logo-icon">RDCM</div>
<div class="logo-text"><h1>Single Claim</h1></div>
</div>
<div class="header-right">
<div class="header-badge"><div class="pulse"></div><span id="s_statusBadge">Ready</span></div>
<button class="btn-back" onclick="goTo('welcomePage')">← Home</button>
</div>
</header>
<div class="single-grid">
<!-- LEFT: JSON Input -->
<div>
<div class="panel">
<div class="panel-header">
<div class="panel-title"><span class="icon">✏️</span> Claim JSON</div>
<div style="display:flex;gap:6px;">
<label class="btn-tiny" style="cursor:pointer;margin:0;">πŸ“‚ Upload
<input type="file" id="s_fileInput" accept=".json,application/json" style="display:none;" onchange="sUploadJSON(event)"/>
</label>
<button class="btn-tiny" onclick="sLoadDefault()">β†Ί Sample</button>
<button class="btn-tiny" onclick="sClear()">βœ• Clear</button>
</div>
</div>
<div class="panel-body">
<div style="font-family:var(--mono);font-size:10px;color:var(--muted);margin-bottom:10px;line-height:1.7;">
<strong style="color:var(--text);">Header fields:</strong>
ClaimId Β· FirstName Β· LastName Β· DateOfBirth Β· Gender Β· Address Β· City Β· State Β· Zip Β·
ClaimPriIns Β· BillAs Β· PolicyNumber Β· DOS Β· POS Β· AllDxCodes Β·
AttendingPhysician Β· BillingPhysician<br/>
<strong style="color:var(--accent);">cpt_lines[]:</strong>
CPTCode Β· Modifier1–4 Β· ServiceUnits Β· TotalCharges Β· DXPointer1–4
</div>
<textarea id="s_jsonInput"
style="width:100%;height:460px;background:var(--surface2);border:1px solid var(--border);color:var(--text);font-family:var(--mono);font-size:11px;padding:12px;border-radius:6px;resize:vertical;outline:none;line-height:1.5;"
placeholder='{"ClaimId":"TC-001", "FirstName":"...", "cpt_lines":[...]}'
spellcheck="false"></textarea>
<button class="btn-primary" style="width:100%;margin-top:10px;justify-content:center;" id="s_runBtn" onclick="sRunCheck()">
<span>⚑</span> Check This Claim
</button>
</div>
</div>
</div>
<!-- RIGHT: Result -->
<div>
<div class="panel" style="min-height:200px;">
<div class="panel-header">
<div class="panel-title"><span class="icon">πŸ”¬</span> Result</div>
<span class="panel-badge" id="s_claimIdBadge">β€”</span>
</div>
<div class="panel-body" id="s_resultBody">
<div class="empty-state">
<div class="big">πŸ“‹</div>
<p>Fill in the claim on the left and click<br><strong>Check This Claim</strong></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════
PAGE 2 β€” MULTI CLAIM
════════════════════════════════════ -->
<div id="multiPage" class="page">
<div class="app">
<header>
<div class="logo">
<div class="logo-icon">RDCM</div>
<div class="logo-text"><h1>Batch Claim</h1></div>
</div>
<div class="header-right">
<div class="header-badge"><div class="pulse"></div><span id="m_statusBadge">Loading…</span></div>
<button class="btn-back" onclick="goTo('welcomePage')">← Home</button>
</div>
</header>
<!-- Controls -->
<div id="m_controlsRow" class="controls-row" style="display:none;">
<div class="file-badge">
<div class="file-icon">πŸ“—</div>
<div class="file-info">
<div class="file-name" id="m_ctrlFilename">β€”</div>
<div class="file-meta" id="m_ctrlMeta">β€”</div>
</div>
<span class="auto-loaded-badge" id="m_ctrlBadge">βœ“ Auto-loaded</span>
</div>
<div class="controls-right">
<button class="btn-upload">πŸ“€ Load Different File<input type="file" id="m_fileInput" accept=".xlsx,.xls,.csv"/></button>
<button class="btn-primary" id="m_runAllBtn" onclick="mRunAll()" disabled><span>⚑</span> Check All Claims</button>
</div>
</div>
<!-- Init loading -->
<div id="m_initLoading">
<div class="loading-banner">
<div class="loading-dots"><span></span><span></span><span></span></div>
<span style="font-family:var(--mono);font-size:12px;color:var(--muted);">Loading claims_to_check.xlsx…</span>
</div>
</div>
<!-- Fallback upload -->
<div id="m_fallback" style="display:none;"></div>
<!-- File Preview -->
<div id="m_previewPanel" class="preview-panel" style="display:none;">
<div class="panel">
<div class="panel-header">
<div class="panel-title"><span class="icon">πŸ‘οΈ</span> File Preview</div>
<span class="panel-badge" id="m_previewBadge">β€”</span>
</div>
<div class="panel-body" style="padding:0;"><div class="preview-wrap" id="m_previewArea"></div></div>
</div>
</div>
<!-- Progress -->
<div id="m_progressSection" style="display:none;">
<div class="progress-wrap">
<div class="progress-header">
<span class="progress-label">βš™οΈ Processing claims…</span>
<span class="progress-pct" id="m_pct">0%</span>
</div>
<div class="progress-track"><div class="progress-bar" id="m_bar"></div></div>
<div class="progress-stats">
<div class="progress-stat">Processed: <span id="m_pDone">0</span></div>
<div class="progress-stat">Accepted: <span id="m_pAcc" style="color:var(--success)">0</span></div>
<div class="progress-stat">Rejected: <span id="m_pRej" style="color:var(--danger)">0</span></div>
<div class="progress-stat">Remaining: <span id="m_pRem">β€”</span></div>
</div>
</div>
</div>
<!-- Summary -->
<div id="m_summarySection" style="display:none;">
<div class="summary-grid">
<div class="sum-card"><div class="sum-num blue" id="m_sTotal">0</div><div class="sum-label">Total Claims</div></div>
<div class="sum-card accepted-card"><div class="sum-num green" id="m_sAcc">0</div><div class="sum-label">Accepted</div></div>
<div class="sum-card rejected-card"><div class="sum-num red" id="m_sRej">0</div><div class="sum-label">Rejected</div></div>
<div class="sum-card"><div class="sum-num" id="m_sPct" style="color:var(--warning);">0%</div><div class="sum-label">Rejection Rate</div></div>
</div>
</div>
<!-- Results panel -->
<div id="m_resultsPanel" style="display:none;">
<div class="panel">
<div class="panel-header">
<div class="panel-title"><span class="icon">πŸ”¬</span> All Claims β€” Rule Check Results</div>
<span class="panel-badge" id="m_resultsBadge">β€”</span>
</div>
<div class="panel-body">
<div class="filter-bar">
<button class="filter-btn active" id="m_fAll" onclick="mSetFilter('all')">All</button>
<button class="filter-btn" id="m_fAcc" onclick="mSetFilter('accepted')">βœ… Accepted</button>
<button class="filter-btn danger" id="m_fRej" onclick="mSetFilter('rejected')">❌ Rejected</button>
<input class="search-input" id="m_search" placeholder="Search Claim ID…" oninput="mRenderTable()"/>
</div>
<div class="results-wrap"><div id="m_tableArea"><div class="empty-state"><div class="big">⏳</div><p>Processing…</p></div></div></div>
</div>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════
PAGE 3 β€” CLAIM DETAIL (from batch)
════════════════════════════════════ -->
<div id="claimDetailPage" class="page">
<div class="app">
<header>
<div class="logo">
<div class="logo-icon">RDCM</div>
<div class="logo-text"><h1>Claim Detail</h1></div>
</div>
<div class="header-right">
<div class="header-badge"><div class="pulse"></div><span id="d_statusBadge">β€”</span></div>
<button class="btn-back" onclick="goTo('multiPage')">← Back to Batch</button>
</div>
</header>
<div class="single-grid">
<!-- LEFT: Claim fields -->
<div>
<div class="panel">
<div class="panel-header">
<div class="panel-title"><span class="icon">πŸ“‹</span> Claim Fields</div>
<span class="panel-badge" id="d_claimIdBadge">β€”</span>
</div>
<div class="panel-body" style="max-height:78vh;overflow-y:auto;" id="d_claimInfoBody">
<div class="empty-state"><div class="big">πŸ“‹</div><p>No claim selected.</p></div>
</div>
</div>
</div>
<!-- RIGHT: Rule check result -->
<div>
<div class="panel">
<div class="panel-header">
<div class="panel-title"><span class="icon">πŸ”¬</span> Rule Check</div>
<button class="btn-tiny" onclick="dRunCheck()" id="d_runBtn">⚑ Re-check</button>
</div>
<div class="panel-body" id="d_resultBody">
<div class="empty-state"><div class="big">πŸ”¬</div><p>Results will appear here.</p></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
/* ═══════════════════════════════════
API
═══════════════════════════════════ */
const BASE = window.location.origin;
const CHECK_ONE = `${BASE}/check-claim`;
const CHECK_BATCH = `${BASE}/check-batch`;
const DEFAULT_URL = `${BASE}/default-claims`;
/* ═══════════════════════════════════
PARTICLES
═══════════════════════════════════ */
(function spawnParticles(){
const wrap = document.getElementById('particles');
const colors = ['#00e5b4','#3b82f6','#00c49a','#6366f1'];
for(let i=0;i<28;i++){
const p=document.createElement('div');
p.className='particle';
const sz = 2+Math.random()*4;
p.style.cssText=`
width:${sz}px;height:${sz}px;
left:${Math.random()*100}%;
background:${colors[Math.floor(Math.random()*colors.length)]};
animation-duration:${8+Math.random()*14}s;
animation-delay:${Math.random()*12}s;
`;
wrap.appendChild(p);
}
})();
/* ═══════════════════════════════════
PAGE ROUTER
═══════════════════════════════════ */
function goTo(pageId, pushHistory=true){
document.querySelectorAll('.page').forEach(p=>{p.classList.remove('active');p.style.display='none';});
const p=document.getElementById(pageId);
p.style.display='';
requestAnimationFrame(()=>p.classList.add('active'));
window.scrollTo({top:0,behavior:'smooth'});
if(pageId==='multiPage' && !mBooted){ mBoot(); mBooted=true; }
if(pushHistory) history.pushState({page:pageId},'','#'+pageId);
}
// Browser back/forward button support
window.addEventListener('popstate', e=>{
const pageId=(e.state&&e.state.page)||'welcomePage';
goTo(pageId, false);
});
// Record initial state so back button can return to welcome
history.replaceState({page:'welcomePage'},'','#welcomePage');
/* ═══════════════════════════════════
HELPERS
═══════════════════════════════════ */
function trunc(v,len){const s=String(v??'');return s.length>len?s.slice(0,len)+'…':s;}
function esc(v){return String(v??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');}
/* ═══════════════════════════════════════════════════
PAGE 1 β€” SINGLE CLAIM (JSON input)
═══════════════════════════════════════════════════ */
const SAMPLE_CLAIM = {
ClaimId: "TC-001",
FirstName: "PAT", LastName: "HYPERTENSION",
DateOfBirth: "1965-06-15", Gender: "Female",
Address: "123 MAIN STREET", City: "Chicago", State: "IL", Zip: "60601",
ClaimPriIns: "MEDICARE IL", BillAs: "Primary", PolicyNumber: "",
DOS: "2024-08-15", POS: "31",
AttendingPhysician: "DR. JOHNSON", BillingPhysician: "DR. JOHNSON",
ReferringPhysician: "DR. JONES",
AllDxCodes: "I10, M54.5",
cpt_lines: [
{ CPTCode: "99308", Modifier1: "", ServiceUnits: "1", TotalCharges: "147.87",
DXPointer1: "1", DXPointer2: "2", DXPointer3: "", DXPointer4: "" },
{ CPTCode: "94010", Modifier1: "", ServiceUnits: "1", TotalCharges: "85.00",
DXPointer1: "1", DXPointer2: "", DXPointer3: "", DXPointer4: "" }
]
};
function sLoadDefault(){
document.getElementById('s_jsonInput').value = JSON.stringify(SAMPLE_CLAIM, null, 2);
}
function sUploadJSON(event){
const file = event.target.files?.[0];
if(!file) return;
const reader = new FileReader();
reader.onload = e => {
const text = String(e.target.result || '');
try {
const parsed = JSON.parse(text);
// If user uploaded {"claim":{...}} unwrap it; otherwise use the object directly
const claim = (parsed && typeof parsed === 'object' && parsed.claim && typeof parsed.claim === 'object') ? parsed.claim : parsed;
document.getElementById('s_jsonInput').value = JSON.stringify(claim, null, 2);
document.getElementById('s_resultBody').innerHTML = `<div class="alert-box" style="border-left:3px solid var(--accent);">βœ“ Loaded <strong>${esc(file.name)}</strong> β€” click <strong>Check This Claim</strong>.</div>`;
document.getElementById('s_statusBadge').textContent = 'File loaded';
} catch(err) {
document.getElementById('s_resultBody').innerHTML = `<div class="alert-box alert-danger">⚠️ Invalid JSON in ${esc(file.name)}: ${esc(err.message)}</div>`;
document.getElementById('s_statusBadge').textContent = 'Error';
}
};
reader.onerror = () => {
document.getElementById('s_resultBody').innerHTML = `<div class="alert-box alert-danger">⚠️ Could not read file.</div>`;
};
reader.readAsText(file);
event.target.value = ''; // allow re-uploading the same file
}
function sClear(){
document.getElementById('s_jsonInput').value = '';
document.getElementById('s_resultBody').innerHTML = '<div class="empty-state"><div class="big">πŸ“‹</div><p>Paste claim JSON on the left and click<br><strong>Check This Claim</strong></p></div>';
document.getElementById('s_claimIdBadge').textContent = 'β€”';
document.getElementById('s_statusBadge').textContent = 'Ready';
}
async function sRunCheck(){
const raw = document.getElementById('s_jsonInput').value.trim();
if(!raw){
document.getElementById('s_resultBody').innerHTML = '<div class="alert-box">⚠️ Paste claim JSON first.</div>';
return;
}
let claim;
try{ claim = JSON.parse(raw); }
catch(e){
document.getElementById('s_resultBody').innerHTML = `<div class="alert-box alert-danger">⚠️ Invalid JSON: ${esc(e.message)}</div>`;
return;
}
const btn = document.getElementById('s_runBtn');
btn.disabled = true;
document.getElementById('s_statusBadge').textContent = 'Checking...';
document.getElementById('s_resultBody').innerHTML = '<div class="spinner-wrap"><div class="spinner"></div><p>Analyzing rules...</p></div>';
document.getElementById('s_claimIdBadge').textContent = claim.ClaimId || 'β€”';
try{
const resp = await fetch(CHECK_ONE, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({claim})});
if(!resp.ok){ let m=`API error ${resp.status}`; try{const e=await resp.json();if(e.error)m=e.error;}catch{} throw new Error(m); }
sRenderResult(await resp.json());
document.getElementById('s_statusBadge').textContent = 'Done';
}catch(err){
document.getElementById('s_resultBody').innerHTML = `<div class="alert-box alert-danger">⚠️ ${esc(err.message)}</div>`;
document.getElementById('s_statusBadge').textContent = 'Error';
}
btn.disabled = false;
}
function sRenderResult(data){
const ok = data.status === 'ACCEPTED';
const verdict = `
<div class="verdict-card ${ok?'accepted':'rejected'}">
<div class="verdict-icon">${ok?'βœ…':'❌'}</div>
<div class="verdict-text">
<h2>${ok?'CLAIM ACCEPTED':'CLAIM REJECTED'}</h2>
<p>${ok?'No rule violations found.':`${data.rules_broken} rule${data.rules_broken!==1?'s':''} violated.`}</p>
</div>
</div>`;
const rulesHtml = (data.broken_rules||[]).length ? `
<div style="margin-top:14px;">
<div style="font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-bottom:8px;">Violated Rules</div>
<div class="rules-list">
${(data.broken_rules||[]).map((r,i)=>`
<div class="rule-item" style="animation-delay:${i*40}ms">
<div class="rule-name">${esc(r.rule_name)}</div>
<div class="rule-msg">${esc(r.message)}</div>
</div>`).join('')}
</div>
</div>` : '';
const stats = `
<div class="mini-stats" style="margin-top:14px;">
<div class="mini-stat"><div class="n" style="color:var(--danger)">${data.rules_broken}</div><div class="l">Issues</div></div>
<div class="mini-stat"><div class="n" style="color:${ok?'var(--accent)':'var(--danger)'}">${ok?'βœ“':'βœ—'}</div><div class="l">Status</div></div>
</div>`;
document.getElementById('s_resultBody').innerHTML = verdict + rulesHtml + stats;
}
(function(){ sLoadDefault(); })();
/* ═══════════════════════════════════════════════════
PAGE 2 β€” MULTI CLAIM
═══════════════════════════════════════════════════ */
let mBooted=false;
let mData=[], mHeaders=[], mFilename='';
let mResults=[], mFilter='all', mDrawer=null;
function mBoot(){
fetch(DEFAULT_URL)
.then(r=>{ if(!r.ok) throw new Error(`Server ${r.status}`); return r.json(); })
.then(d=>{ if(d.error) throw new Error(d.error); mData=d.records; mHeaders=d.columns; mFilename=d.filename; mOnLoaded(d.filename,d.total,true); })
.catch(err=>mShowFallback('Auto-load failed: '+err.message));
}
function mShowFallback(msg){
document.getElementById('m_initLoading').style.display='none';
const fb=document.getElementById('m_fallback');
fb.style.display='block';
fb.innerHTML=`<div class="alert-box" style="margin-bottom:14px;">⚠️ ${msg}</div>
<div class="drop-zone" id="m_dropZone">
<input type="file" id="m_fbInput" accept=".xlsx,.xls"/>
<div class="drop-icon">πŸ“Š</div><div class="drop-title">Drop Excel file here</div>
<div class="drop-sub">or click to browse</div><span class="drop-badge">.xlsx / .xls</span>
</div>`;
const dz=document.getElementById('m_dropZone'),fi=document.getElementById('m_fbInput');
dz.addEventListener('dragover',e=>{e.preventDefault();dz.classList.add('drag-over');});
dz.addEventListener('dragleave',()=>dz.classList.remove('drag-over'));
dz.addEventListener('drop',e=>{e.preventDefault();dz.classList.remove('drag-over');if(e.dataTransfer.files[0])mLoadFile(e.dataTransfer.files[0]);});
fi.addEventListener('change',()=>{if(fi.files[0])mLoadFile(fi.files[0]);});
document.getElementById('m_statusBadge').textContent='Upload required';
}
function mOnLoaded(filename,total,isDefault){
document.getElementById('m_initLoading').style.display='none';
document.getElementById('m_fallback').style.display='none';
document.getElementById('m_ctrlFilename').textContent=filename;
document.getElementById('m_ctrlMeta').textContent=`${total} claims Β· ${mHeaders.length} columns`;
document.getElementById('m_ctrlBadge').textContent=isDefault?'βœ“ Auto-loaded':'βœ“ Uploaded';
document.getElementById('m_controlsRow').style.display='flex';
document.getElementById('m_statusBadge').textContent='Ready';
document.getElementById('m_runAllBtn').disabled=false;
const fi=document.getElementById('m_fileInput');
fi.onchange=()=>{if(fi.files[0])mLoadFile(fi.files[0]);};
mResults=[];mDrawer=null;
document.getElementById('m_progressSection').style.display='none';
document.getElementById('m_summarySection').style.display='none';
document.getElementById('m_resultsPanel').style.display='none';
mRenderPreview();
}
function mLoadFile(file){
const r=new FileReader();
r.onload=e=>mParseExcel(e.target.result,file.name);
r.readAsArrayBuffer(file);
}
function mParseExcel(buf,filename){
const wb=XLSX.read(buf,{type:'array',cellDates:true});
const ws=wb.Sheets[wb.SheetNames.find(s=>s.toLowerCase()==='claims')||wb.SheetNames[0]];
const raw=XLSX.utils.sheet_to_json(ws,{header:1,defval:''});
let hi=0;
for(let i=0;i<Math.min(raw.length,10);i++){if(raw[i].some(c=>String(c).trim()==='ClaimId')){hi=i;break;}}
mHeaders=raw[hi].map(h=>String(h).trim());
mData=raw.slice(hi+1).filter(r=>r.some(c=>c!==null&&c!==undefined&&String(c).trim()!=='')).map(row=>{const o={};mHeaders.forEach((h,i)=>{o[h]=row[i]!==undefined?row[i]:'';});return o;});
mFilename=filename;
mOnLoaded(filename,mData.length,false);
}
function mRenderPreview(){
const panel=document.getElementById('m_previewPanel'),area=document.getElementById('m_previewArea');
if(!mData.length){panel.style.display='none';return;}
const preferred=['ClaimId','PatientName','CPTCode','DOS','POS','ClaimPriIns','Gender','AllDxCodes','TotalCharges'];
const cols=preferred.filter(c=>mHeaders.includes(c));
if(!cols.length)cols.push(...mHeaders.slice(0,8));
let html=`<table><thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead><tbody>`;
mData.forEach(row=>{html+=`<tr>${cols.map((c,ci)=>`<td class="${ci===0?'primary':''}" title="${esc(row[c])}">${trunc(row[c],22)}</td>`).join('')}</tr>`;});
html+=`</tbody></table>`;
area.innerHTML=html;
document.getElementById('m_previewBadge').textContent=`${mData.length} rows Β· ${mHeaders.length} cols`;
panel.style.display='block';
}
async function mRunAll(){
if(!mData.length)return;
const btn=document.getElementById('m_runAllBtn');
btn.disabled=true;btn.innerHTML='<span>⏳</span> Running…';
document.getElementById('m_statusBadge').textContent='Processing…';
mResults=[];mDrawer=null;mFilter='all';
document.getElementById('m_progressSection').style.display='block';
document.getElementById('m_summarySection').style.display='none';
document.getElementById('m_resultsPanel').style.display='none';
mSetProgress(0,0,0,mData.length);
const CHUNK=50,CONC=4,total=mData.length;
let acc=0,rej=0,done=0;
const chunks=[];
for(let i=0;i<total;i+=CHUNK)chunks.push(mData.slice(i,i+CHUNK));
const ordered=new Array(chunks.length);
let ci=0;
async function worker(){
while(ci<chunks.length){
const idx=ci++;const slice=chunks[idx];
const resp=await fetch(CHECK_BATCH,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({claims:slice})});
if(!resp.ok){let m=`API error ${resp.status}`;try{const e=await resp.json();if(e.error)m=e.error;}catch{}throw new Error(m);}
const d=await resp.json();
ordered[idx]=d.results;
for(const r of d.results){if(r.status==='ACCEPTED')acc++;else rej++;}
done+=d.results.length;
mSetProgress(done,acc,rej,total);
}
}
try{
await Promise.all(Array.from({length:CONC},worker));
mResults=ordered.flat();
mShowSummary(total,acc,rej);
mRenderTable();
document.getElementById('m_resultsPanel').style.display='block';
document.getElementById('m_statusBadge').textContent='Done';
mSetFilterBtns();
}catch(err){
document.getElementById('m_progressSection').style.display='none';
document.getElementById('m_resultsPanel').style.display='block';
document.getElementById('m_tableArea').innerHTML=`<div class="alert-box alert-danger">⚠️ ${err.message}</div>`;
document.getElementById('m_statusBadge').textContent='Error';
}
btn.disabled=false;btn.innerHTML='<span>⚑</span> Re-run All Claims';
}
function mSetProgress(done,acc,rej,total){
const pct=total?Math.round(done/total*100):0;
document.getElementById('m_bar').style.width=`${pct}%`;
document.getElementById('m_pct').textContent=`${pct}%`;
document.getElementById('m_pDone').textContent=done;
document.getElementById('m_pAcc').textContent=acc;
document.getElementById('m_pRej').textContent=rej;
document.getElementById('m_pRem').textContent=total-done;
}
function mShowSummary(total,acc,rej){
document.getElementById('m_summarySection').style.display='grid';
document.getElementById('m_sTotal').textContent=total;
document.getElementById('m_sAcc').textContent=acc;
document.getElementById('m_sRej').textContent=rej;
document.getElementById('m_sPct').textContent=total?`${Math.round(rej/total*100)}%`:'0%';
}
function mSetFilter(f){mFilter=f;mDrawer=null;mSetFilterBtns();mRenderTable();}
function mSetFilterBtns(){
document.getElementById('m_fAll').className='filter-btn'+(mFilter==='all'?' active':'');
document.getElementById('m_fAcc').className='filter-btn'+(mFilter==='accepted'?' active':'');
document.getElementById('m_fRej').className='filter-btn danger'+(mFilter==='rejected'?' active':'');
}
function mRenderTable(){
const search=(document.getElementById('m_search')?.value||'').trim().toLowerCase();
const filtered=mResults.filter(r=>{
if(mFilter==='accepted'&&r.status!=='ACCEPTED')return false;
if(mFilter==='rejected'&&r.status!=='REJECTED')return false;
if(search&&!String(r.claim_id).toLowerCase().includes(search))return false;
return true;
});
document.getElementById('m_resultsBadge').textContent=`${filtered.length} of ${mResults.length} claims`;
if(!filtered.length){document.getElementById('m_tableArea').innerHTML=`<div class="empty-state"><div class="big">πŸ”</div><p>No claims match.</p></div>`;return;}
const cols=['ClaimId','PatientName','CPTCode','DOS','ClaimPriIns','TotalCharges'].filter(c=>mHeaders.includes(c));
const cmap={};mData.forEach(row=>{cmap[String(row['ClaimId']??'')]=row;});
let html=`<table><thead><tr><th>Status</th>${cols.map(c=>`<th>${c}</th>`).join('')}<th>Rules Broken</th></tr></thead><tbody>`;
filtered.forEach((r,fi)=>{
const ok=r.status==='ACCEPTED';
const row=cmap[String(r.claim_id)]||{};
const pill=ok?`<span class="status-pill accepted">βœ“ ACCEPTED</span>`:`<span class="status-pill rejected">βœ— REJECTED</span>`;
html+=`<tr onclick="mOpenDetail('${esc(r.claim_id)}')" title="Click to open claim detail" style="cursor:pointer;">
<td>${pill}</td>${cols.map((c,i)=>`<td class="${i===0?'primary':''}" title="${esc(row[c])}">${trunc(row[c],20)}</td>`).join('')}
<td style="color:${ok?'var(--muted)':'var(--danger)'};">${ok?'β€”':r.rules_broken}</td></tr>`;
});
html+=`</tbody></table>`;
document.getElementById('m_tableArea').innerHTML=html;
}
/* Open claim detail page from batch row click */
let dCurrentClaim=null, dCurrentClaimId=null;
function mOpenDetail(claimId){
const cmap={};mData.forEach(row=>{cmap[String(row['ClaimId']??'')]=row;});
const raw=cmap[String(claimId)];
const res=mResults.find(r=>String(r.claim_id)===String(claimId));
if(!raw||!res){alert('Claim not found');return;}
dCurrentClaim=raw;
dCurrentClaimId=String(claimId);
dRender(raw,res);
goTo('claimDetailPage');
}
function dRender(claim,result){
document.getElementById('d_claimIdBadge').textContent=claim.ClaimId||'β€”';
document.getElementById('d_statusBadge').textContent=result.status||'β€”';
const sections=[
{title:'πŸ‘€ Patient',fields:['ClaimId','PatientName','FirstName','LastName','DateOfBirth','Gender','Address','City','State','Zip']},
{title:'πŸ₯ Insurance',fields:['ClaimPriIns','PolicyNumber','BillAs','ClaimSecIns','PANumber','ReferralNumber']},
{title:'πŸ“… Service',fields:['DOS','POS','AdmissionDate','DischargeDate','AttendingPhysician','BillingPhysician','ReferringPhysician']},
{title:'πŸ”¬ Diagnoses',fields:['AllDxCodes','DXCode1','DXCode2','DXCode3','DXCode4','DXCode5','DXCode6','DXCode7','DXCode8']},
{title:'πŸ’Š CPT',fields:['CPTCode','Modifier1','Modifier2','Modifier3','Modifier4','ServiceUnits','TotalCharges','DXPointer1','DXPointer2','DXPointer3','DXPointer4']},
];
const shown=new Set();
let html='';
for(const sec of sections){
const rows=sec.fields.filter(f=>claim[f]!==undefined&&String(claim[f]).trim()!=='');
rows.forEach(r=>shown.add(r));
if(!rows.length) continue;
html+=`<div class="form-section"><div class="sec-hdr"><span>${sec.title}</span></div><div class="sec-body"><div class="fgrid">`;
for(const f of rows){
html+=`<div class="ff"><label>${esc(f)}</label><div style="color:var(--text);font-family:var(--mono);font-size:12px;padding:7px 9px;background:var(--surface2);border:1px solid var(--border);border-radius:5px;word-break:break-word;">${esc(claim[f])}</div></div>`;
}
html+='</div></div></div>';
}
// "Other" section for any remaining non-empty fields
const other=Object.keys(claim).filter(k=>!shown.has(k)&&String(claim[k]).trim()!=='');
if(other.length){
html+=`<div class="form-section"><div class="sec-hdr"><span>πŸ“Ž Other fields</span></div><div class="sec-body"><div class="fgrid">`;
for(const f of other){
html+=`<div class="ff"><label>${esc(f)}</label><div style="color:var(--text);font-family:var(--mono);font-size:11px;padding:6px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:5px;word-break:break-word;">${esc(claim[f])}</div></div>`;
}
html+='</div></div></div>';
}
document.getElementById('d_claimInfoBody').innerHTML=html;
dRenderResult(result);
}
function dRenderResult(data){
const ok=data.status==='ACCEPTED';
const verdict=`<div class="verdict-card ${ok?'accepted':'rejected'}">
<div class="verdict-icon">${ok?'βœ…':'❌'}</div>
<div class="verdict-text">
<h2>${ok?'CLAIM ACCEPTED':'CLAIM REJECTED'}</h2>
<p>${ok?'No rule violations found.':`${data.rules_broken} rule${data.rules_broken!==1?'s':''} violated.`}</p>
</div>
</div>`;
const rulesHtml=(data.broken_rules||[]).length?`
<div style="margin-top:14px;">
<div style="font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-bottom:8px;">Violated Rules</div>
<div class="rules-list">
${data.broken_rules.map((r,i)=>`<div class="rule-item" style="animation-delay:${i*40}ms">
<div class="rule-name">${esc(r.rule_name)}</div>
<div class="rule-msg">${esc(r.message)}</div>
</div>`).join('')}
</div>
</div>`:'';
document.getElementById('d_resultBody').innerHTML=verdict+rulesHtml;
}
async function dRunCheck(){
if(!dCurrentClaim)return;
const btn=document.getElementById('d_runBtn');
btn.disabled=true;
document.getElementById('d_statusBadge').textContent='Checking...';
document.getElementById('d_resultBody').innerHTML='<div class="spinner-wrap"><div class="spinner"></div><p>Re-checking...</p></div>';
try{
const resp=await fetch(CHECK_ONE,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({claim:dCurrentClaim})});
if(!resp.ok){let m=`API error ${resp.status}`;try{const e=await resp.json();if(e.error)m=e.error;}catch{}throw new Error(m);}
const data=await resp.json();
dRenderResult(data);
document.getElementById('d_statusBadge').textContent=data.status;
// Update mResults so batch table reflects new status on return
const idx=mResults.findIndex(r=>String(r.claim_id)===dCurrentClaimId);
if(idx>=0){
mResults[idx]={...mResults[idx],...data};
// Recalculate summary counters from the updated mResults array
const total=mResults.length;
const rej=mResults.filter(r=>r.status==='REJECTED').length;
const acc=total-rej;
mShowSummary(total,acc,rej);
// Also refresh the batch table so the row pill updates immediately on return
mRenderTable();
}
}catch(err){
document.getElementById('d_resultBody').innerHTML=`<div class="alert-box alert-danger">⚠️ ${esc(err.message)}</div>`;
document.getElementById('d_statusBadge').textContent='Error';
}
btn.disabled=false;
}
</script>
</body>
</html>