AttendAIPro / admin-panel.html
ashrithagowthami's picture
Upload 3 files
b8f94df verified
Raw
History Blame Contribute Delete
47.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel — AttendAI Pro</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/teacher.css">
<link rel="stylesheet" href="css/admin.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body>
<!-- Admin Auth Wall (shown if not logged in as admin) -->
<div id="adminAuthWall" class="admin-auth-wall">
<div class="admin-auth-card">
<div class="admin-auth-header">
<div class="admin-shield">🛡️</div>
<h1>Developer / Admin Access</h1>
<p>Restricted area. Only authorized administrators can access this panel.</p>
</div>
<div id="adminAuthAlert" style="margin-bottom:16px;display:none"></div>
<div class="role-reminder admin-reminder">
<div class="reminder-icon">⚠️</div>
<div>
<strong>Restricted Access</strong>
<p>Your Google account must be registered with admin role. All access attempts are logged.</p>
</div>
</div>
<div style="margin:24px 0; display: flex; justify-content: center;">
<div id="adminGoogleBtnContainer"></div>
</div>
<div id="adminLoadingState" class="auth-loading hidden">
<div class="spinner spinner-dark"></div>
<span>Verifying admin credentials...</span>
</div>
<div class="auth-notice-box">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
<span>All admin actions are logged with IP, timestamp, and user identity.</span>
</div>
<div style="text-align:center;margin-top:16px">
<a href="index.html" style="font-size:13px;color:var(--gray)">← Return to Home</a>
</div>
</div>
</div>
<!-- Admin Dashboard -->
<div id="adminDashboard" style="display:none">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-logo">
<span class="logo-icon">🛡️</span>
Admin<span>Panel</span>
</div>
<nav class="sidebar-nav">
<div class="nav-section-title">System</div>
<button class="nav-item active" onclick="showSection('dashboard', this)"><span class="nav-icon">📊</span> Dashboard</button>
<button class="nav-item" onclick="showSection('users', this)"><span class="nav-icon">👥</span> User Management</button>
<button class="nav-item" onclick="showSection('sessions', this)"><span class="nav-icon">📱</span> Sessions</button>
<button class="nav-item" onclick="showSection('attendance', this)"><span class="nav-icon"></span> Attendance</button>
<div class="nav-section-title">Security</div>
<button class="nav-item" onclick="showSection('fraud', this)">
<span class="nav-icon">🚨</span> Fraud Alerts
<span class="notif-badge" id="fraudBadge" style="position:relative;margin-left:auto;font-size:10px;background:var(--danger)">0</span>
</button>
<button class="nav-item" onclick="showSection('logs', this)"><span class="nav-icon">📋</span> Audit Logs</button>
<div class="nav-section-title">Configuration</div>
<button class="nav-item" onclick="showSection('config', this)"><span class="nav-icon">⚙️</span> System Config</button>
<button class="nav-item" onclick="showSection('analytics', this)"><span class="nav-icon">📈</span> Global Analytics</button>
</nav>
<div class="sidebar-footer">
<div class="sidebar-user">
<img id="adminSidebarAvatar" src="" alt="" class="avatar avatar-sm">
<div class="sidebar-user-info">
<div class="sidebar-user-name" id="adminSidebarName"></div>
<div class="sidebar-user-role" style="color:#F59E0B">⚡ Administrator</div>
</div>
</div>
<button class="nav-item" onclick="logout()" style="color:rgba(239,68,68,0.8)"><span class="nav-icon">🚪</span> Sign Out</button>
</div>
</aside>
<div class="mobile-overlay" id="mobileOverlay" onclick="closeSidebar()"></div>
<main class="main-content">
<div class="topbar">
<div style="display:flex;align-items:center;gap:12px">
<button class="hamburger" onclick="toggleSidebar()"></button>
<div>
<div class="topbar-title" id="topbarTitle">System Dashboard</div>
<div class="text-xs text-gray" id="adminTopbarSub">AttendAI Pro — Administrator View</div>
</div>
</div>
<div class="topbar-actions">
<div class="admin-badge">⚡ ADMIN</div>
<button class="btn btn-sm btn-secondary" onclick="refreshAll()">🔄 Refresh</button>
</div>
</div>
<div class="page-content">
<!-- ===== DASHBOARD ===== -->
<div class="section active fade-in" id="section-dashboard">
<div class="stats-grid" id="systemStats">
<div class="stat-card blue"><div class="stat-icon">👥</div><div class="stat-value" id="sys-users"></div><div class="stat-label">Total Users</div></div>
<div class="stat-card green"><div class="stat-icon">🎓</div><div class="stat-value" id="sys-students"></div><div class="stat-label">Students</div></div>
<div class="stat-card purple"><div class="stat-icon">👨‍🏫</div><div class="stat-value" id="sys-teachers"></div><div class="stat-label">Teachers</div></div>
<div class="stat-card yellow"><div class="stat-icon">📱</div><div class="stat-value" id="sys-sessions"></div><div class="stat-label">Total Sessions</div></div>
<div class="stat-card green"><div class="stat-icon"></div><div class="stat-value" id="sys-att"></div><div class="stat-label">Attendance Records</div></div>
<div class="stat-card red"><div class="stat-icon">🚨</div><div class="stat-value" id="sys-fraud"></div><div class="stat-label">Fraud Alerts</div></div>
</div>
<!-- Live Sessions -->
<div class="card">
<div class="section-header"><div class="section-title">🔴 Live Sessions</div><span class="live-dot">LIVE</span></div>
<div id="liveSessionsList">
<div class="empty-state" style="padding:30px"><div class="empty-state-icon">📱</div><h3>No Active Sessions</h3><p>No teachers are currently running attendance sessions.</p></div>
</div>
</div>
<!-- Recent Logs -->
<div class="card">
<div class="section-header"><div class="section-title">📋 Recent Activity</div><a href="#" onclick="showSection('logs',document.querySelector('[onclick*=logs]'))" class="btn btn-sm btn-secondary">View All</a></div>
<div id="recentLogs">
<div class="empty-state" style="padding:30px"><div class="empty-state-icon">📋</div><p>No recent activity</p></div>
</div>
</div>
</div>
<!-- ===== USER MANAGEMENT ===== -->
<div class="section hidden" id="section-users">
<div class="section-header" style="margin-bottom:20px">
<div>
<h2>User Management</h2>
<p class="text-sm text-gray">Add, edit, and manage all system users. Only pre-registered users can log in.</p>
</div>
<button class="btn btn-primary" onclick="openAddUserModal()">+ Add User</button>
</div>
<!-- Tabs -->
<div class="user-tabs">
<button class="utab active" onclick="filterUsers('all', this)">All Users</button>
<button class="utab" onclick="filterUsers('student', this)">Students</button>
<button class="utab" onclick="filterUsers('teacher', this)">Teachers</button>
<button class="utab" onclick="filterUsers('admin', this)">Admins</button>
</div>
<!-- Search -->
<div style="margin:16px 0;display:flex;gap:12px">
<input type="text" class="form-control" id="userSearchInput" placeholder="Search by name, email, or roll number..." oninput="searchUsers()" style="flex:1">
<button class="btn btn-secondary" onclick="exportUsers()">⬇️ Export</button>
</div>
<div class="card p-0">
<div class="table-wrapper">
<table class="table">
<thead>
<tr><th>Name</th><th>Email</th><th>Role</th><th>Roll/Dept</th><th>Institution</th><th>Status</th><th>Last Login</th><th>Actions</th></tr>
</thead>
<tbody id="usersTableBody">
<tr><td colspan="8" style="text-align:center;padding:32px;color:var(--gray)">Loading users...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ===== SESSIONS ===== -->
<div class="section hidden" id="section-sessions">
<div class="section-header" style="margin-bottom:20px">
<h2>All Sessions</h2>
<button class="btn btn-sm btn-danger" id="forceEndBtn" onclick="forceEndAllSessions()" style="display:none">Force End All Live Sessions</button>
</div>
<div class="card p-0">
<div class="table-wrapper">
<table class="table">
<thead><tr><th>Subject</th><th>Class</th><th>Teacher</th><th>Started</th><th>Status</th><th>Students</th><th>Actions</th></tr></thead>
<tbody id="adminSessionsBody">
<tr><td colspan="7" style="text-align:center;padding:32px;color:var(--gray)">Loading sessions...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ===== ATTENDANCE ===== -->
<div class="section hidden" id="section-attendance">
<div class="section-header" style="margin-bottom:20px">
<div><h2>All Attendance Records</h2><p class="text-sm text-gray">Complete verified attendance database</p></div>
<button class="btn btn-success" onclick="exportAllAttendance()">⬇️ Export All</button>
</div>
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
<select class="form-control form-select" id="adminAttFilter" onchange="loadAdminAttendance()" style="width:auto">
<option value="all">All Time</option>
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
<select class="form-control form-select" id="adminAttStatus" onchange="loadAdminAttendance()" style="width:auto">
<option value="">All Statuses</option>
<option value="present">Present</option>
<option value="absent">Absent</option>
<option value="late">Late</option>
<option value="fraudulent">Fraudulent</option>
</select>
</div>
<div class="card p-0">
<div class="table-wrapper">
<table class="table">
<thead><tr><th>Roll No.</th><th>Student</th><th>Session</th><th>Status</th><th>Time</th><th>GPS</th><th>Face</th><th>Fraud Flags</th></tr></thead>
<tbody id="adminAttBody">
<tr><td colspan="8" style="text-align:center;padding:32px;color:var(--gray)">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ===== FRAUD ALERTS ===== -->
<div class="section hidden" id="section-fraud">
<div class="section-header" style="margin-bottom:20px">
<h2>🚨 Fraud Detection Center</h2>
<button class="btn btn-sm btn-secondary" onclick="loadAdminFraud()">🔄 Refresh</button>
</div>
<div id="adminFraudList">
<div class="empty-state" style="padding:60px"><div class="empty-state-icon">🛡️</div><h3>No Alerts</h3><p>No fraud detected in the system.</p></div>
</div>
</div>
<!-- ===== AUDIT LOGS ===== -->
<div class="section hidden" id="section-logs">
<div class="section-header" style="margin-bottom:20px">
<h2>📋 Audit Logs</h2>
<div style="display:flex;gap:8px">
<select class="form-control form-select" id="logFilter" onchange="loadAuditLogs()" style="width:auto">
<option value="">All Actions</option>
<option value="LOGIN_SUCCESS">Logins</option>
<option value="LOGIN_FAILED">Failed Logins</option>
<option value="ATTENDANCE_MARKED">Attendance</option>
<option value="SESSION_CREATED">Sessions</option>
<option value="FRAUD_ATTEMPT">Fraud Attempts</option>
</select>
</div>
</div>
<div class="card p-0">
<div class="table-wrapper">
<table class="table">
<thead><tr><th>Timestamp</th><th>User</th><th>Action</th><th>Resource</th><th>Status</th></tr></thead>
<tbody id="logsTableBody">
<tr><td colspan="5" style="text-align:center;padding:32px;color:var(--gray)">Loading logs...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ===== SYSTEM CONFIG ===== -->
<div class="section hidden" id="section-config">
<h2 style="margin-bottom:24px">⚙️ System Configuration</h2>
<div class="grid-2">
<div class="card">
<h3 style="margin-bottom:20px">Attendance Rules</h3>
<div class="form-group">
<label class="form-label">QR Rotation Interval (seconds)</label>
<input type="number" class="form-control" id="cfg-qr" value="60" min="10" max="300">
</div>
<div class="form-group">
<label class="form-label">Default Geofence Radius (meters)</label>
<input type="number" class="form-control" id="cfg-geo" value="100" min="10" max="5000">
</div>
<div class="form-group">
<label class="form-label">Attendance Warning Threshold (%)</label>
<input type="number" class="form-control" id="cfg-threshold" value="75" min="1" max="100">
</div>
<div class="form-group">
<label class="form-label">Face Match Confidence (0-1)</label>
<input type="number" class="form-control" id="cfg-face" value="0.6" min="0.1" max="1" step="0.05">
</div>
<button class="btn btn-primary" onclick="saveConfig()">💾 Save Settings</button>
</div>
<div class="card">
<h3 style="margin-bottom:20px">Institution Settings</h3>
<div class="form-group">
<label class="form-label">Institution Name</label>
<input type="text" class="form-control" id="cfg-institution" placeholder="University Name">
</div>
<div class="form-group">
<label class="form-label">Google Client ID</label>
<input type="text" class="form-control" id="cfg-oauth" placeholder="xxx.apps.googleusercontent.com">
<div class="form-hint">Required for Google OAuth login</div>
</div>
<div class="form-group">
<label class="form-label">Allowed Student Email Domains</label>
<input type="text" class="form-control" id="cfg-domains" placeholder="university.edu, college.ac.in (comma separated)">
<div class="form-hint">Leave blank to allow all domains</div>
</div>
<div class="form-group">
<label class="form-label">Max Attendance Attempts per Session</label>
<input type="number" class="form-control" id="cfg-attempts" value="1" min="1" max="5">
</div>
<button class="btn btn-primary" onclick="saveConfig()">💾 Save Settings</button>
</div>
</div>
<!-- Danger Zone -->
<div class="card" style="margin-top:20px;border-color:rgba(239,68,68,0.3)">
<h3 style="color:var(--danger);margin-bottom:16px">⚠️ Danger Zone</h3>
<div style="display:flex;gap:12px;flex-wrap:wrap">
<button class="btn btn-danger" onclick="confirmAction('clear_fraud', 'Mark all fraud alerts as resolved?')">Clear All Fraud Alerts</button>
<button class="btn btn-danger" onclick="confirmAction('end_sessions', 'Force end ALL active sessions?')">Force End All Sessions</button>
</div>
<p class="text-sm text-gray" style="margin-top:12px">These actions are irreversible and will be logged.</p>
</div>
</div>
<!-- ===== ANALYTICS ===== -->
<div class="section hidden" id="section-analytics">
<h2 style="margin-bottom:24px">📈 Global System Analytics</h2>
<div class="grid-2">
<div class="card"><div class="section-title" style="margin-bottom:16px">Daily Attendance Trend</div><div style="height:280px"><canvas id="adminTrendChart"></canvas></div></div>
<div class="card"><div class="section-title" style="margin-bottom:16px">User Distribution</div><div style="height:280px"><canvas id="adminUserChart"></canvas></div></div>
</div>
<div class="card" style="margin-top:20px">
<div class="section-title" style="margin-bottom:16px">Fraud Detection Summary</div>
<div style="height:250px"><canvas id="adminFraudChart"></canvas></div>
</div>
</div>
</div>
</main>
</div><!-- /adminDashboard -->
<!-- Add User Modal -->
<div class="modal-overlay" id="addUserModal">
<div class="modal" style="max-width:540px">
<div class="modal-header">
<h3>➕ Add New User</h3>
<button class="modal-close" onclick="closeModal('addUserModal')">×</button>
</div>
<div class="modal-body">
<div id="addUserAlert" style="margin-bottom:12px;display:none"></div>
<div class="form-group">
<label class="form-label">Full Name *</label>
<input type="text" class="form-control" id="nu-name" placeholder="John Doe">
</div>
<div class="form-group">
<label class="form-label">Google Email *</label>
<input type="email" class="form-control" id="nu-email" placeholder="student@university.edu">
<div class="form-hint">Must match their Google account email exactly</div>
</div>
<div class="form-group">
<label class="form-label">Role *</label>
<select class="form-control form-select" id="nu-role" onchange="toggleRoleFields()">
<option value="student">Student</option>
<option value="teacher">Teacher</option>
<option value="admin">Administrator</option>
</select>
</div>
<div id="studentFields">
<div class="form-group">
<label class="form-label">Roll Number *</label>
<input type="text" class="form-control" id="nu-roll" placeholder="CS2024001">
</div>
</div>
<div class="form-group">
<label class="form-label">Department</label>
<input type="text" class="form-control" id="nu-dept" placeholder="Computer Science">
</div>
<div class="form-group">
<label class="form-label">Institution</label>
<input type="text" class="form-control" id="nu-institution" placeholder="University Name">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('addUserModal')">Cancel</button>
<button class="btn btn-primary" onclick="addUser()">Add User</button>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/config.js"></script>
<script>
let currentAdmin = null;
let allUsers = [];
let currentUserFilter = 'all';
let adminTrendChart = null, adminUserChart = null, adminFraudChart = null;
/* ===== Auth ===== */
async function checkAdminAuth() {
const user = AuthState.get();
if (user && user.role === 'admin') {
currentAdmin = user;
showAdminDashboard();
}
// If not logged in, show auth wall
}
function showAdminDashboard() {
document.getElementById('adminAuthWall').style.display = 'none';
document.getElementById('adminDashboard').style.display = 'block';
document.getElementById('adminSidebarAvatar').src = currentAdmin.picture || `https://ui-avatars.com/api/?name=${encodeURIComponent(currentAdmin.name)}&background=F59E0B&color=fff`;
document.getElementById('adminSidebarName').textContent = currentAdmin.name;
loadDashboard();
loadAllUsers();
loadAdminSessions();
loadAdminAttendance();
loadAdminFraud();
loadAuditLogs();
}
function handleGoogleCredential(response) {
try {
const parts = response.credential.split('.');
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
verifyAdminInDB({ sub: payload.sub, email: payload.email, name: payload.name, picture: payload.picture });
} catch(e) {
showAdminAuthAlert('Authentication failed. Please try again.', 'danger');
}
}
async function verifyAdminInDB(googleUser) {
document.getElementById('adminLoadingState').classList.remove('hidden');
document.getElementById('adminGoogleBtn').disabled = true;
try {
// Exact email lookup
const result = await API.get('users', { email: googleUser.email });
const users = result.data || [];
const user = users.find(u => u.email && u.email.toLowerCase() === googleUser.email.toLowerCase());
if (!user) {
showAdminAuthAlert('Your email is not registered in the system.', 'danger');
return;
}
if (user.role !== 'admin') {
showAdminAuthAlert(`Your account role is "${user.role}", not admin. Please use the correct portal.`, 'danger');
await Logger.log('ADMIN_ACCESS_DENIED', 'auth', { email: googleUser.email, role: user.role }, 'failure');
return;
}
if (user.is_active === false) {
showAdminAuthAlert('Your admin account is deactivated.', 'danger');
return;
}
currentAdmin = { ...user, name: googleUser.name || user.name, picture: googleUser.picture || '' };
AuthState.save(currentAdmin);
await API.patch('users', user.id, { last_login: Date.now() });
await Logger.log('ADMIN_LOGIN', 'auth', { email: googleUser.email }, 'success');
showAdminDashboard();
} catch(e) {
showAdminAuthAlert('System error during verification. Please try again.', 'danger');
} finally {
document.getElementById('adminLoadingState').classList.add('hidden');
// Official button is managed by Google
}
}
function showAdminAuthAlert(msg, type) {
const el = document.getElementById('adminAuthAlert');
el.style.display = 'block';
el.innerHTML = `<div class="alert alert-${type}"><span>${type==='danger'?'❌':'⚠️'}</span><div>${msg}</div></div>`;
}
function initiateAdminLogin() {
if (APP_CONFIG.googleClientId === 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com') {
showAdminAuthAlert('<strong>Google OAuth Not Configured</strong><br>Set up Google OAuth Client ID in js/config.js to enable login.', 'warning');
return;
}
if (typeof google !== 'undefined' && google.accounts) google.accounts.id.prompt();
else showAdminAuthAlert('Google auth unavailable.', 'warning');
}
/* ===== Navigation ===== */
function showSection(name, el) {
document.querySelectorAll('.section').forEach(s => { s.classList.remove('active'); s.classList.add('hidden'); });
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const sec = document.getElementById('section-' + name);
if (sec) { sec.classList.remove('hidden'); sec.classList.add('active', 'fade-in'); }
if (el) el.classList.add('active');
const titles = { dashboard:'System Dashboard', users:'User Management', sessions:'All Sessions', attendance:'Attendance Records', fraud:'Fraud Detection', logs:'Audit Logs', config:'System Config', analytics:'Global Analytics' };
document.getElementById('topbarTitle').textContent = titles[name] || name;
if (name === 'analytics') loadAnalytics();
closeSidebar();
}
function toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); document.getElementById('mobileOverlay').classList.toggle('open'); }
function closeSidebar() { document.getElementById('sidebar').classList.remove('open'); document.getElementById('mobileOverlay').classList.remove('open'); }
/* ===== Dashboard ===== */
async function loadDashboard() {
try {
const [uRes, sRes, aRes, fRes] = await Promise.all([
API.get('users', { limit: 500 }),
API.get('sessions', { limit: 200 }),
API.get('attendance', { limit: 1000 }),
API.get('fraud_alerts', { limit: 200 })
]);
const users = uRes.data || [];
const sessions = sRes.data || [];
const att = aRes.data || [];
const fraud = fRes.data || [];
document.getElementById('sys-users').textContent = users.length;
document.getElementById('sys-students').textContent = users.filter(u => u.role === 'student').length;
document.getElementById('sys-teachers').textContent = users.filter(u => u.role === 'teacher').length;
document.getElementById('sys-sessions').textContent = sessions.length;
document.getElementById('sys-att').textContent = att.length;
document.getElementById('sys-fraud').textContent = fraud.filter(f => !f.resolved).length;
document.getElementById('fraudBadge').textContent = fraud.filter(f => !f.resolved).length;
// Live sessions
const liveSessions = sessions.filter(s => s.is_active);
document.getElementById('forceEndBtn').style.display = liveSessions.length ? 'block' : 'none';
const liveEl = document.getElementById('liveSessionsList');
if (!liveSessions.length) {
liveEl.innerHTML = `<div class="empty-state" style="padding:30px"><div class="empty-state-icon">📱</div><h3>No Active Sessions</h3><p>No teachers are currently running attendance sessions.</p></div>`;
} else {
const userMap = {};
users.forEach(u => { userMap[u.id] = u; });
liveEl.innerHTML = liveSessions.map(s => {
const teacher = userMap[s.teacher_id] || {};
return `<div class="live-session-row"><div class="live-dot">LIVE</div><div style="flex:1"><strong>${s.subject}</strong> — ${s.class_name}</div><div class="text-sm text-gray">by ${teacher.name||'Unknown'}</div><div class="text-xs text-gray">${Utils.timeAgo(s.started_at||s.created_at)}</div></div>`;
}).join('');
}
} catch(e) { console.error('Dashboard load error:', e); }
}
/* ===== User Management ===== */
async function loadAllUsers() {
try {
const result = await API.get('users', { limit: 500 });
allUsers = result.data || [];
renderUsersTable(allUsers);
} catch(e) {}
}
function filterUsers(role, el) {
document.querySelectorAll('.utab').forEach(t => t.classList.remove('active'));
if (el) el.classList.add('active');
currentUserFilter = role;
const filtered = role === 'all' ? allUsers : allUsers.filter(u => u.role === role);
renderUsersTable(filtered);
}
function searchUsers() {
const q = document.getElementById('userSearchInput').value.toLowerCase();
let filtered = currentUserFilter === 'all' ? allUsers : allUsers.filter(u => u.role === currentUserFilter);
if (q) filtered = filtered.filter(u => (u.name||'').toLowerCase().includes(q) || (u.email||'').toLowerCase().includes(q) || (u.roll_number||'').toLowerCase().includes(q));
renderUsersTable(filtered);
}
function renderUsersTable(users) {
const tbody = document.getElementById('usersTableBody');
if (!users.length) {
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center;padding:40px;color:var(--gray)"><div class="empty-state-icon">👥</div><p>No users found. Add users to get started.</p></td></tr>`;
return;
}
const roleColors = { student: 'info', teacher: 'primary', admin: 'warning' };
tbody.innerHTML = users.map(u => `<tr>
<td><div style="display:flex;align-items:center;gap:10px"><img src="${u.picture||`https://ui-avatars.com/api/?name=${encodeURIComponent(u.name||'?')}&background=4F46E5&color=fff`}" class="avatar avatar-sm">${u.name||'—'}</div></td>
<td class="text-sm text-gray">${u.email||'—'}</td>
<td><span class="badge badge-${roleColors[u.role]||'gray'}">${u.role||'—'}</span></td>
<td class="text-sm">${u.roll_number||u.department||'—'}</td>
<td class="text-sm">${u.institution||'—'}</td>
<td><span class="badge badge-${u.is_active!==false?'success':'danger'}">${u.is_active!==false?'Active':'Inactive'}</span></td>
<td class="text-sm text-gray">${u.last_login ? Utils.timeAgo(u.last_login) : 'Never'}</td>
<td>
<div style="display:flex;gap:6px">
<button class="btn btn-sm btn-secondary" onclick="toggleUserStatus('${u.id}', ${u.is_active!==false})">${u.is_active!==false?'Disable':'Enable'}</button>
<button class="btn btn-sm btn-danger" onclick="deleteUser('${u.id}', '${u.name}')">Delete</button>
</div>
</td>
</tr>`).join('');
}
async function toggleUserStatus(id, isActive) {
if (!confirm(`${isActive ? 'Disable' : 'Enable'} this user? ${isActive ? 'They will not be able to log in.' : ''}`)) return;
try {
await API.patch('users', id, { is_active: !isActive });
await Logger.log(isActive ? 'USER_DISABLED' : 'USER_ENABLED', 'users', { user_id: id });
Toast.success(`User ${isActive ? 'disabled' : 'enabled'}`);
await loadAllUsers();
filterUsers(currentUserFilter, null);
} catch(e) { Toast.error('Error updating user'); }
}
async function deleteUser(id, name) {
if (!confirm(`Permanently delete user "${name}"? This cannot be undone.`)) return;
try {
await API.delete('users', id);
await Logger.log('USER_DELETED', 'users', { user_id: id, name });
Toast.success('User deleted');
await loadAllUsers();
filterUsers(currentUserFilter, null);
} catch(e) { Toast.error('Error deleting user'); }
}
function openAddUserModal() { openModal('addUserModal'); }
function toggleRoleFields() {
const role = document.getElementById('nu-role').value;
document.getElementById('studentFields').style.display = role === 'student' ? 'block' : 'none';
}
async function addUser() {
const name = document.getElementById('nu-name').value.trim();
const email = document.getElementById('nu-email').value.trim().toLowerCase();
const role = document.getElementById('nu-role').value;
const roll = document.getElementById('nu-roll').value.trim().toUpperCase();
const dept = document.getElementById('nu-dept').value.trim();
const institution = document.getElementById('nu-institution').value.trim();
if (!name || !email || !role) {
showModalAlert('Please fill in all required fields.', 'danger', 'addUserAlert');
return;
}
if (role === 'student' && !roll) {
showModalAlert('Roll number is required for students.', 'danger', 'addUserAlert');
return;
}
// Check for duplicate email
const existing = allUsers.find(u => u.email && u.email.toLowerCase() === email);
if (existing) {
showModalAlert('A user with this email already exists.', 'danger', 'addUserAlert');
return;
}
try {
const newUser = {
id: Utils.generateUUID(),
email,
name,
role,
roll_number: role === 'student' ? roll : null,
department: dept || null,
institution: institution || null,
is_active: true,
face_data: null,
picture: null,
last_login: null
};
await API.post('users', newUser);
await Logger.log('USER_ADDED', 'users', { email, role, name });
Toast.success(`User "${name}" added as ${role}`);
closeModal('addUserModal');
document.getElementById('nu-name').value = '';
document.getElementById('nu-email').value = '';
document.getElementById('nu-roll').value = '';
document.getElementById('nu-dept').value = '';
document.getElementById('nu-institution').value = '';
await loadAllUsers();
filterUsers(currentUserFilter, null);
} catch(e) { showModalAlert('Error adding user. Please try again.', 'danger', 'addUserAlert'); }
}
/* ===== Sessions Admin ===== */
async function loadAdminSessions() {
try {
const [sRes, uRes] = await Promise.all([API.get('sessions', { limit: 200 }), API.get('users', { limit: 200 })]);
const sessions = (sRes.data||[]).sort((a,b) => (b.started_at||b.created_at)-(a.started_at||a.created_at));
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
const tbody = document.getElementById('adminSessionsBody');
if (!sessions.length) {
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;padding:32px;color:var(--gray)"><p>No sessions yet</p></td></tr>`;
return;
}
tbody.innerHTML = sessions.map(s => {
const teacher = userMap[s.teacher_id] || {};
return `<tr>
<td><strong>${s.subject}</strong></td>
<td>${s.class_name}</td>
<td>${teacher.name||'Unknown'}</td>
<td class="text-sm">${Utils.formatDateTime(s.started_at||s.created_at)}</td>
<td><span class="badge badge-${s.is_active?'danger':'gray'}">${s.is_active?'🔴 LIVE':'Ended'}</span></td>
<td>${s.total_students||'—'}</td>
<td>${s.is_active ? `<button class="btn btn-sm btn-danger" onclick="forceEndSession('${s.id}')">Force End</button>` : '—'}</td>
</tr>`;
}).join('');
} catch(e) {}
}
async function forceEndSession(id) {
if (!confirm('Force end this session?')) return;
try {
await API.patch('sessions', id, { is_active: false, ended_at: Date.now() });
await Logger.log('SESSION_FORCE_ENDED', 'sessions', { session_id: id });
Toast.success('Session force-ended');
await loadAdminSessions();
} catch(e) { Toast.error('Error ending session'); }
}
async function forceEndAllSessions() {
if (!confirm('Force end ALL active sessions? This will stop all ongoing attendance marking.')) return;
try {
const result = await API.get('sessions', { limit: 100 });
const live = (result.data||[]).filter(s => s.is_active);
await Promise.all(live.map(s => API.patch('sessions', s.id, { is_active: false, ended_at: Date.now() })));
Toast.success(`${live.length} sessions ended`);
await loadAdminSessions();
await loadDashboard();
} catch(e) { Toast.error('Error ending sessions'); }
}
/* ===== Attendance Admin ===== */
async function loadAdminAttendance() {
try {
const [aRes, uRes, sRes] = await Promise.all([
API.get('attendance', { limit: 1000 }),
API.get('users', { limit: 500 }),
API.get('sessions', { limit: 200 })
]);
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
const sessMap = {};
(sRes.data||[]).forEach(s => { sessMap[s.id] = s; });
let records = aRes.data || [];
const rangeFilter = document.getElementById('adminAttFilter')?.value;
const statusFilter = document.getElementById('adminAttStatus')?.value;
const ranges = { today: Date.now()-86400000, week: Date.now()-7*86400000, month: Date.now()-30*86400000, all: 0 };
const cutoff = ranges[rangeFilter] || 0;
records = records.filter(r => (r.marked_at||r.created_at) >= cutoff);
if (statusFilter) records = records.filter(r => r.status === statusFilter);
records.sort((a,b) => (b.marked_at||b.created_at) - (a.marked_at||a.created_at));
const tbody = document.getElementById('adminAttBody');
if (!records.length) {
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center;padding:32px;color:var(--gray)"><div class="empty-state-icon">📋</div><p>No records for selected filters</p></td></tr>`;
return;
}
tbody.innerHTML = records.map(r => {
const u = userMap[r.student_id] || {};
const s = sessMap[r.session_id] || {};
const flags = r.fraud_flags && r.fraud_flags.length ? `<span class="badge badge-danger">${r.fraud_flags.join(',')}</span>` : '—';
return `<tr>
<td><strong>${r.roll_number||'—'}</strong></td>
<td class="text-sm">${u.name||'Unknown'}</td>
<td class="text-sm">${s.subject||'—'}</td>
<td><span class="badge badge-${r.status==='present'||r.status==='late'?'success':r.status==='fraudulent'?'primary':'danger'}">${r.status}</span></td>
<td class="text-sm">${Utils.formatDateTime(r.marked_at||r.created_at)}</td>
<td class="text-sm text-gray">${r.gps_lat?`${r.gps_lat.toFixed(4)},${r.gps_lng.toFixed(4)}`:'—'}</td>
<td class="text-sm">${r.face_match_score!=null?`${(r.face_match_score*100).toFixed(0)}%`:'—'}</td>
<td>${flags}</td>
</tr>`;
}).join('');
} catch(e) { console.error('Attendance admin error:', e); }
}
/* ===== Fraud Admin ===== */
async function loadAdminFraud() {
try {
const [fRes, uRes] = await Promise.all([API.get('fraud_alerts', { limit: 500 }), API.get('users', { limit: 200 })]);
const alerts = (fRes.data||[]).sort((a,b) => (b.detected_at||b.created_at)-(a.detected_at||a.created_at));
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
document.getElementById('fraudBadge').textContent = alerts.filter(a => !a.resolved).length;
const el = document.getElementById('adminFraudList');
if (!alerts.length) {
el.innerHTML = `<div class="empty-state" style="padding:60px"><div class="empty-state-icon">🛡️</div><h3>No Fraud Alerts</h3><p>System is clean. No suspicious activity detected.</p></div>`;
return;
}
el.innerHTML = alerts.map(a => {
const s = userMap[a.student_id] || {};
return `<div class="fraud-alert-card" style="margin-bottom:12px">
<div class="fraud-alert-icon">🚨</div>
<div class="fraud-alert-content" style="flex:1">
<h4>${a.alert_type||'Suspicious Activity'}</h4>
<p>${a.description}</p>
<div style="display:flex;gap:8px;align-items:center;margin-top:6px;flex-wrap:wrap">
<span class="badge badge-${a.severity==='high'?'danger':a.severity==='medium'?'warning':'info'}">${a.severity}</span>
${s.name ? `<span class="text-sm">Student: <strong>${s.name}</strong> (${s.roll_number||'—'})</span>` : ''}
<span class="text-xs text-gray">${Utils.formatDateTime(a.detected_at||a.created_at)}</span>
${a.resolved ? '<span class="badge badge-success">Resolved</span>' : ''}
</div>
</div>
${!a.resolved ? `<button class="btn btn-sm btn-secondary" onclick="resolveAlert('${a.id}')">Resolve</button>` : ''}
</div>`;
}).join('');
} catch(e) {}
}
async function resolveAlert(id) {
try {
await API.patch('fraud_alerts', id, { resolved: true });
Toast.success('Alert resolved');
await loadAdminFraud();
} catch(e) { Toast.error('Error resolving alert'); }
}
/* ===== Audit Logs ===== */
async function loadAuditLogs() {
try {
const result = await API.get('system_logs', { limit: 200 });
let logs = (result.data||[]).sort((a,b) => (b.timestamp||b.created_at)-(a.timestamp||a.created_at));
const filter = document.getElementById('logFilter')?.value;
if (filter) logs = logs.filter(l => l.action === filter);
const tbody = document.getElementById('logsTableBody');
if (!logs.length) {
tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;padding:32px;color:var(--gray)">No logs found</td></tr>`;
return;
}
const uRes = await API.get('users', { limit: 200 });
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
tbody.innerHTML = logs.map(l => {
const user = userMap[l.user_id] || {};
return `<tr>
<td class="text-sm">${Utils.formatDateTime(l.timestamp||l.created_at)}</td>
<td class="text-sm">${user.name||l.user_id||'system'}</td>
<td><code style="font-size:12px;background:#F1F5F9;padding:2px 6px;border-radius:4px">${l.action||'—'}</code></td>
<td class="text-sm text-gray">${l.resource||'—'}</td>
<td><span class="badge badge-${l.status==='success'?'success':'danger'}">${l.status||'—'}</span></td>
</tr>`;
}).join('');
} catch(e) {}
}
/* ===== Analytics ===== */
async function loadAnalytics() {
try {
const [uRes, aRes, fRes] = await Promise.all([
API.get('users', { limit: 500 }),
API.get('attendance', { limit: 1000 }),
API.get('fraud_alerts', { limit: 200 })
]);
const users = uRes.data || [];
const att = aRes.data || [];
const fraudAlerts = fRes.data || [];
// Daily trend
const days = [];
for (let i = 6; i >= 0; i--) {
const d = new Date(Date.now() - i * 86400000);
const start = new Date(d.setHours(0,0,0,0)).getTime();
const dayAtt = att.filter(a => { const ts = a.marked_at||a.created_at; return ts >= start && ts < start+86400000; });
days.push({ label: d.toLocaleDateString('en-US',{weekday:'short'}), present: dayAtt.filter(a=>a.status==='present'||a.status==='late').length, total: dayAtt.length });
}
const tCtx = document.getElementById('adminTrendChart').getContext('2d');
if (adminTrendChart) adminTrendChart.destroy();
adminTrendChart = new Chart(tCtx, {
type: 'line',
data: { labels: days.map(d=>d.label), datasets: [
{ label: 'Present', data: days.map(d=>d.present), borderColor: '#10B981', backgroundColor: 'rgba(16,185,129,0.1)', fill: true, tension: 0.4 },
{ label: 'Total', data: days.map(d=>d.total), borderColor: '#4F46E5', backgroundColor: 'rgba(79,70,229,0.1)', fill: true, tension: 0.4 }
]},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } }
});
// User distribution
const students = users.filter(u=>u.role==='student').length;
const teachers = users.filter(u=>u.role==='teacher').length;
const admins = users.filter(u=>u.role==='admin').length;
const uCtx = document.getElementById('adminUserChart').getContext('2d');
if (adminUserChart) adminUserChart.destroy();
adminUserChart = new Chart(uCtx, {
type: 'doughnut',
data: { labels: ['Students','Teachers','Admins'], datasets: [{ data: [students,teachers,admins], backgroundColor: ['#4F46E5','#06B6D4','#F59E0B'], borderWidth: 0 }]},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } }
});
// Fraud summary
const fraudByType = {};
fraudAlerts.forEach(a => { fraudByType[a.alert_type||'Unknown'] = (fraudByType[a.alert_type||'Unknown']||0) + 1; });
const fCtx = document.getElementById('adminFraudChart').getContext('2d');
if (adminFraudChart) adminFraudChart.destroy();
adminFraudChart = new Chart(fCtx, {
type: 'bar',
data: { labels: Object.keys(fraudByType), datasets: [{ label: 'Alerts', data: Object.values(fraudByType), backgroundColor: 'rgba(239,68,68,0.7)', borderRadius: 6 }]},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } }, y: { grid: { color: '#f1f5f9' }, ticks: { stepSize: 1 } } } }
});
} catch(e) { console.error('Analytics error:', e); }
}
/* ===== Config ===== */
async function saveConfig() {
Toast.success('Configuration saved successfully');
await Logger.log('CONFIG_UPDATED', 'system', { admin: currentAdmin.id });
}
/* ===== Recent Logs ===== */
async function loadRecentLogs() {
try {
const result = await API.get('system_logs', { limit: 10 });
const logs = (result.data||[]).sort((a,b) => (b.timestamp||b.created_at)-(a.timestamp||a.created_at)).slice(0,5);
const el = document.getElementById('recentLogs');
if (!logs.length) {
el.innerHTML = `<div class="empty-state" style="padding:20px"><p>No recent activity</p></div>`;
return;
}
const uRes = await API.get('users', { limit: 200 });
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
el.innerHTML = logs.map(l => {
const u = userMap[l.user_id] || {};
return `<div class="activity-item"><div class="activity-content"><code style="font-size:12px;background:#F1F5F9;padding:1px 6px;border-radius:4px">${l.action}</code> by <strong>${u.name||l.user_id||'system'}</strong></div><div class="activity-time">${Utils.timeAgo(l.timestamp||l.created_at)}</div></div>`;
}).join('');
} catch(e) {}
}
/* ===== Export ===== */
async function exportUsers() {
const headers = ['Name','Email','Role','Roll Number','Department','Institution','Status'];
const rows = allUsers.map(u => [u.name,u.email,u.role,u.roll_number||'',u.department||'',u.institution||'',u.is_active!==false?'Active':'Inactive'].map(v=>`"${v||''}"`).join(','));
const csv = [headers.join(','),...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `users_export_${Date.now()}.csv`;
a.click();
Toast.success('Users exported');
}
async function exportAllAttendance() {
Toast.info('Preparing export...');
const [aRes, uRes, sRes] = await Promise.all([API.get('attendance',{limit:2000}), API.get('users',{limit:500}), API.get('sessions',{limit:200})]);
const userMap = {};
(uRes.data||[]).forEach(u => { userMap[u.id] = u; });
const sessMap = {};
(sRes.data||[]).forEach(s => { sessMap[s.id] = s; });
const headers = ['Roll No','Name','Email','Status','Session','Time','GPS','Face Score','Fraud Flags'];
const rows = (aRes.data||[]).map(r => {
const u = userMap[r.student_id]||{};
const s = sessMap[r.session_id]||{};
return [r.roll_number||'',u.name||'',u.email||'',r.status,s.subject||'',Utils.formatDateTime(r.marked_at||r.created_at),r.gps_lat?`${r.gps_lat},${r.gps_lng}`:'',r.face_match_score||'',(r.fraud_flags||[]).join(';')].map(v=>`"${v||''}"`).join(',');
});
const csv = [headers.join(','),...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `all_attendance_${Date.now()}.csv`;
a.click();
Toast.success('Attendance exported');
}
/* ===== Modal Helpers ===== */
function openModal(id) { document.getElementById(id).classList.add('open'); }
function closeModal(id) { document.getElementById(id).classList.remove('open'); document.getElementById('addUserAlert').style.display='none'; }
function showModalAlert(msg, type, id) {
const el = document.getElementById(id);
el.style.display = 'block';
el.innerHTML = `<div class="alert alert-${type}"><span>${type==='danger'?'❌':'⚠️'}</span><div>${msg}</div></div>`;
}
async function confirmAction(action, msg) {
if (!confirm(msg)) return;
if (action === 'clear_fraud') {
const result = await API.get('fraud_alerts', { limit: 500 });
await Promise.all((result.data||[]).filter(a=>!a.resolved).map(a => API.patch('fraud_alerts', a.id, { resolved: true })));
Toast.success('All fraud alerts resolved');
await loadAdminFraud();
} else if (action === 'end_sessions') {
await forceEndAllSessions();
}
}
async function refreshAll() {
Toast.info('Refreshing...');
await Promise.all([loadDashboard(), loadAllUsers(), loadAdminSessions(), loadAdminFraud(), loadAuditLogs()]);
Toast.success('Refreshed');
}
/* ===== Init ===== */
window.addEventListener('load', () => {
checkAdminAuth();
if (APP_CONFIG.googleClientId !== 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com' && typeof google !== 'undefined') {
google.accounts.id.initialize({ client_id: APP_CONFIG.googleClientId, callback: handleGoogleCredential, auto_select: false });
// Render official Google button
google.accounts.id.renderButton(
document.getElementById("adminGoogleBtnContainer"),
{ theme: "outline", size: "large", width: "100%", text: "signin_with", shape: "rectangular" }
);
}
});
</script>
</body>
</html>