273 lines
9.5 KiB
JavaScript
273 lines
9.5 KiB
JavaScript
|
|
/**
|
||
|
|
* 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 = `
|
||
|
|
<div class="alert alert-info">
|
||
|
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||
|
|
${data}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'success':
|
||
|
|
html = `
|
||
|
|
<div class="alert alert-success attendance-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-0"><strong>Time:</strong> ${data.data.time}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'error':
|
||
|
|
html = `
|
||
|
|
<div class="alert alert-danger">
|
||
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||
|
|
${data}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
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 = `
|
||
|
|
<div class="alert alert-danger">
|
||
|
|
<i class="bi bi-camera-video-off me-2"></i>
|
||
|
|
${message}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
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 = `
|
||
|
|
<button class="btn btn-success me-2 start-scan">
|
||
|
|
<i class="bi bi-play-circle"></i> Start Scanner
|
||
|
|
</button>
|
||
|
|
<button class="btn btn-danger me-2 stop-scan">
|
||
|
|
<i class="bi bi-stop-circle"></i> Stop Scanner
|
||
|
|
</button>
|
||
|
|
<button class="btn btn-secondary toggle-camera">
|
||
|
|
<i class="bi bi-camera-video"></i> Switch Camera
|
||
|
|
</button>
|
||
|
|
`;
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|