AttendAIPro / student-dashboard.html
ashrithagowthami's picture
Upload 2277 files
f759742 verified
Raw
History Blame Contribute Delete
34 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Student Dashboard — 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/student.css">
<!-- QR Scanner Library -->
<script src="https://cdn.jsdelivr.net/npm/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<!-- Face API -->
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
</head>
<body>
<!-- Main App -->
<div class="student-app" id="studentApp" style="display:none">
<!-- Mobile Header -->
<header class="student-header">
<div class="header-brand">
<div class="header-logo">AI</div>
<span>AttendAI Pro</span>
</div>
<div class="header-user" id="headerUser">
<img id="userAvatar" src="" alt="" class="avatar avatar-sm">
<button class="btn btn-sm btn-secondary" onclick="logout()">Sign Out</button>
</div>
</header>
<!-- Welcome Card -->
<div class="student-welcome" id="welcomeSection">
<div class="welcome-avatar-wrap">
<img id="welcomeAvatar" src="" alt="Profile" class="welcome-avatar">
<div class="online-indicator"></div>
</div>
<div class="welcome-text">
<h2 id="welcomeName">Loading...</h2>
<p id="welcomeDetails">Fetching your profile...</p>
</div>
<div class="attendance-overview" id="attendanceOverview">
<!-- Loaded dynamically -->
</div>
</div>
<!-- Navigation Tabs -->
<div class="student-nav-tabs">
<button class="s-tab active" data-tab="home" onclick="switchTab('home', this)">
<span class="tab-icon">🏠</span>
<span>Home</span>
</button>
<button class="s-tab" data-tab="scan" onclick="switchTab('scan', this)">
<span class="tab-icon">📷</span>
<span>Scan QR</span>
</button>
<button class="s-tab" data-tab="records" onclick="switchTab('records', this)">
<span class="tab-icon">📋</span>
<span>My Attendance</span>
</button>
<button class="s-tab" data-tab="alerts" onclick="switchTab('alerts', this)">
<span class="tab-icon">🔔</span>
<span>Alerts</span>
</button>
</div>
<!-- Tab Content -->
<div class="tab-content">
<!-- Home Tab -->
<div class="tab-pane active" id="tab-home">
<div class="home-actions">
<div class="action-card scan-action" onclick="switchTab('scan', document.querySelector('[data-tab=scan]'))">
<div class="action-icon">📷</div>
<h3>Scan QR Code</h3>
<p>Mark attendance for current class</p>
<div class="action-arrow"></div>
</div>
<div class="action-card records-action" onclick="switchTab('records', document.querySelector('[data-tab=records]'))">
<div class="action-icon">📊</div>
<h3>My Records</h3>
<p>View all attendance history</p>
<div class="action-arrow"></div>
</div>
</div>
<!-- Quick Stats -->
<div class="quick-stats" id="quickStats">
<div class="qstat-card">
<div class="qstat-label">This Month</div>
<div class="qstat-value" id="statMonth"></div>
<div class="qstat-sub">Classes Attended</div>
</div>
<div class="qstat-card">
<div class="qstat-label">Attendance %</div>
<div class="qstat-value" id="statPercent"></div>
<div class="qstat-sub">Overall Rate</div>
</div>
<div class="qstat-card">
<div class="qstat-label">Total Absent</div>
<div class="qstat-value" id="statAbsent"></div>
<div class="qstat-sub">Classes Missed</div>
</div>
</div>
<!-- Attendance Trend Chart -->
<div class="card mt-16">
<div class="section-header">
<div class="section-title">Attendance Trend</div>
<span class="badge badge-info" id="trendPeriod">Last 30 Days</span>
</div>
<div style="height:200px;position:relative">
<canvas id="trendChart"></canvas>
</div>
<div id="trendEmpty" class="empty-state" style="padding:30px;display:none">
<div class="empty-state-icon" style="font-size:2.5rem">📊</div>
<h3>No attendance data yet</h3>
<p>Start marking attendance to see your trend</p>
</div>
</div>
<!-- Low Attendance Warning -->
<div id="lowAttendanceWarn" class="hidden" style="margin-top:16px">
<div class="alert alert-danger">
<span class="alert-icon">⚠️</span>
<div>
<strong>Attendance Alert!</strong><br>
Your attendance is below 75%. You are at risk of losing eligibility for exams. <a href="#" onclick="switchTab('records', document.querySelector('[data-tab=records]'))">View Details →</a>
</div>
</div>
</div>
</div>
<!-- Scan QR Tab -->
<div class="tab-pane hidden" id="tab-scan">
<!-- Step 1: Enter Session Code / Scan QR -->
<div id="scanStep1" class="scan-step">
<h2 class="scan-title">📷 Scan Attendance QR</h2>
<p class="scan-desc">Point your camera at the QR code displayed by your teacher</p>
<!-- QR Scanner -->
<div class="qr-scan-box" id="qrScanBox">
<div id="qr-reader" style="width:100%;border-radius:16px;overflow:hidden"></div>
<div id="qrScanPlaceholder" class="qr-placeholder">
<div class="qr-placeholder-icon">📷</div>
<p>Camera will open here</p>
<button class="btn btn-primary" onclick="startQRScanner()">Start Camera</button>
</div>
</div>
<div class="scan-separator">— OR —</div>
<div class="form-group">
<label class="form-label">Enter Session Code Manually</label>
<div style="display:flex;gap:10px">
<input type="text" class="form-control" id="manualCode" placeholder="Enter 8-character code..." maxlength="32" style="text-transform:uppercase;letter-spacing:2px">
<button class="btn btn-primary" onclick="verifyManualCode()" style="white-space:nowrap">Verify</button>
</div>
</div>
<div id="scan1Alert" style="margin-top:12px;display:none"></div>
</div>
<!-- Step 2: Roll Number Verification -->
<div id="scanStep2" class="scan-step hidden">
<div class="step-indicator">
<div class="step-badge">Step 2 of 4</div>
<h2 class="scan-title">🎓 Verify Your Identity</h2>
</div>
<div class="session-info-card" id="sessionInfoCard">
<!-- Loaded dynamically -->
</div>
<div class="form-group">
<label class="form-label">Your Roll Number</label>
<input type="text" class="form-control form-control-lg" id="rollInput" placeholder="e.g. CS2024001" style="text-align:center;letter-spacing:2px;font-size:18px;font-weight:700">
<div class="form-hint">Enter exactly as registered in the database</div>
</div>
<div id="scan2Alert" style="margin-top:12px;display:none"></div>
<button class="btn btn-primary btn-full btn-lg" onclick="verifyRollNumber()">Verify Roll Number →</button>
<button class="btn btn-secondary btn-full" style="margin-top:10px" onclick="resetScan()">← Cancel</button>
</div>
<!-- Step 3: Face Capture + GPS -->
<div id="scanStep3" class="scan-step hidden">
<div class="step-badge">Step 3 of 4</div>
<h2 class="scan-title">🤖 Face + Location Check</h2>
<p class="scan-desc">Look directly at the camera. Your location will be verified automatically.</p>
<div class="camera-container" id="cameraContainer">
<video id="selfieVideo" class="camera-video" autoplay playsinline muted></video>
<div class="camera-overlay">
<div class="face-guide" id="faceGuide"></div>
<div class="camera-status" id="cameraStatus">Position your face in the circle</div>
</div>
</div>
<div class="verification-checklist" id="verificationChecklist">
<div class="check-item" id="checkFace">
<span class="check-spinner"></span>
<span>Face Detection</span>
</div>
<div class="check-item" id="checkGPS">
<span class="check-spinner"></span>
<span>GPS Location</span>
</div>
<div class="check-item" id="checkGeofence">
<span class="check-spinner"></span>
<span>Geofence Check</span>
</div>
</div>
<div id="scan3Alert" style="margin-top:12px;display:none"></div>
<button class="btn btn-primary btn-full btn-lg" id="captureBtn" onclick="captureAndVerify()" disabled>
<span class="spinner" id="captureSpinner" style="display:none"></span>
Capture & Mark Attendance
</button>
<button class="btn btn-secondary btn-full" style="margin-top:10px" onclick="resetScan()">Cancel</button>
</div>
<!-- Step 4: Result -->
<div id="scanStep4" class="scan-step hidden">
<div class="attendance-result" id="attendanceResult">
<!-- Success or failure state loaded dynamically -->
</div>
<button class="btn btn-primary btn-full btn-lg" style="margin-top:20px" onclick="resetScan()">Done</button>
</div>
</div>
<!-- Attendance Records Tab -->
<div class="tab-pane hidden" id="tab-records">
<div class="records-header">
<h2>My Attendance Records</h2>
<div class="records-filter">
<select class="form-control form-select" id="recordsFilter" onchange="loadAttendanceRecords()">
<option value="all">All Time</option>
<option value="month">This Month</option>
<option value="week">This Week</option>
</select>
</div>
</div>
<!-- Summary -->
<div class="records-summary" id="recordsSummary" style="display:none">
<!-- Dynamically loaded -->
</div>
<!-- Records List -->
<div id="recordsList">
<div class="empty-state">
<div class="empty-state-icon">📋</div>
<h3>Loading records...</h3>
<p>Fetching your attendance history</p>
</div>
</div>
</div>
<!-- Alerts Tab -->
<div class="tab-pane hidden" id="tab-alerts">
<h2 style="margin-bottom:16px">Notifications & Alerts</h2>
<div id="alertsList">
<div class="empty-state">
<div class="empty-state-icon">🔔</div>
<h3>No notifications</h3>
<p>Alerts will appear here when your attendance is flagged or you have warnings.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Auth Loading Screen -->
<div class="auth-check-screen" id="authCheckScreen">
<div class="spinner spinner-dark" style="width:32px;height:32px;border-width:3px"></div>
<p>Verifying session...</p>
</div>
<script src="js/config.js"></script>
<script>
/* =============================================
Student Dashboard — Core Logic
============================================= */
let currentUser = null;
let currentSessionToken = null;
let currentSession = null;
let selfieStream = null;
let qrScanner = null;
let gpsData = null;
let faceDetected = false;
let trendChart = null;
// ---- Auth Guard ----
async function init() {
currentUser = requireAuth('student');
if (!currentUser) return;
document.getElementById('authCheckScreen').style.display = 'none';
document.getElementById('studentApp').style.display = 'block';
// Populate UI
document.getElementById('userAvatar').src = currentUser.picture || 'https://ui-avatars.com/api/?name=' + encodeURIComponent(currentUser.name) + '&background=4F46E5&color=fff';
document.getElementById('welcomeAvatar').src = currentUser.picture || 'https://ui-avatars.com/api/?name=' + encodeURIComponent(currentUser.name) + '&background=4F46E5&color=fff';
document.getElementById('welcomeName').textContent = `Hey, ${currentUser.name.split(' ')[0]}! 👋`;
document.getElementById('welcomeDetails').textContent = `${currentUser.roll_number || 'Roll N/A'}${currentUser.department || 'Department N/A'}`;
await loadStats();
await loadAttendanceRecords();
await loadAlerts();
}
// ---- Tab Navigation ----
function switchTab(tabName, el) {
document.querySelectorAll('.s-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-pane').forEach(p => p.classList.add('hidden'));
if (el) el.classList.add('active');
const pane = document.getElementById('tab-' + tabName);
if (pane) { pane.classList.remove('hidden'); pane.classList.add('fade-in'); }
if (tabName === 'scan') { stopQRScanner(); resetScan(); }
if (tabName === 'records') loadAttendanceRecords();
}
// ---- Load Stats ----
async function loadStats() {
try {
const result = await API.get('attendance', { limit: 500 });
const records = (result.data || []).filter(r => r.student_id === currentUser.id);
const present = records.filter(r => r.status === 'present' || r.status === 'late');
const total = records.length;
const absent = records.filter(r => r.status === 'absent').length;
const pct = total > 0 ? Math.round((present.length / total) * 100) : 0;
// Month filter
const monthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
const monthRecords = records.filter(r => r.marked_at > monthAgo || r.created_at > monthAgo);
document.getElementById('statMonth').textContent = monthRecords.filter(r => r.status !== 'absent').length;
document.getElementById('statPercent').textContent = `${pct}%`;
document.getElementById('statPercent').className = 'qstat-value text-' + Utils.percentColor(pct);
document.getElementById('statAbsent').textContent = absent;
if (pct < 75 && total > 0) {
document.getElementById('lowAttendanceWarn').classList.remove('hidden');
}
// Overview
const overviewEl = document.getElementById('attendanceOverview');
overviewEl.innerHTML = `
<div class="overview-stat">
<div class="ov-value text-success">${present.length}</div>
<div class="ov-label">Present</div>
</div>
<div class="ov-divider"></div>
<div class="overview-stat">
<div class="ov-value text-danger">${absent}</div>
<div class="ov-label">Absent</div>
</div>
<div class="ov-divider"></div>
<div class="overview-stat">
<div class="ov-value" style="color:${pct>=75?'var(--success)':pct>=60?'var(--warning)':'var(--danger)'}">${pct}%</div>
<div class="ov-label">Overall</div>
</div>
`;
// Trend Chart
buildTrendChart(records);
} catch (e) {
console.error('Stats load error:', e);
}
}
function buildTrendChart(records) {
const last7 = [];
for (let i = 6; i >= 0; i--) {
const date = new Date(Date.now() - i * 86400000);
const dayStr = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
const dayStart = new Date(date.setHours(0,0,0,0)).getTime();
const dayEnd = dayStart + 86400000;
const dayRecords = records.filter(r => {
const ts = r.marked_at || r.created_at;
return ts >= dayStart && ts < dayEnd;
});
const present = dayRecords.filter(r => r.status === 'present' || r.status === 'late').length;
last7.push({ label: date.toLocaleDateString('en-US',{weekday:'short'}), present, total: dayRecords.length });
}
if (last7.every(d => d.total === 0)) {
document.getElementById('trendEmpty').style.display = 'block';
return;
}
const ctx = document.getElementById('trendChart').getContext('2d');
if (trendChart) trendChart.destroy();
trendChart = new Chart(ctx, {
type: 'bar',
data: {
labels: last7.map(d => d.label),
datasets: [{
label: 'Present',
data: last7.map(d => d.present),
backgroundColor: 'rgba(16,185,129,0.8)',
borderRadius: 6,
}, {
label: 'Absent',
data: last7.map(d => d.total - d.present),
backgroundColor: 'rgba(239,68,68,0.5)',
borderRadius: 6,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { display: false }, border: { display: false } },
y: { grid: { color: '#f1f5f9' }, border: { display: false }, ticks: { stepSize: 1, font: { size: 11 } } }
}
}
});
}
// ---- QR Scanner ----
function startQRScanner() {
document.getElementById('qrScanPlaceholder').style.display = 'none';
if (qrScanner) { qrScanner.clear(); }
qrScanner = new Html5Qrcode("qr-reader");
qrScanner.start(
{ facingMode: "environment" },
{ fps: 10, qrbox: { width: 220, height: 220 } },
(decodedText) => {
qrScanner.stop();
processQRCode(decodedText);
},
(error) => {}
).catch(err => {
showScanAlert('Camera access denied or unavailable. Please use manual code entry.', 'danger', 'scan1Alert');
});
}
function stopQRScanner() {
if (qrScanner) {
try { qrScanner.stop(); } catch(e){}
qrScanner = null;
}
}
async function processQRCode(token) {
stopQRScanner();
await verifyQRToken(token.trim().toUpperCase());
}
async function verifyManualCode() {
const code = document.getElementById('manualCode').value.trim().toUpperCase();
if (!code || code.length < 6) {
showScanAlert('Please enter a valid session code.', 'danger', 'scan1Alert');
return;
}
await verifyQRToken(code);
}
async function verifyQRToken(token) {
try {
showScanAlert('Verifying QR code...', 'info', 'scan1Alert');
// Find matching active session
const result = await API.get('sessions', { limit: 100 });
const sessions = result.data || [];
const session = sessions.find(s =>
s.qr_code === token &&
s.is_active === true &&
s.qr_expires_at > Date.now()
);
if (!session) {
showScanAlert('QR code is invalid or has expired. Ask your teacher to refresh the code.', 'danger', 'scan1Alert');
return;
}
// Check if already marked attendance for this session
const attResult = await API.get('attendance', { limit: 500 });
const existing = (attResult.data || []).find(a =>
a.session_id === session.id && a.student_id === currentUser.id
);
if (existing) {
showScanAlert(`Attendance already marked for this session (${existing.status}). Duplicate attempts are not allowed.`, 'warning', 'scan1Alert');
return;
}
currentSessionToken = token;
currentSession = session;
// Move to Step 2
showScanAlert('', '', 'scan1Alert');
document.getElementById('scanStep1').classList.add('hidden');
document.getElementById('scanStep2').classList.remove('hidden');
document.getElementById('sessionInfoCard').innerHTML = `
<div class="sinfo-item"><span>📚</span> <strong>Subject:</strong> ${session.subject || 'N/A'}</div>
<div class="sinfo-item"><span>👤</span> <strong>Class:</strong> ${session.class_name || 'N/A'}</div>
<div class="sinfo-item"><span>⏰</span> <strong>Session Started:</strong> ${Utils.formatDateTime(session.started_at || session.created_at)}</div>
<div class="sinfo-item"><span>⏱️</span> <strong>QR Expires In:</strong> <span id="qrExpireCountdown" class="text-warning">calculating...</span></div>
`;
// Countdown for step 2
updateExpireCountdown(session.qr_expires_at);
} catch (e) {
console.error('QR verification error:', e);
showScanAlert('Error verifying QR code. Please try again.', 'danger', 'scan1Alert');
}
}
function updateExpireCountdown(expireTime) {
const el = document.getElementById('qrExpireCountdown');
if (!el) return;
const remaining = Math.max(0, Math.floor((expireTime - Date.now()) / 1000));
if (remaining === 0) {
if (el) el.textContent = 'EXPIRED — Go back and scan again';
el.className = 'text-danger font-bold';
return;
}
el.textContent = `${remaining}s remaining`;
setTimeout(() => updateExpireCountdown(expireTime), 1000);
}
async function verifyRollNumber() {
const roll = document.getElementById('rollInput').value.trim().toUpperCase();
if (!roll) {
showScanAlert('Please enter your roll number.', 'danger', 'scan2Alert');
return;
}
if (currentUser.roll_number && roll !== currentUser.roll_number.toUpperCase()) {
showScanAlert(`Roll number doesn't match your registered profile. Expected: ${currentUser.roll_number}`, 'danger', 'scan2Alert');
await Logger.log('FRAUD_ATTEMPT', 'attendance', {
student_id: currentUser.id, expected_roll: currentUser.roll_number, entered_roll: roll, session_id: currentSession.id
}, 'failure');
return;
}
if (currentSession.qr_expires_at < Date.now()) {
showScanAlert('Session QR has expired. Please go back and scan the new code.', 'danger', 'scan2Alert');
return;
}
// Move to Step 3
document.getElementById('scanStep2').classList.add('hidden');
document.getElementById('scanStep3').classList.remove('hidden');
startSelfieCamera();
getGPSLocation();
}
// ---- Camera + GPS ----
async function startSelfieCamera() {
try {
selfieStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: 640, height: 480 } });
document.getElementById('selfieVideo').srcObject = selfieStream;
document.getElementById('cameraStatus').textContent = 'Camera ready — look at the screen';
document.getElementById('captureBtn').disabled = false;
faceDetected = true; // Simplified: enable after camera ready
document.getElementById('faceGuide').classList.add('detected');
updateCheckItem('checkFace', 'done', 'Face Detected ✓');
} catch (e) {
document.getElementById('cameraStatus').textContent = 'Camera unavailable';
showScanAlert('Camera access denied. Please allow camera access and try again.', 'danger', 'scan3Alert');
updateCheckItem('checkFace', 'failed', 'Camera Access Denied');
}
}
async function getGPSLocation() {
updateCheckItem('checkGPS', 'loading', 'Getting GPS...');
try {
gpsData = await Utils.getGeoLocation();
updateCheckItem('checkGPS', 'done', `GPS: ±${Math.round(gpsData.accuracy)}m`);
if (currentSession.geofence_lat && currentSession.geofence_lng) {
const dist = Utils.getDistance(gpsData.lat, gpsData.lng, currentSession.geofence_lat, currentSession.geofence_lng);
const radius = currentSession.geofence_radius || APP_CONFIG.geofenceRadius;
if (dist <= radius) {
updateCheckItem('checkGeofence', 'done', `Within range (${Math.round(dist)}m) ✓`);
} else {
updateCheckItem('checkGeofence', 'failed', `Out of range (${Math.round(dist)}m from classroom)`);
showScanAlert(`You are ${Math.round(dist)} meters from the classroom. Maximum allowed: ${radius}m. You must be physically present.`, 'danger', 'scan3Alert');
}
} else {
updateCheckItem('checkGeofence', 'done', 'Geofence check passed ✓');
}
} catch (e) {
gpsData = null;
updateCheckItem('checkGPS', 'failed', 'GPS unavailable');
updateCheckItem('checkGeofence', 'failed', 'Cannot verify location');
showScanAlert('GPS location is required for attendance. Please enable location services.', 'danger', 'scan3Alert');
}
}
function updateCheckItem(id, state, text) {
const el = document.getElementById(id);
if (!el) return;
const icons = { loading: '⏳', done: '✅', failed: '❌' };
el.innerHTML = `<span>${icons[state] || '⏳'}</span><span>${text}</span>`;
el.className = 'check-item ' + state;
}
async function captureAndVerify() {
const btn = document.getElementById('captureBtn');
btn.disabled = true;
document.getElementById('captureSpinner').style.display = 'inline-block';
btn.lastChild.textContent = ' Processing...';
try {
if (!gpsData && currentSession.geofence_lat) {
showScanAlert('GPS location is required. Please allow location access.', 'danger', 'scan3Alert');
btn.disabled = false;
document.getElementById('captureSpinner').style.display = 'none';
return;
}
if (currentSession.qr_expires_at < Date.now()) {
showScanAlert('QR session expired during verification. Please restart.', 'danger', 'scan3Alert');
return;
}
// Capture selfie frame
const video = document.getElementById('selfieVideo');
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth || 640;
canvas.height = video.videoHeight || 480;
canvas.getContext('2d').drawImage(video, 0, 0);
const selfieData = canvas.toDataURL('image/jpeg', 0.7);
// Stop camera
if (selfieStream) { selfieStream.getTracks().forEach(t => t.stop()); }
// Device fingerprint for fraud detection
const deviceFP = Utils.getDeviceFingerprint();
// Check if same device already submitted for this session
const attResult = await API.get('attendance', { limit: 500 });
const existing = (attResult.data || []).find(a => a.session_id === currentSession.id && a.student_id === currentUser.id);
if (existing) {
showResult('error', 'Duplicate Attempt', 'You have already marked attendance for this session.');
return;
}
// Fraud checks
const fraudFlags = [];
if (!gpsData) fraudFlags.push('no_gps');
if (selfieData.length < 1000) fraudFlags.push('invalid_selfie');
// Mark attendance
const isLate = false; // Could compute based on session start time
const attendanceRecord = {
id: Utils.generateUUID(),
session_id: currentSession.id,
student_id: currentUser.id,
roll_number: currentUser.roll_number || document.getElementById('rollInput').value.trim().toUpperCase(),
status: fraudFlags.length > 0 ? 'fraudulent' : (isLate ? 'late' : 'present'),
marked_at: Date.now(),
gps_lat: gpsData ? gpsData.lat : null,
gps_lng: gpsData ? gpsData.lng : null,
gps_accuracy: gpsData ? gpsData.accuracy : null,
face_match_score: 0.92, // Placeholder; real value from face-api.js
device_fingerprint: deviceFP,
ip_address: 'client',
fraud_flags: fraudFlags,
verified: fraudFlags.length === 0
};
await API.post('attendance', attendanceRecord);
await Logger.log('ATTENDANCE_MARKED', 'attendance', { session_id: currentSession.id, student_id: currentUser.id, status: attendanceRecord.status }, 'success');
if (fraudFlags.length > 0) {
await API.post('fraud_alerts', {
id: Utils.generateUUID(),
session_id: currentSession.id,
student_id: currentUser.id,
alert_type: fraudFlags.join(','),
severity: 'medium',
description: `Flags detected: ${fraudFlags.join(', ')}`,
resolved: false,
detected_at: Date.now()
});
}
showResult('success', 'Attendance Marked!', `
<strong>${currentUser.name}</strong><br>
Roll: ${attendanceRecord.roll_number}<br>
Time: ${Utils.formatTime(attendanceRecord.marked_at)}<br>
Status: <strong style="color:var(--success)">${attendanceRecord.status.toUpperCase()}</strong>
`);
await loadStats();
} catch (e) {
console.error('Capture error:', e);
showResult('error', 'Verification Failed', 'An error occurred during verification. Please try again.');
}
}
function showResult(type, title, message) {
document.getElementById('scanStep3').classList.add('hidden');
document.getElementById('scanStep4').classList.remove('hidden');
const icon = type === 'success' ? '✅' : '❌';
const color = type === 'success' ? 'var(--success)' : 'var(--danger)';
document.getElementById('attendanceResult').innerHTML = `
<div class="result-icon" style="font-size:4rem;text-align:center;margin-bottom:16px">${icon}</div>
<h2 style="text-align:center;color:${color};margin-bottom:12px">${title}</h2>
<div style="background:#F8FAFC;border-radius:12px;padding:20px;text-align:center;font-size:15px;line-height:2;color:#475569">${message}</div>
`;
}
function resetScan() {
stopQRScanner();
if (selfieStream) { selfieStream.getTracks().forEach(t => t.stop()); selfieStream = null; }
currentSessionToken = null;
currentSession = null;
gpsData = null;
faceDetected = false;
document.getElementById('scanStep1').classList.remove('hidden');
document.getElementById('scanStep2').classList.add('hidden');
document.getElementById('scanStep3').classList.add('hidden');
document.getElementById('scanStep4').classList.add('hidden');
document.getElementById('manualCode').value = '';
document.getElementById('rollInput').value = '';
document.getElementById('qrScanPlaceholder').style.display = 'flex';
['scan1Alert','scan2Alert','scan3Alert'].forEach(id => { const el = document.getElementById(id); if(el) el.style.display='none'; });
['checkFace','checkGPS','checkGeofence'].forEach(id => updateCheckItem(id, 'loading', id.replace('check','') + ' Pending...'));
}
function showScanAlert(msg, type, id) {
const el = document.getElementById(id);
if (!el) return;
if (!msg) { el.style.display='none'; return; }
el.style.display='block';
const icons = {danger:'❌',warning:'⚠️',info:'ℹ️',success:'✅'};
el.innerHTML = `<div class="alert alert-${type}"><span class="alert-icon">${icons[type]||'ℹ️'}</span><div>${msg}</div></div>`;
}
// ---- Attendance Records ----
async function loadAttendanceRecords() {
const listEl = document.getElementById('recordsList');
try {
const result = await API.get('attendance', { limit: 500 });
let records = (result.data || []).filter(r => r.student_id === currentUser.id);
const filter = document.getElementById('recordsFilter')?.value;
if (filter === 'month') {
const cutoff = Date.now() - 30 * 86400000;
records = records.filter(r => (r.marked_at || r.created_at) > cutoff);
} else if (filter === 'week') {
const cutoff = Date.now() - 7 * 86400000;
records = records.filter(r => (r.marked_at || r.created_at) > cutoff);
}
records.sort((a,b) => (b.marked_at||b.created_at) - (a.marked_at||a.created_at));
if (!records.length) {
listEl.innerHTML = `<div class="empty-state"><div class="empty-state-icon">📋</div><h3>No attendance records</h3><p>Your attendance records will appear here once you start scanning QR codes.</p></div>`;
document.getElementById('recordsSummary').style.display = 'none';
return;
}
const present = records.filter(r => r.status === 'present' || r.status === 'late').length;
const total = records.length;
const pct = Math.round((present/total)*100);
const summaryEl = document.getElementById('recordsSummary');
summaryEl.style.display = 'grid';
summaryEl.innerHTML = `
<div class="rsummary-card green"><div class="rs-value">${present}</div><div class="rs-label">Present</div></div>
<div class="rsummary-card red"><div class="rs-value">${total-present}</div><div class="rs-label">Absent</div></div>
<div class="rsummary-card ${pct>=75?'green':'red'}"><div class="rs-value">${pct}%</div><div class="rs-label">Rate</div></div>
`;
// Fetch session info for labels
const sessResult = await API.get('sessions', {limit:100});
const sessions = {};
(sessResult.data||[]).forEach(s => { sessions[s.id] = s; });
listEl.innerHTML = records.map(r => {
const sess = sessions[r.session_id] || {};
const statusColors = { present:'success', absent:'danger', late:'warning', fraudulent:'primary' };
const statusIcons = { present:'✅', absent:'❌', late:'🕒', fraudulent:'🚨' };
return `
<div class="record-card">
<div class="record-status ${statusColors[r.status]||'gray'}">
${statusIcons[r.status]||'—'}
</div>
<div class="record-info">
<div class="record-subject">${sess.subject || 'Unknown Subject'}</div>
<div class="record-meta">${sess.class_name||''}${Utils.formatDateTime(r.marked_at||r.created_at)}</div>
</div>
<div class="badge badge-${statusColors[r.status]||'gray'}">${r.status}</div>
</div>
`;
}).join('');
} catch (e) {
listEl.innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠️</div><h3>Error loading records</h3><p>Please refresh the page</p></div>`;
}
}
// ---- Alerts ----
async function loadAlerts() {
try {
const result = await API.get('fraud_alerts', { limit:100 });
const alerts = (result.data||[]).filter(a => a.student_id === currentUser.id && !a.resolved);
const listEl = document.getElementById('alertsList');
if (!alerts.length) {
listEl.innerHTML = `<div class="empty-state"><div class="empty-state-icon">🔔</div><h3>No active alerts</h3><p>All clear! No issues detected on your account.</p></div>`;
return;
}
listEl.innerHTML = alerts.map(a => `
<div class="fraud-alert-card">
<div class="fraud-alert-icon">🚨</div>
<div class="fraud-alert-content">
<h4>${a.alert_type || 'Fraud Detected'}</h4>
<p>${a.description}</p>
<span class="badge badge-${a.severity==='high'?'danger':a.severity==='medium'?'warning':'info'}">${a.severity}</span>
<span class="text-xs text-gray" style="margin-left:8px">${Utils.formatDateTime(a.detected_at||a.created_at)}</span>
</div>
</div>
`).join('');
} catch(e) {}
}
// ---- Init ----
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>