diff --git a/models/attendance.py b/models/attendance.py index 00fd90d..24d0694 100644 --- a/models/attendance.py +++ b/models/attendance.py @@ -35,6 +35,8 @@ class AttendanceData(base.db.Model): address = base.db.Column(base.db.String(500), nullable=True) # Stores the QR-side address for dynamic QR check-ins (overrides qr_codes.location_address join) qr_address = base.db.Column(base.db.Text, nullable=True) + # True when this record was created via a Dynamic QR code scan + is_dynamic_qr = base.db.Column(base.db.Boolean, default=False, nullable=False) verification_photo = base.db.Column(base.db.Text, nullable=True) # Base64 encoded image verification_required = base.db.Column(base.db.Boolean, default=False) verification_status = base.db.Column(base.db.String(20), nullable=True) # 'pending', 'approved', 'rejected' diff --git a/routes/attendance.py b/routes/attendance.py index 4b7de3f..fcd85a2 100644 --- a/routes/attendance.py +++ b/routes/attendance.py @@ -180,7 +180,8 @@ def attendance_report(): CONCAT(e.firstName, ' ', e.lastName) as employee_name, ad.verification_required, ad.verification_status, - ad.verification_photo + ad.verification_photo, + COALESCE(ad.is_dynamic_qr, 0) as is_dynamic_qr FROM attendance_data ad LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id @@ -208,7 +209,8 @@ def attendance_report(): CONCAT(e.firstName, ' ', e.lastName) as employee_name, ad.verification_required, ad.verification_status, - ad.verification_photo + ad.verification_photo, + COALESCE(ad.is_dynamic_qr, 0) as is_dynamic_qr FROM attendance_data ad LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id @@ -250,6 +252,7 @@ def attendance_report(): query_params['date_to'] = date_to if location_filter: + # Exact match — dropdown value IS the exact location_name string filter_conditions.append("ad.location_name = :location") query_params['location'] = location_filter @@ -268,7 +271,19 @@ def attendance_report(): ) if project_filter: - filter_conditions.append("qc.project_id = :project") + # For standard QR records: match by the QR code's project_id directly. + # For dynamic QR records: the dynamic QR itself may not be in any project, + # but the employee-selected location corresponds to a standard QR in that + # project. Match those by checking if attendance_data.location_name + # appears in the locations of QR codes belonging to the selected project. + filter_conditions.append( + "(qc.project_id = :project OR " + "(ad.is_dynamic_qr = 1 AND ad.location_name IN (" + " SELECT DISTINCT qc2.location FROM qr_codes qc2 " + " WHERE qc2.project_id = :project AND qc2.qr_type = 'standard' " + " AND qc2.location IS NOT NULL AND qc2.location != ''" + ")))" + ) query_params['project'] = project_filter # Combine query with filters @@ -307,7 +322,8 @@ def attendance_report(): 'employee_name': record[15] or 'Unknown Employee', 'verification_required': record[16] if len(record) > 16 else False, 'verification_status': record[17] if len(record) > 17 else None, - 'verification_photo': record[18] if len(record) > 18 else None + 'verification_photo': record[18] if len(record) > 18 else None, + 'is_dynamic_qr': bool(record[19]) if len(record) > 19 else False } # Calculate accuracy_level for template display @@ -339,6 +355,8 @@ def attendance_report(): SELECT DISTINCT location_name FROM attendance_data WHERE location_name IS NOT NULL + AND location_name != 'Dynamic' + AND location_name != '' ORDER BY location_name """)) locations = [row[0] for row in locations_query.fetchall()] @@ -1861,7 +1879,13 @@ def create_excel_export(selected_columns, column_names, filters): elif column_key == 'check_in_time': cell.value = attendance_record.check_in_time.strftime('%H:%M:%S') if attendance_record.check_in_time else '' elif column_key == 'qr_address': - cell.value = qr_record.location_address if qr_record else '' + # Use attendance-level qr_address first (set for dynamic QR check-ins), + # fall back to the QR code's location_address for standard QR. + cell.value = ( + getattr(attendance_record, 'qr_address', None) + or (qr_record.location_address if qr_record else '') + or '' + ) elif column_key == 'address': # Check-in address logic based on location accuracy WITH HYPERLINKS # If location accuracy < 0.3 miles, use QR address; otherwise use actual check-in address @@ -1870,7 +1894,11 @@ def create_excel_export(selected_columns, column_names, filters): accuracy_value = float(attendance_record.location_accuracy) if accuracy_value < 0.3: # High accuracy - use QR code ADDRESS (not location) with hyperlink - address_text = qr_record.location_address if qr_record and qr_record.location_address else '' + address_text = ( + getattr(attendance_record, 'qr_address', None) + or (qr_record.location_address if qr_record and qr_record.location_address else '') + or '' + ) if address_text and hasattr(qr_record, 'address_latitude') and hasattr(qr_record, 'address_longitude') and qr_record.address_latitude and qr_record.address_longitude: # Format coordinates with 10 decimal places lat_formatted = f"{float(qr_record.address_latitude):.10f}" @@ -2161,7 +2189,13 @@ def create_excel_export_ordered(selected_columns, column_names, filters): elif column_key == 'check_in_time': cell.value = attendance_record.check_in_time.strftime('%H:%M:%S') if attendance_record.check_in_time else '' elif column_key == 'qr_address': - cell.value = qr_record.location_address if qr_record else '' + # Use attendance-level qr_address first (set for dynamic QR check-ins), + # fall back to the QR code's location_address for standard QR. + cell.value = ( + getattr(attendance_record, 'qr_address', None) + or (qr_record.location_address if qr_record else '') + or '' + ) elif column_key == 'address': # Check-in address logic based on location accuracy WITH HYPERLINKS # If location accuracy < 0.3 miles, use QR address; otherwise use actual check-in address @@ -2170,7 +2204,11 @@ def create_excel_export_ordered(selected_columns, column_names, filters): accuracy_value = float(attendance_record.location_accuracy) if accuracy_value < 0.3: # High accuracy - use QR code ADDRESS (not location) with hyperlink - address_text = qr_record.location_address if qr_record and qr_record.location_address else '' + address_text = ( + getattr(attendance_record, 'qr_address', None) + or (qr_record.location_address if qr_record and qr_record.location_address else '') + or '' + ) if address_text and hasattr(qr_record, 'address_latitude') and hasattr(qr_record, 'address_longitude') and qr_record.address_latitude and qr_record.address_longitude: # Format coordinates with 10 decimal places lat_formatted = f"{float(qr_record.address_latitude):.10f}" @@ -2333,4 +2371,4 @@ def create_excel_export_ordered(selected_columns, column_names, filters): except Exception as log_error: logger_handler.logger.warning(f"Could not log error: {log_error}") - return None + return None \ No newline at end of file diff --git a/routes/qr_codes.py b/routes/qr_codes.py index 48119e8..6ee2a2c 100644 --- a/routes/qr_codes.py +++ b/routes/qr_codes.py @@ -701,6 +701,21 @@ def qr_checkin(qr_url): selected_location_name = request.form.get('selected_location_name', '').strip() selected_location_address = request.form.get('selected_location_address', '').strip() + # Server-side guard: if this is a dynamic QR and no location was submitted, + # reject the check-in so "Dynamic" is never stored as location_name. + if getattr(qr_code, 'qr_type', 'standard') == 'dynamic' and not selected_location_name: + logger_handler.logger.warning( + f"DYNAMIC check-in REJECTED: employee={employee_id}, " + f"qr_id={qr_code.id} — no location selected" + ) + return jsonify({ + 'success': False, + 'message': ( + 'Please select a location before checking in. / ' + 'Por favor seleccione una ubicación antes de registrarse.' + ) + }), 400 + if getattr(qr_code, 'qr_type', 'standard') == 'dynamic' and selected_location_name: effective_location_name = selected_location_name effective_location_address = selected_location_address or '' @@ -800,11 +815,10 @@ def qr_checkin(qr_url): # Create attendance record logger_handler.logger.debug("Creating attendance record") - # For dynamic QR: append tag to location_name so reports distinguish the source + # Flag whether this check-in came from a dynamic QR scan is_dynamic = getattr(qr_code, 'qr_type', 'standard') == 'dynamic' and bool(selected_location_name) - record_location_name = ( - f"{effective_location_name} (Dynamic QR)" if is_dynamic else effective_location_name - ) + # Store clean location_name (no suffix) so filtering/export works normally + record_location_name = effective_location_name # For dynamic QR: store the selected location's address as the QR-side address record_qr_address = effective_location_address if is_dynamic else None @@ -818,6 +832,7 @@ def qr_checkin(qr_url): ip_address=client_ip, location_name=record_location_name, qr_address=record_qr_address, + is_dynamic_qr=is_dynamic, latitude=location_data['latitude'], longitude=location_data['longitude'], accuracy=location_data['accuracy'], diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index bc74ff4..d2ced6e 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -148,7 +148,7 @@ function applyFilters() { record.event.toLowerCase().includes(searchTerm); const matchesLocation = - !locationFilter || record.location === locationFilter; + !locationFilter || (record.location && record.location.trim() === locationFilter.trim()); const matchesEmployee = employeeFilterIds.length === 0 || employeeFilterIds.some(function(id) { @@ -514,6 +514,7 @@ function loadTableData() { ? cells[10].textContent.trim() : "", isModified: row.classList.contains('modified-record'), + isDynamic: row.dataset.isDynamic === '1', verification_required: verificationData.required, verification_status: verificationData.status }; @@ -629,6 +630,10 @@ function createTableRow(record, displayIndex) { if (record.isModified) { row.classList.add('modified-record'); } + // Apply blue-border highlight for Dynamic QR records + if (record.isDynamic) { + row.classList.add('dynamic-qr-record'); + } // Debug logging for first few records if (displayIndex <= 3) { diff --git a/static/js/qr_destination.js b/static/js/qr_destination.js index a110700..25103f5 100644 --- a/static/js/qr_destination.js +++ b/static/js/qr_destination.js @@ -1,1412 +1,1445 @@ -/** - * QR Code Destination Page JavaScript - Enhanced with Multiple Check-ins Support - * Handles staff check-in functionality with GPS location support, language switching, and 30-minute interval validation - */ - -// Global variables -let isSubmitting = false; -let currentTime = new Date(); - -// Camera verification variables -let cameraStream = null; -let capturedPhotoData = null; -let verificationAttemptData = null; - -// Location tracking variables -let userLocation = { - latitude: null, - longitude: null, - accuracy: null, - altitude: null, - timestamp: null, - source: "manual", - address: null, -}; -let locationRequestActive = false; -let locationWatchId = null; - -// BILINGUAL FUNCTIONALITY -let currentLanguage = "en"; -const translations = { - en: { - languageText: "ES", - statusMessages: { - processing: "Processing check-in...", - success: "Check-in successful!", - error: "Check-in failed. Please try again.", - duplicate: "You have already checked in today.", - tooSoon: "Please wait before checking in again.", - multipleSuccess: "Submitted successfully!", - invalidId: "Please enter a valid Employee ID.", - locationError: "Unable to get location data.", - networkError: "Network error. Please check your connection.", - }, - }, - es: { - languageText: "EN", - statusMessages: { - processing: "Procesando registro...", - success: "¡Registro exitoso!", - error: "Error en el registro. Por favor intente de nuevo.", - duplicate: "Ya se ha registrado hoy.", - tooSoon: "Por favor espere antes de registrarse nuevamente.", - multipleSuccess: "Submitted successfully!!", - invalidId: "Por favor ingrese un ID de empleado válido.", - locationError: "No se pudo obtener datos de ubicación.", - networkError: "Error de red. Verifique su conexión.", - }, - }, -}; - -// DOM Content Loaded Event (PRESERVED FROM ORIGINAL) -document.addEventListener("DOMContentLoaded", function () { - // CRITICAL: Disable form FIRST before anything else - window.locationServicesBlocked = true; - disableFormImmediately(); - // CRITICAL: Initialize systems in correct order - initializeLanguage(); - initializeLocationServicesCheck(); - initializeStaffIdPersistence(); - initializeForm(); - initializeLocation(); - startClock(); - - // Add fade-in animation to elements - setTimeout(() => { - document.querySelectorAll(".fade-transition").forEach((el) => { - el.classList.add("active"); - }); - }, 100); -}); - -// ENHANCED STAFF ID PERSISTENCE FUNCTIONALITY -function initializeStaffIdPersistence() { - // Load last staff ID from localStorage - lastStaffId = loadLastStaffId(); - if (lastStaffId) { - // Automatically fill the last staff ID - const employeeIdInput = document.getElementById("employee_id"); - if (employeeIdInput) { - employeeIdInput.value = lastStaffId; - validateEmployeeId(); - } - } -} - -function loadLastStaffId() { - try { - const saved = localStorage.getItem("qr_last_staff_id"); - if (saved && saved.trim().length >= 2) { - return saved.trim().toUpperCase(); - } - return null; - } catch (error) { - return null; - } -} - -function saveLastStaffId(staffId) { - try { - if (!staffId || typeof staffId !== "string" || staffId.trim().length < 2) { - return false; - } - - const cleanId = staffId.trim().toUpperCase(); - lastStaffId = cleanId; - - // Save to localStorage - localStorage.setItem("qr_last_staff_id", cleanId); - - return true; - } catch (error) { - return false; - } -} - -// ENHANCED FORM HANDLING FOR MULTIPLE CHECK-INS -function initializeForm() { - const form = document.getElementById("checkinForm"); - const submitButton = document.getElementById("submitCheckin"); - - if (form && submitButton) { - form.addEventListener("submit", handleFormSubmit); - - // Add real-time Employee ID validation - const employeeIdInput = document.getElementById("employee_id"); - if (employeeIdInput) { - employeeIdInput.addEventListener("input", validateEmployeeId); - employeeIdInput.addEventListener("keypress", function (e) { - if (e.key === "Enter") { - e.preventDefault(); - handleFormSubmit(e); - } - }); - } - } -} - -function disableFormImmediately() { - // Find and disable submit button immediately - const submitButton = - document.getElementById("submitCheckin") || - document.getElementById("submitButton") || - document.querySelector('button[type="submit"]') || - document.querySelector(".btn-primary"); - - const employeeIdInput = document.getElementById("employee_id"); - const form = document.getElementById("checkinForm"); - - if (submitButton) { - submitButton.disabled = true; - submitButton.style.opacity = "0.5"; - submitButton.style.cursor = "not-allowed"; - submitButton.style.pointerEvents = "none"; - submitButton.setAttribute("data-location-blocked", "true"); - - // Store original content - if (!submitButton.getAttribute("data-original-content")) { - submitButton.setAttribute( - "data-original-content", - submitButton.innerHTML - ); - } - - // Show loading/checking state - submitButton.innerHTML = ` - - Checking Location Services... - `; - } - - if (employeeIdInput) { - employeeIdInput.disabled = true; - employeeIdInput.style.opacity = "0.7"; - employeeIdInput.setAttribute("data-location-blocked", "true"); - employeeIdInput.placeholder = "Checking location services..."; - } - - if (form) { - form.classList.add("location-blocked"); - form.style.pointerEvents = "none"; - } - - console.log("🚫 Form DISABLED by default - Checking Location Services..."); -} - -function handleFormSubmit(event) { - event.preventDefault(); - event.stopPropagation(); - - // CRITICAL: Check if location services are blocked - if (window.locationServicesBlocked === true) { - console.log("🚫 Form submission blocked - Location Services not enabled"); - showLocationServicesBlockedMessage(); - return false; - } - - if (isSubmitting) { - return false; - } - - // STEP 1: Check Location Services FIRST - console.log("🔍 Step 1: Validating Location Services..."); - - checkLocationServicesStatus() - .then(() => { - // Location services are working, proceed with check-in - console.log("✅ Location Services validated successfully"); - proceedWithCheckin(); - }) - .catch((error) => { - // Location services are not working, block check-in - console.log("❌ Location Services validation failed:", error); - showLocationServicesBlockedMessage(); - return false; - }); - - return false; -} - -/** - * Proceed with the actual check-in process after location validation - */ -function proceedWithCheckin() { - // Double-check location services are not blocked - if (window.locationServicesBlocked === true) { - console.log("🚫 Check-in blocked - Location Services not enabled"); - showLocationServicesBlockedMessage(); - return; - } - - // ADDED: For dynamic QR codes, require a location to be selected before proceeding - var selLocField = document.getElementById("selected_location_name"); - var locationSelectCard = document.getElementById("locationSelectCard"); - if (locationSelectCard && selLocField && !selLocField.value.trim()) { - // No location selected — redirect employee back to Step 1 - document.getElementById("checkinFormCard").style.display = "none"; - locationSelectCard.style.display = "block"; - showLocalizedStatusMessage("invalidId", "error"); - console.log("❌ Dynamic QR: no location selected, returning to Step 1"); - return; - } - - const employeeId = document.getElementById("employee_id")?.value?.trim(); - - if (!employeeId) { - showLocalizedStatusMessage("invalidId", "error"); - return; - } - - if (employeeId.length < 2) { - showLocalizedStatusMessage("invalidId", "error"); - return; - } - - // Save the staff ID for future use - saveLastStaffId(employeeId); - - // Show processing status - showLocalizedStatusMessage("processing", "info"); - - // Submit the check-in - submitCheckin(); -} - -/** - * Show message when check-in is blocked due to location services - */ -function showLocationServicesBlockedMessage() { - const messages = { - en: "Check-in blocked: Location Services must be enabled to continue.", - es: "Registro bloqueado: Los Servicios de Ubicación deben estar habilitados para continuar.", - }; - - const currentLang = currentLanguage || "en"; - const message = messages[currentLang]; - - showCustomStatusMessage(message, "error"); -} - -// ENHANCED CHECK-IN SUBMISSION WITH MULTIPLE CHECK-IN SUPPORT -function submitCheckin() { - if (isSubmitting) { - return; - } - - isSubmitting = true; - updateSubmitButton(true); - - const employeeId = document.getElementById("employee_id").value.trim(); - - if (!employeeId) { - showLocalizedStatusMessage("invalidId", "error"); - isSubmitting = false; - updateSubmitButton(false); - return; - } - - // Prepare form data - const formData = new FormData(); - formData.append("employee_id", employeeId); - formData.append( - "latitude", - userLocation.latitude ? userLocation.latitude.toFixed(10) : "" - ); - formData.append( - "longitude", - userLocation.longitude ? userLocation.longitude.toFixed(10) : "" - ); - formData.append("accuracy", userLocation.accuracy || ""); - formData.append("altitude", userLocation.altitude || ""); - formData.append("location_source", userLocation.source || "manual"); - formData.append("address", userLocation.address || ""); - - // ADDED: Forward the employee-selected location for dynamic QR check-in - const selLocName = document.getElementById("selected_location_name"); - const selLocAddr = document.getElementById("selected_location_address"); - if (selLocName && selLocName.value.trim()) { - formData.append("selected_location_name", selLocName.value.trim()); - } - if (selLocAddr && selLocAddr.value.trim()) { - formData.append("selected_location_address", selLocAddr.value.trim()); - } - - const currentUrl = window.location.pathname; - const checkinUrl = `${currentUrl}/checkin`; - - fetch(checkinUrl, { - method: "POST", - body: formData, - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - }) - .then((response) => { - return response.json(); - }) - .then((data) => { - // Check if photo verification is required - if (data.requires_verification) { - console.log('⚠️ Photo verification required'); - verificationAttemptData = formData; // Store for retry with photo - showVerificationModal(data.distance || 0, data.threshold || 0.3); - } else { - handleCheckinResponse(data); - } - }) - .catch((error) => { - handleCheckinError(error); - }) - .finally(() => { - isSubmitting = false; - updateSubmitButton(false); - }); -} - -// ENHANCED RESPONSE HANDLING FOR MULTIPLE CHECK-INS -function handleCheckinResponse(data) { - if (data.success) { - handleCheckinSuccess(data); - } else { - const errorMsg = data.message || "Submission failed"; - - // NEW: Handle different types of check-in failures - if (errorMsg.toLowerCase().includes("already submitted")) { - showLocalizedStatusMessage("duplicate", "warning"); - } else if ( - errorMsg.toLowerCase().includes("submit again in") || - errorMsg.toLowerCase().includes("minutes") - ) { - // Handle 30-minute interval message - showCustomStatusMessage(errorMsg, "warning"); - } else { - showLocalizedStatusMessage("error", "error"); - } - } -} - -// ENHANCED SUCCESS HANDLING WITH MULTIPLE CHECK-IN INFO -function handleCheckinSuccess(data) { - console.log("✅ Success data received:", data); - - const responseData = data.data || data || {}; - const checkinCount = responseData.checkin_count_today || 1; - const checkinSequence = responseData.checkin_sequence || "Check-in"; - - // Show appropriate success message based on check-in count - if (checkinCount > 1) { - showLocalizedStatusMessage("multipleSuccess", "success"); - } else { - showLocalizedStatusMessage("success", "success"); - } - - // Hide the entire checkin-card (includes instruction header and form) - const checkinCard = document.querySelector(".checkin-card"); - if (checkinCard) { - checkinCard.style.display = "none"; - checkinCard.classList.remove("active"); - checkinCard.classList.add("hidden-after-success"); - } - - // Also hide form separately for backward compatibility (PRESERVED FROM ORIGINAL) - const form = document.getElementById("checkinForm"); - if (form) { - form.style.display = "none"; - } - - // Update success card with enhanced information - const successCard = document.getElementById("successCard"); - if (successCard) { - successCard.style.display = "block"; - successCard.classList.add("active"); - - const updateElement = (id, value) => { - const el = document.getElementById(id); - if (el) { - el.textContent = value || "N/A"; - } - }; - - // Get employee ID from form input if not in response data - const employeeIdInput = document.getElementById("employee_id"); - const employeeId = - responseData.employee_id || - (employeeIdInput - ? employeeIdInput.value.trim().toUpperCase() - : "Unknown"); - - const location = responseData.location || "Unknown Location"; - const event = - responseData.event || responseData.location_event || "Check-in"; - - // Format current date and time if not provided in response - const now = new Date(); - const checkInTime = - responseData.check_in_time || - now.toLocaleTimeString("en-US", { - hour: "2-digit", - minute: "2-digit", - hour12: true, - }); - const checkInDate = - responseData.check_in_date || - now.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - - // Update success card elements - updateElement("successEmployeeId", employeeId); - updateElement("successLocation", location); - updateElement("successEvent", event); - updateElement("successCheckInTime", checkInTime); - updateElement("successCheckInDate", checkInDate); - - // NEW: Add check-in sequence information - updateElement("successCheckinSequence", checkinSequence); - - // Update additional info if available - if (responseData.device_info) { - updateElement("successDeviceInfo", responseData.device_info); - } - - if (responseData.coordinates) { - updateElement("successCoordinates", responseData.coordinates); - } - - if (responseData.address) { - updateElement("successAddress", responseData.address); - } - - if (responseData.location_accuracy) { - updateElement( - "successLocationAccuracy", - `${responseData.location_accuracy} miles` - ); - } - - // Log successful check-in with details - console.log( - `✅ Check-in successful for Employee ID: ${employeeId}, Location: ${location}, Time: ${checkInTime}, Date: ${checkInDate}, Action: ${event}` - ); - } -} - -// NEW: Reset form for new check-in -function resetForNewCheckin() { - // Show checkin-card again (includes instruction header and form) - const checkinCard = document.querySelector(".checkin-card"); - if (checkinCard) { - checkinCard.style.display = "block"; - checkinCard.classList.add("active"); - checkinCard.classList.remove("hidden-after-success"); - } - - // Show form again (PRESERVED FROM ORIGINAL) - const form = document.getElementById("checkinForm"); - if (form) { - form.style.display = "block"; - } - - // Hide success card - const successCard = document.getElementById("successCard"); - if (successCard) { - successCard.style.display = "none"; - successCard.classList.remove("active"); - } - - // Clear previous employee ID - const employeeIdInput = document.getElementById("employee_id"); - if (employeeIdInput) { - employeeIdInput.value = ""; - employeeIdInput.focus(); - } - - // Clear status messages - clearStatusMessages(); - - // Reset location if needed - if (!userLocation.latitude || !userLocation.longitude) { - requestLocationData(); - } -} - -// NEW: Show custom status message (for interval warnings) -function showCustomStatusMessage(message, type = "info") { - const statusContainer = document.getElementById("statusMessage"); - if (statusContainer) { - statusContainer.className = `status-message ${type}`; - statusContainer.innerHTML = ` -
- - ${message} -
- `; - statusContainer.style.display = "block"; - - // Auto-hide after 5 seconds - setTimeout(() => { - statusContainer.style.display = "none"; - }, 5000); - } -} - -// Helper function to get appropriate icon for status type -function getStatusIcon(type) { - switch (type) { - case "success": - return "fa-check-circle"; - case "error": - return "fa-exclamation-circle"; - case "warning": - return "fa-clock"; - case "info": - default: - return "fa-info-circle"; - } -} - -// PRESERVED: All other existing functions remain unchanged -function showLocalizedStatusMessage(messageKey, type = "info") { - const message = - translations[currentLanguage].statusMessages[messageKey] || - translations["en"].statusMessages[messageKey] || - "Status update"; - - showCustomStatusMessage(message, type); -} - -function clearStatusMessages() { - const statusContainer = document.getElementById("statusMessage"); - if (statusContainer) { - statusContainer.style.display = "none"; - } -} - -function handleCheckinError(error) { - console.error("❌ Check-in submission error:", error); - showLocalizedStatusMessage("networkError", "error"); -} - -function updateSubmitButton(isLoading) { - const submitButton = document.getElementById("submitCheckin"); - if (submitButton) { - if (isLoading) { - submitButton.disabled = true; - submitButton.innerHTML = - ' Processing...'; - } else { - submitButton.disabled = false; - submitButton.innerHTML = - ' Submit'; - } - applyTranslations(); - } -} - -function validateEmployeeId() { - const employeeIdInput = document.getElementById("employee_id"); - const submitButton = document.getElementById("submitCheckin"); - - if (employeeIdInput && submitButton) { - const isValid = employeeIdInput.value.trim().length >= 2; - submitButton.disabled = !isValid || isSubmitting; - - if (isValid) { - employeeIdInput.classList.remove("invalid"); - employeeIdInput.classList.add("valid"); - } else { - employeeIdInput.classList.remove("valid"); - if (employeeIdInput.value.length > 0) { - employeeIdInput.classList.add("invalid"); - } - } - } -} - -// All location and language functions remain unchanged from original -function initializeLocation() { - // Check if Android enhanced location handler is available - if ( - typeof AndroidLocationHandler !== "undefined" && - AndroidLocationHandler.isAndroidDevice() - ) { - console.log("📱 Using Android-enhanced location initialization"); - AndroidLocationHandler.initializeAndroidLocation(); - } else { - console.log("📍 Using standard location initialization"); - requestLocationData(); - } -} - -function requestLocationData() { - if (locationRequestActive) { - console.log("📍 Location request already active, skipping"); - return; - } - - if (!navigator.geolocation) { - console.log("❌ Geolocation not supported"); - userLocation.source = "manual"; - return; - } - - locationRequestActive = true; - console.log("📍 Requesting location data..."); - - const options = { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 300000, - }; - - navigator.geolocation.getCurrentPosition( - handleLocationSuccess, - (error) => { - console.log("❌ High accuracy failed, trying low accuracy..."); - - // Simple fallback with low accuracy - const lowAccuracyOptions = { - enableHighAccuracy: false, - timeout: 15000, - maximumAge: 600000, - }; - - navigator.geolocation.getCurrentPosition( - handleLocationSuccess, - handleLocationError, - lowAccuracyOptions - ); - }, - options - ); -} - -function handleLocationSuccess(position) { - console.log("✅ Location obtained successfully"); - - userLocation = { - latitude: position.coords.latitude, - longitude: position.coords.longitude, - accuracy: position.coords.accuracy, - altitude: position.coords.altitude, - timestamp: new Date(), - source: "gps", - address: null, - }; - - // Reverse geocode to get address - reverseGeocode(userLocation.latitude, userLocation.longitude); - - locationRequestActive = false; -} - -function handleLocationError(error) { - userLocation.source = "manual"; - locationRequestActive = false; -} - -function reverseGeocode(lat, lng) { - // The server will use Google Maps API first, then fall back to OpenStreetMap - // This provides better accuracy and address formatting - const url = "/api/reverse-geocode"; // You may want to create this endpoint - - // For now, using direct OpenStreetMap as fallback - // In production, this should go through your server API - const osmUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&addressdetails=1&zoom=18`; - - fetch(osmUrl, { - method: "GET", - headers: { - "User-Agent": "QR-Attendance-System/1.0", - }, - }) - .then((response) => response.json()) - .then((data) => { - if (data && data.display_name) { - userLocation.address = data.display_name; - } else { - userLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(10)}`; - } - }) - .catch((error) => { - console.error(`❌ Reverse geocoding error:`, error); - // Fallback to coordinates if reverse geocoding fails - userLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(10)}`; - }); -} - -// ENHANCED LANGUAGE FUNCTIONALITY WITH PERSISTENCE -function initializeLanguage() { - // Load saved language preference from localStorage - const savedLanguage = loadLanguagePreference(); - if (savedLanguage && savedLanguage !== currentLanguage) { - currentLanguage = savedLanguage; - } - - // Set up language toggle button event listener - const languageToggle = document.getElementById("languageToggle"); - if (languageToggle) { - languageToggle.addEventListener("click", toggleLanguage); - } - - // Apply initial translations based on loaded language - applyTranslations(); -} - -function toggleLanguage() { - // Switch between languages - const newLanguage = currentLanguage === "en" ? "es" : "en"; - currentLanguage = newLanguage; - - // Save new language preference to localStorage - saveLanguagePreference(currentLanguage); - - // Apply translations immediately - applyTranslations(); - - // Optional: Show brief confirmation message - showLanguageChangeConfirmation(); -} - -function loadLanguagePreference() { - try { - // Retrieve language preference from localStorage - const savedLanguage = localStorage.getItem("qr_staff_language"); - - // Validate saved language is supported - if (savedLanguage && translations.hasOwnProperty(savedLanguage)) { - return savedLanguage; - } else if (savedLanguage) { - // Clean up invalid preference - localStorage.removeItem("qr_staff_language"); - } - - return null; - } catch (error) { - return null; - } -} - -function saveLanguagePreference(language) { - try { - // Validate language before saving - if (!translations.hasOwnProperty(language)) { - console.error(`❌ Invalid language code: ${language}`); - return false; - } - - // Save to localStorage - localStorage.setItem("qr_staff_language", language); - return true; - } catch (error) { - return false; - } -} - -function showLanguageChangeConfirmation() { - // Brief visual feedback for language change - const languageToggle = document.getElementById("languageToggle"); - if (languageToggle) { - // Add temporary visual feedback - languageToggle.style.transform = "scale(1.05)"; - languageToggle.style.background = "rgba(255, 255, 255, 0.4)"; - - setTimeout(() => { - languageToggle.style.transform = ""; - languageToggle.style.background = ""; - }, 200); - } -} - -function applyTranslations() { - // Update language toggle button text - const languageText = document.getElementById("languageText"); - if (languageText) { - languageText.textContent = translations[currentLanguage].languageText; - } - - // Apply translations to all elements with data attributes - document.querySelectorAll(`[data-${currentLanguage}]`).forEach((element) => { - element.textContent = element.getAttribute(`data-${currentLanguage}`); - }); - - // Update any dynamic content that might have been generated after initial load - updateDynamicTranslations(); -} - -function updateDynamicTranslations() { - // Update submit button text if it exists and has been modified - const submitButton = document.getElementById("submitCheckin"); - if (submitButton && submitButton.innerHTML.includes("data-")) { - // Re-apply translations to submit button content - const spans = submitButton.querySelectorAll(`[data-${currentLanguage}]`); - spans.forEach((span) => { - span.textContent = span.getAttribute(`data-${currentLanguage}`); - }); - } -} - -function startClock() { - function updateClock() { - currentTime = new Date(); - const timeElements = document.querySelectorAll(".current-time"); - timeElements.forEach((el) => { - el.textContent = currentTime.toLocaleTimeString(); - }); - } - - updateClock(); - setInterval(updateClock, 1000); -} - -function checkLocationServicesStatus() { - // Check if geolocation is supported - if (!navigator.geolocation) { - showLocationServicesWarning("not_supported"); - blockCheckInProcess(true); - return Promise.reject("Location services not supported"); - } - - return new Promise((resolve, reject) => { - // Test location access with a quick check - const timeoutId = setTimeout(() => { - showLocationServicesWarning("timeout"); - blockCheckInProcess(true); - reject("Location services timeout"); - }, 5000); // 5 second timeout - - navigator.geolocation.getCurrentPosition( - (position) => { - // Success - location services are working - clearTimeout(timeoutId); - hideLocationServicesWarning(); - blockCheckInProcess(false); - - // Log successful location access - console.log("✅ Location Services: ENABLED and working"); - - resolve(position); - }, - (error) => { - // Error - location services may be disabled - clearTimeout(timeoutId); - blockCheckInProcess(true); - - switch (error.code) { - case error.PERMISSION_DENIED: - showLocationServicesWarning("permission_denied"); - console.log("❌ Location Services: PERMISSION DENIED"); - break; - case error.POSITION_UNAVAILABLE: - showLocationServicesWarning("position_unavailable"); - console.log("❌ Location Services: POSITION UNAVAILABLE"); - break; - case error.TIMEOUT: - showLocationServicesWarning("timeout"); - console.log("❌ Location Services: TIMEOUT"); - break; - default: - showLocationServicesWarning("unknown_error"); - console.log("❌ Location Services: UNKNOWN ERROR"); - break; - } - - reject(error); - }, - { - enableHighAccuracy: false, - timeout: 10000, - maximumAge: 30000, - } - ); - }); -} - -function blockCheckInProcess(shouldBlock) { - // Try multiple possible submit button IDs from your codebase - const submitButton = - document.getElementById("submitCheckin") || - document.getElementById("submitButton") || - document.querySelector('button[type="submit"]') || - document.querySelector(".btn-primary"); - - const employeeIdInput = document.getElementById("employee_id"); - const form = document.getElementById("checkinForm"); - - if (shouldBlock) { - // Set global blocking flag FIRST - window.locationServicesBlocked = true; - - // Block check-in process - if (submitButton) { - submitButton.disabled = true; - submitButton.style.opacity = "0.5"; - submitButton.style.cursor = "not-allowed"; - submitButton.style.pointerEvents = "none"; - - // Add data attribute to track blocking state - submitButton.setAttribute("data-location-blocked", "true"); - - // Store original button content - if (!submitButton.getAttribute("data-original-content")) { - submitButton.setAttribute( - "data-original-content", - submitButton.innerHTML - ); - } - - // Update button text to show it's blocked - submitButton.innerHTML = ` - - Location Required / Ubicación Requerida - `; - - // Remove all event listeners by cloning - const newButton = submitButton.cloneNode(true); - submitButton.parentNode.replaceChild(newButton, submitButton); - - // Add blocking event listener - newButton.addEventListener("click", function (e) { - e.preventDefault(); - e.stopPropagation(); - showLocationServicesBlockedMessage(); - return false; - }); - } - - if (employeeIdInput) { - employeeIdInput.disabled = true; - employeeIdInput.style.opacity = "0.7"; - employeeIdInput.setAttribute("data-location-blocked", "true"); - } - - if (form) { - form.classList.add("location-blocked"); - form.style.pointerEvents = "none"; - - // Override form submission completely - form.onsubmit = function (e) { - e.preventDefault(); - e.stopPropagation(); - showLocationServicesBlockedMessage(); - return false; - }; - } - - console.log("🚫 Check-in process BLOCKED - Location Services required"); - } else { - // Clear global blocking flag FIRST - window.locationServicesBlocked = false; - - // Unblock check-in process - if (submitButton) { - submitButton.disabled = false; - submitButton.style.opacity = "1"; - submitButton.style.cursor = "pointer"; - submitButton.style.pointerEvents = "auto"; - - // Remove blocking data attribute - submitButton.removeAttribute("data-location-blocked"); - - // Restore original button content - const originalContent = submitButton.getAttribute( - "data-original-content" - ); - if (originalContent) { - submitButton.innerHTML = originalContent; - } - - // Re-attach proper event listeners - submitButton.onclick = function (e) { - e.preventDefault(); - handleFormSubmit(e); - return false; - }; - } - - if (employeeIdInput) { - employeeIdInput.disabled = false; - employeeIdInput.style.opacity = "1"; - employeeIdInput.removeAttribute("data-location-blocked"); - } - - if (form) { - form.classList.remove("location-blocked"); - form.style.pointerEvents = "auto"; - - // Restore proper form submission handler - form.onsubmit = function (e) { - e.preventDefault(); - handleFormSubmit(e); - return false; - }; - } - - console.log("✅ Check-in process UNBLOCKED - Location Services working"); - } -} - -/** - * Show location services warning banner - */ -function showLocationServicesWarning(errorType) { - // Remove existing warning if present - hideLocationServicesWarning(); - - const warningMessages = { - en: { - not_supported: - "⚠️ Location Services Not Supported
Check-in is currently blocked.
Your browser does not support location services required for check-in.

