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 = `
+