Spaces:
Sleeping
Sleeping
| <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,'&').replace(/</g,'<').replace(/"/g,'"');} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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> | |