Los servicios de ubicación no son compatibles. El registro está bloqueado.", - permission_denied: - "⚠️ Location Access Denied
Check-in is currently blocked.
Please enable location access in your browser settings to continue with check-in.

Acceso a ubicación denegado. Habilite el acceso para continuar.", - position_unavailable: - "⚠️ Location Services Disabled
Check-in is currently blocked.
Please turn on Location Services in your device settings and refresh the page.

Servicios de ubicación deshabilitados. Active los servicios y actualice la página.", - timeout: - "⚠️ Location Services Not Responding
Check-in is currently blocked.
Location services may be disabled. Please check your device settings.

Los servicios de ubicación no responden. Verifique la configuración.", - unknown_error: - "⚠️ Location Services Error
Check-in is currently blocked.
Unable to access location services. Please check your settings and try again.

Error de servicios de ubicación. Verifique la configuración.", - }, - es: { - not_supported: - "⚠️ Servicios de Ubicación No Compatibles
El registro está bloqueado.
Su navegador no es compatible con los servicios de ubicación requeridos.", - permission_denied: - "⚠️ Acceso a Ubicación Denegado
El registro está bloqueado.
Habilite el acceso a la ubicación en la configuración de su navegador.", - position_unavailable: - "⚠️ Servicios de Ubicación Deshabilitados
El registro está bloqueado.
Active los Servicios de Ubicación en la configuración y actualice la página.", - timeout: - "⚠️ Servicios de Ubicación No Responden
El registro está bloqueado.
Los servicios pueden estar deshabilitados. Verifique la configuración.", - unknown_error: - "⚠️ Error de Servicios de Ubicación
El registro está bloqueado.
No se puede acceder a los servicios. Verifique la configuración.", - }, - }; - - const currentLang = currentLanguage || "en"; - const message = - warningMessages[currentLang][errorType] || warningMessages["en"][errorType]; - - // Create warning banner - const warningBanner = document.createElement("div"); - warningBanner.id = "locationServicesWarning"; - warningBanner.className = "location-warning-banner"; - warningBanner.innerHTML = ` -
- -
- ${message} -
- - -
-
-
- `; - - // Insert warning at the top of the page - const container = document.querySelector(".destination-container"); - if (container) { - container.insertBefore(warningBanner, container.firstChild); - } -} - -/** - * Hide location services warning banner - */ -function hideLocationServicesWarning() { - const existingWarning = document.getElementById("locationServicesWarning"); - if (existingWarning) { - existingWarning.remove(); - } -} - -function initializeLocationServicesCheck() { - // Initialize global blocking flag - window.locationServicesBlocked = false; - - // Check location services status when page loads and block if necessary - setTimeout(() => { - console.log("🔍 Initializing Location Services check..."); - checkLocationServicesStatus() - .then(() => { - console.log("✅ Initial Location Services check passed"); - }) - .catch(() => { - console.log( - "❌ Initial Location Services check failed - Check-in blocked" - ); - }); - }, 1000); - - // Override form initialization to ensure our handlers are used - setTimeout(() => { - const form = document.getElementById("checkinForm"); - const submitButton = - document.getElementById("submitCheckin") || - document.querySelector('button[type="submit"]'); - - if (form) { - // Remove existing event listeners by cloning - const newForm = form.cloneNode(true); - form.parentNode.replaceChild(newForm, form); - - // Add our controlled event listener - newForm.addEventListener("submit", handleFormSubmit); - } - - if (submitButton) { - // Find the new submit button after form cloning - const newSubmitButton = - document.getElementById("submitCheckin") || - document.querySelector('button[type="submit"]'); - - if (newSubmitButton) { - newSubmitButton.addEventListener("click", function (e) { - e.preventDefault(); - e.stopPropagation(); - handleFormSubmit(e); - return false; - }); - } - } - }, 1500); -} - -// =================================== -// CAMERA VERIFICATION FUNCTIONALITY -// =================================== - -function showVerificationModal(distance, threshold) { - console.log(`📸 Showing verification modal - Distance: ${distance}, Threshold: ${threshold}`); - - const modal = document.getElementById('verificationModal'); - const distanceSpan = document.getElementById('verificationDistance'); - const thresholdSpan = document.getElementById('verificationThreshold'); - - if (distanceSpan) distanceSpan.textContent = distance.toFixed(3); - if (thresholdSpan) thresholdSpan.textContent = threshold.toFixed(3); - - modal.classList.add('active'); - startCamera(); -} - -function hideVerificationModal() { - const modal = document.getElementById('verificationModal'); - modal.classList.remove('active'); - stopCamera(); - resetCameraInterface(); -} - -function startCamera() { - const video = document.getElementById('cameraVideo'); - - console.log('📸 Starting camera...'); - - const constraints = { - video: { - facingMode: 'environment', // Use back camera on mobile - width: { ideal: 1280 }, - height: { ideal: 720 } - }, - audio: false - }; - - navigator.mediaDevices.getUserMedia(constraints) - .then(stream => { - cameraStream = stream; - video.srcObject = stream; - video.style.display = 'block'; - console.log('✅ Camera started successfully'); - }) - .catch(error => { - console.error('❌ Camera error:', error); - alert('Unable to access camera. Please check permissions and try again. / No se puede acceder a la cámara.'); - hideVerificationModal(); - isSubmitting = false; - updateSubmitButton(false); - }); -} - -function stopCamera() { - if (cameraStream) { - cameraStream.getTracks().forEach(track => track.stop()); - cameraStream = null; - console.log('📸 Camera stopped'); - } -} - -function capturePhoto() { - const video = document.getElementById('cameraVideo'); - const canvas = document.getElementById('cameraCanvas'); - const capturedImage = document.getElementById('capturedPhoto'); - const captureBtn = document.getElementById('captureBtn'); - const retakeBtn = document.getElementById('retakeBtn'); - const submitBtn = document.getElementById('submitVerificationBtn'); - - // Set canvas dimensions to match video - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - - // Draw video frame to canvas - const context = canvas.getContext('2d'); - context.drawImage(video, 0, 0, canvas.width, canvas.height); - - // Get photo data as base64 - capturedPhotoData = canvas.toDataURL('image/jpeg', 0.8); - - // Show captured photo - capturedImage.src = capturedPhotoData; - capturedImage.style.display = 'block'; - video.style.display = 'none'; - - // Update button visibility - captureBtn.style.display = 'none'; - retakeBtn.style.display = 'inline-flex'; - submitBtn.style.display = 'inline-flex'; - - console.log('📸 Photo captured'); -} - -function retakePhoto() { - const video = document.getElementById('cameraVideo'); - const capturedImage = document.getElementById('capturedPhoto'); - const captureBtn = document.getElementById('captureBtn'); - const retakeBtn = document.getElementById('retakeBtn'); - const submitBtn = document.getElementById('submitVerificationBtn'); - - // Reset interface - capturedImage.style.display = 'none'; - video.style.display = 'block'; - - captureBtn.style.display = 'inline-flex'; - retakeBtn.style.display = 'none'; - submitBtn.style.display = 'none'; - - capturedPhotoData = null; - - console.log('📸 Ready to retake photo'); -} - -function resetCameraInterface() { - const video = document.getElementById('cameraVideo'); - const capturedImage = document.getElementById('capturedPhoto'); - const captureBtn = document.getElementById('captureBtn'); - const retakeBtn = document.getElementById('retakeBtn'); - const submitBtn = document.getElementById('submitVerificationBtn'); - - if (capturedImage) capturedImage.style.display = 'none'; - if (video) video.style.display = 'block'; - - if (captureBtn) captureBtn.style.display = 'inline-flex'; - if (retakeBtn) retakeBtn.style.display = 'none'; - if (submitBtn) submitBtn.style.display = 'none'; - - capturedPhotoData = null; - verificationAttemptData = null; -} - -function submitWithVerification() { - if (!capturedPhotoData) { - alert('Please capture a photo first. / Por favor capture una foto primero.'); - return; - } - - console.log('📸 Submitting check-in with verification photo...'); - - const submitBtn = document.getElementById('submitVerificationBtn'); - submitBtn.disabled = true; - submitBtn.innerHTML = ' Submitting...'; - - // Use stored form data from initial attempt - if (verificationAttemptData) { - verificationAttemptData.append('verification_photo', capturedPhotoData); - - const currentUrl = window.location.pathname; - const checkinUrl = `${currentUrl}/checkin`; - - fetch(checkinUrl, { - method: "POST", - body: verificationAttemptData, - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - }) - .then((response) => response.json()) - .then((data) => { - hideVerificationModal(); - - if (data.success) { - showCustomStatusMessage( - "Submission successful! Photo pending review. / ¡Envío exitoso! Foto pendiente de revisión.", - "success" - ); - handleCheckinSuccess(data); - } else { - showCustomStatusMessage( - data.message || "Submission failed / Envío fallido", - "error" - ); - } - }) - .catch((error) => { - hideVerificationModal(); - console.error('❌ Verification submit error:', error); - showLocalizedStatusMessage("networkError", "error"); - }) - .finally(() => { - isSubmitting = false; - updateSubmitButton(false); - submitBtn.disabled = false; - submitBtn.innerHTML = ' Submit with Photo'; - }); - } -} - -function cancelVerification() { - hideVerificationModal(); - isSubmitting = false; - updateSubmitButton(false); -} - -// Initialize camera button event listeners -function initializeCameraButtons() { - const captureBtn = document.getElementById('captureBtn'); - const retakeBtn = document.getElementById('retakeBtn'); - const submitBtn = document.getElementById('submitVerificationBtn'); - const cancelBtn = document.getElementById('cancelVerificationBtn'); - - if (captureBtn) { - captureBtn.addEventListener('click', capturePhoto); - } - - if (retakeBtn) { - retakeBtn.addEventListener('click', retakePhoto); - } - - if (submitBtn) { - submitBtn.addEventListener('click', submitWithVerification); - } - - if (cancelBtn) { - cancelBtn.addEventListener('click', cancelVerification); - } - - console.log('📸 Camera verification buttons initialized'); -} - -// Call initialization when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializeCameraButtons); -} else { - initializeCameraButtons(); +/** + * QR Code Destination Page JavaScript - Enhanced with Multiple Check-ins Support + * Handles staff check-in functionality with GPS location support, language switching, and 30-minute interval validation + */ + +// Global variables +let isSubmitting = false; +let currentTime = new Date(); + +// Camera verification variables +let cameraStream = null; +let capturedPhotoData = null; +let verificationAttemptData = null; + +// Location tracking variables +let userLocation = { + latitude: null, + longitude: null, + accuracy: null, + altitude: null, + timestamp: null, + source: "manual", + address: null, +}; +let locationRequestActive = false; +let locationWatchId = null; + +// BILINGUAL FUNCTIONALITY +let currentLanguage = "en"; +const translations = { + en: { + languageText: "ES", + statusMessages: { + processing: "Processing check-in...", + success: "Check-in successful!", + error: "Check-in failed. Please try again.", + duplicate: "You have already checked in today.", + tooSoon: "Please wait before checking in again.", + multipleSuccess: "Submitted successfully!", + invalidId: "Please enter a valid Employee ID.", + locationError: "Unable to get location data.", + networkError: "Network error. Please check your connection.", + }, + }, + es: { + languageText: "EN", + statusMessages: { + processing: "Procesando registro...", + success: "¡Registro exitoso!", + error: "Error en el registro. Por favor intente de nuevo.", + duplicate: "Ya se ha registrado hoy.", + tooSoon: "Por favor espere antes de registrarse nuevamente.", + multipleSuccess: "Submitted successfully!!", + invalidId: "Por favor ingrese un ID de empleado válido.", + locationError: "No se pudo obtener datos de ubicación.", + networkError: "Error de red. Verifique su conexión.", + }, + }, +}; + +// DOM Content Loaded Event (PRESERVED FROM ORIGINAL) +document.addEventListener("DOMContentLoaded", function () { + // CRITICAL: Disable form FIRST before anything else + window.locationServicesBlocked = true; + disableFormImmediately(); + // CRITICAL: Initialize systems in correct order + initializeLanguage(); + initializeLocationServicesCheck(); + initializeStaffIdPersistence(); + initializeForm(); + initializeLocation(); + startClock(); + + // Add fade-in animation to elements + setTimeout(() => { + document.querySelectorAll(".fade-transition").forEach((el) => { + el.classList.add("active"); + }); + }, 100); +}); + +// ENHANCED STAFF ID PERSISTENCE FUNCTIONALITY +function initializeStaffIdPersistence() { + // Load last staff ID from localStorage + lastStaffId = loadLastStaffId(); + if (lastStaffId) { + // Automatically fill the last staff ID + const employeeIdInput = document.getElementById("employee_id"); + if (employeeIdInput) { + employeeIdInput.value = lastStaffId; + validateEmployeeId(); + } + } +} + +function loadLastStaffId() { + try { + const saved = localStorage.getItem("qr_last_staff_id"); + if (saved && saved.trim().length >= 2) { + return saved.trim().toUpperCase(); + } + return null; + } catch (error) { + return null; + } +} + +function saveLastStaffId(staffId) { + try { + if (!staffId || typeof staffId !== "string" || staffId.trim().length < 2) { + return false; + } + + const cleanId = staffId.trim().toUpperCase(); + lastStaffId = cleanId; + + // Save to localStorage + localStorage.setItem("qr_last_staff_id", cleanId); + + return true; + } catch (error) { + return false; + } +} + +// ENHANCED FORM HANDLING FOR MULTIPLE CHECK-INS +function initializeForm() { + const form = document.getElementById("checkinForm"); + const submitButton = document.getElementById("submitCheckin"); + + if (form && submitButton) { + form.addEventListener("submit", handleFormSubmit); + + // Add real-time Employee ID validation + const employeeIdInput = document.getElementById("employee_id"); + if (employeeIdInput) { + employeeIdInput.addEventListener("input", validateEmployeeId); + employeeIdInput.addEventListener("keypress", function (e) { + if (e.key === "Enter") { + e.preventDefault(); + handleFormSubmit(e); + } + }); + } + } +} + +function disableFormImmediately() { + // Find and disable submit button immediately + const submitButton = + document.getElementById("submitCheckin") || + document.getElementById("submitButton") || + document.querySelector('button[type="submit"]') || + document.querySelector(".btn-primary"); + + const employeeIdInput = document.getElementById("employee_id"); + const form = document.getElementById("checkinForm"); + + if (submitButton) { + submitButton.disabled = true; + submitButton.style.opacity = "0.5"; + submitButton.style.cursor = "not-allowed"; + submitButton.style.pointerEvents = "none"; + submitButton.setAttribute("data-location-blocked", "true"); + + // Store original content + if (!submitButton.getAttribute("data-original-content")) { + submitButton.setAttribute( + "data-original-content", + submitButton.innerHTML + ); + } + + // Show loading/checking state + submitButton.innerHTML = ` + + Checking Location Services... + `; + } + + if (employeeIdInput) { + employeeIdInput.disabled = true; + employeeIdInput.style.opacity = "0.7"; + employeeIdInput.setAttribute("data-location-blocked", "true"); + employeeIdInput.placeholder = "Checking location services..."; + } + + if (form) { + form.classList.add("location-blocked"); + form.style.pointerEvents = "none"; + } + + console.log("🚫 Form DISABLED by default - Checking Location Services..."); +} + +function handleFormSubmit(event) { + event.preventDefault(); + event.stopPropagation(); + + // CRITICAL: Check if location services are blocked + if (window.locationServicesBlocked === true) { + console.log("🚫 Form submission blocked - Location Services not enabled"); + showLocationServicesBlockedMessage(); + return false; + } + + if (isSubmitting) { + return false; + } + + // STEP 1: Check Location Services FIRST + console.log("🔍 Step 1: Validating Location Services..."); + + checkLocationServicesStatus() + .then(() => { + // Location services are working, proceed with check-in + console.log("✅ Location Services validated successfully"); + proceedWithCheckin(); + }) + .catch((error) => { + // Location services are not working, block check-in + console.log("❌ Location Services validation failed:", error); + showLocationServicesBlockedMessage(); + return false; + }); + + return false; +} + +/** + * Proceed with the actual check-in process after location validation + */ +function proceedWithCheckin() { + // Double-check location services are not blocked + if (window.locationServicesBlocked === true) { + console.log("🚫 Check-in blocked - Location Services not enabled"); + showLocationServicesBlockedMessage(); + return; + } + + // ADDED: For dynamic QR codes, require a location to be selected before proceeding + var selLocField = document.getElementById("selected_location_name"); + var locationSelectCard = document.getElementById("locationSelectCard"); + var dropdown = document.getElementById("locationDropdown"); + var hasLocationSelected = (window._dynamicQRSelectedName && window._dynamicQRSelectedName.trim()) || + (selLocField && selLocField.value.trim()) || + (dropdown && dropdown.value.trim()); + + if (locationSelectCard && !hasLocationSelected) { + // No location selected — redirect employee back to Step 1 + document.getElementById("checkinFormCard").style.display = "none"; + locationSelectCard.style.display = "block"; + showLocalizedStatusMessage("invalidId", "error"); + console.log("❌ Dynamic QR: no location selected, returning to Step 1"); + return; + } + + const employeeId = document.getElementById("employee_id")?.value?.trim(); + + if (!employeeId) { + showLocalizedStatusMessage("invalidId", "error"); + return; + } + + if (employeeId.length < 2) { + showLocalizedStatusMessage("invalidId", "error"); + return; + } + + // Save the staff ID for future use + saveLastStaffId(employeeId); + + // Show processing status + showLocalizedStatusMessage("processing", "info"); + + // Submit the check-in + submitCheckin(); +} + +/** + * Show message when check-in is blocked due to location services + */ +function showLocationServicesBlockedMessage() { + const messages = { + en: "Check-in blocked: Location Services must be enabled to continue.", + es: "Registro bloqueado: Los Servicios de Ubicación deben estar habilitados para continuar.", + }; + + const currentLang = currentLanguage || "en"; + const message = messages[currentLang]; + + showCustomStatusMessage(message, "error"); +} + +// ENHANCED CHECK-IN SUBMISSION WITH MULTIPLE CHECK-IN SUPPORT +function submitCheckin() { + if (isSubmitting) { + return; + } + + isSubmitting = true; + updateSubmitButton(true); + + const employeeId = document.getElementById("employee_id").value.trim(); + + if (!employeeId) { + showLocalizedStatusMessage("invalidId", "error"); + isSubmitting = false; + updateSubmitButton(false); + return; + } + + // Prepare form data + const formData = new FormData(); + formData.append("employee_id", employeeId); + formData.append( + "latitude", + userLocation.latitude ? userLocation.latitude.toFixed(10) : "" + ); + formData.append( + "longitude", + userLocation.longitude ? userLocation.longitude.toFixed(10) : "" + ); + formData.append("accuracy", userLocation.accuracy || ""); + formData.append("altitude", userLocation.altitude || ""); + formData.append("location_source", userLocation.source || "manual"); + formData.append("address", userLocation.address || ""); + + // ADDED: Forward the employee-selected location for dynamic QR check-in. + // Priority: window globals (set by confirmLocationSelection) → hidden field → dropdown. + var locationNameValue = (window._dynamicQRSelectedName && window._dynamicQRSelectedName.trim()) + ? window._dynamicQRSelectedName.trim() + : ""; + var locationAddrValue = (window._dynamicQRSelectedAddress && window._dynamicQRSelectedAddress.trim()) + ? window._dynamicQRSelectedAddress.trim() + : ""; + + // Fallback to hidden fields + if (!locationNameValue) { + var _hn = document.getElementById("selected_location_name"); + var _ha = document.getElementById("selected_location_address"); + if (_hn && _hn.value.trim()) { + locationNameValue = _hn.value.trim(); + locationAddrValue = (_ha && _ha.value.trim()) ? _ha.value.trim() : ""; + } + } + + // Final fallback to dropdown directly + if (!locationNameValue) { + var _dd = document.getElementById("locationDropdown"); + if (_dd && _dd.value.trim()) { + locationNameValue = _dd.value.trim(); + var _so = _dd.options[_dd.selectedIndex]; + locationAddrValue = _so ? (_so.getAttribute("data-address") || "") : ""; + } + } + + console.log("📍 [qr_destination.js] Location — name:", locationNameValue, "| addr:", locationAddrValue); + + if (locationNameValue) { + formData.append("selected_location_name", locationNameValue); + } + if (locationAddrValue) { + formData.append("selected_location_address", locationAddrValue); + } + + const currentUrl = window.location.pathname; + const checkinUrl = `${currentUrl}/checkin`; + + fetch(checkinUrl, { + method: "POST", + body: formData, + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + }) + .then((response) => { + return response.json(); + }) + .then((data) => { + // Check if photo verification is required + if (data.requires_verification) { + console.log('⚠️ Photo verification required'); + verificationAttemptData = formData; // Store for retry with photo + showVerificationModal(data.distance || 0, data.threshold || 0.3); + } else { + handleCheckinResponse(data); + } + }) + .catch((error) => { + handleCheckinError(error); + }) + .finally(() => { + isSubmitting = false; + updateSubmitButton(false); + }); +} + +// ENHANCED RESPONSE HANDLING FOR MULTIPLE CHECK-INS +function handleCheckinResponse(data) { + if (data.success) { + handleCheckinSuccess(data); + } else { + const errorMsg = data.message || "Submission failed"; + + // NEW: Handle different types of check-in failures + if (errorMsg.toLowerCase().includes("already submitted")) { + showLocalizedStatusMessage("duplicate", "warning"); + } else if ( + errorMsg.toLowerCase().includes("submit again in") || + errorMsg.toLowerCase().includes("minutes") + ) { + // Handle 30-minute interval message + showCustomStatusMessage(errorMsg, "warning"); + } else { + showLocalizedStatusMessage("error", "error"); + } + } +} + +// ENHANCED SUCCESS HANDLING WITH MULTIPLE CHECK-IN INFO +function handleCheckinSuccess(data) { + console.log("✅ Success data received:", data); + + const responseData = data.data || data || {}; + const checkinCount = responseData.checkin_count_today || 1; + const checkinSequence = responseData.checkin_sequence || "Check-in"; + + // Show appropriate success message based on check-in count + if (checkinCount > 1) { + showLocalizedStatusMessage("multipleSuccess", "success"); + } else { + showLocalizedStatusMessage("success", "success"); + } + + // Hide the entire checkin-card (includes instruction header and form) + const checkinCard = document.querySelector(".checkin-card"); + if (checkinCard) { + checkinCard.style.display = "none"; + checkinCard.classList.remove("active"); + checkinCard.classList.add("hidden-after-success"); + } + + // Also hide form separately for backward compatibility (PRESERVED FROM ORIGINAL) + const form = document.getElementById("checkinForm"); + if (form) { + form.style.display = "none"; + } + + // Update success card with enhanced information + const successCard = document.getElementById("successCard"); + if (successCard) { + successCard.style.display = "block"; + successCard.classList.add("active"); + + const updateElement = (id, value) => { + const el = document.getElementById(id); + if (el) { + el.textContent = value || "N/A"; + } + }; + + // Get employee ID from form input if not in response data + const employeeIdInput = document.getElementById("employee_id"); + const employeeId = + responseData.employee_id || + (employeeIdInput + ? employeeIdInput.value.trim().toUpperCase() + : "Unknown"); + + const location = responseData.location || "Unknown Location"; + const event = + responseData.event || responseData.location_event || "Check-in"; + + // Format current date and time if not provided in response + const now = new Date(); + const checkInTime = + responseData.check_in_time || + now.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: true, + }); + const checkInDate = + responseData.check_in_date || + now.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + + // Update success card elements + updateElement("successEmployeeId", employeeId); + updateElement("successLocation", location); + updateElement("successEvent", event); + updateElement("successCheckInTime", checkInTime); + updateElement("successCheckInDate", checkInDate); + + // NEW: Add check-in sequence information + updateElement("successCheckinSequence", checkinSequence); + + // Update additional info if available + if (responseData.device_info) { + updateElement("successDeviceInfo", responseData.device_info); + } + + if (responseData.coordinates) { + updateElement("successCoordinates", responseData.coordinates); + } + + if (responseData.address) { + updateElement("successAddress", responseData.address); + } + + if (responseData.location_accuracy) { + updateElement( + "successLocationAccuracy", + `${responseData.location_accuracy} miles` + ); + } + + // Log successful check-in with details + console.log( + `✅ Check-in successful for Employee ID: ${employeeId}, Location: ${location}, Time: ${checkInTime}, Date: ${checkInDate}, Action: ${event}` + ); + } +} + +// NEW: Reset form for new check-in +function resetForNewCheckin() { + // Show checkin-card again (includes instruction header and form) + const checkinCard = document.querySelector(".checkin-card"); + if (checkinCard) { + checkinCard.style.display = "block"; + checkinCard.classList.add("active"); + checkinCard.classList.remove("hidden-after-success"); + } + + // Show form again (PRESERVED FROM ORIGINAL) + const form = document.getElementById("checkinForm"); + if (form) { + form.style.display = "block"; + } + + // Hide success card + const successCard = document.getElementById("successCard"); + if (successCard) { + successCard.style.display = "none"; + successCard.classList.remove("active"); + } + + // Clear previous employee ID + const employeeIdInput = document.getElementById("employee_id"); + if (employeeIdInput) { + employeeIdInput.value = ""; + employeeIdInput.focus(); + } + + // Clear status messages + clearStatusMessages(); + + // Reset location if needed + if (!userLocation.latitude || !userLocation.longitude) { + requestLocationData(); + } +} + +// NEW: Show custom status message (for interval warnings) +function showCustomStatusMessage(message, type = "info") { + const statusContainer = document.getElementById("statusMessage"); + if (statusContainer) { + statusContainer.className = `status-message ${type}`; + statusContainer.innerHTML = ` +
+ + ${message} +
+ `; + statusContainer.style.display = "block"; + + // Auto-hide after 5 seconds + setTimeout(() => { + statusContainer.style.display = "none"; + }, 5000); + } +} + +// Helper function to get appropriate icon for status type +function getStatusIcon(type) { + switch (type) { + case "success": + return "fa-check-circle"; + case "error": + return "fa-exclamation-circle"; + case "warning": + return "fa-clock"; + case "info": + default: + return "fa-info-circle"; + } +} + +// PRESERVED: All other existing functions remain unchanged +function showLocalizedStatusMessage(messageKey, type = "info") { + const message = + translations[currentLanguage].statusMessages[messageKey] || + translations["en"].statusMessages[messageKey] || + "Status update"; + + showCustomStatusMessage(message, type); +} + +function clearStatusMessages() { + const statusContainer = document.getElementById("statusMessage"); + if (statusContainer) { + statusContainer.style.display = "none"; + } +} + +function handleCheckinError(error) { + console.error("❌ Check-in submission error:", error); + showLocalizedStatusMessage("networkError", "error"); +} + +function updateSubmitButton(isLoading) { + const submitButton = document.getElementById("submitCheckin"); + if (submitButton) { + if (isLoading) { + submitButton.disabled = true; + submitButton.innerHTML = + ' Processing...'; + } else { + submitButton.disabled = false; + submitButton.innerHTML = + ' Submit'; + } + applyTranslations(); + } +} + +function validateEmployeeId() { + const employeeIdInput = document.getElementById("employee_id"); + const submitButton = document.getElementById("submitCheckin"); + + if (employeeIdInput && submitButton) { + const isValid = employeeIdInput.value.trim().length >= 2; + submitButton.disabled = !isValid || isSubmitting; + + if (isValid) { + employeeIdInput.classList.remove("invalid"); + employeeIdInput.classList.add("valid"); + } else { + employeeIdInput.classList.remove("valid"); + if (employeeIdInput.value.length > 0) { + employeeIdInput.classList.add("invalid"); + } + } + } +} + +// All location and language functions remain unchanged from original +function initializeLocation() { + // Check if Android enhanced location handler is available + if ( + typeof AndroidLocationHandler !== "undefined" && + AndroidLocationHandler.isAndroidDevice() + ) { + console.log("📱 Using Android-enhanced location initialization"); + AndroidLocationHandler.initializeAndroidLocation(); + } else { + console.log("📍 Using standard location initialization"); + requestLocationData(); + } +} + +function requestLocationData() { + if (locationRequestActive) { + console.log("📍 Location request already active, skipping"); + return; + } + + if (!navigator.geolocation) { + console.log("❌ Geolocation not supported"); + userLocation.source = "manual"; + return; + } + + locationRequestActive = true; + console.log("📍 Requesting location data..."); + + const options = { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 300000, + }; + + navigator.geolocation.getCurrentPosition( + handleLocationSuccess, + (error) => { + console.log("❌ High accuracy failed, trying low accuracy..."); + + // Simple fallback with low accuracy + const lowAccuracyOptions = { + enableHighAccuracy: false, + timeout: 15000, + maximumAge: 600000, + }; + + navigator.geolocation.getCurrentPosition( + handleLocationSuccess, + handleLocationError, + lowAccuracyOptions + ); + }, + options + ); +} + +function handleLocationSuccess(position) { + console.log("✅ Location obtained successfully"); + + userLocation = { + latitude: position.coords.latitude, + longitude: position.coords.longitude, + accuracy: position.coords.accuracy, + altitude: position.coords.altitude, + timestamp: new Date(), + source: "gps", + address: null, + }; + + // Reverse geocode to get address + reverseGeocode(userLocation.latitude, userLocation.longitude); + + locationRequestActive = false; +} + +function handleLocationError(error) { + userLocation.source = "manual"; + locationRequestActive = false; +} + +function reverseGeocode(lat, lng) { + // The server will use Google Maps API first, then fall back to OpenStreetMap + // This provides better accuracy and address formatting + const url = "/api/reverse-geocode"; // You may want to create this endpoint + + // For now, using direct OpenStreetMap as fallback + // In production, this should go through your server API + const osmUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&addressdetails=1&zoom=18`; + + fetch(osmUrl, { + method: "GET", + headers: { + "User-Agent": "QR-Attendance-System/1.0", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (data && data.display_name) { + userLocation.address = data.display_name; + } else { + userLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(10)}`; + } + }) + .catch((error) => { + console.error(`❌ Reverse geocoding error:`, error); + // Fallback to coordinates if reverse geocoding fails + userLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(10)}`; + }); +} + +// ENHANCED LANGUAGE FUNCTIONALITY WITH PERSISTENCE +function initializeLanguage() { + // Load saved language preference from localStorage + const savedLanguage = loadLanguagePreference(); + if (savedLanguage && savedLanguage !== currentLanguage) { + currentLanguage = savedLanguage; + } + + // Set up language toggle button event listener + const languageToggle = document.getElementById("languageToggle"); + if (languageToggle) { + languageToggle.addEventListener("click", toggleLanguage); + } + + // Apply initial translations based on loaded language + applyTranslations(); +} + +function toggleLanguage() { + // Switch between languages + const newLanguage = currentLanguage === "en" ? "es" : "en"; + currentLanguage = newLanguage; + + // Save new language preference to localStorage + saveLanguagePreference(currentLanguage); + + // Apply translations immediately + applyTranslations(); + + // Optional: Show brief confirmation message + showLanguageChangeConfirmation(); +} + +function loadLanguagePreference() { + try { + // Retrieve language preference from localStorage + const savedLanguage = localStorage.getItem("qr_staff_language"); + + // Validate saved language is supported + if (savedLanguage && translations.hasOwnProperty(savedLanguage)) { + return savedLanguage; + } else if (savedLanguage) { + // Clean up invalid preference + localStorage.removeItem("qr_staff_language"); + } + + return null; + } catch (error) { + return null; + } +} + +function saveLanguagePreference(language) { + try { + // Validate language before saving + if (!translations.hasOwnProperty(language)) { + console.error(`❌ Invalid language code: ${language}`); + return false; + } + + // Save to localStorage + localStorage.setItem("qr_staff_language", language); + return true; + } catch (error) { + return false; + } +} + +function showLanguageChangeConfirmation() { + // Brief visual feedback for language change + const languageToggle = document.getElementById("languageToggle"); + if (languageToggle) { + // Add temporary visual feedback + languageToggle.style.transform = "scale(1.05)"; + languageToggle.style.background = "rgba(255, 255, 255, 0.4)"; + + setTimeout(() => { + languageToggle.style.transform = ""; + languageToggle.style.background = ""; + }, 200); + } +} + +function applyTranslations() { + // Update language toggle button text + const languageText = document.getElementById("languageText"); + if (languageText) { + languageText.textContent = translations[currentLanguage].languageText; + } + + // Apply translations to all elements with data attributes + document.querySelectorAll(`[data-${currentLanguage}]`).forEach((element) => { + element.textContent = element.getAttribute(`data-${currentLanguage}`); + }); + + // Update any dynamic content that might have been generated after initial load + updateDynamicTranslations(); +} + +function updateDynamicTranslations() { + // Update submit button text if it exists and has been modified + const submitButton = document.getElementById("submitCheckin"); + if (submitButton && submitButton.innerHTML.includes("data-")) { + // Re-apply translations to submit button content + const spans = submitButton.querySelectorAll(`[data-${currentLanguage}]`); + spans.forEach((span) => { + span.textContent = span.getAttribute(`data-${currentLanguage}`); + }); + } +} + +function startClock() { + function updateClock() { + currentTime = new Date(); + const timeElements = document.querySelectorAll(".current-time"); + timeElements.forEach((el) => { + el.textContent = currentTime.toLocaleTimeString(); + }); + } + + updateClock(); + setInterval(updateClock, 1000); +} + +function checkLocationServicesStatus() { + // Check if geolocation is supported + if (!navigator.geolocation) { + showLocationServicesWarning("not_supported"); + blockCheckInProcess(true); + return Promise.reject("Location services not supported"); + } + + return new Promise((resolve, reject) => { + // Test location access with a quick check + const timeoutId = setTimeout(() => { + showLocationServicesWarning("timeout"); + blockCheckInProcess(true); + reject("Location services timeout"); + }, 5000); // 5 second timeout + + navigator.geolocation.getCurrentPosition( + (position) => { + // Success - location services are working + clearTimeout(timeoutId); + hideLocationServicesWarning(); + blockCheckInProcess(false); + + // Log successful location access + console.log("✅ Location Services: ENABLED and working"); + + resolve(position); + }, + (error) => { + // Error - location services may be disabled + clearTimeout(timeoutId); + blockCheckInProcess(true); + + switch (error.code) { + case error.PERMISSION_DENIED: + showLocationServicesWarning("permission_denied"); + console.log("❌ Location Services: PERMISSION DENIED"); + break; + case error.POSITION_UNAVAILABLE: + showLocationServicesWarning("position_unavailable"); + console.log("❌ Location Services: POSITION UNAVAILABLE"); + break; + case error.TIMEOUT: + showLocationServicesWarning("timeout"); + console.log("❌ Location Services: TIMEOUT"); + break; + default: + showLocationServicesWarning("unknown_error"); + console.log("❌ Location Services: UNKNOWN ERROR"); + break; + } + + reject(error); + }, + { + enableHighAccuracy: false, + timeout: 10000, + maximumAge: 30000, + } + ); + }); +} + +function blockCheckInProcess(shouldBlock) { + // Try multiple possible submit button IDs from your codebase + const submitButton = + document.getElementById("submitCheckin") || + document.getElementById("submitButton") || + document.querySelector('button[type="submit"]') || + document.querySelector(".btn-primary"); + + const employeeIdInput = document.getElementById("employee_id"); + const form = document.getElementById("checkinForm"); + + if (shouldBlock) { + // Set global blocking flag FIRST + window.locationServicesBlocked = true; + + // Block check-in process + if (submitButton) { + submitButton.disabled = true; + submitButton.style.opacity = "0.5"; + submitButton.style.cursor = "not-allowed"; + submitButton.style.pointerEvents = "none"; + + // Add data attribute to track blocking state + submitButton.setAttribute("data-location-blocked", "true"); + + // Store original button content + if (!submitButton.getAttribute("data-original-content")) { + submitButton.setAttribute( + "data-original-content", + submitButton.innerHTML + ); + } + + // Update button text to show it's blocked + submitButton.innerHTML = ` + + Location Required / Ubicación Requerida + `; + + // Remove all event listeners by cloning + const newButton = submitButton.cloneNode(true); + submitButton.parentNode.replaceChild(newButton, submitButton); + + // Add blocking event listener + newButton.addEventListener("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + showLocationServicesBlockedMessage(); + return false; + }); + } + + if (employeeIdInput) { + employeeIdInput.disabled = true; + employeeIdInput.style.opacity = "0.7"; + employeeIdInput.setAttribute("data-location-blocked", "true"); + } + + if (form) { + form.classList.add("location-blocked"); + form.style.pointerEvents = "none"; + + // Override form submission completely + form.onsubmit = function (e) { + e.preventDefault(); + e.stopPropagation(); + showLocationServicesBlockedMessage(); + return false; + }; + } + + console.log("🚫 Check-in process BLOCKED - Location Services required"); + } else { + // Clear global blocking flag FIRST + window.locationServicesBlocked = false; + + // Unblock check-in process + if (submitButton) { + submitButton.disabled = false; + submitButton.style.opacity = "1"; + submitButton.style.cursor = "pointer"; + submitButton.style.pointerEvents = "auto"; + + // Remove blocking data attribute + submitButton.removeAttribute("data-location-blocked"); + + // Restore original button content + const originalContent = submitButton.getAttribute( + "data-original-content" + ); + if (originalContent) { + submitButton.innerHTML = originalContent; + } + + // Re-attach proper event listeners + submitButton.onclick = function (e) { + e.preventDefault(); + handleFormSubmit(e); + return false; + }; + } + + if (employeeIdInput) { + employeeIdInput.disabled = false; + employeeIdInput.style.opacity = "1"; + employeeIdInput.removeAttribute("data-location-blocked"); + } + + if (form) { + form.classList.remove("location-blocked"); + form.style.pointerEvents = "auto"; + + // Restore proper form submission handler + form.onsubmit = function (e) { + e.preventDefault(); + handleFormSubmit(e); + return false; + }; + } + + console.log("✅ Check-in process UNBLOCKED - Location Services working"); + } +} + +/** + * Show location services warning banner + */ +function showLocationServicesWarning(errorType) { + // Remove existing warning if present + hideLocationServicesWarning(); + + const warningMessages = { + en: { + not_supported: + "⚠️ Location Services Not Supported
Check-in is currently blocked.
Your browser does not support location services required for check-in.

