big-screen-2 / static /index.html
Mathias
Larger sales feed text, fix timezone offset in relative times
29e7b6b
Raw
History Blame Contribute Delete
43.5 kB
<!DOCTYPE html>
<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) !important; }
/* ===== 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> &mdash; 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>