590 lines
17 KiB
JavaScript
590 lines
17 KiB
JavaScript
/**
|
|
* QR Code Destination Page JavaScript
|
|
* Handles staff check-in functionality and form interactions
|
|
*/
|
|
|
|
// Global variables
|
|
let isSubmitting = false;
|
|
let currentTime = new Date();
|
|
|
|
// Initialize page when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('QR Destination page initialized');
|
|
|
|
initializePage();
|
|
setupEventListeners();
|
|
startTimeUpdater();
|
|
});
|
|
|
|
function initializePage() {
|
|
// Focus on employee ID input
|
|
const employeeInput = document.getElementById('employee_id');
|
|
if (employeeInput) {
|
|
employeeInput.focus();
|
|
}
|
|
|
|
// Initialize current time display
|
|
updateCurrentTime();
|
|
|
|
// Add page load animation
|
|
document.body.classList.add('page-loaded');
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
const form = document.getElementById('checkinForm');
|
|
const employeeInput = document.getElementById('employee_id');
|
|
|
|
if (form) {
|
|
form.addEventListener('submit', handleFormSubmit);
|
|
}
|
|
|
|
if (employeeInput) {
|
|
// Real-time input validation
|
|
employeeInput.addEventListener('input', handleInputChange);
|
|
employeeInput.addEventListener('blur', validateEmployeeId);
|
|
employeeInput.addEventListener('keypress', handleKeyPress);
|
|
|
|
// Auto-uppercase input
|
|
employeeInput.addEventListener('input', function() {
|
|
this.value = this.value.toUpperCase();
|
|
});
|
|
}
|
|
|
|
// Handle page visibility changes
|
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
}
|
|
|
|
function handleFormSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
if (isSubmitting) {
|
|
return false;
|
|
}
|
|
|
|
const employeeId = document.getElementById('employee_id').value.trim();
|
|
|
|
if (!validateEmployeeId()) {
|
|
return false;
|
|
}
|
|
|
|
submitCheckin(employeeId);
|
|
}
|
|
|
|
function handleInputChange(e) {
|
|
const input = e.target;
|
|
const value = input.value.trim();
|
|
|
|
// Clear previous validation states
|
|
input.classList.remove('error', 'success');
|
|
hideStatusMessage();
|
|
|
|
// Real-time validation feedback
|
|
if (value.length >= 3) {
|
|
if (isValidEmployeeId(value)) {
|
|
input.classList.add('success');
|
|
} else {
|
|
input.classList.add('error');
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleKeyPress(e) {
|
|
// Allow only alphanumeric characters
|
|
const char = String.fromCharCode(e.which);
|
|
if (!/[A-Za-z0-9]/.test(char)) {
|
|
e.preventDefault();
|
|
shakeInput(e.target);
|
|
}
|
|
|
|
// Submit on Enter key
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleFormSubmit(e);
|
|
}
|
|
}
|
|
|
|
function validateEmployeeId() {
|
|
const employeeInput = document.getElementById('employee_id');
|
|
const employeeId = employeeInput.value.trim();
|
|
|
|
if (!employeeId) {
|
|
showValidationError(employeeInput, 'Employee ID is required');
|
|
return false;
|
|
}
|
|
|
|
if (employeeId.length < 3) {
|
|
showValidationError(employeeInput, 'Employee ID must be at least 3 characters');
|
|
return false;
|
|
}
|
|
|
|
if (employeeId.length > 20) {
|
|
showValidationError(employeeInput, 'Employee ID must be less than 20 characters');
|
|
return false;
|
|
}
|
|
|
|
if (!isValidEmployeeId(employeeId)) {
|
|
showValidationError(employeeInput, 'Employee ID can only contain letters and numbers');
|
|
return false;
|
|
}
|
|
|
|
// Clear validation error
|
|
employeeInput.classList.remove('error');
|
|
employeeInput.classList.add('success');
|
|
hideStatusMessage();
|
|
|
|
return true;
|
|
}
|
|
|
|
function isValidEmployeeId(id) {
|
|
return /^[A-Za-z0-9]{3,20}$/.test(id);
|
|
}
|
|
|
|
function showValidationError(input, message) {
|
|
input.classList.add('error');
|
|
input.classList.remove('success');
|
|
showStatusMessage(message, 'error');
|
|
shakeInput(input);
|
|
input.focus();
|
|
}
|
|
|
|
function shakeInput(input) {
|
|
input.classList.add('shake');
|
|
setTimeout(() => {
|
|
input.classList.remove('shake');
|
|
}, 500);
|
|
}
|
|
|
|
function submitCheckin(employeeId) {
|
|
if (isSubmitting) return;
|
|
|
|
isSubmitting = true;
|
|
showLoadingState();
|
|
showLoadingOverlay();
|
|
|
|
// Prepare form data
|
|
const formData = new FormData();
|
|
formData.append('employee_id', employeeId);
|
|
|
|
// Submit to server
|
|
fetch(`/qr/${window.qrUrl}/checkin`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
handleCheckinResponse(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('Check-in error:', error);
|
|
handleCheckinError('Network error. Please check your connection and try again.');
|
|
})
|
|
.finally(() => {
|
|
isSubmitting = false;
|
|
hideLoadingState();
|
|
hideLoadingOverlay();
|
|
});
|
|
}
|
|
|
|
function handleCheckinResponse(data) {
|
|
if (data.success) {
|
|
showSuccessCard(data.data);
|
|
logSuccessfulCheckin(data.data);
|
|
|
|
// Optional: Analytics tracking
|
|
if (typeof gtag !== 'undefined') {
|
|
gtag('event', 'checkin_success', {
|
|
'location': window.locationName,
|
|
'event_name': window.eventName
|
|
});
|
|
}
|
|
} else {
|
|
handleCheckinError(data.message);
|
|
}
|
|
}
|
|
|
|
function handleCheckinError(message) {
|
|
showStatusMessage(message, 'error');
|
|
|
|
// Shake the form to draw attention
|
|
const form = document.getElementById('checkinForm');
|
|
if (form) {
|
|
form.classList.add('shake');
|
|
setTimeout(() => {
|
|
form.classList.remove('shake');
|
|
}, 500);
|
|
}
|
|
|
|
// Re-focus on input
|
|
const employeeInput = document.getElementById('employee_id');
|
|
if (employeeInput) {
|
|
employeeInput.focus();
|
|
employeeInput.select();
|
|
}
|
|
}
|
|
|
|
function showSuccessCard(data) {
|
|
// Hide the check-in form
|
|
const checkinCard = document.querySelector('.checkin-card');
|
|
if (checkinCard) {
|
|
checkinCard.style.display = 'none';
|
|
}
|
|
|
|
// Populate and show success card
|
|
const successCard = document.getElementById('successCard');
|
|
if (successCard) {
|
|
document.getElementById('successEmployeeId').textContent = data.employee_id || '-';
|
|
document.getElementById('successLocation').textContent = data.location || '-';
|
|
document.getElementById('successEvent').textContent = data.event || '-';
|
|
document.getElementById('successTime').textContent = data.time || '-';
|
|
document.getElementById('successDate').textContent = data.date || '-';
|
|
|
|
successCard.style.display = 'block';
|
|
successCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
|
|
// Optional: Auto-hide success card after some time
|
|
setTimeout(() => {
|
|
showAutoHideOption();
|
|
}, 10000); // 10 seconds
|
|
}
|
|
|
|
function showAutoHideOption() {
|
|
const successCard = document.getElementById('successCard');
|
|
if (successCard && successCard.style.display !== 'none') {
|
|
const actions = successCard.querySelector('.success-actions');
|
|
if (actions && !actions.querySelector('.auto-hide-btn')) {
|
|
const autoHideBtn = document.createElement('button');
|
|
autoHideBtn.className = 'btn btn-outline auto-hide-btn';
|
|
autoHideBtn.innerHTML = '<i class="fas fa-clock"></i> Auto-hide in <span id="countdown">30</span>s';
|
|
actions.appendChild(autoHideBtn);
|
|
|
|
startCountdown(30, () => {
|
|
checkInAnother();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function startCountdown(seconds, callback) {
|
|
const countdownElement = document.getElementById('countdown');
|
|
let remaining = seconds;
|
|
|
|
const interval = setInterval(() => {
|
|
remaining--;
|
|
if (countdownElement) {
|
|
countdownElement.textContent = remaining;
|
|
}
|
|
|
|
if (remaining <= 0) {
|
|
clearInterval(interval);
|
|
callback();
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
function checkInAnother() {
|
|
// Show the check-in form again
|
|
const checkinCard = document.querySelector('.checkin-card');
|
|
const successCard = document.getElementById('successCard');
|
|
|
|
if (checkinCard) {
|
|
checkinCard.style.display = 'block';
|
|
}
|
|
|
|
if (successCard) {
|
|
successCard.style.display = 'none';
|
|
}
|
|
|
|
// Reset form
|
|
const form = document.getElementById('checkinForm');
|
|
if (form) {
|
|
form.reset();
|
|
}
|
|
|
|
// Clear validation states
|
|
const employeeInput = document.getElementById('employee_id');
|
|
if (employeeInput) {
|
|
employeeInput.classList.remove('error', 'success');
|
|
employeeInput.focus();
|
|
}
|
|
|
|
hideStatusMessage();
|
|
|
|
// Scroll back to form
|
|
checkinCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
|
|
function showLoadingState() {
|
|
const btn = document.querySelector('.btn-primary');
|
|
if (btn) {
|
|
const content = btn.querySelector('.btn-content');
|
|
const loader = btn.querySelector('.btn-loader');
|
|
|
|
if (content) content.style.display = 'none';
|
|
if (loader) loader.style.display = 'flex';
|
|
|
|
btn.disabled = true;
|
|
}
|
|
}
|
|
|
|
function hideLoadingState() {
|
|
const btn = document.querySelector('.btn-primary');
|
|
if (btn) {
|
|
const content = btn.querySelector('.btn-content');
|
|
const loader = btn.querySelector('.btn-loader');
|
|
|
|
if (content) content.style.display = 'flex';
|
|
if (loader) loader.style.display = 'none';
|
|
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function showLoadingOverlay() {
|
|
const overlay = document.getElementById('loadingOverlay');
|
|
if (overlay) {
|
|
overlay.style.display = 'flex';
|
|
setTimeout(() => {
|
|
overlay.classList.add('show');
|
|
}, 10);
|
|
}
|
|
}
|
|
|
|
function hideLoadingOverlay() {
|
|
const overlay = document.getElementById('loadingOverlay');
|
|
if (overlay) {
|
|
overlay.classList.remove('show');
|
|
setTimeout(() => {
|
|
overlay.style.display = 'none';
|
|
}, 200);
|
|
}
|
|
}
|
|
|
|
function showStatusMessage(message, type = 'info') {
|
|
const statusDiv = document.getElementById('statusMessage');
|
|
if (statusDiv) {
|
|
statusDiv.textContent = message;
|
|
statusDiv.className = `status-message ${type}`;
|
|
statusDiv.style.display = 'block';
|
|
|
|
// Auto-hide success messages
|
|
if (type === 'success') {
|
|
setTimeout(() => {
|
|
hideStatusMessage();
|
|
}, 5000);
|
|
}
|
|
|
|
// Scroll to message
|
|
statusDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}
|
|
|
|
function hideStatusMessage() {
|
|
const statusDiv = document.getElementById('statusMessage');
|
|
if (statusDiv) {
|
|
statusDiv.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function updateCurrentTime() {
|
|
const timeElement = document.getElementById('currentTime');
|
|
if (timeElement) {
|
|
const now = new Date();
|
|
const options = {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit'
|
|
};
|
|
|
|
timeElement.textContent = now.toLocaleDateString('en-US', options);
|
|
}
|
|
}
|
|
|
|
function startTimeUpdater() {
|
|
updateCurrentTime();
|
|
setInterval(updateCurrentTime, 1000);
|
|
}
|
|
|
|
function handleVisibilityChange() {
|
|
if (document.hidden) {
|
|
// Page is hidden - pause operations
|
|
console.log('Page hidden - pausing operations');
|
|
} else {
|
|
// Page is visible - resume operations
|
|
console.log('Page visible - resuming operations');
|
|
updateCurrentTime();
|
|
|
|
// Re-focus on input if form is visible
|
|
const checkinCard = document.querySelector('.checkin-card');
|
|
const employeeInput = document.getElementById('employee_id');
|
|
|
|
if (checkinCard && checkinCard.style.display !== 'none' && employeeInput) {
|
|
setTimeout(() => {
|
|
employeeInput.focus();
|
|
}, 100);
|
|
}
|
|
}
|
|
}
|
|
|
|
function logSuccessfulCheckin(data) {
|
|
console.log('Successful check-in:', {
|
|
employee_id: data.employee_id,
|
|
location: data.location,
|
|
event: data.event,
|
|
time: data.time,
|
|
date: data.date
|
|
});
|
|
}
|
|
|
|
// Utility functions
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
function throttle(func, limit) {
|
|
let inThrottle;
|
|
return function() {
|
|
const args = arguments;
|
|
const context = this;
|
|
if (!inThrottle) {
|
|
func.apply(context, args);
|
|
inThrottle = true;
|
|
setTimeout(() => inThrottle = false, limit);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export functions for global access
|
|
window.checkInAnother = checkInAnother;
|
|
window.validateEmployeeId = validateEmployeeId;
|
|
|
|
// Service Worker registration for offline support (optional)
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', function() {
|
|
navigator.serviceWorker.register('/sw.js')
|
|
.then(function(registration) {
|
|
console.log('ServiceWorker registration successful');
|
|
})
|
|
.catch(function(err) {
|
|
console.log('ServiceWorker registration failed: ', err);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Error handling for unhandled promises
|
|
window.addEventListener('unhandledrejection', function(event) {
|
|
console.error('Unhandled promise rejection:', event.reason);
|
|
handleCheckinError('An unexpected error occurred. Please try again.');
|
|
event.preventDefault();
|
|
});
|
|
|
|
// Handle online/offline status
|
|
window.addEventListener('online', function() {
|
|
showStatusMessage('Connection restored', 'success');
|
|
});
|
|
|
|
window.addEventListener('offline', function() {
|
|
showStatusMessage('No internet connection. Please check your network.', 'warning');
|
|
});
|
|
|
|
// Performance monitoring
|
|
if ('performance' in window) {
|
|
window.addEventListener('load', function() {
|
|
setTimeout(function() {
|
|
const perfData = performance.getEntriesByType('navigation')[0];
|
|
console.log('Page load time:', perfData.loadEventEnd - perfData.loadEventStart, 'ms');
|
|
}, 0);
|
|
});
|
|
}
|
|
|
|
// Accessibility enhancements
|
|
document.addEventListener('keydown', function(e) {
|
|
// Escape key to reset form
|
|
if (e.key === 'Escape') {
|
|
const successCard = document.getElementById('successCard');
|
|
if (successCard && successCard.style.display !== 'none') {
|
|
checkInAnother();
|
|
} else {
|
|
// Reset form
|
|
const form = document.getElementById('checkinForm');
|
|
if (form) {
|
|
form.reset();
|
|
hideStatusMessage();
|
|
|
|
const employeeInput = document.getElementById('employee_id');
|
|
if (employeeInput) {
|
|
employeeInput.classList.remove('error', 'success');
|
|
employeeInput.focus();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ctrl+R to refresh (prevent default and reload page cleanly)
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'r') {
|
|
e.preventDefault();
|
|
window.location.reload();
|
|
}
|
|
});
|
|
|
|
// Touch device optimizations
|
|
if ('ontouchstart' in window) {
|
|
// Add touch-friendly classes
|
|
document.body.classList.add('touch-device');
|
|
|
|
// Prevent zoom on input focus for iOS
|
|
const inputs = document.querySelectorAll('input[type="text"]');
|
|
inputs.forEach(input => {
|
|
input.addEventListener('focus', function() {
|
|
const viewport = document.querySelector('meta[name="viewport"]');
|
|
if (viewport) {
|
|
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
|
|
}
|
|
});
|
|
|
|
input.addEventListener('blur', function() {
|
|
const viewport = document.querySelector('meta[name="viewport"]');
|
|
if (viewport) {
|
|
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Auto-refresh page if idle for too long (optional)
|
|
let idleTimer;
|
|
const IDLE_TIME = 30 * 60 * 1000; // 30 minutes
|
|
|
|
function resetIdleTimer() {
|
|
clearTimeout(idleTimer);
|
|
idleTimer = setTimeout(() => {
|
|
if (confirm('This page has been idle for 30 minutes. Would you like to refresh it?')) {
|
|
window.location.reload();
|
|
} else {
|
|
resetIdleTimer(); // Reset timer if user chooses not to refresh
|
|
}
|
|
}, IDLE_TIME);
|
|
}
|
|
|
|
// Track user activity
|
|
['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'].forEach(event => {
|
|
document.addEventListener(event, resetIdleTimer, true);
|
|
});
|
|
|
|
// Initialize idle timer
|
|
resetIdleTimer(); |