Los servicios de ubicación no son compatibles. El registro está bloqueado.", + permission_denied: + "⚠️ Location Access Denied
Check-in is currently blocked.
Please enable location access in your browser settings to continue with check-in.

Acceso a ubicación denegado. Habilite el acceso para continuar.", + position_unavailable: + "⚠️ Location Services Disabled
Check-in is currently blocked.
Please turn on Location Services in your device settings and refresh the page.

Servicios de ubicación deshabilitados. Active los servicios y actualice la página.", + timeout: + "⚠️ Location Services Not Responding
Check-in is currently blocked.
Location services may be disabled. Please check your device settings.

Los servicios de ubicación no responden. Verifique la configuración.", + unknown_error: + "⚠️ Location Services Error
Check-in is currently blocked.
Unable to access location services. Please check your settings and try again.

Error de servicios de ubicación. Verifique la configuración.", + }, + es: { + not_supported: + "⚠️ Servicios de Ubicación No Compatibles
El registro está bloqueado.
Su navegador no es compatible con los servicios de ubicación requeridos.", + permission_denied: + "⚠️ Acceso a Ubicación Denegado
El registro está bloqueado.
Habilite el acceso a la ubicación en la configuración de su navegador.", + position_unavailable: + "⚠️ Servicios de Ubicación Deshabilitados
El registro está bloqueado.
Active los Servicios de Ubicación en la configuración y actualice la página.", + timeout: + "⚠️ Servicios de Ubicación No Responden
El registro está bloqueado.
Los servicios pueden estar deshabilitados. Verifique la configuración.", + unknown_error: + "⚠️ Error de Servicios de Ubicación
El registro está bloqueado.
No se puede acceder a los servicios. Verifique la configuración.", + }, + }; + + const currentLang = currentLanguage || "en"; + const message = + warningMessages[currentLang][errorType] || warningMessages["en"][errorType]; + + // Create warning banner + const warningBanner = document.createElement("div"); + warningBanner.id = "locationServicesWarning"; + warningBanner.className = "location-warning-banner"; + warningBanner.innerHTML = ` +
+ +
+ ${message} +
+ + +
+
+
+ `; + + // Insert warning at the top of the page + const container = document.querySelector(".destination-container"); + if (container) { + container.insertBefore(warningBanner, container.firstChild); + } +} + +/** + * Hide location services warning banner + */ +function hideLocationServicesWarning() { + const existingWarning = document.getElementById("locationServicesWarning"); + if (existingWarning) { + existingWarning.remove(); + } +} + +function initializeLocationServicesCheck() { + // Initialize global blocking flag + window.locationServicesBlocked = false; + + // Check location services status when page loads and block if necessary + setTimeout(() => { + console.log("🔍 Initializing Location Services check..."); + checkLocationServicesStatus() + .then(() => { + console.log("✅ Initial Location Services check passed"); + }) + .catch(() => { + console.log( + "❌ Initial Location Services check failed - Check-in blocked" + ); + }); + }, 1000); + + // Override form initialization to ensure our handlers are used + setTimeout(() => { + const form = document.getElementById("checkinForm"); + const submitButton = + document.getElementById("submitCheckin") || + document.querySelector('button[type="submit"]'); + + if (form) { + // Remove existing event listeners by cloning + const newForm = form.cloneNode(true); + form.parentNode.replaceChild(newForm, form); + + // Add our controlled event listener + newForm.addEventListener("submit", handleFormSubmit); + } + + if (submitButton) { + // Find the new submit button after form cloning + const newSubmitButton = + document.getElementById("submitCheckin") || + document.querySelector('button[type="submit"]'); + + if (newSubmitButton) { + newSubmitButton.addEventListener("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + handleFormSubmit(e); + return false; + }); + } + } + }, 1500); +} + +// =================================== +// CAMERA VERIFICATION FUNCTIONALITY +// =================================== + +function showVerificationModal(distance, threshold) { + console.log(`📸 Showing verification modal - Distance: ${distance}, Threshold: ${threshold}`); + + const modal = document.getElementById('verificationModal'); + const distanceSpan = document.getElementById('verificationDistance'); + const thresholdSpan = document.getElementById('verificationThreshold'); + + if (distanceSpan) distanceSpan.textContent = distance.toFixed(3); + if (thresholdSpan) thresholdSpan.textContent = threshold.toFixed(3); + + modal.classList.add('active'); + startCamera(); +} + +function hideVerificationModal() { + const modal = document.getElementById('verificationModal'); + modal.classList.remove('active'); + stopCamera(); + resetCameraInterface(); +} + +function startCamera() { + const video = document.getElementById('cameraVideo'); + + console.log('📸 Starting camera...'); + + const constraints = { + video: { + facingMode: 'environment', // Use back camera on mobile + width: { ideal: 1280 }, + height: { ideal: 720 } + }, + audio: false + }; + + navigator.mediaDevices.getUserMedia(constraints) + .then(stream => { + cameraStream = stream; + video.srcObject = stream; + video.style.display = 'block'; + console.log('✅ Camera started successfully'); + }) + .catch(error => { + console.error('❌ Camera error:', error); + alert('Unable to access camera. Please check permissions and try again. / No se puede acceder a la cámara.'); + hideVerificationModal(); + isSubmitting = false; + updateSubmitButton(false); + }); +} + +function stopCamera() { + if (cameraStream) { + cameraStream.getTracks().forEach(track => track.stop()); + cameraStream = null; + console.log('📸 Camera stopped'); + } +} + +function capturePhoto() { + const video = document.getElementById('cameraVideo'); + const canvas = document.getElementById('cameraCanvas'); + const capturedImage = document.getElementById('capturedPhoto'); + const captureBtn = document.getElementById('captureBtn'); + const retakeBtn = document.getElementById('retakeBtn'); + const submitBtn = document.getElementById('submitVerificationBtn'); + + // Set canvas dimensions to match video + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + // Draw video frame to canvas + const context = canvas.getContext('2d'); + context.drawImage(video, 0, 0, canvas.width, canvas.height); + + // Get photo data as base64 + capturedPhotoData = canvas.toDataURL('image/jpeg', 0.8); + + // Show captured photo + capturedImage.src = capturedPhotoData; + capturedImage.style.display = 'block'; + video.style.display = 'none'; + + // Update button visibility + captureBtn.style.display = 'none'; + retakeBtn.style.display = 'inline-flex'; + submitBtn.style.display = 'inline-flex'; + + console.log('📸 Photo captured'); +} + +function retakePhoto() { + const video = document.getElementById('cameraVideo'); + const capturedImage = document.getElementById('capturedPhoto'); + const captureBtn = document.getElementById('captureBtn'); + const retakeBtn = document.getElementById('retakeBtn'); + const submitBtn = document.getElementById('submitVerificationBtn'); + + // Reset interface + capturedImage.style.display = 'none'; + video.style.display = 'block'; + + captureBtn.style.display = 'inline-flex'; + retakeBtn.style.display = 'none'; + submitBtn.style.display = 'none'; + + capturedPhotoData = null; + + console.log('📸 Ready to retake photo'); +} + +function resetCameraInterface() { + const video = document.getElementById('cameraVideo'); + const capturedImage = document.getElementById('capturedPhoto'); + const captureBtn = document.getElementById('captureBtn'); + const retakeBtn = document.getElementById('retakeBtn'); + const submitBtn = document.getElementById('submitVerificationBtn'); + + if (capturedImage) capturedImage.style.display = 'none'; + if (video) video.style.display = 'block'; + + if (captureBtn) captureBtn.style.display = 'inline-flex'; + if (retakeBtn) retakeBtn.style.display = 'none'; + if (submitBtn) submitBtn.style.display = 'none'; + + capturedPhotoData = null; + verificationAttemptData = null; +} + +function submitWithVerification() { + if (!capturedPhotoData) { + alert('Please capture a photo first. / Por favor capture una foto primero.'); + return; + } + + console.log('📸 Submitting check-in with verification photo...'); + + const submitBtn = document.getElementById('submitVerificationBtn'); + submitBtn.disabled = true; + submitBtn.innerHTML = ' Submitting...'; + + // Use stored form data from initial attempt + if (verificationAttemptData) { + verificationAttemptData.append('verification_photo', capturedPhotoData); + + const currentUrl = window.location.pathname; + const checkinUrl = `${currentUrl}/checkin`; + + fetch(checkinUrl, { + method: "POST", + body: verificationAttemptData, + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + }) + .then((response) => response.json()) + .then((data) => { + hideVerificationModal(); + + if (data.success) { + showCustomStatusMessage( + "Submission successful! Photo pending review. / ¡Envío exitoso! Foto pendiente de revisión.", + "success" + ); + handleCheckinSuccess(data); + } else { + showCustomStatusMessage( + data.message || "Submission failed / Envío fallido", + "error" + ); + } + }) + .catch((error) => { + hideVerificationModal(); + console.error('❌ Verification submit error:', error); + showLocalizedStatusMessage("networkError", "error"); + }) + .finally(() => { + isSubmitting = false; + updateSubmitButton(false); + submitBtn.disabled = false; + submitBtn.innerHTML = ' Submit with Photo'; + }); + } +} + +function cancelVerification() { + hideVerificationModal(); + isSubmitting = false; + updateSubmitButton(false); +} + +// Initialize camera button event listeners +function initializeCameraButtons() { + const captureBtn = document.getElementById('captureBtn'); + const retakeBtn = document.getElementById('retakeBtn'); + const submitBtn = document.getElementById('submitVerificationBtn'); + const cancelBtn = document.getElementById('cancelVerificationBtn'); + + if (captureBtn) { + captureBtn.addEventListener('click', capturePhoto); + } + + if (retakeBtn) { + retakeBtn.addEventListener('click', retakePhoto); + } + + if (submitBtn) { + submitBtn.addEventListener('click', submitWithVerification); + } + + if (cancelBtn) { + cancelBtn.addEventListener('click', cancelVerification); + } + + console.log('📸 Camera verification buttons initialized'); +} + +// Call initialization when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeCameraButtons); +} else { + initializeCameraButtons(); } \ No newline at end of file diff --git a/templates/attendance_report.html b/templates/attendance_report.html index f943780..6a28017 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -5,6 +5,16 @@ {% block extra_head %} + @@ -221,7 +231,9 @@ {% for record in attendance_records %} - + {{ loop.index }}
@@ -237,7 +249,7 @@
- {{ record.location_name }} + {{ record.location_name if record.location_name != 'Dynamic' else '(No location recorded)' }}
@@ -1082,4 +1094,5 @@ document.addEventListener('DOMContentLoaded', function() { }); }()); + {% endblock %} \ No newline at end of file diff --git a/templates/create_qr_code.html b/templates/create_qr_code.html index ac705c1..fbeff47 100644 --- a/templates/create_qr_code.html +++ b/templates/create_qr_code.html @@ -1457,4 +1457,4 @@ - \ No newline at end of file + diff --git a/templates/qr_destination.html b/templates/qr_destination.html index c44831e..e9c4a62 100644 --- a/templates/qr_destination.html +++ b/templates/qr_destination.html @@ -1333,6 +1333,11 @@
+ +