Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SDR Status Tracker</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/howler@2.2.4/dist/howler.min.js"></script> | |
| <style> | |
| :root { | |
| --primary: #6D3EF3; | |
| --primary-dark: #602DF2; | |
| --primary-glow: rgba(109, 62, 243, 0.35); | |
| --lilac-300: #B89EFF; --tangerine-300: #FF8474; --emerald-300: #70E0B1; | |
| --marigold-300: #FFF09B; --zodiac-300: #ABECFA; --sierra-300: #FAB987; | |
| --lilac-100: #F4F0FF; --tangerine-100: #FFEBE9; --emerald-100: #E8FAF3; | |
| --marigold-100: #FFFDEF; --zodiac-100: #F2FCFE; --sierra-100: #FEF4EC; | |
| --lilac-200: #E8E0FF; --emerald-200: #D1F5E6; | |
| --lilac-400: #1E0C32; --emerald-400: #00302A; --tangerine-400: #391418; | |
| --base-100: #FFFFFF; --base-200: #F5F5F6; --base-300: #D8D8D9; | |
| --base-400: #B2B1B3; --base-500: #6E6D71; --base-600: #0D0B13; | |
| } | |
| [data-theme="dark"] { | |
| --bg: #0D0B13; | |
| --bg-surface: rgba(109, 62, 243, 0.06); | |
| --bg-card: rgba(109, 62, 243, 0.10); | |
| --bg-card-hover: rgba(109, 62, 243, 0.16); | |
| --border: rgba(109, 62, 243, 0.2); | |
| --border-strong: rgba(109, 62, 243, 0.4); | |
| --text: #F5F5F6; --text-muted: #B2B1B3; --text-dim: #6E6D71; | |
| --input-bg: rgba(255,255,255,0.06); --modal-bg: #1a1726; | |
| --table-header: rgba(109, 62, 243, 0.18); | |
| --table-stripe: rgba(109, 62, 243, 0.04); | |
| --sdr-bg: rgba(109, 62, 243, 0.12); | |
| } | |
| [data-theme="light"] { | |
| --bg: #F8F7FC; | |
| --bg-surface: #FFFFFF; | |
| --bg-card: #FFFFFF; | |
| --bg-card-hover: var(--lilac-100); | |
| --border: #E8E0FF; | |
| --border-strong: var(--primary); | |
| --text: #0D0B13; --text-muted: #6E6D71; --text-dim: #B2B1B3; | |
| --input-bg: #F5F5F6; --modal-bg: #FFFFFF; | |
| --table-header: var(--lilac-100); | |
| --table-stripe: rgba(109, 62, 243, 0.02); | |
| --sdr-bg: var(--lilac-100); | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| html { font-size: 16px; } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| height: 100vh; overflow: hidden; | |
| display: flex; flex-direction: column; | |
| } | |
| /* ===== TOOLBAR ===== */ | |
| .toolbar { | |
| display: flex; align-items: center; justify-content: space-between; | |
| padding: 0 20px; height: 42px; flex-shrink: 0; | |
| background: linear-gradient(135deg, #6D3EF3 0%, #8B5CF6 50%, #602DF2 100%); | |
| box-shadow: 0 2px 12px rgba(109, 62, 243, 0.3); | |
| } | |
| .toolbar-left { display: flex; align-items: center; gap: 16px; } | |
| .toolbar-title { | |
| font-weight: 900; font-size: 0.95rem; color: #fff; | |
| letter-spacing: -0.3px; | |
| } | |
| .toolbar-subtitle { font-size: 0.72rem; color: rgba(255,255,255,0.7); font-weight: 500; } | |
| .toolbar-right { display: flex; align-items: center; gap: 5px; } | |
| .toolbar select { | |
| background: rgba(255,255,255,0.15); color: #fff; | |
| border: 1px solid rgba(255,255,255,0.25); border-radius: 5px; | |
| padding: 3px 24px 3px 8px; font-size: 0.7rem; font-weight: 600; | |
| cursor: pointer; font-family: 'Inter', sans-serif; | |
| appearance: none; -webkit-appearance: none; | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='white' d='M6 8L2 4h8z'/%3E%3C/svg%3E"); | |
| background-repeat: no-repeat; background-position: right 6px center; | |
| } | |
| .toolbar select:focus { outline: none; border-color: rgba(255,255,255,0.5); } | |
| .toolbar-btn { | |
| background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.2); | |
| padding: 3px 10px; border-radius: 5px; cursor: pointer; | |
| font-size: 0.68rem; font-weight: 600; color: rgba(255,255,255,0.85); | |
| transition: all 0.15s; font-family: 'Inter', sans-serif; | |
| } | |
| .toolbar-btn:hover { background: rgba(255,255,255,0.22); color: #fff; } | |
| /* ===== MAIN LAYOUT ===== */ | |
| .main-layout { | |
| flex: 1; display: flex; gap: 10px; | |
| padding: 8px 10px 6px; min-height: 0; | |
| } | |
| /* ===== TABLE ===== */ | |
| .table-section { | |
| flex: 0 0 38%; overflow: auto; min-height: 0; | |
| border-radius: 10px; | |
| background: var(--bg-surface); | |
| border: 1px solid var(--border); | |
| } | |
| table { width: 100%; border-collapse: separate; border-spacing: 0; } | |
| th, td { padding: 5px 8px; white-space: nowrap; border-bottom: 1px solid var(--border); } | |
| th { | |
| background: var(--table-header); color: var(--primary); | |
| font-weight: 800; font-size: 0.65rem; text-transform: uppercase; | |
| letter-spacing: 0.5px; position: sticky; top: 0; z-index: 2; | |
| text-align: center; | |
| } | |
| th:first-child, th:nth-child(2) { text-align: left; } | |
| td { font-size: 0.82rem; } | |
| .col-pct { width: 1px; } | |
| tr:hover td { background: var(--bg-card-hover); } | |
| .sdr-name-cell { vertical-align: middle; padding-right: 10px; } | |
| .sdr-name-visible { background: var(--sdr-bg); } | |
| .sdr-name-hidden { background: transparent; } | |
| .sdr-name { | |
| font-weight: 900; color: var(--primary); font-size: 0.85rem; | |
| white-space: nowrap; line-height: 1.3; | |
| } | |
| [data-theme="dark"] .sdr-name { color: var(--lilac-300); } | |
| .sdr-group-first td { border-top: 2px solid var(--border-strong); } | |
| .case-name { font-weight: 600; font-size: 0.78rem; color: var(--text-muted); padding-right: 8px; } | |
| .divider-left { border-left: 2px solid var(--primary) ; } | |
| /* ===== PROGRESS BARS (Brand 300-level colors — same in both themes) ===== */ | |
| .progress-bar { | |
| width: 84px; height: 20px; border-radius: 10px; | |
| background: var(--border); overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; border-radius: 10px; transition: width 0.3s; | |
| display: flex; align-items: center; justify-content: center; | |
| min-width: 28px; | |
| } | |
| .progress-text { | |
| font-size: 0.64rem; font-weight: 800; color: var(--base-600); | |
| white-space: nowrap; | |
| } | |
| /* Brand 300-level palette — vibrant in both light & dark per guidelines */ | |
| .progress-success { background: var(--emerald-300); } | |
| .progress-good { background: var(--zodiac-300); } | |
| .progress-warning { background: var(--marigold-300); } | |
| .progress-caution { background: var(--sierra-300); } | |
| .progress-danger { background: var(--tangerine-300); } | |
| .progress-none { background: var(--border); } | |
| [data-theme="dark"] .progress-bar { background: rgba(255,255,255,0.08); } | |
| [data-theme="dark"] .progress-text { color: var(--base-600); } | |
| /* ===== LEADERBOARD SECTION ===== */ | |
| .leaderboard-section { | |
| flex: 1; min-width: 0; | |
| display: grid; grid-template-columns: repeat(4, 1fr); | |
| gap: 8px; align-content: start; | |
| overflow-y: auto; min-height: 0; | |
| } | |
| .lb-card { | |
| border-radius: 12px; padding: 10px 14px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| position: relative; overflow: hidden; | |
| transition: border-color 0.2s; | |
| } | |
| .lb-card:hover { border-color: var(--border-strong); } | |
| /* Colored left accent strip */ | |
| .lb-card::before { | |
| content: ''; position: absolute; left: 0; top: 0; bottom: 0; | |
| width: 4px; border-radius: 12px 0 0 12px; | |
| } | |
| .lb-card.cat-overall::before { background: var(--primary); } | |
| .lb-card.cat-activity::before { background: var(--emerald-300); } | |
| .lb-card.cat-sql::before { background: var(--zodiac-300); } | |
| .lb-card.cat-target::before { background: var(--marigold-300); } | |
| .lb-card h3 { | |
| font-size: 0.78rem; font-weight: 800; text-transform: uppercase; | |
| letter-spacing: 0.4px; margin-bottom: 6px; padding-left: 6px; | |
| display: flex; align-items: center; gap: 5px; | |
| } | |
| .lb-card.cat-overall h3 { color: var(--primary); } | |
| .lb-card.cat-activity h3 { color: #2BA874; } | |
| [data-theme="dark"] .lb-card.cat-activity h3 { color: var(--emerald-300); } | |
| .lb-card.cat-sql h3 { color: #2898C4; } | |
| [data-theme="dark"] .lb-card.cat-sql h3 { color: var(--zodiac-300); } | |
| .lb-card.cat-target h3 { color: #8A7020; } | |
| [data-theme="dark"] .lb-card.cat-target h3 { color: var(--marigold-300); } | |
| .lb-list { list-style: none; } | |
| .lb-item { | |
| display: flex; align-items: center; gap: 8px; | |
| padding: 4px 6px; border-radius: 6px; | |
| font-size: 0.82rem; | |
| } | |
| .lb-item + .lb-item { margin-top: 2px; } | |
| /* Rank badges */ | |
| .lb-rank { | |
| width: 24px; height: 24px; border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| font-weight: 900; font-size: 0.68rem; flex-shrink: 0; | |
| } | |
| .lb-rank-1 { | |
| background: linear-gradient(135deg, var(--marigold-300), var(--sierra-300)); | |
| color: var(--base-600); box-shadow: 0 1px 4px rgba(255, 220, 100, 0.4); | |
| width: 28px; height: 28px; font-size: 0.75rem; | |
| } | |
| .lb-rank-2 { | |
| background: linear-gradient(135deg, #D0D0D5, #A8A8AD); | |
| color: var(--base-600); width: 26px; height: 26px; | |
| } | |
| .lb-rank-3 { | |
| background: linear-gradient(135deg, var(--sierra-300), #E8956A); | |
| color: #fff; | |
| } | |
| .lb-rank-other { | |
| background: var(--border); color: var(--text-dim); | |
| width: 22px; height: 22px; font-size: 0.62rem; | |
| } | |
| [data-theme="dark"] .lb-rank-1 { background: linear-gradient(135deg, #D4A520, #B8860B); color: #fff; box-shadow: 0 1px 6px rgba(212, 165, 32, 0.4); } | |
| [data-theme="dark"] .lb-rank-2 { background: linear-gradient(135deg, #A0A4AD, #7A7E85); color: #fff; } | |
| [data-theme="dark"] .lb-rank-3 { background: linear-gradient(135deg, #CD7F32, #A0652A); color: #fff; } | |
| .lb-name { font-weight: 700; color: var(--text); font-size: 0.85rem; flex: 1; overflow: hidden; text-overflow: ellipsis; } | |
| .lb-score { font-weight: 900; color: var(--primary); font-size: 0.88rem; white-space: nowrap; margin-left: auto; } | |
| [data-theme="dark"] .lb-score { color: var(--lilac-300); } | |
| /* Top-1 highlight */ | |
| .lb-item-1 { background: linear-gradient(90deg, rgba(255,240,155,0.15), transparent); } | |
| .lb-item-1 .lb-name { font-size: 0.92rem; } | |
| .lb-item-1 .lb-score { font-size: 0.95rem; } | |
| [data-theme="dark"] .lb-item-1 { background: linear-gradient(90deg, rgba(139,117,48,0.15), transparent); } | |
| /* Tiered scaling for high performers */ | |
| .lb-item-1.tier-200 .lb-name { font-size: 1rem; } | |
| .lb-item-1.tier-200 .lb-score { font-size: 1.02rem; } | |
| .lb-item-1.tier-150 .lb-name { font-size: 0.95rem; } | |
| /* ===== SALES FEED ===== */ | |
| .sales-feed { | |
| grid-column: 1 / -1; max-height: 120px; overflow-y: auto; | |
| border-radius: 8px; padding: 8px 12px; | |
| background: var(--bg-card); border: 1px solid var(--border); | |
| font-size: 0.85rem; | |
| } | |
| .sales-feed h4 { | |
| font-size: 0.72rem; font-weight: 800; color: var(--primary); | |
| margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.3px; | |
| } | |
| .sales-feed-empty { color: var(--text-dim); font-style: italic; } | |
| .sales-feed-item { | |
| display: flex; align-items: center; gap: 8px; | |
| padding: 4px 0; border-bottom: 1px solid var(--border); | |
| animation: feedSlideIn 0.3s ease-out; | |
| } | |
| .sales-feed-item:last-child { border-bottom: none; } | |
| .sales-feed-icon { font-size: 1rem; flex-shrink: 0; } | |
| .sales-feed-sdr { font-weight: 700; color: var(--primary); font-size: 0.88rem; } | |
| [data-theme="dark"] .sales-feed-sdr { color: var(--lilac-300); } | |
| .sales-feed-case { color: var(--text-muted); font-size: 0.85rem; } | |
| .sales-feed-time { margin-left: auto; color: var(--text-dim); font-size: 0.75rem; white-space: nowrap; } | |
| @keyframes feedSlideIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } } | |
| /* ===== ACHIEVERS BANNER ===== */ | |
| .achievers-banner { | |
| padding: 4px 16px; flex-shrink: 0; font-size: 0.7rem; display: none; | |
| background: linear-gradient(90deg, var(--emerald-100), #d6f5e8); | |
| border-top: 2px solid var(--emerald-300); | |
| } | |
| .achievers-banner.visible { display: flex; align-items: center; gap: 8px; } | |
| .achievers-banner .banner-label { font-weight: 900; color: var(--emerald-400); } | |
| .achievers-banner .banner-names { font-weight: 600; color: var(--text); } | |
| [data-theme="dark"] .achievers-banner { background: linear-gradient(90deg, rgba(112,224,177,0.08), rgba(112,224,177,0.03)); } | |
| [data-theme="dark"] .achievers-banner .banner-label { color: var(--emerald-300); } | |
| /* ===== MODAL ===== */ | |
| .modal-overlay { | |
| display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(0,0,0,0.6); z-index: 1000; justify-content: center; align-items: center; | |
| } | |
| .modal-overlay.active { display: flex; } | |
| .modal { | |
| background: var(--modal-bg); border-radius: 12px; padding: 20px; | |
| width: 90%; max-width: 420px; border: 2px solid var(--primary); | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.3); | |
| } | |
| .modal h2 { color: var(--primary); margin-bottom: 14px; font-size: 1rem; font-weight: 800; } | |
| .form-group { margin-bottom: 10px; } | |
| .form-group label { display: block; color: var(--text-muted); margin-bottom: 4px; font-size: 0.78rem; font-weight: 500; } | |
| .form-group input, .form-group select { | |
| width: 100%; padding: 6px; border: 1px solid var(--border); border-radius: 5px; | |
| background: var(--input-bg); color: var(--text); font-size: 0.8rem; | |
| } | |
| .modal-buttons { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; } | |
| .btn { | |
| padding: 6px 14px; border: none; border-radius: 5px; | |
| cursor: pointer; font-size: 0.78rem; font-weight: 600; transition: all 0.2s; | |
| } | |
| .btn-primary { background: var(--primary); color: #fff; } | |
| .btn-primary:hover { background: var(--primary-dark); } | |
| .btn-secondary { background: transparent; border: 1px solid var(--border); color: var(--text-muted); } | |
| .save-indicator { | |
| position: fixed; bottom: 16px; right: 16px; background: var(--emerald-300); | |
| color: #000; padding: 8px 16px; border-radius: 6px; font-weight: 700; font-size: 0.75rem; | |
| opacity: 0; transform: translateY(16px); transition: all 0.3s; z-index: 1000; | |
| } | |
| .save-indicator.show { opacity: 1; transform: translateY(0); } | |
| /* ===== RESPONSIVE ===== */ | |
| @media (max-width: 1200px) { | |
| .main-layout { flex-direction: column; overflow-y: auto; } | |
| .table-section { flex: 0 0 auto; } | |
| .leaderboard-section { grid-template-columns: repeat(2, 1fr); } | |
| body { overflow: auto; height: auto; } | |
| } | |
| @media (max-width: 768px) { | |
| .leaderboard-section { grid-template-columns: 1fr; } | |
| .sales-feed { grid-column: 1; } | |
| .toolbar { height: 36px; padding: 0 10px; } | |
| .toolbar-title { font-size: 0.8rem; } | |
| } | |
| </style> | |
| </head> | |
| <body data-theme="dark"> | |
| <div class="toolbar"> | |
| <div class="toolbar-left"> | |
| <span class="toolbar-title">SDR Status Tracker</span> | |
| <span class="toolbar-subtitle"><span id="currentMonthLabel">Loading...</span> — Week <span id="subtitleWeek"></span></span> | |
| </div> | |
| <div class="toolbar-right"> | |
| <select id="monthSelect" onchange="changeMonth(this.value)"><option value="">Loading...</option></select> | |
| <button class="toolbar-btn" onclick="refreshData()">Refresh</button> | |
| <button class="toolbar-btn" id="themeBtn" onclick="toggleTheme()">Light</button> | |
| <button class="toolbar-btn" onclick="soundManager.toggle()"><span id="soundIcon">Off</span></button> | |
| <button class="toolbar-btn" onclick="openSoundSettings()">Settings</button> | |
| </div> | |
| </div> | |
| <div class="main-layout"> | |
| <div class="table-section"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th class="col-sdr">SDR</th> | |
| <th class="col-case">Case</th> | |
| <th class="col-pct">Wk SQL</th> | |
| <th class="col-pct">Wk Act</th> | |
| <th class="col-pct divider-left">Mo SQL</th> | |
| <th class="col-pct">Mo Act</th> | |
| </tr> | |
| </thead> | |
| <tbody id="caseTable"></tbody> | |
| </table> | |
| </div> | |
| <div class="leaderboard-section" id="leaderboardSection"></div> | |
| </div> | |
| <div class="achievers-banner" id="achieversBanner"> | |
| <span class="banner-label">Target Achievers:</span> | |
| <span class="banner-names" id="achieversNames"></span> | |
| </div> | |
| <!-- Sound Settings Modal --> | |
| <div class="modal-overlay" id="soundSettingsOverlay" onclick="closeSoundSettingsOnOverlay(event)"> | |
| <div class="modal"> | |
| <h2>Sound Settings</h2> | |
| <div class="form-group"> | |
| <label style="display:flex;align-items:center;gap:8px;cursor:pointer"> | |
| <input type="checkbox" id="soundEnabled" onchange="soundManager.setEnabled(this.checked)" style="width:16px;height:16px"> | |
| <span>Enable sounds</span> | |
| </label> | |
| </div> | |
| <hr style="border:none;border-top:1px solid var(--border);margin:12px 0"> | |
| <div class="form-group"> | |
| <label>SQL Increment</label> | |
| <div style="display:flex;gap:6px"> | |
| <select id="soundSqlIncrement" onchange="soundManager.setSoundChoice('sqlIncrement',this.value)" style="flex:1"> | |
| <option value="chime1">Chime 1</option><option value="chime2">Chime 2</option> | |
| <option value="chime3" selected>Chime 3</option><option value="chime4">Chime 4</option> | |
| <option value="chime5">Chime 5</option><option value="bling1">Bling 1</option> | |
| <option value="bling2">Bling 2</option><option value="bling3">Bling 3</option> | |
| <option value="click1">Click 1</option><option value="click2">Click 2</option> | |
| <option value="none">None</option> | |
| </select> | |
| <button class="btn btn-secondary" onclick="soundManager.previewSound('sqlIncrement')">Play</button> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>Target Hit (100%)</label> | |
| <div style="display:flex;gap:6px"> | |
| <select id="soundTargetHit" onchange="soundManager.setSoundChoice('targetHit',this.value)" style="flex:1"> | |
| <option value="fanfare1" selected>Fanfare 1</option><option value="fanfare2">Fanfare 2</option> | |
| <option value="jingle1">Jingle 1</option><option value="jingle2">Jingle 2</option> | |
| <option value="jingle3">Jingle 3</option><option value="scale1">Scale 1</option> | |
| <option value="scale2">Scale 2</option><option value="scale3">Scale 3</option> | |
| <option value="none">None</option> | |
| </select> | |
| <button class="btn btn-secondary" onclick="soundManager.previewSound('targetHit')">Play</button> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>Beyond Target (Bonus)</label> | |
| <div style="display:flex;gap:6px"> | |
| <select id="soundBeyondTarget" onchange="soundManager.setSoundChoice('beyondTarget',this.value)" style="flex:1"> | |
| <option value="bling1">Bling 1</option><option value="bling2">Bling 2</option> | |
| <option value="bling3" selected>Bling 3</option><option value="chime1">Chime 1</option> | |
| <option value="chime2">Chime 2</option><option value="click1">Click 1</option> | |
| <option value="click2">Click 2</option><option value="none">None</option> | |
| </select> | |
| <button class="btn btn-secondary" onclick="soundManager.previewSound('beyondTarget')">Play</button> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>Volume</label> | |
| <input type="range" id="soundVolume" min="0" max="100" value="60" onchange="soundManager.setVolume(this.value/100)" style="width:100%"> | |
| </div> | |
| <div class="modal-buttons"> | |
| <button class="btn btn-secondary" onclick="closeSoundSettings()">Close</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="save-indicator" id="saveIndicator">Saved!</div> | |
| <script> | |
| // ===== THEME ===== | |
| function applyTheme(t) { | |
| document.body.setAttribute('data-theme', t); | |
| localStorage.setItem('theme', t); | |
| const b = document.getElementById('themeBtn'); | |
| if (b) b.textContent = t === 'dark' ? 'Light' : 'Dark'; | |
| } | |
| function setTheme(t) { applyTheme(t); if (typeof dataLoaded !== 'undefined' && dataLoaded) refreshAll(); } | |
| function toggleTheme() { setTheme(document.body.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'); } | |
| applyTheme(localStorage.getItem('theme') || 'dark'); | |
| // ===== SOUND ===== | |
| class SoundManager { | |
| constructor() { | |
| this.enabled = localStorage.getItem('soundEnabled') !== 'false'; | |
| this.volume = parseFloat(localStorage.getItem('soundVolume')) || 0.6; | |
| this.sounds = {}; this.loaded = false; | |
| const s = localStorage.getItem('soundChoices'); | |
| this.choices = s ? JSON.parse(s) : { sqlIncrement:'none', targetHit:'fanfare1', beyondTarget:'none' }; | |
| } | |
| init() { | |
| if (this.loaded) return; | |
| const base = 'https://raw.githubusercontent.com/rse/soundfx/master/soundfx.d/'; | |
| ['chime1','chime2','chime3','chime4','chime5','bling1','bling2','bling3','click1','click2','fanfare1','fanfare2','jingle1','jingle2','jingle3','scale1','scale2','scale3'].forEach(n => { | |
| this.sounds[n] = new Howl({ src: [base+n+'.mp3'], volume: this.volume }); | |
| }); | |
| this.loaded = true; | |
| } | |
| setEnabled(e) { this.enabled=e; localStorage.setItem('soundEnabled',e); this.updateToggleIcon(); if(e){this.init();this.playSound(this.choices.targetHit);} } | |
| setVolume(v) { this.volume=v; localStorage.setItem('soundVolume',v); Object.values(this.sounds).forEach(s=>s.volume(v)); } | |
| setSoundChoice(k,v) { this.choices[k]=v; localStorage.setItem('soundChoices',JSON.stringify(this.choices)); } | |
| previewSound(k) { this.init(); this.playSound(this.choices[k]); } | |
| playSound(n) { if(n==='none'||!this.sounds[n])return; this.sounds[n]?.play(); } | |
| toggle() { this.setEnabled(!this.enabled); } | |
| updateToggleIcon() { const i=document.getElementById('soundIcon'); if(i) i.textContent=this.enabled?'On':'Off'; } | |
| loadSettingsUI() { | |
| document.getElementById('soundEnabled').checked=this.enabled; | |
| document.getElementById('soundVolume').value=this.volume*100; | |
| document.getElementById('soundSqlIncrement').value=this.choices.sqlIncrement; | |
| document.getElementById('soundTargetHit').value=this.choices.targetHit; | |
| document.getElementById('soundBeyondTarget').value=this.choices.beyondTarget; | |
| } | |
| } | |
| function openSoundSettings(){soundManager.init();soundManager.loadSettingsUI();document.getElementById('soundSettingsOverlay').classList.add('active');} | |
| function closeSoundSettings(){document.getElementById('soundSettingsOverlay').classList.remove('active');} | |
| function closeSoundSettingsOnOverlay(e){if(e.target===document.getElementById('soundSettingsOverlay'))closeSoundSettings();} | |
| const soundManager = new SoundManager(); | |
| // ===== DATA STATE ===== | |
| let currentStartWeek = 2; | |
| let availableWeeks = []; | |
| function getCurrentWeek() { | |
| if (availableWeeks.length > 0) { | |
| const d = new Date().getDate(); | |
| const slot = d<=7?0:d<=14?1:d<=21?2:3; | |
| return availableWeeks[Math.min(slot, availableWeeks.length-1)]; | |
| } | |
| const d = new Date().getDate(); | |
| return currentStartWeek + (d<=7?0:d<=14?1:d<=21?2:3); | |
| } | |
| let currentWeek = getCurrentWeek(); | |
| function updateAvailableWeeks() { | |
| if (!cases.length) return; | |
| const wk = Object.keys(cases[0].weeks).map(Number).sort((a,b)=>a-b); | |
| if (wk.length) { availableWeeks=wk; currentWeek=getCurrentWeek(); document.getElementById('subtitleWeek').textContent=currentWeek; } | |
| } | |
| const STORAGE_KEY = 'caseStatusTracker_data_v5'; | |
| let cases = [], dataLoaded = false, availableMonths = [], currentMonth = null, currentMonthLabel = '', achievementsData = null; | |
| // ===== MONTHS ===== | |
| function getSelectedMonth() { return new URLSearchParams(window.location.search).get('month') || localStorage.getItem('selectedMonth') || null; } | |
| function saveSelectedMonth(m) { localStorage.setItem('selectedMonth',m); const u=new URL(window.location); u.searchParams.set('month',m); window.history.replaceState({},'',u); } | |
| async function fetchMonths() { | |
| try { | |
| const r = await fetch('/api/months'); if(!r.ok) throw new Error(r.status); | |
| const d = await r.json(); availableMonths=d.months||[]; | |
| const sel=document.getElementById('monthSelect'); | |
| sel.innerHTML=availableMonths.map(m=>`<option value="${m.id}">${m.label}</option>`).join(''); | |
| const s=getSelectedMonth()||d.default_month; | |
| if(s&&availableMonths.some(m=>m.id===s)){sel.value=s;currentMonth=s;}else{currentMonth=d.default_month;sel.value=d.default_month;} | |
| updateStartWeek(currentMonth); return currentMonth; | |
| } catch(e){console.error('Months fetch failed:',e);return null;} | |
| } | |
| function updateStartWeek(m) { const mo=availableMonths.find(x=>x.id===m); if(mo&&mo.start_week){currentStartWeek=mo.start_week;currentWeek=getCurrentWeek();document.getElementById('subtitleWeek').textContent=currentWeek;} } | |
| async function changeMonth(m) { if(m===currentMonth)return; currentMonth=m; saveSelectedMonth(m); updateStartWeek(m); showLoadingState(); const u=await fetchFromAPI(); if(u)refreshAll(); } | |
| function updateMonthDisplay(l) { currentMonthLabel=l; document.getElementById('currentMonthLabel').textContent=l; document.title=`SDR Tracker - ${l}`; } | |
| // ===== FETCH ===== | |
| function getSQLAchievements(cd) { const a=new Set(); cd.forEach(c=>{const w=c.weeks[currentWeek]||{};if(w.sqlTarget>0&&(w.sql||0)/w.sqlTarget*100>=100)a.add(`${c.gs}-${c.case}`);}); return a; } | |
| async function fetchFromAPI(chk=false) { | |
| const prev=chk&&cases.length?getSQLAchievements(cases):new Set(); | |
| try { | |
| let u='/api/data'; if(currentMonth)u+=`?month=${encodeURIComponent(currentMonth)}`; | |
| const r=await fetch(u); if(!r.ok)throw new Error(r.status); | |
| const d=await r.json(); | |
| if(d.cases&&d.cases.length>0){ | |
| cases=d.cases; localStorage.setItem(`${STORAGE_KEY}_${d.month||'default'}`,JSON.stringify(cases)); | |
| dataLoaded=true; updateAvailableWeeks(); | |
| if(d.month_label)updateMonthDisplay(d.month_label); | |
| if(chk){const n=getSQLAchievements(cases);for(const a of n){if(!prev.has(a)){soundManager.init();soundManager.playSound(soundManager.choices.targetHit);break;}}} | |
| return true; | |
| } | |
| }catch(e){console.error('API:',e.message);} | |
| return false; | |
| } | |
| async function fetchAchievements() { try{let u='/api/achievements';if(currentMonth)u+=`?month=${encodeURIComponent(currentMonth)}`;const r=await fetch(u);if(r.ok)achievementsData=await r.json();}catch(e){} } | |
| function loadFromCache(m) { const s=localStorage.getItem(`${STORAGE_KEY}_${m||currentMonth||'default'}`); if(s){try{const p=JSON.parse(s);if(p&&p.length){cases=p;dataLoaded=true;updateAvailableWeeks();return true;}}catch(e){}} return false; } | |
| // ===== HELPERS ===== | |
| function showSaveIndicator(){const e=document.getElementById('saveIndicator');e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500);} | |
| function showLoadingState(){document.getElementById('caseTable').innerHTML='<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-muted)">Loading...</td></tr>';} | |
| function showErrorState(m){document.getElementById('caseTable').innerHTML=`<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--tangerine-300)">${m}</td></tr>`;} | |
| // ===== PROGRESS ===== | |
| function wkClass(p){return p>=100?'progress-success':p>=75?'progress-good':p>=50?'progress-warning':p>=25?'progress-caution':'progress-danger';} | |
| function moClass(p){if(p>=100)return'progress-success';const e=(new Date().getDate()/new Date(new Date().getFullYear(),new Date().getMonth()+1,0).getDate())*100;return p>=e?'progress-good':p>=e*.75?'progress-warning':p>=e*.5?'progress-caution':'progress-danger';} | |
| function pbar(p, weekly=true) { | |
| if(p===null) return `<div class="progress-bar"><div class="progress-fill progress-none" style="width:100%"><span class="progress-text">-</span></div></div>`; | |
| const c=weekly?wkClass(p):moClass(p); | |
| return `<div class="progress-bar"><div class="progress-fill ${c}" style="width:${Math.min(p,100)}%"><span class="progress-text" data-value="${p}">${p}%</span></div></div>`; | |
| } | |
| function animateCount(el,t,dur=600,s='%'){const c=parseInt(el.textContent.replace(/[^0-9-]/g,''))||0;if(c===t)return;const st=performance.now();function ease(x){return 1-Math.pow(1-x,4);}function up(n){const p=Math.min((n-st)/dur,1);el.textContent=Math.round(c+(t-c)*ease(p))+s;if(p<1)requestAnimationFrame(up);}requestAnimationFrame(up);} | |
| // ===== METRICS ===== | |
| function wkActPct(c,w){const d=c.weeks[w];if(!d)return null;const p=[];for(const t of['calls','emails','linkedin','prospects']){const g=d[t+'Target'];if(g>0)p.push((d[t]/g)*100);}return p.length?Math.round(p.reduce((a,b)=>a+b)/p.length):null;} | |
| function wkSqlPct(c,w){const d=c.weeks[w];if(!d)return null;const p=[];for(const t of['sql','discovery']){const g=d[t+'Target'];if(g>0)p.push((d[t]/g)*100);}return p.length?Math.round(p.reduce((a,b)=>a+b)/p.length):null;} | |
| function moActPct(c){const l=c.monthlyTotal.activityPctList||[];if(l.length)return Math.round(l.reduce((a,b)=>a+b,0)/l.length);return c.monthlyTotal.activityTarget?Math.round(c.monthlyTotal.activity/c.monthlyTotal.activityTarget*100):null;} | |
| function moSqlPct(c){const l=c.monthlyTotal.sqlPctList||[];if(l.length)return Math.round(l.reduce((a,b)=>a+b,0)/l.length);return c.monthlyTotal.sqlTarget?Math.round(c.monthlyTotal.sql/c.monthlyTotal.sqlTarget*100):null;} | |
| // ===== SCORING ===== | |
| function calcScore(tgts){if(!tgts.length)return 0;const sh=100/tgts.length;const all=tgts.every(t=>(t.actual/t.target)*100>=100);let s=0;tgts.forEach(t=>{const p=(t.actual/t.target)*100;s+=(all?(p/100):Math.min(p/100,1))*sh;});return Math.round(s);} | |
| function calcScorePct(pcts){if(!pcts.length)return 0;const sh=100/pcts.length;const all=pcts.every(p=>p>=100);let s=0;pcts.forEach(p=>{s+=(all?(p/100):Math.min(p/100,1))*sh;});return Math.round(s);} | |
| // ===== RANKINGS ===== | |
| function groupBySDR(){const g={};cases.forEach(c=>{(g[c.gs]=g[c.gs]||[]).push(c);});return g;} | |
| function rankOverall(mo){const g=groupBySDR(),s={};Object.entries(g).forEach(([n,cl])=>{if(mo){const p=[];cl.forEach(c=>{(c.monthlyTotal.sqlPctList||[]).forEach(x=>p.push(x));(c.monthlyTotal.activityPctList||[]).forEach(x=>p.push(x));});s[n]=calcScorePct(p);}else{const t=[];cl.forEach(c=>{const w=c.weeks[currentWeek];if(!w)return;for(const k of['sql','calls','emails','linkedin','prospects','discovery'])if(w[k+'Target']>0)t.push({actual:w[k]||0,target:w[k+'Target']});});s[n]=calcScore(t);}});return Object.entries(s).sort((a,b)=>b[1]-a[1]);} | |
| function rankActivity(mo){const g=groupBySDR(),s={};Object.entries(g).forEach(([n,cl])=>{const p=[];cl.forEach(c=>{const v=mo?moActPct(c):wkActPct(c,currentWeek);if(v!==null)p.push(v);});s[n]=p.length?Math.round(p.reduce((a,b)=>a+b)/p.length):0;});return Object.entries(s).sort((a,b)=>b[1]-a[1]);} | |
| function rankSQL(mo){const g=groupBySDR(),s={};Object.entries(g).forEach(([n,cl])=>{const p=[];cl.forEach(c=>{const v=mo?moSqlPct(c):wkSqlPct(c,currentWeek);if(v!==null)p.push(v);});s[n]=p.length?Math.round(p.reduce((a,b)=>a+b)/p.length):0;});return Object.entries(s).sort((a,b)=>b[1]-a[1]);} | |
| function rankActivityRaw(mo){const g=groupBySDR(),s={};Object.entries(g).forEach(([n,cl])=>{let t=0;cl.forEach(c=>{if(mo){t+=c.monthlyTotal.activity||0;}else{const w=c.weeks[currentWeek];if(w)for(const k of['calls','emails','linkedin','prospects'])t+=w[k]||0;}});s[n]=t;});return Object.entries(s).sort((a,b)=>b[1]-a[1]);} | |
| function rankSQLRaw(mo){const g=groupBySDR(),s={};Object.entries(g).forEach(([n,cl])=>{let t=0;cl.forEach(c=>{if(mo){t+=c.monthlyTotal.sql||0;}else{const w=c.weeks[currentWeek];if(w)t+=(w.sql||0)+(w.discovery||0);}});s[n]=t;});return Object.entries(s).sort((a,b)=>b[1]-a[1]);} | |
| function rankTargetHit(mo){ | |
| if(!achievementsData)return[]; | |
| const actMap={},sqlMap={}; | |
| if(mo){(achievementsData.monthly?.activity||[]).forEach(a=>{if(!actMap[a.sdr])actMap[a.sdr]=a.timestamp;});(achievementsData.monthly?.sql||[]).forEach(a=>{if(!sqlMap[a.sdr])sqlMap[a.sdr]=a.timestamp;});} | |
| else{const wd=achievementsData.weekly?.[String(currentWeek)]||{};(wd.activity||[]).forEach(a=>{if(!actMap[a.sdr])actMap[a.sdr]=a.timestamp;});(wd.sql||[]).forEach(a=>{if(!sqlMap[a.sdr])sqlMap[a.sdr]=a.timestamp;});} | |
| const res=[]; | |
| for(const sdr of Object.keys(actMap)){if(sqlMap[sdr]){const t=actMap[sdr]>sqlMap[sdr]?actMap[sdr]:sqlMap[sdr];res.push([sdr,t]);}} | |
| res.sort((a,b)=>a[1].localeCompare(b[1])); | |
| const days=['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; | |
| return res.map(([n,ts])=>{const d=new Date(ts);return[n,days[d.getDay()]+' '+d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')];}); | |
| } | |
| // ===== MEDALS & STARS ===== | |
| function assignMedals(){const r=rankOverall(true);const m={};['\u{1F947}','\u{1F948}','\u{1F949}'].forEach((e,i)=>{if(r[i])m[r[i][0]]=e;});return m;} | |
| function getStarSDRs(){const g=groupBySDR(),stars=new Set();Object.entries(g).forEach(([n,cl])=>{let ok=true,has=false;cl.forEach(c=>{const w=c.weeks[currentWeek];if(!w)return;for(const t of['sql','calls','emails','linkedin','prospects','discovery']){const tg=w[t+'Target'];if(tg>0){has=true;if((w[t]||0)/tg*100<100)ok=false;}}});if(has&&ok)stars.add(n);});return stars;} | |
| // ===== RENDER TABLE ===== | |
| function renderTable() { | |
| const tb = document.getElementById('caseTable'); tb.innerHTML = ''; | |
| const medals = assignMedals(), stars = getStarSDRs(); | |
| const grp = {}; cases.forEach(c=>{(grp[c.gs]=grp[c.gs]||[]).push(c);}); | |
| const sorted = Object.entries(grp).map(([s,cl])=>({sdr:s,cases:cl})).sort((a,b)=>a.sdr.localeCompare(b.sdr)); | |
| sorted.forEach(({sdr, cases:sc})=>{ | |
| sc.sort((a,b)=>(moSqlPct(b)||0)-(moSqlPct(a)||0)); | |
| sc.forEach((c,i)=>{ | |
| const r=document.createElement('tr'); | |
| if(i===0)r.classList.add('sdr-group-first'); | |
| if(i===sc.length-1)r.classList.add('sdr-group-last'); | |
| let name=sdr; if(medals[sdr])name=medals[sdr]+' '+name; if(stars.has(sdr))name+=' \u2B50'; | |
| r.innerHTML=` | |
| <td class="col-sdr sdr-name-cell ${i===0?'sdr-name-visible':'sdr-name-hidden'}">${i===0?`<div class="sdr-name">${name}</div>`:''}</td> | |
| <td class="col-case"><span class="case-name">${c.case}</span></td> | |
| <td class="col-pct">${pbar(wkSqlPct(c,currentWeek),true)}</td> | |
| <td class="col-pct">${pbar(wkActPct(c,currentWeek),true)}</td> | |
| <td class="col-pct divider-left">${pbar(moSqlPct(c),false)}</td> | |
| <td class="col-pct">${pbar(moActPct(c),false)}</td>`; | |
| tb.appendChild(r); | |
| }); | |
| }); | |
| setTimeout(()=>{document.querySelectorAll('.progress-text[data-value]').forEach(el=>{animateCount(el,parseInt(el.dataset.value),500);});},30); | |
| } | |
| // ===== RENDER LEADERBOARDS ===== | |
| function tier(s){return s>=200?'tier-200':s>=150?'tier-150':s>=100?'tier-100':'';} | |
| function lbCard(title, emoji, data, suffix, cat) { | |
| let items=''; | |
| data.slice(0,5).forEach(([name,sd],i)=>{ | |
| const score=sd+suffix; | |
| const t=tier(typeof sd==='number'?sd:0); | |
| items+=`<li class="lb-item lb-item-${i+1} ${t}"><span class="lb-rank lb-rank-${i<3?i+1:'other'}">${i+1}</span><span class="lb-name">${name}</span><span class="lb-score">${score}</span></li>`; | |
| }); | |
| return `<div class="lb-card cat-${cat}"><h3>${emoji} ${title}</h3><ul class="lb-list">${items}</ul></div>`; | |
| } | |
| function renderLeaderboards() { | |
| const s=document.getElementById('leaderboardSection'); | |
| const w=currentWeek; | |
| let h=''; | |
| // Row 1: Monthly — SQL | Activity | Case Progress | Target | |
| h+=lbCard('SQL Mo','\u{1F3AF}',rankSQLRaw(true),'','sql'); | |
| h+=lbCard('Activity Mo','\u26A1',rankActivityRaw(true),'','activity'); | |
| h+=lbCard('Case Progress Mo','\u{1F3C6}',rankOverall(true),'%','overall'); | |
| h+=lbCard('Target Mo','\u{1F3C5}',rankTargetHit(true),'','target'); | |
| // Row 2: Weekly — SQL | Activity | Case Progress | Target | |
| h+=lbCard(`SQL Wk${w}`,'\u{1F3AF}',rankSQLRaw(false),'','sql'); | |
| h+=lbCard(`Activity Wk${w}`,'\u26A1',rankActivityRaw(false),'','activity'); | |
| h+=lbCard(`Case Progress Wk${w}`,'\u{1F3C6}',rankOverall(false),'%','overall'); | |
| h+=lbCard(`Target Wk${w}`,'\u{1F3C5}',rankTargetHit(false),'','target'); | |
| // Row 3: Sales feed (spans all 4 columns) | |
| h+='<div class="sales-feed" id="salesFeed"></div>'; | |
| s.innerHTML=h; | |
| } | |
| function renderAchieversBanner(){const s=getStarSDRs();const b=document.getElementById('achieversBanner'),n=document.getElementById('achieversNames');if(s.size){n.textContent=Array.from(s).map(x=>x+' \u2B50').join(', ');b.classList.add('visible');}else{b.classList.remove('visible');}} | |
| // ===== SALES FEED ===== | |
| let salesFeedData = []; | |
| function relativeTime(ts){const d=new Date(ts.endsWith('Z')?ts:ts+'Z'),now=new Date(),s=Math.round((now-d)/1000);if(s<60)return'just now';if(s<3600)return Math.floor(s/60)+'m ago';if(s<86400)return Math.floor(s/3600)+'h ago';return Math.floor(s/86400)+'d ago';} | |
| async function fetchSalesFeed(){try{let u='/api/sales-feed?limit=20';if(currentMonth)u+=`&month=${encodeURIComponent(currentMonth)}`;const r=await fetch(u);if(r.ok)salesFeedData=await r.json();}catch(e){}} | |
| function renderSalesFeed(){ | |
| const el=document.getElementById('salesFeed');if(!el)return; | |
| if(!salesFeedData.length){el.innerHTML='<h4>\u{1F4E2} Sales Feed</h4><div class="sales-feed-empty">No SQL registrations yet</div>';return;} | |
| let h='<h4>\u{1F4E2} Sales Feed</h4>'; | |
| salesFeedData.forEach(e=>{h+=`<div class="sales-feed-item"><span class="sales-feed-icon">\u{1F3AF}</span><span class="sales-feed-sdr">${e.sdr}</span><span class="sales-feed-case">${e.case}</span><span class="sales-feed-time">${relativeTime(e.timestamp)}</span></div>`;}); | |
| el.innerHTML=h; | |
| } | |
| // ===== REFRESH ===== | |
| function refreshAll(){if(!dataLoaded)return;renderTable();renderLeaderboards();renderSalesFeed();renderAchieversBanner();} | |
| async function refreshData(){try{await fetch(`/api/invalidate-cache?month=${encodeURIComponent(currentMonth)}`,{method:'POST'});}catch(e){}const u=await fetchFromAPI();await Promise.all([fetchAchievements(),fetchSalesFeed()]);if(u){refreshAll();showSaveIndicator();}} | |
| // ===== INIT ===== | |
| document.getElementById('subtitleWeek').textContent=currentWeek; | |
| soundManager.updateToggleIcon(); | |
| async function initApp(){ | |
| showLoadingState(); await fetchMonths(); | |
| loadFromCache(currentMonth); if(dataLoaded)refreshAll(); | |
| const[u]=await Promise.all([fetchFromAPI(),fetchAchievements(),fetchSalesFeed()]); | |
| if(u)refreshAll(); else if(!dataLoaded)showErrorState('Failed to load data. Check connection and refresh.'); | |
| } | |
| initApp(); | |
| function connectSSE(){const es=new EventSource('/api/events');es.onmessage=async e=>{if(e.data==='refresh'){const u=await fetchFromAPI(true);await Promise.all([fetchAchievements(),fetchSalesFeed()]);if(u){refreshAll();showSaveIndicator();}}};es.onerror=()=>{es.close();setTimeout(connectSSE,5000);};} | |
| connectSSE(); | |
| </script> | |
| </body> | |
| </html> | |