admin
Browse files- admin.css +150 -0
- admin.html +227 -0
- admin.js +437 -0
admin.css
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================
|
| 2 |
+
SpeakFlow AI β admin.css
|
| 3 |
+
Professional Dark Admin Dashboard
|
| 4 |
+
============================================================ */
|
| 5 |
+
:root{
|
| 6 |
+
--blue:#2563EB;--blue-light:#3B82F6;--blue-dark:#1D4ED8;
|
| 7 |
+
--teal:#14B8A6;--purple:#7C3AED;--green:#10B981;--orange:#F59E0B;--red:#EF4444;
|
| 8 |
+
--bg:#0B1120;--sidebar-bg:#0F172A;--surface:rgba(30,41,59,0.85);
|
| 9 |
+
--surface-2:#1E293B;--surface-3:#263148;
|
| 10 |
+
--border:rgba(148,163,184,0.15);--border-solid:#334155;
|
| 11 |
+
--text:#F1F5F9;--text-muted:#94A3B8;--text-light:#64748B;
|
| 12 |
+
--radius:14px;--radius-lg:20px;
|
| 13 |
+
--shadow:0 4px 20px rgba(0,0,0,.35);--shadow-lg:0 20px 60px rgba(0,0,0,.5);
|
| 14 |
+
--font-display:'Space Grotesk',sans-serif;--font-body:'Inter',sans-serif;
|
| 15 |
+
--tr:.25s ease;
|
| 16 |
+
--sidebar-w:260px;
|
| 17 |
+
}
|
| 18 |
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
| 19 |
+
html{scroll-behavior:smooth;}
|
| 20 |
+
body{font-family:var(--font-body);background:var(--bg);color:var(--text);display:flex;min-height:100vh;overflow-x:hidden;}
|
| 21 |
+
a{color:inherit;text-decoration:none;}
|
| 22 |
+
button{cursor:pointer;font-family:var(--font-body);border:none;background:none;}
|
| 23 |
+
.logo-ai{background:linear-gradient(135deg,#3B82F6,#14B8A6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}
|
| 24 |
+
|
| 25 |
+
/* ββ Sidebar βββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 26 |
+
.sidebar{width:var(--sidebar-w);background:var(--sidebar-bg);border-right:1px solid var(--border);display:flex;flex-direction:column;position:fixed;top:0;left:0;height:100vh;z-index:100;transition:transform var(--tr);}
|
| 27 |
+
.sidebar-logo{display:flex;align-items:center;gap:12px;padding:24px 20px;border-bottom:1px solid var(--border);}
|
| 28 |
+
.sidebar-logo-icon{width:44px;height:44px;background:linear-gradient(135deg,var(--blue),var(--teal));border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:1.2rem;color:#fff;flex-shrink:0;}
|
| 29 |
+
.sidebar-logo-text{font-family:var(--font-display);font-size:1.1rem;font-weight:700;display:block;}
|
| 30 |
+
.sidebar-logo-sub{font-size:.72rem;color:var(--text-muted);margin-top:2px;}
|
| 31 |
+
.sidebar-nav{flex:1;padding:16px 12px;display:flex;flex-direction:column;gap:4px;overflow-y:auto;}
|
| 32 |
+
.nav-item{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:11px;font-size:.9rem;font-weight:500;color:var(--text-muted);transition:all var(--tr);}
|
| 33 |
+
.nav-item i{width:18px;text-align:center;font-size:1rem;}
|
| 34 |
+
.nav-item:hover{background:rgba(255,255,255,.05);color:var(--text);}
|
| 35 |
+
.nav-item.active{background:linear-gradient(135deg,rgba(37,99,235,.25),rgba(20,184,166,.15));color:var(--blue-light);border-left:3px solid var(--blue);}
|
| 36 |
+
.sidebar-footer{padding:16px 20px;border-top:1px solid var(--border);}
|
| 37 |
+
.admin-profile{display:flex;align-items:center;gap:10px;margin-bottom:14px;}
|
| 38 |
+
.admin-avatar{width:38px;height:38px;background:linear-gradient(135deg,var(--purple),var(--blue));border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.95rem;color:#fff;flex-shrink:0;}
|
| 39 |
+
.admin-profile strong{font-size:.88rem;display:block;}
|
| 40 |
+
.admin-profile p{font-size:.73rem;color:var(--text-muted);}
|
| 41 |
+
.btn-logout-sidebar{width:100%;padding:10px;background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.2);border-radius:10px;color:#F87171;font-size:.88rem;font-weight:600;display:flex;align-items:center;justify-content:center;gap:8px;transition:all var(--tr);}
|
| 42 |
+
.btn-logout-sidebar:hover{background:rgba(239,68,68,.2);}
|
| 43 |
+
|
| 44 |
+
/* ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 45 |
+
.admin-main{margin-left:var(--sidebar-w);flex:1;display:flex;flex-direction:column;min-height:100vh;}
|
| 46 |
+
|
| 47 |
+
/* ββ Topbar ββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 48 |
+
.topbar{display:flex;align-items:center;gap:16px;padding:0 28px;height:64px;background:var(--sidebar-bg);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:50;}
|
| 49 |
+
.sidebar-toggle{display:none;background:rgba(255,255,255,.07);border-radius:8px;width:38px;height:38px;align-items:center;justify-content:center;font-size:1rem;color:var(--text-muted);}
|
| 50 |
+
.topbar-title{font-family:var(--font-display);font-size:1rem;font-weight:600;flex:1;}
|
| 51 |
+
.topbar-right{display:flex;align-items:center;gap:14px;}
|
| 52 |
+
.topbar-badge{background:rgba(124,58,237,.2);color:#A78BFA;padding:5px 12px;border-radius:50px;font-size:.78rem;font-weight:700;display:flex;align-items:center;gap:6px;}
|
| 53 |
+
.topbar-time{font-size:.82rem;color:var(--text-muted);}
|
| 54 |
+
|
| 55 |
+
/* ββ Tab Sections ββββββββββββββββββββββββββββββββββββββββββ */
|
| 56 |
+
.tab-section{display:none;padding:28px;animation:fadeIn .3s ease;}
|
| 57 |
+
.tab-section.active{display:block;}
|
| 58 |
+
@keyframes fadeIn{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}}
|
| 59 |
+
.section-heading{margin-bottom:24px;}
|
| 60 |
+
.section-heading h2{font-family:var(--font-display);font-size:1.35rem;font-weight:700;display:flex;align-items:center;gap:10px;margin-bottom:4px;}
|
| 61 |
+
.section-heading p{color:var(--text-muted);font-size:.88rem;}
|
| 62 |
+
|
| 63 |
+
/* ββ Stats Cards βββββββββββββββββββββββββββββββββββββββββββ */
|
| 64 |
+
.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:24px;}
|
| 65 |
+
.stat-card{background:var(--surface-2);border:1px solid var(--border-solid);border-radius:var(--radius);padding:20px;display:flex;align-items:center;gap:16px;position:relative;overflow:hidden;transition:transform var(--tr),box-shadow var(--tr);}
|
| 66 |
+
.stat-card::before{content:'';position:absolute;inset:0;opacity:.07;border-radius:var(--radius);}
|
| 67 |
+
.stat-card:hover{transform:translateY(-3px);box-shadow:var(--shadow);}
|
| 68 |
+
.card-blue .stat-icon{background:rgba(37,99,235,.2);color:var(--blue-light);}
|
| 69 |
+
.card-blue::before{background:var(--blue);}
|
| 70 |
+
.card-teal .stat-icon{background:rgba(20,184,166,.2);color:var(--teal);}
|
| 71 |
+
.card-teal::before{background:var(--teal);}
|
| 72 |
+
.card-green .stat-icon{background:rgba(16,185,129,.2);color:var(--green);}
|
| 73 |
+
.card-green::before{background:var(--green);}
|
| 74 |
+
.card-orange .stat-icon{background:rgba(245,158,11,.2);color:var(--orange);}
|
| 75 |
+
.card-orange::before{background:var(--orange);}
|
| 76 |
+
.stat-icon{width:50px;height:50px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:1.3rem;flex-shrink:0;}
|
| 77 |
+
.stat-number{font-family:var(--font-display);font-size:1.8rem;font-weight:700;line-height:1;}
|
| 78 |
+
.stat-label{font-size:.8rem;color:var(--text-muted);margin-top:4px;}
|
| 79 |
+
|
| 80 |
+
/* ββ Charts ββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 81 |
+
.quick-charts{display:grid;grid-template-columns:1fr 2fr;gap:16px;}
|
| 82 |
+
.charts-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;}
|
| 83 |
+
.chart-card{background:var(--surface-2);border:1px solid var(--border-solid);border-radius:var(--radius);padding:20px;}
|
| 84 |
+
.chart-wide{grid-column:span 1;}
|
| 85 |
+
.chart-full{grid-column:1/-1;}
|
| 86 |
+
.chart-title{font-family:var(--font-display);font-size:.95rem;font-weight:600;margin-bottom:16px;display:flex;align-items:center;gap:8px;color:var(--text);}
|
| 87 |
+
.chart-title i{color:var(--blue-light);}
|
| 88 |
+
.chart-wrap{position:relative;height:220px;display:flex;align-items:center;justify-content:center;}
|
| 89 |
+
.chart-tall{height:280px;}
|
| 90 |
+
|
| 91 |
+
/* ββ Table βββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 92 |
+
.table-toolbar{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap;}
|
| 93 |
+
.search-box{display:flex;align-items:center;gap:10px;background:var(--surface-2);border:1px solid var(--border-solid);border-radius:10px;padding:10px 14px;flex:1;min-width:200px;}
|
| 94 |
+
.search-box i{color:var(--text-muted);}
|
| 95 |
+
.search-box input{background:none;border:none;outline:none;color:var(--text);font-size:.9rem;font-family:var(--font-body);width:100%;}
|
| 96 |
+
.search-box input::placeholder{color:var(--text-light);}
|
| 97 |
+
.count-badge{background:rgba(37,99,235,.15);color:var(--blue-light);padding:6px 14px;border-radius:50px;font-size:.8rem;font-weight:600;white-space:nowrap;}
|
| 98 |
+
.btn-export{background:linear-gradient(135deg,var(--green),#059669);color:#fff;padding:10px 18px;border-radius:10px;font-size:.85rem;font-weight:700;display:flex;align-items:center;gap:7px;transition:all var(--tr);}
|
| 99 |
+
.btn-export:hover{transform:translateY(-2px);box-shadow:0 4px 16px rgba(16,185,129,.3);}
|
| 100 |
+
.table-wrap{background:var(--surface-2);border:1px solid var(--border-solid);border-radius:var(--radius);overflow:auto;}
|
| 101 |
+
.admin-table{width:100%;border-collapse:collapse;}
|
| 102 |
+
.admin-table th{padding:13px 16px;text-align:left;font-size:.78rem;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--border-solid);background:rgba(255,255,255,.02);white-space:nowrap;}
|
| 103 |
+
.admin-table td{padding:13px 16px;font-size:.88rem;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle;}
|
| 104 |
+
.admin-table tbody tr:last-child td{border-bottom:none;}
|
| 105 |
+
.admin-table tbody tr:hover td{background:rgba(255,255,255,.03);}
|
| 106 |
+
.table-empty{text-align:center;padding:48px;color:var(--text-muted);}
|
| 107 |
+
.table-empty i{font-size:2rem;margin-bottom:12px;opacity:.4;}
|
| 108 |
+
.table-empty p{font-size:.9rem;}
|
| 109 |
+
|
| 110 |
+
/* ββ Badges ββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 111 |
+
.badge{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:50px;font-size:.75rem;font-weight:700;}
|
| 112 |
+
.badge-green{background:rgba(16,185,129,.15);color:var(--green);}
|
| 113 |
+
.badge-orange{background:rgba(245,158,11,.15);color:var(--orange);}
|
| 114 |
+
.badge-blue{background:rgba(37,99,235,.15);color:var(--blue-light);}
|
| 115 |
+
|
| 116 |
+
/* ββ Action Buttons βββββββββββββββββοΏ½οΏ½ββββββββββββββββββββββ */
|
| 117 |
+
.btn-tbl{padding:5px 10px;border-radius:7px;font-size:.78rem;font-weight:600;display:inline-flex;align-items:center;gap:5px;transition:all var(--tr);}
|
| 118 |
+
.btn-view{background:rgba(37,99,235,.15);color:var(--blue-light);}
|
| 119 |
+
.btn-view:hover{background:rgba(37,99,235,.3);}
|
| 120 |
+
.btn-delete{background:rgba(239,68,68,.12);color:#F87171;}
|
| 121 |
+
.btn-delete:hover{background:rgba(239,68,68,.25);}
|
| 122 |
+
|
| 123 |
+
/* ββ System Stats ββββββββββββββββββββββββββββββββββββββββββ */
|
| 124 |
+
.sys-stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;}
|
| 125 |
+
.sys-stat-item{background:var(--surface-3);border-radius:12px;padding:16px;text-align:center;}
|
| 126 |
+
.sys-stat-val{font-family:var(--font-display);font-size:1.4rem;font-weight:700;margin-bottom:4px;}
|
| 127 |
+
.sys-stat-lbl{font-size:.75rem;color:var(--text-muted);}
|
| 128 |
+
|
| 129 |
+
/* ββ Modal βββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 130 |
+
.modal-overlay{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:16px;}
|
| 131 |
+
.modal-box{background:var(--surface-2);border:1px solid var(--border-solid);border-radius:var(--radius-lg);padding:28px;width:100%;max-width:460px;box-shadow:var(--shadow-lg);}
|
| 132 |
+
.modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;}
|
| 133 |
+
.modal-header h3{font-size:1.05rem;font-weight:700;display:flex;align-items:center;gap:9px;}
|
| 134 |
+
.modal-close{background:rgba(255,255,255,.07);border:none;color:var(--text-muted);width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;}
|
| 135 |
+
.modal-close:hover{background:rgba(255,255,255,.14);}
|
| 136 |
+
.user-detail-row{display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border-solid);font-size:.9rem;}
|
| 137 |
+
.user-detail-row:last-child{border-bottom:none;}
|
| 138 |
+
.user-detail-row span:first-child{color:var(--text-muted);}
|
| 139 |
+
|
| 140 |
+
/* ββ Responsive ββββββββββββββββββββββββββββββββββββββββββββ */
|
| 141 |
+
@media(max-width:1100px){.stats-grid{grid-template-columns:repeat(2,1fr);}
|
| 142 |
+
.quick-charts,.charts-grid{grid-template-columns:1fr;}.chart-wide,.chart-full{grid-column:1;}
|
| 143 |
+
.sys-stats-grid{grid-template-columns:repeat(2,1fr);}}
|
| 144 |
+
@media(max-width:768px){
|
| 145 |
+
.sidebar{transform:translateX(-100%);}.sidebar.open{transform:translateX(0);}
|
| 146 |
+
.admin-main{margin-left:0;}.sidebar-toggle{display:flex;}
|
| 147 |
+
.tab-section{padding:16px;}.stats-grid{grid-template-columns:1fr 1fr;}
|
| 148 |
+
.topbar{padding:0 16px;}
|
| 149 |
+
}
|
| 150 |
+
@media(max-width:480px){.stats-grid{grid-template-columns:1fr;}.sys-stats-grid{grid-template-columns:1fr 1fr;}}
|
admin.html
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="dark">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Admin Dashboard β SpeakFlow AI</title>
|
| 7 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
| 9 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
|
| 10 |
+
<link rel="stylesheet" href="admin.css" />
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
|
| 14 |
+
<!-- ββ Sidebar ββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 15 |
+
<aside class="sidebar" id="sidebar">
|
| 16 |
+
<div class="sidebar-logo">
|
| 17 |
+
<div class="sidebar-logo-icon"><i class="fas fa-wave-square"></i></div>
|
| 18 |
+
<div>
|
| 19 |
+
<span class="sidebar-logo-text">SpeakFlow <span class="logo-ai">AI</span></span>
|
| 20 |
+
<p class="sidebar-logo-sub">Admin Panel</p>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
|
| 24 |
+
<nav class="sidebar-nav">
|
| 25 |
+
<a href="#" class="nav-item active" data-tab="dashboard">
|
| 26 |
+
<i class="fas fa-tachometer-alt"></i> <span>Dashboard</span>
|
| 27 |
+
</a>
|
| 28 |
+
<a href="#" class="nav-item" data-tab="users">
|
| 29 |
+
<i class="fas fa-users"></i> <span>User Management</span>
|
| 30 |
+
</a>
|
| 31 |
+
<a href="#" class="nav-item" data-tab="analyses">
|
| 32 |
+
<i class="fas fa-history"></i> <span>Analysis History</span>
|
| 33 |
+
</a>
|
| 34 |
+
<a href="#" class="nav-item" data-tab="charts">
|
| 35 |
+
<i class="fas fa-chart-line"></i> <span>Analytics Charts</span>
|
| 36 |
+
</a>
|
| 37 |
+
</nav>
|
| 38 |
+
|
| 39 |
+
<div class="sidebar-footer">
|
| 40 |
+
<div class="admin-profile">
|
| 41 |
+
<div class="admin-avatar"><i class="fas fa-shield-alt"></i></div>
|
| 42 |
+
<div>
|
| 43 |
+
<strong>Administrator</strong>
|
| 44 |
+
<p>admin@speakflow.ai</p>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
<button class="btn-logout-sidebar" id="adminLogout">
|
| 48 |
+
<i class="fas fa-sign-out-alt"></i> Logout
|
| 49 |
+
</button>
|
| 50 |
+
</div>
|
| 51 |
+
</aside>
|
| 52 |
+
|
| 53 |
+
<!-- ββ Main Content βββββββββββββββββββββββββββββββββββββββ -->
|
| 54 |
+
<main class="admin-main">
|
| 55 |
+
|
| 56 |
+
<!-- Top Bar -->
|
| 57 |
+
<div class="topbar">
|
| 58 |
+
<button class="sidebar-toggle" id="sidebarToggle"><i class="fas fa-bars"></i></button>
|
| 59 |
+
<div class="topbar-title" id="topbarTitle">Dashboard Overview</div>
|
| 60 |
+
<div class="topbar-right">
|
| 61 |
+
<div class="topbar-badge"><i class="fas fa-shield-alt"></i> Admin</div>
|
| 62 |
+
<div class="topbar-time" id="topbarTime"></div>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<!-- βββ DASHBOARD TAB ββββββββββββββββββββββββββββββββββββ -->
|
| 67 |
+
<section class="tab-section active" id="tab-dashboard">
|
| 68 |
+
<div class="section-heading">
|
| 69 |
+
<h2><i class="fas fa-tachometer-alt"></i> Dashboard Overview</h2>
|
| 70 |
+
<p>Real-time platform statistics and system health.</p>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<!-- Stats Cards -->
|
| 74 |
+
<div class="stats-grid" id="statsGrid">
|
| 75 |
+
<div class="stat-card card-blue">
|
| 76 |
+
<div class="stat-icon"><i class="fas fa-users"></i></div>
|
| 77 |
+
<div class="stat-body">
|
| 78 |
+
<div class="stat-number" id="sc-users">0</div>
|
| 79 |
+
<div class="stat-label">Total Users</div>
|
| 80 |
+
</div>
|
| 81 |
+
<div class="stat-trend" id="st-users"></div>
|
| 82 |
+
</div>
|
| 83 |
+
<div class="stat-card card-teal">
|
| 84 |
+
<div class="stat-icon"><i class="fas fa-brain"></i></div>
|
| 85 |
+
<div class="stat-body">
|
| 86 |
+
<div class="stat-number" id="sc-analyses">0</div>
|
| 87 |
+
<div class="stat-label">Total Analyses</div>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="stat-card card-green">
|
| 91 |
+
<div class="stat-icon"><i class="fas fa-check-circle"></i></div>
|
| 92 |
+
<div class="stat-body">
|
| 93 |
+
<div class="stat-number" id="sc-normal">0</div>
|
| 94 |
+
<div class="stat-label">Normal Detections</div>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
<div class="stat-card card-orange">
|
| 98 |
+
<div class="stat-icon"><i class="fas fa-exclamation-triangle"></i></div>
|
| 99 |
+
<div class="stat-body">
|
| 100 |
+
<div class="stat-number" id="sc-stutter">0</div>
|
| 101 |
+
<div class="stat-label">Stutter Detections</div>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<!-- Quick Charts Row -->
|
| 107 |
+
<div class="quick-charts">
|
| 108 |
+
<div class="chart-card">
|
| 109 |
+
<h3 class="chart-title"><i class="fas fa-chart-pie"></i> Detection Ratio</h3>
|
| 110 |
+
<div class="chart-wrap"><canvas id="dashPieChart"></canvas></div>
|
| 111 |
+
</div>
|
| 112 |
+
<div class="chart-card chart-wide">
|
| 113 |
+
<h3 class="chart-title"><i class="fas fa-chart-bar"></i> Recent Activity (Last 7 Days)</h3>
|
| 114 |
+
<div class="chart-wrap"><canvas id="dashBarChart"></canvas></div>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</section>
|
| 118 |
+
|
| 119 |
+
<!-- βββ USERS TAB ββββββββββββββββββββββββββββββββββββββββ -->
|
| 120 |
+
<section class="tab-section" id="tab-users">
|
| 121 |
+
<div class="section-heading">
|
| 122 |
+
<h2><i class="fas fa-users"></i> User Management</h2>
|
| 123 |
+
<p>View, search, and manage registered users.</p>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="table-toolbar">
|
| 126 |
+
<div class="search-box">
|
| 127 |
+
<i class="fas fa-search"></i>
|
| 128 |
+
<input type="text" id="userSearch" placeholder="Search by name or email..." />
|
| 129 |
+
</div>
|
| 130 |
+
<span id="userCount" class="count-badge">0 users</span>
|
| 131 |
+
</div>
|
| 132 |
+
<div class="table-wrap">
|
| 133 |
+
<table class="admin-table" id="usersTable">
|
| 134 |
+
<thead>
|
| 135 |
+
<tr>
|
| 136 |
+
<th>#</th>
|
| 137 |
+
<th>Name</th>
|
| 138 |
+
<th>Email</th>
|
| 139 |
+
<th>Registered</th>
|
| 140 |
+
<th>Analyses</th>
|
| 141 |
+
<th>Actions</th>
|
| 142 |
+
</tr>
|
| 143 |
+
</thead>
|
| 144 |
+
<tbody id="usersBody"></tbody>
|
| 145 |
+
</table>
|
| 146 |
+
<div class="table-empty" id="usersEmpty" style="display:none;">
|
| 147 |
+
<i class="fas fa-user-slash"></i><p>No users found.</p>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</section>
|
| 151 |
+
|
| 152 |
+
<!-- βββ ANALYSES TAB βββββββββββββββββββββββββββββββββββββ -->
|
| 153 |
+
<section class="tab-section" id="tab-analyses">
|
| 154 |
+
<div class="section-heading">
|
| 155 |
+
<h2><i class="fas fa-history"></i> Analysis History</h2>
|
| 156 |
+
<p>All speech analyses performed across all users.</p>
|
| 157 |
+
</div>
|
| 158 |
+
<div class="table-toolbar">
|
| 159 |
+
<div class="search-box">
|
| 160 |
+
<i class="fas fa-search"></i>
|
| 161 |
+
<input type="text" id="analysisSearch" placeholder="Search by user, file, or result..." />
|
| 162 |
+
</div>
|
| 163 |
+
<button class="btn-export" id="exportCsvBtn"><i class="fas fa-file-csv"></i> Export CSV</button>
|
| 164 |
+
</div>
|
| 165 |
+
<div class="table-wrap">
|
| 166 |
+
<table class="admin-table" id="analysesTable">
|
| 167 |
+
<thead>
|
| 168 |
+
<tr>
|
| 169 |
+
<th>#</th>
|
| 170 |
+
<th>User</th>
|
| 171 |
+
<th>File Name</th>
|
| 172 |
+
<th>Date</th>
|
| 173 |
+
<th>Result</th>
|
| 174 |
+
<th>Confidence</th>
|
| 175 |
+
<th>Actions</th>
|
| 176 |
+
</tr>
|
| 177 |
+
</thead>
|
| 178 |
+
<tbody id="analysesBody"></tbody>
|
| 179 |
+
</table>
|
| 180 |
+
<div class="table-empty" id="analysesEmpty" style="display:none;">
|
| 181 |
+
<i class="fas fa-history"></i><p>No analyses found.</p>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</section>
|
| 185 |
+
|
| 186 |
+
<!-- βββ CHARTS TAB ββββββββββββββββββββββββββββββββββββββββ -->
|
| 187 |
+
<section class="tab-section" id="tab-charts">
|
| 188 |
+
<div class="section-heading">
|
| 189 |
+
<h2><i class="fas fa-chart-line"></i> Analytics Charts</h2>
|
| 190 |
+
<p>Visual insights into platform usage and detection trends.</p>
|
| 191 |
+
</div>
|
| 192 |
+
<div class="charts-grid">
|
| 193 |
+
<div class="chart-card chart-full">
|
| 194 |
+
<h3 class="chart-title"><i class="fas fa-chart-line"></i> User Growth Over Time</h3>
|
| 195 |
+
<div class="chart-wrap chart-tall"><canvas id="userGrowthChart"></canvas></div>
|
| 196 |
+
</div>
|
| 197 |
+
<div class="chart-card">
|
| 198 |
+
<h3 class="chart-title"><i class="fas fa-chart-pie"></i> Normal vs Stutter</h3>
|
| 199 |
+
<div class="chart-wrap"><canvas id="detectionPieChart"></canvas></div>
|
| 200 |
+
</div>
|
| 201 |
+
<div class="chart-card">
|
| 202 |
+
<h3 class="chart-title"><i class="fas fa-chart-bar"></i> Analysis Activity</h3>
|
| 203 |
+
<div class="chart-wrap"><canvas id="activityChart"></canvas></div>
|
| 204 |
+
</div>
|
| 205 |
+
<div class="chart-card chart-full">
|
| 206 |
+
<h3 class="chart-title"><i class="fas fa-server"></i> System Statistics</h3>
|
| 207 |
+
<div class="sys-stats-grid" id="sysStatsGrid"></div>
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
</section>
|
| 211 |
+
|
| 212 |
+
</main>
|
| 213 |
+
|
| 214 |
+
<!-- User View Modal -->
|
| 215 |
+
<div id="userViewModal" class="modal-overlay" style="display:none;">
|
| 216 |
+
<div class="modal-box glass-card">
|
| 217 |
+
<div class="modal-header">
|
| 218 |
+
<h3><i class="fas fa-user"></i> User Details</h3>
|
| 219 |
+
<button class="modal-close" id="userViewClose"><i class="fas fa-times"></i></button>
|
| 220 |
+
</div>
|
| 221 |
+
<div id="userViewBody"></div>
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
|
| 225 |
+
<script src="admin.js"></script>
|
| 226 |
+
</body>
|
| 227 |
+
</html>
|
admin.js
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================
|
| 2 |
+
SpeakFlow AI β admin.js
|
| 3 |
+
Full Admin Dashboard Logic
|
| 4 |
+
============================================================ */
|
| 5 |
+
'use strict';
|
| 6 |
+
|
| 7 |
+
/* ββ Auth Guard βββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 8 |
+
const session = JSON.parse(localStorage.getItem('sf_session') || 'null');
|
| 9 |
+
if (!session || !session.loggedIn || !session.isAdmin) {
|
| 10 |
+
window.location.href = 'login.html';
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
/* ββ Seed demo data if empty βββββββββββββββββββββββββββββββββ */
|
| 14 |
+
(function seedDemoData() {
|
| 15 |
+
const users = JSON.parse(localStorage.getItem('sf_users') || '[]');
|
| 16 |
+
if (users.length === 0) {
|
| 17 |
+
const demoUsers = [
|
| 18 |
+
{ id: 'usr_001', name: 'Alice Johnson', email: 'alice@example.com', password: 'pass123', createdAt: daysAgo(30) },
|
| 19 |
+
{ id: 'usr_002', name: 'Bob Martinez', email: 'bob@example.com', password: 'pass123', createdAt: daysAgo(25) },
|
| 20 |
+
{ id: 'usr_003', name: 'Carol Williams', email: 'carol@example.com', password: 'pass123', createdAt: daysAgo(18) },
|
| 21 |
+
{ id: 'usr_004', name: 'David Chen', email: 'david@example.com', password: 'pass123', createdAt: daysAgo(10) },
|
| 22 |
+
{ id: 'usr_005', name: 'Eva Brown', email: 'eva@example.com', password: 'pass123', createdAt: daysAgo(4) },
|
| 23 |
+
];
|
| 24 |
+
localStorage.setItem('sf_users', JSON.stringify(demoUsers));
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const history = JSON.parse(localStorage.getItem('sf_analysis_history') || '[]');
|
| 28 |
+
if (history.length === 0) {
|
| 29 |
+
const files = ['speech_sample.wav','recording_01.mp3','audio_test.wav','voice_clip.mp3','session_audio.wav'];
|
| 30 |
+
const results= ['Normal','Stutter Detected','Normal','Normal','Stutter Detected'];
|
| 31 |
+
const confs = [92, 87, 95, 78, 83];
|
| 32 |
+
const stored = JSON.parse(localStorage.getItem('sf_users') || '[]');
|
| 33 |
+
const demo = stored.length ? stored : [{ id:'usr_001', name:'Alice Johnson', email:'alice@example.com' }];
|
| 34 |
+
const entries = [];
|
| 35 |
+
for (let i = 0; i < 12; i++) {
|
| 36 |
+
const u = demo[i % demo.length];
|
| 37 |
+
entries.push({
|
| 38 |
+
id: 'ana_' + Date.now() + i,
|
| 39 |
+
userId: u.id,
|
| 40 |
+
userName: u.name,
|
| 41 |
+
userEmail: u.email,
|
| 42 |
+
fileName: files[i % files.length],
|
| 43 |
+
date: daysAgo(Math.floor(Math.random() * 28)),
|
| 44 |
+
result: results[i % results.length],
|
| 45 |
+
confidence: confs[i % confs.length] + Math.floor(Math.random() * 5),
|
| 46 |
+
});
|
| 47 |
+
}
|
| 48 |
+
localStorage.setItem('sf_analysis_history', JSON.stringify(entries));
|
| 49 |
+
}
|
| 50 |
+
})();
|
| 51 |
+
|
| 52 |
+
function daysAgo(n) {
|
| 53 |
+
const d = new Date();
|
| 54 |
+
d.setDate(d.getDate() - n);
|
| 55 |
+
return d.toISOString();
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/* ββ DOM helpers βββββββββββββββββββββββββββββββββββββββββββ */
|
| 59 |
+
const $ = id => document.getElementById(id);
|
| 60 |
+
const users = () => JSON.parse(localStorage.getItem('sf_users') || '[]');
|
| 61 |
+
const analyses = () => JSON.parse(localStorage.getItem('sf_analysis_history') || '[]');
|
| 62 |
+
|
| 63 |
+
/* ββ Clock βββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 64 |
+
function updateClock() {
|
| 65 |
+
const now = new Date();
|
| 66 |
+
$('topbarTime').textContent = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
| 67 |
+
}
|
| 68 |
+
setInterval(updateClock, 1000);
|
| 69 |
+
updateClock();
|
| 70 |
+
|
| 71 |
+
/* ββ Sidebar Toggle ββββββββββββββββββββββββββββββββββββββββ */
|
| 72 |
+
$('sidebarToggle').addEventListener('click', () => {
|
| 73 |
+
document.getElementById('sidebar').classList.toggle('open');
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
/* ββ Logout ββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 77 |
+
$('adminLogout').addEventListener('click', () => {
|
| 78 |
+
localStorage.removeItem('sf_session');
|
| 79 |
+
window.location.href = 'login.html';
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
/* ββ Tab Navigation ββββββββββββββββββββββββββββββββββββββββ */
|
| 83 |
+
const tabTitles = {
|
| 84 |
+
dashboard: 'Dashboard Overview',
|
| 85 |
+
users: 'User Management',
|
| 86 |
+
analyses: 'Analysis History',
|
| 87 |
+
charts: 'Analytics Charts',
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
document.querySelectorAll('.nav-item').forEach(item => {
|
| 91 |
+
item.addEventListener('click', e => {
|
| 92 |
+
e.preventDefault();
|
| 93 |
+
const tab = item.dataset.tab;
|
| 94 |
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
| 95 |
+
document.querySelectorAll('.tab-section').forEach(s => s.classList.remove('active'));
|
| 96 |
+
item.classList.add('active');
|
| 97 |
+
$('tab-' + tab).classList.add('active');
|
| 98 |
+
$('topbarTitle').textContent = tabTitles[tab];
|
| 99 |
+
if (tab === 'charts') renderChartsTab();
|
| 100 |
+
if (tab === 'users') renderUsersTable(users());
|
| 101 |
+
if (tab === 'analyses') renderAnalysesTable(analyses());
|
| 102 |
+
});
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
+
DASHBOARD
|
| 107 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 108 |
+
function renderDashboard() {
|
| 109 |
+
const u = users();
|
| 110 |
+
const a = analyses();
|
| 111 |
+
const normal = a.filter(x => x.result === 'Normal').length;
|
| 112 |
+
const stutter = a.filter(x => x.result !== 'Normal').length;
|
| 113 |
+
|
| 114 |
+
animateCount('sc-users', u.length);
|
| 115 |
+
animateCount('sc-analyses', a.length);
|
| 116 |
+
animateCount('sc-normal', normal);
|
| 117 |
+
animateCount('sc-stutter', stutter);
|
| 118 |
+
|
| 119 |
+
renderDashPie(normal, stutter);
|
| 120 |
+
renderDashBar(a);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
function animateCount(id, target) {
|
| 124 |
+
const el = $(id);
|
| 125 |
+
let current = 0;
|
| 126 |
+
const step = Math.max(1, Math.ceil(target / 40));
|
| 127 |
+
const timer = setInterval(() => {
|
| 128 |
+
current = Math.min(current + step, target);
|
| 129 |
+
el.textContent = current;
|
| 130 |
+
if (current >= target) clearInterval(timer);
|
| 131 |
+
}, 30);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/* Dash Pie */
|
| 135 |
+
let dashPieInst = null;
|
| 136 |
+
function renderDashPie(normal, stutter) {
|
| 137 |
+
const ctx = $('dashPieChart').getContext('2d');
|
| 138 |
+
if (dashPieInst) dashPieInst.destroy();
|
| 139 |
+
dashPieInst = new Chart(ctx, {
|
| 140 |
+
type: 'doughnut',
|
| 141 |
+
data: {
|
| 142 |
+
labels: ['Normal', 'Stutter'],
|
| 143 |
+
datasets: [{ data: [normal || 1, stutter || 0], backgroundColor: ['#10B981','#F59E0B'], borderWidth: 0, hoverOffset: 8 }]
|
| 144 |
+
},
|
| 145 |
+
options: {
|
| 146 |
+
responsive: true, maintainAspectRatio: false,
|
| 147 |
+
plugins: { legend: { position: 'bottom', labels: { color: '#94A3B8', font: { size: 12 }, padding: 16 } } }
|
| 148 |
+
}
|
| 149 |
+
});
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/* Dash Bar β last 7 days */
|
| 153 |
+
let dashBarInst = null;
|
| 154 |
+
function renderDashBar(a) {
|
| 155 |
+
const days = [];
|
| 156 |
+
const counts = [];
|
| 157 |
+
for (let i = 6; i >= 0; i--) {
|
| 158 |
+
const d = new Date();
|
| 159 |
+
d.setDate(d.getDate() - i);
|
| 160 |
+
const label = d.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' });
|
| 161 |
+
days.push(label);
|
| 162 |
+
const ds = d.toDateString();
|
| 163 |
+
counts.push(a.filter(x => new Date(x.date).toDateString() === ds).length);
|
| 164 |
+
}
|
| 165 |
+
const ctx = $('dashBarChart').getContext('2d');
|
| 166 |
+
if (dashBarInst) dashBarInst.destroy();
|
| 167 |
+
dashBarInst = new Chart(ctx, {
|
| 168 |
+
type: 'bar',
|
| 169 |
+
data: {
|
| 170 |
+
labels: days,
|
| 171 |
+
datasets: [{ label: 'Analyses', data: counts, backgroundColor: 'rgba(59,130,246,0.7)', borderRadius: 8, borderSkipped: false }]
|
| 172 |
+
},
|
| 173 |
+
options: {
|
| 174 |
+
responsive: true, maintainAspectRatio: false,
|
| 175 |
+
plugins: { legend: { display: false } },
|
| 176 |
+
scales: {
|
| 177 |
+
x: { ticks: { color: '#64748B', font: { size: 11 } }, grid: { color: 'rgba(255,255,255,.04)' } },
|
| 178 |
+
y: { ticks: { color: '#64748B', stepSize: 1 }, grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true }
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
});
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 185 |
+
USERS TABLE
|
| 186 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 187 |
+
function renderUsersTable(list) {
|
| 188 |
+
const body = $('usersBody');
|
| 189 |
+
const empty = $('usersEmpty');
|
| 190 |
+
const a = analyses();
|
| 191 |
+
body.innerHTML = '';
|
| 192 |
+
$('userCount').textContent = list.length + ' user' + (list.length !== 1 ? 's' : '');
|
| 193 |
+
|
| 194 |
+
if (!list.length) { empty.style.display = 'block'; return; }
|
| 195 |
+
empty.style.display = 'none';
|
| 196 |
+
|
| 197 |
+
list.forEach((u, i) => {
|
| 198 |
+
const userAna = a.filter(x => x.userEmail === u.email).length;
|
| 199 |
+
const tr = document.createElement('tr');
|
| 200 |
+
tr.innerHTML = `
|
| 201 |
+
<td><span style="color:var(--text-muted)">${i + 1}</span></td>
|
| 202 |
+
<td><strong>${esc(u.name)}</strong></td>
|
| 203 |
+
<td style="color:var(--text-muted)">${esc(u.email)}</td>
|
| 204 |
+
<td style="color:var(--text-muted)">${new Date(u.createdAt).toLocaleDateString()}</td>
|
| 205 |
+
<td><span class="badge badge-blue">${userAna}</span></td>
|
| 206 |
+
<td style="display:flex;gap:6px;flex-wrap:wrap;">
|
| 207 |
+
<button class="btn-tbl btn-view" data-id="${u.id}"><i class="fas fa-eye"></i> View</button>
|
| 208 |
+
<button class="btn-tbl btn-delete" data-id="${u.id}"><i class="fas fa-trash"></i> Delete</button>
|
| 209 |
+
</td>`;
|
| 210 |
+
body.appendChild(tr);
|
| 211 |
+
});
|
| 212 |
+
|
| 213 |
+
body.querySelectorAll('.btn-view').forEach(btn => btn.addEventListener('click', () => openUserView(btn.dataset.id)));
|
| 214 |
+
body.querySelectorAll('.btn-delete').forEach(btn => btn.addEventListener('click', () => deleteUser(btn.dataset.id)));
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
$('userSearch').addEventListener('input', function () {
|
| 218 |
+
const q = this.value.toLowerCase();
|
| 219 |
+
renderUsersTable(users().filter(u => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)));
|
| 220 |
+
});
|
| 221 |
+
|
| 222 |
+
function deleteUser(id) {
|
| 223 |
+
if (!confirm('Delete this user? This cannot be undone.')) return;
|
| 224 |
+
let u = users().filter(x => x.id !== id);
|
| 225 |
+
localStorage.setItem('sf_users', JSON.stringify(u));
|
| 226 |
+
// Also remove their analyses
|
| 227 |
+
let a = analyses().filter(x => {
|
| 228 |
+
const del = JSON.parse(localStorage.getItem('sf_users') || '[]');
|
| 229 |
+
return x.userId !== id;
|
| 230 |
+
});
|
| 231 |
+
localStorage.setItem('sf_analysis_history', JSON.stringify(analyses().filter(x => x.userId !== id)));
|
| 232 |
+
renderUsersTable(u);
|
| 233 |
+
renderDashboard();
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
function openUserView(id) {
|
| 237 |
+
const u = users().find(x => x.id === id);
|
| 238 |
+
if (!u) return;
|
| 239 |
+
const a = analyses().filter(x => x.userEmail === u.email);
|
| 240 |
+
const nb = $('userViewBody');
|
| 241 |
+
nb.innerHTML = `
|
| 242 |
+
<div class="user-detail-row"><span><i class="fas fa-user" style="width:18px;text-align:center;"></i> Full Name</span><strong>${esc(u.name)}</strong></div>
|
| 243 |
+
<div class="user-detail-row"><span><i class="fas fa-envelope" style="width:18px;text-align:center;"></i> Email</span><strong>${esc(u.email)}</strong></div>
|
| 244 |
+
<div class="user-detail-row"><span><i class="fas fa-calendar" style="width:18px;text-align:center;"></i> Registered</span><strong>${new Date(u.createdAt).toLocaleString()}</strong></div>
|
| 245 |
+
<div class="user-detail-row"><span><i class="fas fa-chart-bar" style="width:18px;text-align:center;"></i> Total Analyses</span><strong>${a.length}</strong></div>
|
| 246 |
+
<div class="user-detail-row"><span><i class="fas fa-check-circle" style="width:18px;text-align:center;"></i> Normal Results</span><strong style="color:var(--green)">${a.filter(x=>x.result==='Normal').length}</strong></div>
|
| 247 |
+
<div class="user-detail-row"><span><i class="fas fa-exclamation-triangle" style="width:18px;text-align:center;"></i> Stutter Results</span><strong style="color:var(--orange)">${a.filter(x=>x.result!=='Normal').length}</strong></div>
|
| 248 |
+
`;
|
| 249 |
+
$('userViewModal').style.display = 'flex';
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
$('userViewClose').addEventListener('click', () => { $('userViewModal').style.display = 'none'; });
|
| 253 |
+
$('userViewModal').addEventListener('click', function(e) { if (e.target === this) this.style.display = 'none'; });
|
| 254 |
+
|
| 255 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 256 |
+
ANALYSES TABLE
|
| 257 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 258 |
+
function renderAnalysesTable(list) {
|
| 259 |
+
const body = $('analysesBody');
|
| 260 |
+
const empty = $('analysesEmpty');
|
| 261 |
+
body.innerHTML = '';
|
| 262 |
+
if (!list.length) { empty.style.display = 'block'; return; }
|
| 263 |
+
empty.style.display = 'none';
|
| 264 |
+
|
| 265 |
+
list.forEach((a, i) => {
|
| 266 |
+
const isNormal = a.result === 'Normal';
|
| 267 |
+
const tr = document.createElement('tr');
|
| 268 |
+
tr.innerHTML = `
|
| 269 |
+
<td><span style="color:var(--text-muted)">${i + 1}</span></td>
|
| 270 |
+
<td><strong>${esc(a.userName || 'Unknown')}</strong></td>
|
| 271 |
+
<td style="color:var(--text-muted);font-size:.83rem">${esc(a.fileName || 'N/A')}</td>
|
| 272 |
+
<td style="color:var(--text-muted)">${new Date(a.date).toLocaleDateString()}</td>
|
| 273 |
+
<td><span class="badge ${isNormal ? 'badge-green' : 'badge-orange'}"><i class="fas fa-${isNormal ? 'check-circle' : 'exclamation-triangle'}"></i> ${esc(a.result)}</span></td>
|
| 274 |
+
<td><strong>${a.confidence || '--'}%</strong></td>
|
| 275 |
+
<td><button class="btn-tbl btn-delete" data-id="${a.id}"><i class="fas fa-trash"></i> Delete</button></td>`;
|
| 276 |
+
body.appendChild(tr);
|
| 277 |
+
});
|
| 278 |
+
|
| 279 |
+
body.querySelectorAll('.btn-delete').forEach(btn => btn.addEventListener('click', () => deleteAnalysis(btn.dataset.id)));
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
$('analysisSearch').addEventListener('input', function () {
|
| 283 |
+
const q = this.value.toLowerCase();
|
| 284 |
+
renderAnalysesTable(analyses().filter(a =>
|
| 285 |
+
(a.userName || '').toLowerCase().includes(q) ||
|
| 286 |
+
(a.fileName || '').toLowerCase().includes(q) ||
|
| 287 |
+
(a.result || '').toLowerCase().includes(q)
|
| 288 |
+
));
|
| 289 |
+
});
|
| 290 |
+
|
| 291 |
+
function deleteAnalysis(id) {
|
| 292 |
+
if (!confirm('Delete this analysis record?')) return;
|
| 293 |
+
const updated = analyses().filter(x => x.id !== id);
|
| 294 |
+
localStorage.setItem('sf_analysis_history', JSON.stringify(updated));
|
| 295 |
+
renderAnalysesTable(updated);
|
| 296 |
+
renderDashboard();
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
/* ββ Export CSV ββββββββββββββββββββββββββββββββββββββββββββ */
|
| 300 |
+
$('exportCsvBtn').addEventListener('click', () => {
|
| 301 |
+
const a = analyses();
|
| 302 |
+
if (!a.length) { alert('No analysis data to export.'); return; }
|
| 303 |
+
const header = ['#','User Name','User Email','File Name','Date','Result','Confidence'];
|
| 304 |
+
const rows = a.map((x, i) => [
|
| 305 |
+
i + 1, `"${x.userName || ''}"`, `"${x.userEmail || ''}"`,
|
| 306 |
+
`"${x.fileName || ''}"`, new Date(x.date).toLocaleString(),
|
| 307 |
+
x.result, x.confidence + '%'
|
| 308 |
+
]);
|
| 309 |
+
const csv = [header.join(','), ...rows.map(r => r.join(','))].join('\n');
|
| 310 |
+
const blob = new Blob([csv], { type: 'text/csv' });
|
| 311 |
+
const url = URL.createObjectURL(blob);
|
| 312 |
+
const link = document.createElement('a');
|
| 313 |
+
link.href = url; link.download = 'speakflow_analyses.csv';
|
| 314 |
+
link.click(); URL.revokeObjectURL(url);
|
| 315 |
+
});
|
| 316 |
+
|
| 317 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 318 |
+
CHARTS TAB
|
| 319 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 320 |
+
let chartsRendered = false;
|
| 321 |
+
let growthInst = null, pieInst = null, activityInst = null;
|
| 322 |
+
|
| 323 |
+
function renderChartsTab() {
|
| 324 |
+
if (chartsRendered) { chartsRendered = false; } // always re-render fresh
|
| 325 |
+
const a = analyses();
|
| 326 |
+
const u = users();
|
| 327 |
+
|
| 328 |
+
// 1. User Growth Line Chart
|
| 329 |
+
const growthCtx = $('userGrowthChart').getContext('2d');
|
| 330 |
+
if (growthInst) growthInst.destroy();
|
| 331 |
+
|
| 332 |
+
const months = getLast6Months();
|
| 333 |
+
const growthData = months.map(m => u.filter(x => {
|
| 334 |
+
const d = new Date(x.createdAt);
|
| 335 |
+
return d.getFullYear() === m.year && d.getMonth() === m.month;
|
| 336 |
+
}).length);
|
| 337 |
+
|
| 338 |
+
growthInst = new Chart(growthCtx, {
|
| 339 |
+
type: 'line',
|
| 340 |
+
data: {
|
| 341 |
+
labels: months.map(m => m.label),
|
| 342 |
+
datasets: [{
|
| 343 |
+
label: 'New Users', data: growthData,
|
| 344 |
+
borderColor: '#3B82F6', backgroundColor: 'rgba(59,130,246,0.12)',
|
| 345 |
+
fill: true, tension: 0.4, pointBackgroundColor: '#3B82F6', pointRadius: 5
|
| 346 |
+
}]
|
| 347 |
+
},
|
| 348 |
+
options: {
|
| 349 |
+
responsive: true, maintainAspectRatio: false,
|
| 350 |
+
plugins: { legend: { labels: { color: '#94A3B8' } } },
|
| 351 |
+
scales: {
|
| 352 |
+
x: { ticks: { color: '#64748B' }, grid: { color: 'rgba(255,255,255,.04)' } },
|
| 353 |
+
y: { ticks: { color: '#64748B', stepSize: 1 }, grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true }
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
});
|
| 357 |
+
|
| 358 |
+
// 2. Detection Pie
|
| 359 |
+
const normal = a.filter(x => x.result === 'Normal').length;
|
| 360 |
+
const stutter = a.filter(x => x.result !== 'Normal').length;
|
| 361 |
+
const pieCtx = $('detectionPieChart').getContext('2d');
|
| 362 |
+
if (pieInst) pieInst.destroy();
|
| 363 |
+
pieInst = new Chart(pieCtx, {
|
| 364 |
+
type: 'pie',
|
| 365 |
+
data: {
|
| 366 |
+
labels: ['Normal', 'Stutter Detected'],
|
| 367 |
+
datasets: [{ data: [normal || 1, stutter || 0], backgroundColor: ['#10B981','#F59E0B'], borderWidth: 0, hoverOffset: 10 }]
|
| 368 |
+
},
|
| 369 |
+
options: {
|
| 370 |
+
responsive: true, maintainAspectRatio: false,
|
| 371 |
+
plugins: { legend: { position: 'bottom', labels: { color: '#94A3B8', padding: 16 } } }
|
| 372 |
+
}
|
| 373 |
+
});
|
| 374 |
+
|
| 375 |
+
// 3. Activity Bar β last 7 days
|
| 376 |
+
const days = [];
|
| 377 |
+
const dayCounts = [];
|
| 378 |
+
for (let i = 6; i >= 0; i--) {
|
| 379 |
+
const d = new Date(); d.setDate(d.getDate() - i);
|
| 380 |
+
days.push(d.toLocaleDateString([], { weekday: 'short' }));
|
| 381 |
+
dayCounts.push(a.filter(x => new Date(x.date).toDateString() === d.toDateString()).length);
|
| 382 |
+
}
|
| 383 |
+
const actCtx = $('activityChart').getContext('2d');
|
| 384 |
+
if (activityInst) activityInst.destroy();
|
| 385 |
+
activityInst = new Chart(actCtx, {
|
| 386 |
+
type: 'bar',
|
| 387 |
+
data: {
|
| 388 |
+
labels: days,
|
| 389 |
+
datasets: [
|
| 390 |
+
{ label: 'Analyses', data: dayCounts, backgroundColor: 'rgba(20,184,166,0.75)', borderRadius: 8, borderSkipped: false }
|
| 391 |
+
]
|
| 392 |
+
},
|
| 393 |
+
options: {
|
| 394 |
+
responsive: true, maintainAspectRatio: false,
|
| 395 |
+
plugins: { legend: { labels: { color: '#94A3B8' } } },
|
| 396 |
+
scales: {
|
| 397 |
+
x: { ticks: { color: '#64748B' }, grid: { color: 'rgba(255,255,255,.04)' } },
|
| 398 |
+
y: { ticks: { color: '#64748B', stepSize: 1 }, grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true }
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
});
|
| 402 |
+
|
| 403 |
+
// 4. System Stats
|
| 404 |
+
const avg = a.length ? Math.round(a.reduce((s, x) => s + (x.confidence || 0), 0) / a.length) : 0;
|
| 405 |
+
const sysGrid = $('sysStatsGrid');
|
| 406 |
+
sysGrid.innerHTML = `
|
| 407 |
+
<div class="sys-stat-item"><div class="sys-stat-val" style="color:var(--blue-light)">${u.length}</div><div class="sys-stat-lbl">Registered Users</div></div>
|
| 408 |
+
<div class="sys-stat-item"><div class="sys-stat-val" style="color:var(--teal)">${a.length}</div><div class="sys-stat-lbl">Total Analyses</div></div>
|
| 409 |
+
<div class="sys-stat-item"><div class="sys-stat-val" style="color:var(--green)">${avg}%</div><div class="sys-stat-lbl">Avg Confidence</div></div>
|
| 410 |
+
<div class="sys-stat-item"><div class="sys-stat-val" style="color:var(--orange)">${stutter}</div><div class="sys-stat-lbl">Stutter Detections</div></div>
|
| 411 |
+
`;
|
| 412 |
+
|
| 413 |
+
chartsRendered = true;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
function getLast6Months() {
|
| 417 |
+
const result = [];
|
| 418 |
+
for (let i = 5; i >= 0; i--) {
|
| 419 |
+
const d = new Date();
|
| 420 |
+
d.setMonth(d.getMonth() - i);
|
| 421 |
+
result.push({
|
| 422 |
+
year: d.getFullYear(), month: d.getMonth(),
|
| 423 |
+
label: d.toLocaleDateString([], { month: 'short', year: '2-digit' })
|
| 424 |
+
});
|
| 425 |
+
}
|
| 426 |
+
return result;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
/* ββ Escape HTML βββββββββββββββββββββββββββββββββββββββββββ */
|
| 430 |
+
function esc(s) {
|
| 431 |
+
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
/* ββ Init ββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 435 |
+
renderDashboard();
|
| 436 |
+
renderUsersTable(users());
|
| 437 |
+
renderAnalysesTable(analyses());
|