403 lines
18 KiB
JavaScript
403 lines
18 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function () {
|
|
|
|
// --- Utility Functions ---
|
|
function escapeHTML(str) {
|
|
if (str === null || str === undefined) return '';
|
|
return str.toString().replace(/[&<>()"']/g, match => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
'\'' : '''
|
|
}[match]));
|
|
}
|
|
|
|
// --- General UI ---
|
|
const navbar = document.querySelector('.navbar');
|
|
if (navbar) {
|
|
window.addEventListener('scroll', () => {
|
|
navbar.classList.toggle('scrolled', window.scrollY > 50);
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
anchor.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
document.querySelector(this.getAttribute('href')).scrollIntoView({ behavior: 'smooth' });
|
|
});
|
|
});
|
|
|
|
// --- Contact Form ---
|
|
const contactForm = document.getElementById('contactForm');
|
|
if (contactForm) {
|
|
contactForm.addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
// ... (existing contact form logic) ...
|
|
});
|
|
}
|
|
|
|
// --- Attendance Page ---
|
|
const attendancePage = document.getElementById('attendance-page');
|
|
if (attendancePage) {
|
|
const attendanceState = {
|
|
selfieStream: null,
|
|
qrScanner: null,
|
|
qrLocationId: null
|
|
};
|
|
|
|
const elements = {
|
|
startCameraBtn: document.getElementById('start-camera-btn'),
|
|
scanQrBtn: document.getElementById('scan-qr-btn'),
|
|
checkInBtn: document.getElementById('check-in-btn'),
|
|
videoPreview: document.getElementById('video-preview'),
|
|
qrReader: document.getElementById('qr-reader'),
|
|
selfieCanvas: document.getElementById('selfie-canvas'),
|
|
status: document.getElementById('attendance-status'),
|
|
courseSelect: document.getElementById('course-select'),
|
|
studentIdInput: document.getElementById('student-id'),
|
|
qrLocationIdInput: document.getElementById('qr-location-id')
|
|
};
|
|
|
|
// --- Initialization ---
|
|
fetch('api/attendance_handler.php?action=get_courses')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.status === 'success' && data.courses) {
|
|
data.courses.forEach(course => {
|
|
const option = new Option(course.name, course.id);
|
|
elements.courseSelect.add(option);
|
|
});
|
|
}
|
|
});
|
|
|
|
// --- Event Listeners ---
|
|
elements.startCameraBtn.addEventListener('click', startSelfieCamera);
|
|
elements.scanQrBtn.addEventListener('click', startQrScanner);
|
|
elements.checkInBtn.addEventListener('click', handleCheckIn);
|
|
elements.courseSelect.addEventListener('change', updateCheckInButtonState);
|
|
|
|
// --- Functions ---
|
|
async function startSelfieCamera() {
|
|
stopAllStreams();
|
|
elements.qrReader.classList.add('d-none');
|
|
elements.videoPreview.classList.remove('d-none');
|
|
try {
|
|
attendanceState.selfieStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } });
|
|
elements.videoPreview.srcObject = attendanceState.selfieStream;
|
|
showAttendanceMessage('info', 'Selfie camera started. You can now check in.');
|
|
updateCheckInButtonState();
|
|
} catch (err) {
|
|
showAttendanceMessage('error', 'Could not access camera. Please grant permission.');
|
|
}
|
|
}
|
|
|
|
function startQrScanner() {
|
|
stopAllStreams();
|
|
elements.videoPreview.classList.add('d-none');
|
|
elements.qrReader.classList.remove('d-none');
|
|
showAttendanceMessage('info', 'Point the camera at a QR code.');
|
|
|
|
attendanceState.qrScanner = new Html5Qrcode("qr-reader");
|
|
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
|
|
|
|
attendanceState.qrScanner.start({ facingMode: "environment" }, config, onScanSuccess, onScanError);
|
|
}
|
|
|
|
function onScanSuccess(decodedText, decodedResult) {
|
|
stopAllStreams();
|
|
try {
|
|
const data = JSON.parse(decodedText);
|
|
if (data.courseId && data.locationId) {
|
|
elements.courseSelect.value = data.courseId;
|
|
elements.qrLocationIdInput.value = data.locationId;
|
|
attendanceState.qrLocationId = data.locationId;
|
|
showAttendanceMessage('success', `QR code scanned successfully! Course selected. Please start your selfie camera to complete check-in.`);
|
|
elements.courseSelect.disabled = true;
|
|
updateCheckInButtonState();
|
|
} else {
|
|
throw new Error('Invalid QR code format.');
|
|
}
|
|
} catch (e) {
|
|
showAttendanceMessage('error', 'Invalid QR code. Please scan a valid attendance code.');
|
|
}
|
|
}
|
|
|
|
function onScanError(error) {
|
|
// console.warn(`QR scan error: ${error}`);
|
|
}
|
|
|
|
function handleCheckIn() {
|
|
if (!elements.courseSelect.value || !attendanceState.selfieStream) {
|
|
showAttendanceMessage('error', 'Please select a course and start the selfie camera.');
|
|
return;
|
|
}
|
|
|
|
setCheckInButtonState('loading');
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => {
|
|
const { latitude, longitude } = position.coords;
|
|
captureAndSubmit(latitude, longitude);
|
|
},
|
|
(error) => {
|
|
showAttendanceMessage('error', 'Could not get location. Please grant permission.');
|
|
setCheckInButtonState('reset');
|
|
},
|
|
{ enableHighAccuracy: true }
|
|
);
|
|
}
|
|
|
|
function captureAndSubmit(latitude, longitude) {
|
|
const context = elements.selfieCanvas.getContext('2d');
|
|
elements.selfieCanvas.width = elements.videoPreview.videoWidth;
|
|
elements.selfieCanvas.height = elements.videoPreview.videoHeight;
|
|
context.drawImage(elements.videoPreview, 0, 0, elements.selfieCanvas.width, elements.selfieCanvas.height);
|
|
|
|
elements.selfieCanvas.toBlob((blob) => {
|
|
const formData = new FormData();
|
|
formData.append('student_id', elements.studentIdInput.value);
|
|
formData.append('course_id', elements.courseSelect.value);
|
|
formData.append('latitude', latitude);
|
|
formData.append('longitude', longitude);
|
|
formData.append('selfie', blob, 'selfie.jpg');
|
|
if (attendanceState.qrLocationId) {
|
|
formData.append('location_id', attendanceState.qrLocationId);
|
|
}
|
|
|
|
submitAttendance(formData);
|
|
}, 'image/jpeg');
|
|
}
|
|
|
|
function submitAttendance(formData) {
|
|
fetch('api/attendance_handler.php?action=check_in', { method: 'POST', body: formData })
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showAttendanceMessage('success', data.message);
|
|
setCheckInButtonState('success');
|
|
stopAllStreams();
|
|
} else {
|
|
showAttendanceMessage('error', data.message);
|
|
setCheckInButtonState('reset');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showAttendanceMessage('error', 'An unexpected error occurred.');
|
|
setCheckInButtonState('reset');
|
|
});
|
|
}
|
|
|
|
function stopAllStreams() {
|
|
if (attendanceState.selfieStream) {
|
|
attendanceState.selfieStream.getTracks().forEach(track => track.stop());
|
|
attendanceState.selfieStream = null;
|
|
}
|
|
if (attendanceState.qrScanner && attendanceState.qrScanner.isScanning) {
|
|
attendanceState.qrScanner.stop().catch(err => console.error('QR Scanner stop error', err));
|
|
}
|
|
elements.qrReader.classList.add('d-none');
|
|
}
|
|
|
|
function updateCheckInButtonState() {
|
|
elements.checkInBtn.disabled = !(elements.courseSelect.value && attendanceState.selfieStream);
|
|
}
|
|
|
|
function showAttendanceMessage(type, message) {
|
|
const classMap = { success: 'alert-success', error: 'alert-danger', info: 'alert-info' };
|
|
elements.status.className = `alert ${classMap[type] || 'alert-secondary'}`;
|
|
elements.status.textContent = message;
|
|
}
|
|
|
|
function setCheckInButtonState(state) {
|
|
const btn = elements.checkInBtn;
|
|
switch (state) {
|
|
case 'loading':
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Checking In...';
|
|
break;
|
|
case 'success':
|
|
btn.disabled = true;
|
|
btn.innerHTML = 'Checked In Successfully';
|
|
btn.classList.replace('btn-primary', 'btn-success');
|
|
break;
|
|
case 'reset':
|
|
btn.disabled = false;
|
|
btn.innerHTML = 'Check In Now';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Attendance History Page ---
|
|
const historyBody = document.getElementById('attendance-history-body');
|
|
if (historyBody) {
|
|
historyBody.innerHTML = '<tr><td colspan="4" class="text-center">Loading history...</td></tr>';
|
|
|
|
fetch('api/attendance_handler.php?action=get_attendance_history')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success' && data.history) {
|
|
if (data.history.length > 0) {
|
|
historyBody.innerHTML = ''; // Clear loading message
|
|
data.history.forEach(record => {
|
|
const row = document.createElement('tr');
|
|
|
|
let statusClass = '';
|
|
if (record.status === 'success') statusClass = 'text-success';
|
|
if (record.status.startsWith('failed')) statusClass = 'text-danger';
|
|
|
|
row.innerHTML = `
|
|
<td>${escapeHTML(record.course_name)}</td>
|
|
<td>${new Date(record.check_in_time).toLocaleString()}</td>
|
|
<td class="fw-bold ${statusClass}">${escapeHTML(record.status.replace('_', ' '))}</td>
|
|
<td>
|
|
${record.selfie_path ? `<a href="${escapeHTML(record.selfie_path)}" target="_blank" class="btn btn-sm btn-outline-secondary">View</a>` : 'N/A'}
|
|
</td>
|
|
`;
|
|
historyBody.appendChild(row);
|
|
});
|
|
} else {
|
|
historyBody.innerHTML = '<tr><td colspan="4" class="text-center">No attendance records found.</td></tr>';
|
|
}
|
|
} else {
|
|
throw new Error(data.message || 'Failed to load history.');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching attendance history:', error);
|
|
historyBody.innerHTML = `<tr><td colspan="4" class="text-center text-danger">Error: ${error.message}</td></tr>`;
|
|
});
|
|
}
|
|
|
|
// --- Faculty Attendance Dashboard ---
|
|
const facultyAttendancePage = document.getElementById('faculty-attendance-page');
|
|
if (facultyAttendancePage) {
|
|
const courseSelect = document.getElementById('faculty-course-select');
|
|
const attendanceBody = document.getElementById('faculty-attendance-body');
|
|
|
|
fetch('api/attendance_handler.php?action=get_courses')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success' && data.courses) {
|
|
data.courses.forEach(course => {
|
|
const option = new Option(course.name, course.id);
|
|
courseSelect.add(option);
|
|
});
|
|
}
|
|
});
|
|
|
|
courseSelect.addEventListener('change', () => {
|
|
const courseId = courseSelect.value;
|
|
if (!courseId) {
|
|
attendanceBody.innerHTML = '<tr><td colspan="5" class="text-center">Please select a course to view records.</td></tr>';
|
|
return;
|
|
}
|
|
|
|
attendanceBody.innerHTML = '<tr><td colspan="5" class="text-center">Loading records...</td></tr>';
|
|
|
|
fetch(`api/attendance_handler.php?action=get_faculty_attendance&course_id=${courseId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success' && data.records) {
|
|
if (data.records.length > 0) {
|
|
attendanceBody.innerHTML = '';
|
|
data.records.forEach(record => {
|
|
const row = document.createElement('tr');
|
|
let statusClass = record.status === 'success' ? 'text-success' : 'text-danger';
|
|
row.innerHTML = `
|
|
<td>${escapeHTML(record.student_id)}</td>
|
|
<td>${new Date(record.check_in_time).toLocaleString()}</td>
|
|
<td class="fw-bold ${statusClass}">${escapeHTML(record.status.replace('_', ' '))}</td>
|
|
<td>${record.selfie_path ? `<a href="${escapeHTML(record.selfie_path)}" target="_blank" class="btn btn-sm btn-outline-secondary">View</a>` : 'N/A'}</td>
|
|
<td>${record.latitude ? `${parseFloat(record.latitude).toFixed(5)}, ${parseFloat(record.longitude).toFixed(5)}` : 'N/A'}</td>
|
|
`;
|
|
attendanceBody.appendChild(row);
|
|
});
|
|
} else {
|
|
attendanceBody.innerHTML = '<tr><td colspan="5" class="text-center">No records found.</td></tr>';
|
|
}
|
|
} else {
|
|
throw new Error(data.message || 'Failed to load records.');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
attendanceBody.innerHTML = `<tr><td colspan="5" class="text-center text-danger">Error: ${error.message}</td></tr>`;
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- QR Code Generator Page ---
|
|
const qrGeneratorPage = document.getElementById('qr-generator-page');
|
|
if (qrGeneratorPage) {
|
|
const courseSelect = document.getElementById('qr-course-select');
|
|
const locationSelect = document.getElementById('qr-location-select');
|
|
const generateBtn = document.getElementById('generate-qr-btn');
|
|
const qrCodeContainer = document.getElementById('qr-code-container');
|
|
const qrCodeImg = document.getElementById('qr-code-img');
|
|
const qrCodeCaption = document.getElementById('qr-code-caption');
|
|
const loadingSpinner = document.getElementById('qr-loading-spinner');
|
|
|
|
fetch('api/attendance_handler.php?action=get_courses')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success' && data.courses) {
|
|
data.courses.forEach(course => {
|
|
const option = new Option(course.name, course.id);
|
|
courseSelect.add(option);
|
|
});
|
|
}
|
|
});
|
|
|
|
courseSelect.addEventListener('change', () => {
|
|
const courseId = courseSelect.value;
|
|
locationSelect.innerHTML = '<option value="">-- Loading... --</option>';
|
|
locationSelect.disabled = true;
|
|
generateBtn.disabled = true;
|
|
qrCodeContainer.classList.add('d-none');
|
|
|
|
if (!courseId) {
|
|
locationSelect.innerHTML = '<option value="">-- Select course --</option>';
|
|
return;
|
|
}
|
|
|
|
fetch(`api/attendance_handler.php?action=get_locations_for_course&course_id=${courseId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
locationSelect.innerHTML = '<option value="">-- Select location --</option>';
|
|
if (data.status === 'success' && data.locations && data.locations.length > 0) {
|
|
data.locations.forEach(loc => {
|
|
const option = new Option(`${loc.name} (Radius: ${loc.radius}m)`, loc.id);
|
|
locationSelect.add(option);
|
|
});
|
|
locationSelect.disabled = false;
|
|
} else {
|
|
locationSelect.innerHTML = '<option value="">-- No locations --</option>';
|
|
}
|
|
});
|
|
});
|
|
|
|
locationSelect.addEventListener('change', () => {
|
|
generateBtn.disabled = !locationSelect.value;
|
|
});
|
|
|
|
generateBtn.addEventListener('click', () => {
|
|
const courseId = courseSelect.value;
|
|
const locationId = locationSelect.value;
|
|
if (!courseId || !locationId) return;
|
|
|
|
const qrData = JSON.stringify({ courseId, locationId });
|
|
const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(qrData)}`;
|
|
|
|
loadingSpinner.classList.remove('d-none');
|
|
qrCodeContainer.classList.add('d-none');
|
|
|
|
qrCodeImg.onload = () => {
|
|
loadingSpinner.classList.add('d-none');
|
|
qrCodeContainer.classList.remove('d-none');
|
|
qrCodeCaption.textContent = `QR Code for ${courseSelect.options[courseSelect.selectedIndex].text}`;
|
|
};
|
|
qrCodeImg.src = qrApiUrl;
|
|
});
|
|
}
|
|
});
|