Initial commit

This commit is contained in:
2026-01-07 14:09:59 +08:00
commit 8a00aa71d5
939 changed files with 40616 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
/* Main Stylesheet */
:root {
--primary-color: #3498db;
--secondary-color: #2c3e50;
--success-color: #27ae60;
--danger-color: #e74c3c;
--warning-color: #f39c12;
--info-color: #17a2b8;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
color: #336494;
margin: 10;
padding: 10px;
}
.login-page {
background: linear-gradient(135deg, #8bea66 0%, #a0a24b 100%);
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.card {
border: none;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
}
.card-header {
border-radius: 15px 15px 0 0 !important;
}
/* Table Styles */
.table-hover tbody tr:hover {
background-color: rgba(52, 152, 219, 0.1);
}
/* Button Styles */
.btn {
border-radius: 8px;
padding: 8px 20px;
font-weight: 500;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
/* Badge Styles */
.badge {
padding: 5px 10px;
border-radius: 10px;
font-weight: 500;
}
/* Form Styles */
.form-control {
border-radius: 8px;
border: 1px solid #dee2e6;
padding: 10px 15px;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
}
/* Alert Styles */
.alert {
border-radius: 10px;
border: none;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #2980b9;
}
/* Animation for attendance success */
@keyframes successPulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.attendance-success {
animation: successPulse 0.5s ease-in-out;
}
/* Scanner container */
#qr-reader {
border: 2px dashed #dee2e6;
border-radius: 10px;
padding: 10px;
}
/* Calendar icon */
.calendar-icon {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
width: 50px;
border-radius: 8px;
}
/* Print styles */
@media print {
.sidebar, .navbar, .btn {
display: none !important;
}
.main-content {
margin-left: 0 !important;
width: 100% !important;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

View File

@@ -0,0 +1,273 @@
/**
* 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);
}
});
});