/** * QR Code Scanner Module */ class QRScanner { constructor(options = {}) { this.options = { scannerId: 'qr-reader', resultId: 'scan-result', facingMode: 'environment', fps: 10, qrbox: { width: 250, height: 250 }, ...options }; this.scanner = null; this.isScanning = false; this.lastScanTime = 0; this.scanCooldown = 2000; // 2 seconds cooldown this.init(); } init() { this.scanner = new Html5Qrcode(this.options.scannerId); // Create result container if not exists if (!document.getElementById(this.options.resultId)) { const resultDiv = document.createElement('div'); resultDiv.id = this.options.resultId; document.querySelector(`#${this.options.scannerId}`).parentNode.appendChild(resultDiv); } } async start() { if (this.isScanning) return; try { await this.scanner.start( { facingMode: this.options.facingMode }, this.options, this.onScanSuccess.bind(this), this.onScanError.bind(this) ); this.isScanning = true; this.updateUI('started'); } catch (error) { console.error('Failed to start scanner:', error); this.showError('Failed to start camera. Please check permissions.'); } } stop() { if (!this.isScanning) return; this.scanner.stop().then(() => { this.isScanning = false; this.updateUI('stopped'); }).catch(error => { console.error('Failed to stop scanner:', error); }); } onScanSuccess(decodedText, decodedResult) { const currentTime = Date.now(); // Prevent multiple scans in short time if (currentTime - this.lastScanTime < this.scanCooldown) { return; } this.lastScanTime = currentTime; // Show scanning indicator this.showResult('scanning', 'Scanning QR code...'); // Process the scan this.processQRCode(decodedText); } onScanError(error) { console.warn('QR Scan Error:', error); // Don't show errors to user unless critical } async processQRCode(qrData) { try { const response = await fetch('../api/scan_qr.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ qr_code: qrData }) }); const data = await response.json(); if (data.success) { this.showResult('success', data); this.playSound('success'); // Auto-stop after successful scan (optional) if (this.options.autoStop) { setTimeout(() => this.stop(), 3000); } } else { this.showResult('error', data.message); this.playSound('error'); } } catch (error) { console.error('Processing error:', error); this.showResult('error', 'Network error. Please try again.'); this.playSound('error'); } } showResult(type, data) { const resultDiv = document.getElementById(this.options.resultId); let html = ''; switch(type) { case 'scanning': html = `
${data}
`; break; case 'success': html = `
Attendance Recorded!

Student: ${data.data.student_name}

Activity: ${data.data.activity_name}

Time: ${data.data.time}

`; break; case 'error': html = `
${data}
`; break; } resultDiv.innerHTML = html; // Auto-clear result after some time if (type !== 'scanning') { setTimeout(() => { if (resultDiv.innerHTML.includes(html)) { resultDiv.innerHTML = ''; } }, 5000); } } showError(message) { const resultDiv = document.getElementById(this.options.resultId); resultDiv.innerHTML = `
${message}
`; } updateUI(state) { const scannerDiv = document.getElementById(this.options.scannerId); if (state === 'started') { scannerDiv.classList.add('scanning'); } else { scannerDiv.classList.remove('scanning'); } } playSound(type) { // Create audio context for sounds try { 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); // C5 oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); // E5 oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); // G5 } else { oscillator.frequency.setValueAtTime(200, audioContext.currentTime); // Low tone } gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.3); } catch (error) { console.log('Audio not supported'); } } toggleCamera() { if (this.options.facingMode === 'environment') { this.options.facingMode = 'user'; } else { this.options.facingMode = 'environment'; } this.stop(); setTimeout(() => this.start(), 500); } } // Initialize scanner when DOM is loaded document.addEventListener('DOMContentLoaded', function() { // Check for scanner elements on page const scannerElements = document.querySelectorAll('[data-scanner]'); scannerElements.forEach(element => { const scannerId = element.getAttribute('data-scanner') || 'qr-reader'; const scanner = new QRScanner({ scannerId: scannerId, autoStop: element.getAttribute('data-auto-stop') === 'true' }); // Add start/stop buttons if not auto-start if (element.getAttribute('data-auto-start') !== 'true') { const controls = document.createElement('div'); controls.className = 'scanner-controls mt-3'; controls.innerHTML = ` `; element.parentNode.appendChild(controls); // Add event listeners controls.querySelector('.start-scan').addEventListener('click', () => scanner.start()); controls.querySelector('.stop-scan').addEventListener('click', () => scanner.stop()); controls.querySelector('.toggle-camera').addEventListener('click', () => scanner.toggleCamera()); } else { // Auto-start scanner setTimeout(() => scanner.start(), 1000); } }); });