/* ============================================================ SpeakFlow AI – admin.js Full Admin Dashboard Logic ============================================================ */ 'use strict'; /* ── Auth Guard ─────────────────────────────────────────────── */ const session = JSON.parse(localStorage.getItem('sf_session') || 'null'); if (!session || !session.loggedIn || !session.isAdmin) { window.location.href = 'login.html'; } /* ── Seed demo data if empty ───────────────────────────────── */ (function seedDemoData() { const users = JSON.parse(localStorage.getItem('sf_users') || '[]'); if (users.length === 0) { const demoUsers = [ { id: 'usr_001', name: 'Alice Johnson', email: 'alice@example.com', password: 'pass123', createdAt: daysAgo(30) }, { id: 'usr_002', name: 'Bob Martinez', email: 'bob@example.com', password: 'pass123', createdAt: daysAgo(25) }, { id: 'usr_003', name: 'Carol Williams', email: 'carol@example.com', password: 'pass123', createdAt: daysAgo(18) }, { id: 'usr_004', name: 'David Chen', email: 'david@example.com', password: 'pass123', createdAt: daysAgo(10) }, { id: 'usr_005', name: 'Eva Brown', email: 'eva@example.com', password: 'pass123', createdAt: daysAgo(4) }, ]; localStorage.setItem('sf_users', JSON.stringify(demoUsers)); } const history = JSON.parse(localStorage.getItem('sf_analysis_history') || '[]'); if (history.length === 0) { const files = ['speech_sample.wav','recording_01.mp3','audio_test.wav','voice_clip.mp3','session_audio.wav']; const results= ['Normal','Stutter Detected','Normal','Normal','Stutter Detected']; const confs = [92, 87, 95, 78, 83]; const stored = JSON.parse(localStorage.getItem('sf_users') || '[]'); const demo = stored.length ? stored : [{ id:'usr_001', name:'Alice Johnson', email:'alice@example.com' }]; const entries = []; for (let i = 0; i < 12; i++) { const u = demo[i % demo.length]; entries.push({ id: 'ana_' + Date.now() + i, userId: u.id, userName: u.name, userEmail: u.email, fileName: files[i % files.length], date: daysAgo(Math.floor(Math.random() * 28)), result: results[i % results.length], confidence: confs[i % confs.length] + Math.floor(Math.random() * 5), }); } localStorage.setItem('sf_analysis_history', JSON.stringify(entries)); } })(); function daysAgo(n) { const d = new Date(); d.setDate(d.getDate() - n); return d.toISOString(); } /* ── DOM helpers ─────────────────────────────────────────── */ const $ = id => document.getElementById(id); const users = () => JSON.parse(localStorage.getItem('sf_users') || '[]'); const analyses = () => JSON.parse(localStorage.getItem('sf_analysis_history') || '[]'); /* ── Clock ───────────────────────────────────────────────── */ function updateClock() { const now = new Date(); $('topbarTime').textContent = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } setInterval(updateClock, 1000); updateClock(); /* ── Sidebar Toggle ──────────────────────────────────────── */ $('sidebarToggle').addEventListener('click', () => { document.getElementById('sidebar').classList.toggle('open'); }); /* ── Logout ──────────────────────────────────────────────── */ $('adminLogout').addEventListener('click', () => { localStorage.removeItem('sf_session'); window.location.href = 'login.html'; }); /* ── Tab Navigation ──────────────────────────────────────── */ const tabTitles = { dashboard: 'Dashboard Overview', users: 'User Management', analyses: 'Analysis History', charts: 'Analytics Charts', }; document.querySelectorAll('.nav-item').forEach(item => { item.addEventListener('click', e => { e.preventDefault(); const tab = item.dataset.tab; document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.querySelectorAll('.tab-section').forEach(s => s.classList.remove('active')); item.classList.add('active'); $('tab-' + tab).classList.add('active'); $('topbarTitle').textContent = tabTitles[tab]; if (tab === 'charts') renderChartsTab(); if (tab === 'users') renderUsersTable(users()); if (tab === 'analyses') renderAnalysesTable(analyses()); }); }); /* ═══════════════════════════════════════════════════════════ DASHBOARD ═══════════════════════════════════════════════════════════ */ function renderDashboard() { const u = users(); const a = analyses(); const normal = a.filter(x => x.result === 'Normal').length; const stutter = a.filter(x => x.result !== 'Normal').length; animateCount('sc-users', u.length); animateCount('sc-analyses', a.length); animateCount('sc-normal', normal); animateCount('sc-stutter', stutter); renderDashPie(normal, stutter); renderDashBar(a); } function animateCount(id, target) { const el = $(id); let current = 0; const step = Math.max(1, Math.ceil(target / 40)); const timer = setInterval(() => { current = Math.min(current + step, target); el.textContent = current; if (current >= target) clearInterval(timer); }, 30); } /* Dash Pie */ let dashPieInst = null; function renderDashPie(normal, stutter) { const ctx = $('dashPieChart').getContext('2d'); if (dashPieInst) dashPieInst.destroy(); dashPieInst = new Chart(ctx, { type: 'doughnut', data: { labels: ['Normal', 'Stutter'], datasets: [{ data: [normal || 1, stutter || 0], backgroundColor: ['#10B981','#F59E0B'], borderWidth: 0, hoverOffset: 8 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: '#94A3B8', font: { size: 12 }, padding: 16 } } } } }); } /* Dash Bar – last 7 days */ let dashBarInst = null; function renderDashBar(a) { const days = []; const counts = []; for (let i = 6; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); const label = d.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' }); days.push(label); const ds = d.toDateString(); counts.push(a.filter(x => new Date(x.date).toDateString() === ds).length); } const ctx = $('dashBarChart').getContext('2d'); if (dashBarInst) dashBarInst.destroy(); dashBarInst = new Chart(ctx, { type: 'bar', data: { labels: days, datasets: [{ label: 'Analyses', data: counts, backgroundColor: 'rgba(59,130,246,0.7)', borderRadius: 8, borderSkipped: false }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#64748B', font: { size: 11 } }, grid: { color: 'rgba(255,255,255,.04)' } }, y: { ticks: { color: '#64748B', stepSize: 1 }, grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true } } } }); } /* ═══════════════════════════════════════════════════════════ USERS TABLE ═══════════════════════════════════════════════════════════ */ function renderUsersTable(list) { const body = $('usersBody'); const empty = $('usersEmpty'); const a = analyses(); body.innerHTML = ''; $('userCount').textContent = list.length + ' user' + (list.length !== 1 ? 's' : ''); if (!list.length) { empty.style.display = 'block'; return; } empty.style.display = 'none'; list.forEach((u, i) => { const userAna = a.filter(x => x.userEmail === u.email).length; const tr = document.createElement('tr'); tr.innerHTML = ` ${i + 1} ${esc(u.name)} ${esc(u.email)} ${new Date(u.createdAt).toLocaleDateString()} ${userAna} `; body.appendChild(tr); }); body.querySelectorAll('.btn-view').forEach(btn => btn.addEventListener('click', () => openUserView(btn.dataset.id))); body.querySelectorAll('.btn-delete').forEach(btn => btn.addEventListener('click', () => deleteUser(btn.dataset.id))); } $('userSearch').addEventListener('input', function () { const q = this.value.toLowerCase(); renderUsersTable(users().filter(u => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q))); }); function deleteUser(id) { if (!confirm('Delete this user? This cannot be undone.')) return; let u = users().filter(x => x.id !== id); localStorage.setItem('sf_users', JSON.stringify(u)); // Also remove their analyses let a = analyses().filter(x => { const del = JSON.parse(localStorage.getItem('sf_users') || '[]'); return x.userId !== id; }); localStorage.setItem('sf_analysis_history', JSON.stringify(analyses().filter(x => x.userId !== id))); renderUsersTable(u); renderDashboard(); } function openUserView(id) { const u = users().find(x => x.id === id); if (!u) return; const a = analyses().filter(x => x.userEmail === u.email); const nb = $('userViewBody'); nb.innerHTML = `
Full Name${esc(u.name)}
Email${esc(u.email)}
Registered${new Date(u.createdAt).toLocaleString()}
Total Analyses${a.length}
Normal Results${a.filter(x=>x.result==='Normal').length}
Stutter Results${a.filter(x=>x.result!=='Normal').length}
`; $('userViewModal').style.display = 'flex'; } $('userViewClose').addEventListener('click', () => { $('userViewModal').style.display = 'none'; }); $('userViewModal').addEventListener('click', function(e) { if (e.target === this) this.style.display = 'none'; }); /* ═══════════════════════════════════════════════════════════ ANALYSES TABLE ═══════════════════════════════════════════════════════════ */ function renderAnalysesTable(list) { const body = $('analysesBody'); const empty = $('analysesEmpty'); body.innerHTML = ''; if (!list.length) { empty.style.display = 'block'; return; } empty.style.display = 'none'; list.forEach((a, i) => { const isNormal = a.result === 'Normal'; const tr = document.createElement('tr'); tr.innerHTML = ` ${i + 1} ${esc(a.userName || 'Unknown')} ${esc(a.fileName || 'N/A')} ${new Date(a.date).toLocaleDateString()} ${esc(a.result)} ${a.confidence || '--'}% `; body.appendChild(tr); }); body.querySelectorAll('.btn-delete').forEach(btn => btn.addEventListener('click', () => deleteAnalysis(btn.dataset.id))); } $('analysisSearch').addEventListener('input', function () { const q = this.value.toLowerCase(); renderAnalysesTable(analyses().filter(a => (a.userName || '').toLowerCase().includes(q) || (a.fileName || '').toLowerCase().includes(q) || (a.result || '').toLowerCase().includes(q) )); }); function deleteAnalysis(id) { if (!confirm('Delete this analysis record?')) return; const updated = analyses().filter(x => x.id !== id); localStorage.setItem('sf_analysis_history', JSON.stringify(updated)); renderAnalysesTable(updated); renderDashboard(); } /* ── Export CSV ──────────────────────────────────────────── */ $('exportCsvBtn').addEventListener('click', () => { const a = analyses(); if (!a.length) { alert('No analysis data to export.'); return; } const header = ['#','User Name','User Email','File Name','Date','Result','Confidence']; const rows = a.map((x, i) => [ i + 1, `"${x.userName || ''}"`, `"${x.userEmail || ''}"`, `"${x.fileName || ''}"`, new Date(x.date).toLocaleString(), x.result, x.confidence + '%' ]); const csv = [header.join(','), ...rows.map(r => r.join(','))].join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'speakflow_analyses.csv'; link.click(); URL.revokeObjectURL(url); }); /* ═══════════════════════════════════════════════════════════ CHARTS TAB ═══════════════════════════════════════════════════════════ */ let chartsRendered = false; let growthInst = null, pieInst = null, activityInst = null; function renderChartsTab() { if (chartsRendered) { chartsRendered = false; } // always re-render fresh const a = analyses(); const u = users(); // 1. User Growth Line Chart const growthCtx = $('userGrowthChart').getContext('2d'); if (growthInst) growthInst.destroy(); const months = getLast6Months(); const growthData = months.map(m => u.filter(x => { const d = new Date(x.createdAt); return d.getFullYear() === m.year && d.getMonth() === m.month; }).length); growthInst = new Chart(growthCtx, { type: 'line', data: { labels: months.map(m => m.label), datasets: [{ label: 'New Users', data: growthData, borderColor: '#3B82F6', backgroundColor: 'rgba(59,130,246,0.12)', fill: true, tension: 0.4, pointBackgroundColor: '#3B82F6', pointRadius: 5 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#94A3B8' } } }, scales: { x: { ticks: { color: '#64748B' }, grid: { color: 'rgba(255,255,255,.04)' } }, y: { ticks: { color: '#64748B', stepSize: 1 }, grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true } } } }); // 2. Detection Pie const normal = a.filter(x => x.result === 'Normal').length; const stutter = a.filter(x => x.result !== 'Normal').length; const pieCtx = $('detectionPieChart').getContext('2d'); if (pieInst) pieInst.destroy(); pieInst = new Chart(pieCtx, { type: 'pie', data: { labels: ['Normal', 'Stutter Detected'], datasets: [{ data: [normal || 1, stutter || 0], backgroundColor: ['#10B981','#F59E0B'], borderWidth: 0, hoverOffset: 10 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: '#94A3B8', padding: 16 } } } } }); // 3. Activity Bar – last 7 days const days = []; const dayCounts = []; for (let i = 6; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); days.push(d.toLocaleDateString([], { weekday: 'short' })); dayCounts.push(a.filter(x => new Date(x.date).toDateString() === d.toDateString()).length); } const actCtx = $('activityChart').getContext('2d'); if (activityInst) activityInst.destroy(); activityInst = new Chart(actCtx, { type: 'bar', data: { labels: days, datasets: [ { label: 'Analyses', data: dayCounts, backgroundColor: 'rgba(20,184,166,0.75)', borderRadius: 8, borderSkipped: false } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#94A3B8' } } }, scales: { x: { ticks: { color: '#64748B' }, grid: { color: 'rgba(255,255,255,.04)' } }, y: { ticks: { color: '#64748B', stepSize: 1 }, grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true } } } }); // 4. System Stats const avg = a.length ? Math.round(a.reduce((s, x) => s + (x.confidence || 0), 0) / a.length) : 0; const sysGrid = $('sysStatsGrid'); sysGrid.innerHTML = `
${u.length}
Registered Users
${a.length}
Total Analyses
${avg}%
Avg Confidence
${stutter}
Stutter Detections
`; chartsRendered = true; } function getLast6Months() { const result = []; for (let i = 5; i >= 0; i--) { const d = new Date(); d.setMonth(d.getMonth() - i); result.push({ year: d.getFullYear(), month: d.getMonth(), label: d.toLocaleDateString([], { month: 'short', year: '2-digit' }) }); } return result; } /* ── Escape HTML ─────────────────────────────────────────── */ function esc(s) { return String(s || '').replace(/&/g,'&').replace(//g,'>'); } /* ── Init ────────────────────────────────────────────────── */ renderDashboard(); renderUsersTable(users()); renderAnalysesTable(analyses());