363 lines
14 KiB
PHP
363 lines
14 KiB
PHP
<?php
|
|
require_once '../includes/config.php';
|
|
|
|
// Check if user is logged in and is admin/teacher
|
|
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true ||
|
|
($_SESSION['role'] !== 'admin' && $_SESSION['role'] !== 'teacher')) {
|
|
header('Location: ../auth/login.php');
|
|
exit();
|
|
}
|
|
|
|
$title = "QR Code Scanner";
|
|
|
|
// Get active activities for today
|
|
$activities = [];
|
|
$sql = "SELECT * FROM activities WHERE status = 1 AND date = CURDATE() ORDER BY time_in ASC";
|
|
$result = query($conn, $sql);
|
|
while ($row = mysqli_fetch_assoc($result)) {
|
|
$activities[] = $row;
|
|
}
|
|
|
|
include '../includes/header.php';
|
|
?>
|
|
|
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
|
<h1 class="h2">QR Code Scanner</h1>
|
|
<div class="btn-toolbar mb-2 mb-md-0">
|
|
<div class="btn-group me-2">
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="switchCamera()">
|
|
<i class="bi bi-camera-video"></i> Switch Camera
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="toggleScanner()">
|
|
<i class="bi bi-power"></i> Toggle Scanner
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-8">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Scanner</h6>
|
|
</div>
|
|
<div class="card-body text-center">
|
|
<!-- Scanner Container -->
|
|
<div id="qr-reader" style="width: 100%; max-width: 500px; margin: 0 auto;"></div>
|
|
|
|
<!-- Scan Result -->
|
|
<div id="scan-result" class="mt-4"></div>
|
|
|
|
<!-- Manual Entry Form -->
|
|
<div class="mt-4">
|
|
<h6 class="text-muted mb-3">Or enter student ID manually:</h6>
|
|
<form id="manual-entry-form" class="row g-3">
|
|
<div class="col-md-8">
|
|
<input type="text" class="form-control" id="student_id"
|
|
placeholder="Enter Student ID" required>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="bi bi-check-circle"></i> Submit
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-4">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Today's Activities</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<?php if (empty($activities)): ?>
|
|
<div class="alert alert-warning">
|
|
<i class="bi bi-info-circle"></i> No activities scheduled for today.
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="list-group">
|
|
<?php foreach ($activities as $activity): ?>
|
|
<div class="list-group-item">
|
|
<h6 class="mb-1"><?php echo $activity['name']; ?></h6>
|
|
<small class="text-muted">
|
|
<i class="bi bi-clock"></i>
|
|
<?php echo date('h:i A', strtotime($activity['time_in'])); ?> -
|
|
<?php echo date('h:i A', strtotime($activity['time_out'])); ?>
|
|
</small><br>
|
|
<small class="text-muted">
|
|
<i class="bi bi-geo-alt"></i> <?php echo $activity['location']; ?>
|
|
</small>
|
|
<div class="mt-2">
|
|
<span class="badge bg-<?php echo time() >= strtotime($activity['time_in']) && time() <= strtotime($activity['time_out']) ? 'success' : 'secondary'; ?>">
|
|
<?php echo time() >= strtotime($activity['time_in']) && time() <= strtotime($activity['time_out']) ? 'Active Now' : 'Scheduled'; ?>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Quick Stats</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<?php
|
|
$sql = "SELECT COUNT(*) as total FROM attendance WHERE DATE(created_at) = CURDATE()";
|
|
$result = query($conn, $sql);
|
|
$today_attendance = mysqli_fetch_assoc($result)['total'];
|
|
|
|
$sql = "SELECT COUNT(*) as total FROM students WHERE status = 1";
|
|
$result = query($conn, $sql);
|
|
$total_students = mysqli_fetch_assoc($result)['total'];
|
|
?>
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span>Today's Attendance:</span>
|
|
<strong><?php echo $today_attendance; ?></strong>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span>Total Students:</span>
|
|
<strong><?php echo $total_students; ?></strong>
|
|
</div>
|
|
<div class="progress mb-3" style="height: 10px;">
|
|
<div class="progress-bar" role="progressbar"
|
|
style="width: <?php echo $total_students > 0 ? ($today_attendance / $total_students * 100) : 0; ?>%">
|
|
</div>
|
|
</div>
|
|
<small class="text-muted">
|
|
<?php echo number_format($total_students > 0 ? ($today_attendance / $total_students * 100) : 0, 1); ?>% attendance rate today
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php
|
|
$page_scripts = '
|
|
<script src="https://unpkg.com/html5-qrcode"></script>
|
|
<script>
|
|
let html5QrCode;
|
|
let isScanning = false;
|
|
let cameraId = null;
|
|
|
|
// Initialize scanner
|
|
function initScanner() {
|
|
html5QrCode = new Html5Qrcode("qr-reader");
|
|
}
|
|
|
|
// Start scanner
|
|
async function startScanner() {
|
|
if (isScanning) return;
|
|
|
|
const devices = await Html5Qrcode.getCameras();
|
|
if (devices && devices.length > 0) {
|
|
const facingMode = cameraId ? { exact: cameraId } : { facingMode: "environment" };
|
|
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
|
|
|
|
try {
|
|
await html5QrCode.start(facingMode, config, onScanSuccess, onScanError);
|
|
isScanning = true;
|
|
document.getElementById("scan-result").innerHTML = \'<div class="alert alert-info"><i class="bi bi-info-circle"></i> Scanner started. Point camera at QR code.</div>\';
|
|
} catch (error) {
|
|
console.error("Scanner start error:", error);
|
|
document.getElementById("scan-result").innerHTML = \'<div class="alert alert-danger"><i class="bi bi-exclamation-triangle"></i> Failed to start camera: \' + error + \'</div>\';
|
|
}
|
|
} else {
|
|
document.getElementById("scan-result").innerHTML = \'<div class="alert alert-warning"><i class="bi bi-camera-video-off"></i> No camera found.</div>\';
|
|
}
|
|
}
|
|
|
|
// Stop scanner
|
|
function stopScanner() {
|
|
if (!isScanning) return;
|
|
|
|
html5QrCode.stop().then(() => {
|
|
isScanning = false;
|
|
document.getElementById("scan-result").innerHTML = \'<div class="alert alert-secondary"><i class="bi bi-info-circle"></i> Scanner stopped.</div>\';
|
|
}).catch(error => {
|
|
console.error("Scanner stop error:", error);
|
|
});
|
|
}
|
|
|
|
// Toggle scanner
|
|
function toggleScanner() {
|
|
if (isScanning) {
|
|
stopScanner();
|
|
} else {
|
|
startScanner();
|
|
}
|
|
}
|
|
|
|
// Switch camera
|
|
async function switchCamera() {
|
|
const devices = await Html5Qrcode.getCameras();
|
|
if (devices.length > 1) {
|
|
stopScanner();
|
|
// Switch to next camera
|
|
const currentIndex = devices.findIndex(device => device.id === cameraId);
|
|
const nextIndex = (currentIndex + 1) % devices.length;
|
|
cameraId = devices[nextIndex].id;
|
|
startScanner();
|
|
}
|
|
}
|
|
|
|
// Scan success callback
|
|
function onScanSuccess(decodedText, decodedResult) {
|
|
// Show scanning indicator
|
|
document.getElementById("scan-result").innerHTML = \'<div class="alert alert-info"><div class="spinner-border spinner-border-sm me-2"></div>Processing QR code...</div>\';
|
|
|
|
// Send to server
|
|
fetch(\'../api/scan_qr.php\', {
|
|
method: \'POST\',
|
|
headers: {
|
|
\'Content-Type\': \'application/json\',
|
|
},
|
|
body: JSON.stringify({ qr_code: decodedText })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
document.getElementById("scan-result").innerHTML = `
|
|
<div class="alert alert-success">
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-check-circle-fill me-3" style="font-size: 2rem;"></i>
|
|
<div>
|
|
<h5 class="mb-1">✓ Attendance Recorded!</h5>
|
|
<p class="mb-1"><strong>Student:</strong> \${data.data.student_name}</p>
|
|
<p class="mb-1"><strong>Activity:</strong> \${data.data.activity_name}</p>
|
|
<p class="mb-1"><strong>Time:</strong> \${data.data.time}</p>
|
|
<p class="mb-0"><strong>Status:</strong> <span class="badge bg-success">\${data.data.status}</span></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Play success sound
|
|
playSound(\'success\');
|
|
|
|
} else {
|
|
document.getElementById("scan-result").innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
\${data.message}
|
|
</div>
|
|
`;
|
|
|
|
// Play error sound
|
|
playSound(\'error\');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
document.getElementById("scan-result").innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
Network error: \${error}
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
// Scan error callback
|
|
function onScanError(error) {
|
|
// Don\'t show all errors to user
|
|
console.warn("QR Scan Error:", error);
|
|
}
|
|
|
|
// Manual entry form
|
|
document.getElementById("manual-entry-form").addEventListener("submit", function(e) {
|
|
e.preventDefault();
|
|
|
|
const studentId = document.getElementById("student_id").value.trim();
|
|
if (!studentId) return;
|
|
|
|
// Show processing indicator
|
|
document.getElementById("scan-result").innerHTML = \'<div class="alert alert-info"><div class="spinner-border spinner-border-sm me-2"></div>Processing manual entry...</div>\';
|
|
|
|
// Send to server
|
|
fetch(\'../api/manual_entry.php\', {
|
|
method: \'POST\',
|
|
headers: {
|
|
\'Content-Type\': \'application/json\',
|
|
},
|
|
body: JSON.stringify({ student_id: studentId })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
document.getElementById("scan-result").innerHTML = `
|
|
<div class="alert alert-success">
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-check-circle-fill me-3" style="font-size: 2rem;"></i>
|
|
<div>
|
|
<h5 class="mb-1">✓ Manual Entry Recorded!</h5>
|
|
<p class="mb-1"><strong>Student:</strong> \${data.data.student_name}</p>
|
|
<p class="mb-1"><strong>Activity:</strong> \${data.data.activity_name}</p>
|
|
<p class="mb-0"><strong>Time:</strong> \${data.data.time}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Clear input
|
|
document.getElementById("student_id").value = \'\';
|
|
playSound(\'success\');
|
|
|
|
} else {
|
|
document.getElementById("scan-result").innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
\${data.message}
|
|
</div>
|
|
`;
|
|
playSound(\'error\');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
document.getElementById("scan-result").innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
Network error: \${error}
|
|
</div>
|
|
`;
|
|
});
|
|
});
|
|
|
|
// Play sound
|
|
function playSound(type) {
|
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
const oscillator = audioContext.createOscillator();
|
|
const gainNode = audioContext.createGain();
|
|
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(audioContext.destination);
|
|
|
|
if (type === \'success\') {
|
|
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime);
|
|
oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1);
|
|
oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2);
|
|
} else {
|
|
oscillator.frequency.setValueAtTime(200, audioContext.currentTime);
|
|
}
|
|
|
|
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
|
|
|
|
oscillator.start();
|
|
oscillator.stop(audioContext.currentTime + 0.3);
|
|
}
|
|
|
|
// Initialize scanner when page loads
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
initScanner();
|
|
startScanner();
|
|
});
|
|
</script>
|
|
';
|
|
|
|
include '../includes/footer.php';
|
|
?>
|