From e810488e68208254ec3e631cfb87e88cceba2435 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Fri, 1 Aug 2025 11:01:52 -0400 Subject: [PATCH] Update destination functionality to get user's location --- app.py | 184 ++++++- database_migration.py | 148 ++++++ static/js/qr_destination.js | 970 ++++++++++++++++++++-------------- templates/qr_destination.html | 365 ++++++++++++- 4 files changed, 1261 insertions(+), 406 deletions(-) create mode 100644 database_migration.py diff --git a/app.py b/app.py index 7788e5a..788c363 100644 --- a/app.py +++ b/app.py @@ -1105,7 +1105,7 @@ def qr_destination(qr_url): @app.route('/qr//checkin', methods=['POST']) def qr_checkin(qr_url): - """Handle staff check-in submission""" + """Handle staff check-in submission with geolocation support""" try: # Find QR code by URL qr_code = QRCode.query.filter_by(qr_url=qr_url, active_status=True).first() @@ -1119,13 +1119,21 @@ def qr_checkin(qr_url): # Get form data employee_id = request.form.get('employee_id', '').strip() + # NEW: Get location data from form + latitude = request.form.get('latitude', '').strip() + longitude = request.form.get('longitude', '').strip() + accuracy = request.form.get('accuracy', '').strip() + altitude = request.form.get('altitude', '').strip() + location_source = request.form.get('location_source', 'manual').strip() + address = request.form.get('address', '').strip() + if not employee_id: return jsonify({ 'success': False, 'message': 'Employee ID is required.' }), 400 - # Validate employee ID format (adjust regex as needed) + # Validate employee ID format if not re.match(r'^[A-Za-z0-9]{3,20}$', employee_id): return jsonify({ 'success': False, @@ -1146,10 +1154,34 @@ def qr_checkin(qr_url): 'message': f'You have already checked in today at {existing_checkin.check_in_time.strftime("%H:%M")}.' }), 409 - # Get device and location info - user_agent_string = request.headers.get('User-Agent', '') - device_info = detect_device_info(user_agent_string) - ip_address = get_client_ip() + # Parse user agent + user_agent = request.headers.get('User-Agent', '') + parsed_agent = parse(user_agent) + device_info = f"{parsed_agent.browser.family} on {parsed_agent.os.family}" + + # Get client IP + client_ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr) + if client_ip and ',' in client_ip: + client_ip = client_ip.split(',')[0].strip() + + # NEW: Process location data safely + lat_value = None + lng_value = None + acc_value = None + alt_value = None + + try: + if latitude and latitude != 'null' and latitude != '': + lat_value = float(latitude) + if longitude and longitude != 'null' and longitude != '': + lng_value = float(longitude) + if accuracy and accuracy != 'null' and accuracy != '': + acc_value = float(accuracy) + if altitude and altitude != 'null' and altitude != '': + alt_value = float(altitude) + except (ValueError, TypeError) as e: + print(f"⚠️ Invalid location data: {e}") + # Continue without location data # Create attendance record attendance = AttendanceData( @@ -1158,35 +1190,68 @@ def qr_checkin(qr_url): check_in_date=today, check_in_time=datetime.now().time(), device_info=device_info, - user_agent=user_agent_string, - ip_address=ip_address, + user_agent=user_agent, + ip_address=client_ip, location_name=qr_code.location, status='present' ) + # NEW: Add location data if available (safe attribute setting) + try: + if lat_value is not None: + attendance.latitude = lat_value + if lng_value is not None: + attendance.longitude = lng_value + if acc_value is not None: + attendance.accuracy = acc_value + if alt_value is not None: + attendance.altitude = alt_value + if location_source: + attendance.location_source = location_source + if address: + attendance.address = address + except AttributeError as e: + print(f"⚠️ Location columns not available: {e}") + # Continue without location data + db.session.add(attendance) db.session.commit() - print(f"Check-in recorded: {employee_id} at {qr_code.name}") - - return jsonify({ + # Prepare response + response_data = { 'success': True, - 'message': f'Check-in successful! Welcome to {qr_code.location_event}.', - 'data': { - 'employee_id': employee_id.upper(), - 'location': qr_code.location, - 'event': qr_code.location_event, - 'time': datetime.now().strftime('%H:%M'), - 'date': today.strftime('%B %d, %Y') - } - }) + 'message': 'Check-in successful!', + 'employee_id': employee_id.upper(), + 'location': qr_code.location, + 'event': qr_code.location_event, + 'time': datetime.now().strftime('%H:%M'), + 'date': today.strftime('%Y-%m-%d'), + 'has_location': bool(lat_value and lng_value) + } + + # NEW: Add location info to response + if lat_value and lng_value: + response_data.update({ + 'location_accuracy': acc_value, + 'location_address': address + }) + + print(f"✅ Check-in successful: {employee_id.upper()} at {qr_code.location}") + if lat_value and lng_value: + print(f"📍 Location: {lat_value:.6f}, {lng_value:.6f} (±{acc_value}m) - {location_source}") + if address: + print(f"🏠 Address: {address}") + + return jsonify(response_data) except Exception as e: db.session.rollback() - print(f"Error during check-in: {e}") + print(f"❌ Check-in error: {e}") + import traceback + traceback.print_exc() return jsonify({ 'success': False, - 'message': 'An error occurred during check-in. Please try again.' + 'message': 'System error occurred. Please try again.' }), 500 @app.route('/qr-codes//toggle-status', methods=['POST']) @@ -1355,6 +1420,81 @@ def attendance_report(): print(f"Error loading attendance report: {e}") flash('Error loading attendance report.', 'error') return redirect(url_for('dashboard')) + +# 3. ADD NEW ROUTE FOR LOCATION STATISTICS (Optional) +@app.route('/admin/location-stats') +@admin_required +def location_stats(): + """View location statistics for admin""" + try: + # Get basic attendance stats + total_checkins = AttendanceData.query.count() + + # Try to get location data (will work only if columns exist) + location_checkins = 0 + recent_locations = [] + location_accuracy_stats = { + 'high': 0, + 'medium': 0, + 'low': 0, + 'unknown': 0 + } + + try: + # Count check-ins with location data + location_checkins = db.session.execute(text(""" + SELECT COUNT(*) FROM attendance_data + WHERE latitude IS NOT NULL AND longitude IS NOT NULL + """)).fetchone()[0] + + # Get recent locations + recent_locations_result = db.session.execute(text(""" + SELECT employee_id, latitude, longitude, accuracy, address, + check_in_date, check_in_time, location_name, location_source + FROM attendance_data + WHERE latitude IS NOT NULL + ORDER BY created_timestamp DESC + LIMIT 20 + """)).fetchall() + + recent_locations = [] + for row in recent_locations_result: + recent_locations.append({ + 'employee_id': row[0], + 'latitude': row[1], + 'longitude': row[2], + 'accuracy': row[3], + 'address': row[4], + 'check_in_date': row[5].strftime('%Y-%m-%d') if row[5] else '', + 'check_in_time': row[6].strftime('%H:%M') if row[6] else '', + 'location_name': row[7], + 'location_source': row[8] + }) + + # Count accuracy stats + accuracy = row[3] + if accuracy is None: + location_accuracy_stats['unknown'] += 1 + elif accuracy <= 50: + location_accuracy_stats['high'] += 1 + elif accuracy <= 100: + location_accuracy_stats['medium'] += 1 + else: + location_accuracy_stats['low'] += 1 + + except Exception as e: + print(f"⚠️ Location stats query failed: {e}") + + return render_template('location_stats.html', + total_checkins=total_checkins, + location_checkins=location_checkins, + recent_locations=recent_locations, + accuracy_stats=location_accuracy_stats) + + except Exception as e: + print(f"Error loading location stats: {e}") + flash('Error loading location statistics.', 'error') + return redirect(url_for('dashboard')) @app.route('/api/attendance/stats') @admin_required diff --git a/database_migration.py b/database_migration.py new file mode 100644 index 0000000..8e4b1fa --- /dev/null +++ b/database_migration.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +""" +Simple Database Migration for Geolocation +Run this script FIRST before updating your app.py +""" + +import psycopg2 +import os +import sys + +# Database connection (adjust if needed) +DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://postgres:postgres1411@localhost/qr_management') + +def add_location_columns(): + """Add location columns to attendance_data table""" + + print("🚀 Adding location tracking columns to your database...") + + try: + # Connect to database + conn = psycopg2.connect(DATABASE_URL) + cursor = conn.cursor() + + print("✅ Connected to database") + + # Check if attendance_data table exists + cursor.execute(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'attendance_data' + ); + """) + + if not cursor.fetchone()[0]: + print("❌ attendance_data table not found!") + print(" Make sure your QR system is set up first") + return False + + print("✅ attendance_data table found") + + # Add location columns (using IF NOT EXISTS for safety) + location_columns = [ + "ALTER TABLE attendance_data ADD COLUMN IF NOT EXISTS latitude FLOAT", + "ALTER TABLE attendance_data ADD COLUMN IF NOT EXISTS longitude FLOAT", + "ALTER TABLE attendance_data ADD COLUMN IF NOT EXISTS accuracy FLOAT", + "ALTER TABLE attendance_data ADD COLUMN IF NOT EXISTS altitude FLOAT", + "ALTER TABLE attendance_data ADD COLUMN IF NOT EXISTS location_source VARCHAR(50) DEFAULT 'manual'", + "ALTER TABLE attendance_data ADD COLUMN IF NOT EXISTS address VARCHAR(255)" + ] + + print("\n📝 Adding columns...") + for sql in location_columns: + try: + cursor.execute(sql) + column_name = sql.split()[4] # Extract column name + print(f" ✅ Added: {column_name}") + except Exception as e: + print(f" ⚠️ Column may already exist: {e}") + + # Commit changes + conn.commit() + + # Verify columns were added + cursor.execute(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'attendance_data' + AND column_name IN ('latitude', 'longitude', 'accuracy', + 'altitude', 'location_source', 'address') + ORDER BY column_name; + """) + + added_columns = [row[0] for row in cursor.fetchall()] + + print(f"\n📊 Verification:") + print(f" ✅ Location columns found: {len(added_columns)}") + if added_columns: + print(f" 📝 Columns: {', '.join(added_columns)}") + + # Check existing data + cursor.execute("SELECT COUNT(*) FROM attendance_data") + total_records = cursor.fetchone()[0] + print(f" 📊 Total attendance records: {total_records}") + + cursor.close() + conn.close() + + print("\n🎉 Database migration completed successfully!") + + return True + + except psycopg2.Error as e: + print(f"❌ Database error: {e}") + return False + except Exception as e: + print(f"❌ Unexpected error: {e}") + return False + +def test_connection(): + """Test database connection""" + try: + conn = psycopg2.connect(DATABASE_URL) + cursor = conn.cursor() + cursor.execute("SELECT version();") + version = cursor.fetchone()[0] + print(f"✅ Database connection successful") + cursor.close() + conn.close() + return True + except Exception as e: + print(f"❌ Database connection failed: {e}") + print(f" Check your DATABASE_URL: {DATABASE_URL}") + return False + +if __name__ == "__main__": + print("📍 QR Code System - Geolocation Migration") + print("=" * 50) + + # Test connection first + if not test_connection(): + print("\n❌ Cannot connect to database. Please check:") + print("1. PostgreSQL is running") + print("2. Database credentials are correct") + print("3. Database exists") + sys.exit(1) + + # Run migration + success = add_location_columns() + + if success: + print("\n✅ Ready for geolocation integration!") + print("\nNext steps:") + print("1. Replace your qr_destination.html template") + print("2. Replace your qr_destination.js file") + print("3. Update your qr_checkin route in app.py") + print("4. Restart your Flask application") + print("5. Test geolocation on a mobile device") + else: + print("\n❌ Migration failed. Please check the errors above.") + print("\nYou can also add the columns manually:") + print("ALTER TABLE attendance_data ADD COLUMN latitude FLOAT;") + print("ALTER TABLE attendance_data ADD COLUMN longitude FLOAT;") + print("ALTER TABLE attendance_data ADD COLUMN accuracy FLOAT;") + print("ALTER TABLE attendance_data ADD COLUMN altitude FLOAT;") + print("ALTER TABLE attendance_data ADD COLUMN location_source VARCHAR(50) DEFAULT 'manual';") + print("ALTER TABLE attendance_data ADD COLUMN address VARCHAR(255);") + + print("\n" + "=" * 50) \ No newline at end of file diff --git a/static/js/qr_destination.js b/static/js/qr_destination.js index ecd9696..62e94af 100644 --- a/static/js/qr_destination.js +++ b/static/js/qr_destination.js @@ -1,5 +1,5 @@ /** - * QR Code Destination Page JavaScript + * QR Code Destination Page JavaScript - Complete with Geolocation * Handles staff check-in functionality and form interactions */ @@ -7,13 +7,29 @@ let isSubmitting = false; let currentTime = new Date(); +// NEW: Geolocation variables +let userLocation = { + latitude: null, + longitude: null, + accuracy: null, + altitude: null, + timestamp: null, + source: 'manual', + address: null +}; +let locationRequestActive = false; +let locationWatchId = null; + // Initialize page when DOM is loaded document.addEventListener('DOMContentLoaded', function() { - console.log('QR Destination page initialized'); + console.log('QR Destination page initialized with geolocation'); initializePage(); setupEventListeners(); startTimeUpdater(); + + // NEW: Initialize geolocation + initializeGeolocation(); }); function initializePage() { @@ -54,6 +70,381 @@ function setupEventListeners() { document.addEventListener('visibilitychange', handleVisibilityChange); } +// NEW: Initialize geolocation functionality +function initializeGeolocation() { + console.log('📍 Initializing geolocation...'); + + if (!navigator.geolocation) { + console.warn('❌ Geolocation not supported'); + showLocationStatus('error', 'Location not supported by this browser'); + return; + } + + console.log('✅ Geolocation supported'); + + // Check permissions first + checkLocationPermission(); + + // Request location + requestUserLocation(); +} + +// NEW: Check location permissions +function checkLocationPermission() { + if (navigator.permissions) { + navigator.permissions.query({name: 'geolocation'}).then(function(result) { + console.log('🔐 Location permission status:', result.state); + + if (result.state === 'granted') { + console.log('✅ Location permission granted'); + } else if (result.state === 'prompt') { + console.log('❓ Location permission will be requested'); + } else if (result.state === 'denied') { + console.log('❌ Location permission denied'); + showLocationStatus('error', 'Location permission denied - enable in browser settings'); + } + }).catch(function(error) { + console.log('⚠️ Permission query failed:', error); + }); + } +} + +// NEW: Request user's current location +function requestUserLocation() { + if (locationRequestActive) { + console.log('⏭️ Location request already active'); + return; + } + + locationRequestActive = true; + showLocationStatus('loading', 'Getting your location...'); + + const options = { + enableHighAccuracy: true, // Use GPS for better accuracy + timeout: 15000, // Wait up to 15 seconds + maximumAge: 300000 // Accept cached location up to 5 minutes old + }; + + console.log('📡 Requesting location with options:', options); + + navigator.geolocation.getCurrentPosition( + handleLocationSuccess, + handleLocationError, + options + ); + + // Set backup timeout + setTimeout(() => { + if (locationRequestActive && !userLocation.latitude) { + console.log('⏰ Location request backup timeout'); + handleLocationError({ code: 3, message: 'Request timed out' }); + } + }, 16000); +} + +// NEW: Handle successful location retrieval +function handleLocationSuccess(position) { + locationRequestActive = false; + + const coords = position.coords; + console.log('✅ Location obtained:', coords); + + // Store location data + userLocation = { + latitude: coords.latitude, + longitude: coords.longitude, + accuracy: coords.accuracy, + altitude: coords.altitude, + timestamp: position.timestamp, + source: 'gps', + address: null + }; + + // Update form fields + updateLocationFormFields(); + + // Update display + updateLocationDisplay(); + + // Show success status + const accuracyText = coords.accuracy ? Math.round(coords.accuracy) : 'unknown'; + const accuracyLevel = getAccuracyLevel(coords.accuracy); + + showLocationStatus('success', + `Location captured (±${accuracyText}m - ${accuracyLevel} accuracy)` + ); + + console.log('📍 Location stored:', userLocation); + + // Try to get address from coordinates + reverseGeocodeLocation(coords.latitude, coords.longitude); + + // Start watching for better accuracy (optional) + startLocationWatching(); +} + +// NEW: Handle location errors +function handleLocationError(error) { + locationRequestActive = false; + + console.error('❌ Location error:', error); + + let message = 'Unable to get location'; + let details = 'Check-in will work without location'; + + switch(error.code) { + case error.PERMISSION_DENIED: + message = 'Location access denied'; + details = 'Enable location permission in browser settings'; + break; + case error.POSITION_UNAVAILABLE: + message = 'Location unavailable'; + details = 'GPS signal is weak or unavailable'; + break; + case error.TIMEOUT: + message = 'Location request timed out'; + details = 'Try refreshing or check connection'; + break; + default: + message = 'Location error occurred'; + details = 'Please try again'; + break; + } + + showLocationStatus('error', `${message} - ${details}`); + + // Set location source to manual + userLocation.source = 'manual'; + updateLocationFormFields(); +} + +// NEW: Start watching location for continuous updates +function startLocationWatching() { + if (locationWatchId !== null) { + console.log('👁️ Already watching location'); + return; + } + + console.log('👁️ Starting location watching for better accuracy...'); + + const options = { + enableHighAccuracy: true, + timeout: 30000, + maximumAge: 60000 + }; + + locationWatchId = navigator.geolocation.watchPosition( + function(position) { + // Only update if accuracy is better + if (!userLocation.accuracy || position.coords.accuracy < userLocation.accuracy) { + console.log('📍 Location updated with better accuracy:', position.coords.accuracy); + handleLocationSuccess(position); + } + }, + function(error) { + console.log('⚠️ Location watch error:', error); + }, + options + ); +} + +// NEW: Stop watching location +function stopLocationWatching() { + if (locationWatchId !== null) { + navigator.geolocation.clearWatch(locationWatchId); + locationWatchId = null; + console.log('⏹️ Stopped watching location'); + } +} + +// NEW: Update form fields with location data +function updateLocationFormFields() { + const fields = { + 'latitude': userLocation.latitude || '', + 'longitude': userLocation.longitude || '', + 'accuracy': userLocation.accuracy || '', + 'altitude': userLocation.altitude || '', + 'locationSource': userLocation.source || 'manual', + 'address': userLocation.address || '' + }; + + Object.keys(fields).forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field) { + field.value = fields[fieldId]; + } + }); + + console.log('📝 Updated form fields with location data'); +} + +// NEW: Update location display +function updateLocationDisplay() { + if (userLocation.latitude && userLocation.longitude) { + const elements = { + 'displayLatitude': userLocation.latitude.toFixed(6), + 'displayLongitude': userLocation.longitude.toFixed(6), + 'displayAccuracy': userLocation.accuracy ? `±${Math.round(userLocation.accuracy)}m` : 'Unknown', + 'displayAddress': userLocation.address || 'Loading...' + }; + + Object.keys(elements).forEach(elementId => { + const element = document.getElementById(elementId); + if (element) { + element.textContent = elements[elementId]; + } + }); + } +} + +// NEW: Reverse geocode coordinates to get address +function reverseGeocodeLocation(lat, lng) { + console.log('🏠 Getting address from coordinates...'); + + // Try multiple geocoding services for better reliability + const services = [ + { + name: 'BigDataCloud', + url: `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lng}&localityLanguage=en`, + parser: (data) => data.locality || data.city || data.neighbourhood || '' + }, + { + name: 'OpenStreetMap', + url: `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`, + parser: (data) => data.display_name ? data.display_name.split(',')[0] : '' + } + ]; + + tryGeocodingService(0, services); +} + +function tryGeocodingService(index, services) { + if (index >= services.length) { + console.log('⚠️ All geocoding services failed'); + const displayAddress = document.getElementById('displayAddress'); + if (displayAddress) { + displayAddress.textContent = 'Address not available'; + } + return; + } + + const service = services[index]; + + fetch(service.url) + .then(response => response.json()) + .then(data => { + const address = service.parser(data); + + if (address) { + userLocation.address = address; + document.getElementById('address').value = address; + + const displayAddress = document.getElementById('displayAddress'); + if (displayAddress) { + displayAddress.textContent = address; + } + + console.log(`🏠 Address found using ${service.name}:`, address); + return; + } + + // Try next service + tryGeocodingService(index + 1, services); + }) + .catch(error => { + console.log(`⚠️ ${service.name} geocoding failed:`, error); + // Try next service + tryGeocodingService(index + 1, services); + }); +} + +// NEW: Show location status to user +function showLocationStatus(type, message) { + const statusElement = document.getElementById('locationStatus'); + const messageElement = document.getElementById('locationMessage'); + + if (!statusElement || !messageElement) { + console.log('📍 Location status elements not found'); + return; + } + + statusElement.className = `location-status ${type}`; + statusElement.style.display = 'flex'; + + let icon = '📡'; + if (type === 'success') icon = '✅'; + if (type === 'error') icon = '⚠️'; + + messageElement.innerHTML = `${icon} ${message}`; + + // Auto-hide after 8 seconds unless it's loading + if (type !== 'loading') { + setTimeout(() => { + statusElement.style.display = 'none'; + }, 8000); + } +} + +// NEW: Get accuracy level description +function getAccuracyLevel(accuracy) { + if (!accuracy) return 'unknown'; + if (accuracy <= 50) return 'high'; + if (accuracy <= 100) return 'medium'; + return 'low'; +} + +// NEW: Toggle location info display +function toggleLocationInfo() { + const locationInfo = document.getElementById('locationInfo'); + if (!locationInfo) return; + + if (locationInfo.style.display === 'none' || !locationInfo.style.display) { + updateLocationDisplay(); + locationInfo.style.display = 'block'; + } else { + locationInfo.style.display = 'none'; + } +} + +// NEW: Retry location request +function retryLocationRequest() { + console.log('🔄 Retrying location request...'); + + // Stop any existing watch + stopLocationWatching(); + + // Reset location data + userLocation = { + latitude: null, + longitude: null, + accuracy: null, + altitude: null, + timestamp: null, + source: 'manual', + address: null + }; + + // Clear form fields + updateLocationFormFields(); + + // Request location again + requestUserLocation(); +} + +// NEW: Get current location data for external use +function getCurrentLocationData() { + return { + hasLocation: !!(userLocation.latitude && userLocation.longitude), + latitude: userLocation.latitude, + longitude: userLocation.longitude, + accuracy: userLocation.accuracy, + altitude: userLocation.altitude, + source: userLocation.source, + timestamp: userLocation.timestamp, + address: userLocation.address + }; +} + function handleFormSubmit(e) { e.preventDefault(); @@ -67,6 +458,9 @@ function handleFormSubmit(e) { return false; } + // NEW: Ensure location data is up to date before submission + updateLocationFormFields(); + submitCheckin(employeeId); } @@ -118,7 +512,7 @@ function validateEmployeeId() { } if (employeeId.length > 20) { - showValidationError(employeeInput, 'Employee ID must be less than 20 characters'); + showValidationError(employeeInput, 'Employee ID must be 20 characters or less'); return false; } @@ -127,46 +521,66 @@ function validateEmployeeId() { return false; } - // Clear validation error - employeeInput.classList.remove('error'); - employeeInput.classList.add('success'); - hideStatusMessage(); - + clearValidationError(employeeInput); return true; } function isValidEmployeeId(id) { - return /^[A-Za-z0-9]{3,20}$/.test(id); + return /^[A-Za-z0-9]+$/.test(id); } function showValidationError(input, message) { input.classList.add('error'); - input.classList.remove('success'); showStatusMessage(message, 'error'); shakeInput(input); input.focus(); } +function clearValidationError(input) { + input.classList.remove('error'); + input.classList.add('success'); +} + function shakeInput(input) { - input.classList.add('shake'); + input.style.animation = 'shake 0.5s'; setTimeout(() => { - input.classList.remove('shake'); + input.style.animation = ''; }, 500); } function submitCheckin(employeeId) { - if (isSubmitting) return; + console.log('🚀 Submitting check-in for:', employeeId); + + // NEW: Log location data being submitted + const locationData = getCurrentLocationData(); + console.log('📍 Location data:', locationData); isSubmitting = true; - showLoadingState(); - showLoadingOverlay(); + updateSubmitButton(true); + showStatusMessage('Processing check-in...', 'info'); + + // NEW: Ensure all location data is in the form + updateLocationFormFields(); // Prepare form data const formData = new FormData(); formData.append('employee_id', employeeId); - // Submit to server - fetch(`/qr/${window.qrUrl}/checkin`, { + // NEW: Add location data to form submission + formData.append('latitude', userLocation.latitude || ''); + formData.append('longitude', userLocation.longitude || ''); + formData.append('accuracy', userLocation.accuracy || ''); + formData.append('altitude', userLocation.altitude || ''); + formData.append('location_source', userLocation.source || 'manual'); + formData.append('address', userLocation.address || ''); + + // Get the current URL for the check-in endpoint + const currentUrl = window.location.pathname; + const checkinUrl = `${currentUrl}/checkin`; + + console.log('📤 Submitting to:', checkinUrl); + + fetch(checkinUrl, { method: 'POST', body: formData, headers: { @@ -175,235 +589,139 @@ function submitCheckin(employeeId) { }) .then(response => response.json()) .then(data => { - handleCheckinResponse(data); + isSubmitting = false; + updateSubmitButton(false); + + if (data.success) { + showSuccessPage(data); + console.log('✅ Check-in successful:', data); + + // Stop location watching after successful check-in + stopLocationWatching(); + } else { + showStatusMessage(data.message || 'Check-in failed', 'error'); + console.log('❌ Check-in failed:', data.message); + } }) .catch(error => { - console.error('Check-in error:', error); - handleCheckinError('Network error. Please check your connection and try again.'); - }) - .finally(() => { isSubmitting = false; - hideLoadingState(); - hideLoadingOverlay(); + updateSubmitButton(false); + showStatusMessage('Network error. Please check your connection and try again.', 'error'); + console.error('❌ Check-in error:', error); }); } -function handleCheckinResponse(data) { - if (data.success) { - showSuccessCard(data.data); - logSuccessfulCheckin(data.data); - - // Optional: Analytics tracking - if (typeof gtag !== 'undefined') { - gtag('event', 'checkin_success', { - 'location': window.locationName, - 'event_name': window.eventName - }); - } - } else { - handleCheckinError(data.message); - } -} - -function handleCheckinError(message) { - showStatusMessage(message, 'error'); - - // Shake the form to draw attention - const form = document.getElementById('checkinForm'); - if (form) { - form.classList.add('shake'); - setTimeout(() => { - form.classList.remove('shake'); - }, 500); - } - - // Re-focus on input - const employeeInput = document.getElementById('employee_id'); - if (employeeInput) { - employeeInput.focus(); - employeeInput.select(); - } -} - -function showSuccessCard(data) { - // Hide the check-in form +function showSuccessPage(data) { + // Hide the check-in form and location status const checkinCard = document.querySelector('.checkin-card'); - if (checkinCard) { - checkinCard.style.display = 'none'; - } + const locationStatus = document.getElementById('locationStatus'); + const locationInfo = document.getElementById('locationInfo'); + const locationControls = document.querySelector('.location-controls'); - // Populate and show success card + if (checkinCard) checkinCard.style.display = 'none'; + if (locationStatus) locationStatus.style.display = 'none'; + if (locationInfo) locationInfo.style.display = 'none'; + if (locationControls) locationControls.style.display = 'none'; + + // Show success card const successCard = document.getElementById('successCard'); if (successCard) { - document.getElementById('successEmployeeId').textContent = data.employee_id || '-'; - document.getElementById('successLocation').textContent = data.location || '-'; - document.getElementById('successEvent').textContent = data.event || '-'; - document.getElementById('successTime').textContent = data.time || '-'; - document.getElementById('successDate').textContent = data.date || '-'; - successCard.style.display = 'block'; - successCard.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - - // Optional: Auto-hide success card after some time - setTimeout(() => { - showAutoHideOption(); - }, 10000); // 10 seconds -} - -function showAutoHideOption() { - const successCard = document.getElementById('successCard'); - if (successCard && successCard.style.display !== 'none') { - const actions = successCard.querySelector('.success-actions'); - if (actions && !actions.querySelector('.auto-hide-btn')) { - const autoHideBtn = document.createElement('button'); - autoHideBtn.className = 'btn btn-outline auto-hide-btn'; - autoHideBtn.innerHTML = ' Auto-hide in 30s'; - actions.appendChild(autoHideBtn); + + // Populate success details + const elements = { + 'successEmployeeId': data.employee_id || '-', + 'successLocation': data.location || window.locationName || '-', + 'successEvent': data.event || window.eventName || '-', + 'successTime': data.time || new Date().toLocaleTimeString(), + 'successDate': data.date || new Date().toLocaleDateString() + }; + + Object.keys(elements).forEach(elementId => { + const element = document.getElementById(elementId); + if (element) { + element.textContent = elements[elementId]; + } + }); + + // NEW: Show location info in success card if available + const successLocationInfo = document.getElementById('successLocationInfo'); + const successGpsInfo = document.getElementById('successGpsInfo'); + + if (data.has_location && userLocation.latitude && userLocation.longitude) { + let locationText = `Captured (±${Math.round(userLocation.accuracy || 0)}m)`; + if (userLocation.address) { + locationText += ` - ${userLocation.address}`; + } - startCountdown(30, () => { - checkInAnother(); - }); - } - } -} - -function startCountdown(seconds, callback) { - const countdownElement = document.getElementById('countdown'); - let remaining = seconds; - - const interval = setInterval(() => { - remaining--; - if (countdownElement) { - countdownElement.textContent = remaining; + if (successGpsInfo) successGpsInfo.textContent = locationText; + if (successLocationInfo) successLocationInfo.style.display = 'block'; } - if (remaining <= 0) { - clearInterval(interval); - callback(); - } - }, 1000); + // Scroll to success card + successCard.scrollIntoView({ behavior: 'smooth' }); + } + + // Auto-refresh page after 30 seconds + setTimeout(() => { + console.log('🔄 Auto-refreshing page...'); + window.location.reload(); + }, 30000); } -function checkInAnother() { - // Show the check-in form again - const checkinCard = document.querySelector('.checkin-card'); - const successCard = document.getElementById('successCard'); +function showStatusMessage(message, type) { + const statusElement = document.getElementById('statusMessage'); + if (!statusElement) return; - if (checkinCard) { - checkinCard.style.display = 'block'; - } + statusElement.className = `status-message ${type}`; + statusElement.innerHTML = ` +
+ + ${message} +
+ `; + statusElement.style.display = 'block'; - if (successCard) { - successCard.style.display = 'none'; - } - - // Reset form - const form = document.getElementById('checkinForm'); - if (form) { - form.reset(); - } - - // Clear validation states - const employeeInput = document.getElementById('employee_id'); - if (employeeInput) { - employeeInput.classList.remove('error', 'success'); - employeeInput.focus(); - } - - hideStatusMessage(); - - // Scroll back to form - checkinCard.scrollIntoView({ behavior: 'smooth', block: 'center' }); -} - -function showLoadingState() { - const btn = document.querySelector('.btn-primary'); - if (btn) { - const content = btn.querySelector('.btn-content'); - const loader = btn.querySelector('.btn-loader'); - - if (content) content.style.display = 'none'; - if (loader) loader.style.display = 'flex'; - - btn.disabled = true; - } -} - -function hideLoadingState() { - const btn = document.querySelector('.btn-primary'); - if (btn) { - const content = btn.querySelector('.btn-content'); - const loader = btn.querySelector('.btn-loader'); - - if (content) content.style.display = 'flex'; - if (loader) loader.style.display = 'none'; - - btn.disabled = false; - } -} - -function showLoadingOverlay() { - const overlay = document.getElementById('loadingOverlay'); - if (overlay) { - overlay.style.display = 'flex'; + // Auto-hide info messages + if (type === 'info') { setTimeout(() => { - overlay.classList.add('show'); - }, 10); - } -} - -function hideLoadingOverlay() { - const overlay = document.getElementById('loadingOverlay'); - if (overlay) { - overlay.classList.remove('show'); - setTimeout(() => { - overlay.style.display = 'none'; - }, 200); - } -} - -function showStatusMessage(message, type = 'info') { - const statusDiv = document.getElementById('statusMessage'); - if (statusDiv) { - statusDiv.textContent = message; - statusDiv.className = `status-message ${type}`; - statusDiv.style.display = 'block'; - - // Auto-hide success messages - if (type === 'success') { - setTimeout(() => { - hideStatusMessage(); - }, 5000); - } - - // Scroll to message - statusDiv.scrollIntoView({ behavior: 'smooth', block: 'center' }); + statusElement.style.display = 'none'; + }, 3000); } } function hideStatusMessage() { - const statusDiv = document.getElementById('statusMessage'); - if (statusDiv) { - statusDiv.style.display = 'none'; + const statusElement = document.getElementById('statusMessage'); + if (statusElement) { + statusElement.style.display = 'none'; } } -function updateCurrentTime() { - const timeElement = document.getElementById('currentTime'); - if (timeElement) { - const now = new Date(); - const options = { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }; - - timeElement.textContent = now.toLocaleDateString('en-US', options); +function getStatusIcon(type) { + switch(type) { + case 'success': return 'fa-check-circle'; + case 'error': return 'fa-exclamation-triangle'; + case 'info': return 'fa-info-circle'; + case 'warning': return 'fa-exclamation-circle'; + default: return 'fa-info-circle'; + } +} + +function updateSubmitButton(isLoading) { + const submitBtn = document.getElementById('submitBtn') || document.querySelector('button[type="submit"]'); + if (!submitBtn) return; + + const btnContent = submitBtn.querySelector('.btn-content'); + const btnLoader = submitBtn.querySelector('.btn-loader'); + + if (isLoading) { + submitBtn.disabled = true; + if (btnContent) btnContent.style.display = 'none'; + if (btnLoader) btnLoader.style.display = 'flex'; + } else { + submitBtn.disabled = false; + if (btnContent) btnContent.style.display = 'flex'; + if (btnLoader) btnLoader.style.display = 'none'; } } @@ -412,179 +730,65 @@ function startTimeUpdater() { setInterval(updateCurrentTime, 1000); } +function updateCurrentTime() { + const now = new Date(); + const timeString = now.toLocaleString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + const timeElement = document.getElementById('currentTime'); + if (timeElement) { + timeElement.textContent = timeString; + } + + currentTime = now; +} + function handleVisibilityChange() { - if (document.hidden) { - // Page is hidden - pause operations - console.log('Page hidden - pausing operations'); - } else { - // Page is visible - resume operations - console.log('Page visible - resuming operations'); + if (!document.hidden) { + // Page became visible, update time immediately updateCurrentTime(); - // Re-focus on input if form is visible - const checkinCard = document.querySelector('.checkin-card'); - const employeeInput = document.getElementById('employee_id'); - - if (checkinCard && checkinCard.style.display !== 'none' && employeeInput) { - setTimeout(() => { - employeeInput.focus(); - }, 100); + // NEW: Request location again if we don't have it and haven't submitted yet + if (!userLocation.latitude && !isSubmitting) { + console.log('🔄 Page visible again, retrying location...'); + setTimeout(requestUserLocation, 1000); } } } -function logSuccessfulCheckin(data) { - console.log('Successful check-in:', { - employee_id: data.employee_id, - location: data.location, - event: data.event, - time: data.time, - date: data.date - }); -} - -// Utility functions -function debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -} - -function throttle(func, limit) { - let inThrottle; - return function() { - const args = arguments; - const context = this; - if (!inThrottle) { - func.apply(context, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - } -} - -// Export functions for global access -window.checkInAnother = checkInAnother; -window.validateEmployeeId = validateEmployeeId; - -// Service Worker registration for offline support (optional) -if ('serviceWorker' in navigator) { - window.addEventListener('load', function() { - navigator.serviceWorker.register('/sw.js') - .then(function(registration) { - console.log('ServiceWorker registration successful'); - }) - .catch(function(err) { - console.log('ServiceWorker registration failed: ', err); - }); - }); -} - -// Error handling for unhandled promises -window.addEventListener('unhandledrejection', function(event) { - console.error('Unhandled promise rejection:', event.reason); - handleCheckinError('An unexpected error occurred. Please try again.'); - event.preventDefault(); -}); - -// Handle online/offline status -window.addEventListener('online', function() { - showStatusMessage('Connection restored', 'success'); -}); - -window.addEventListener('offline', function() { - showStatusMessage('No internet connection. Please check your network.', 'warning'); -}); - -// Performance monitoring -if ('performance' in window) { - window.addEventListener('load', function() { - setTimeout(function() { - const perfData = performance.getEntriesByType('navigation')[0]; - console.log('Page load time:', perfData.loadEventEnd - perfData.loadEventStart, 'ms'); - }, 0); - }); -} - -// Accessibility enhancements -document.addEventListener('keydown', function(e) { - // Escape key to reset form - if (e.key === 'Escape') { - const successCard = document.getElementById('successCard'); - if (successCard && successCard.style.display !== 'none') { - checkInAnother(); - } else { - // Reset form - const form = document.getElementById('checkinForm'); - if (form) { - form.reset(); - hideStatusMessage(); - - const employeeInput = document.getElementById('employee_id'); - if (employeeInput) { - employeeInput.classList.remove('error', 'success'); - employeeInput.focus(); - } - } - } - } +function checkInAnother() { + // Stop location watching + stopLocationWatching(); - // Ctrl+R to refresh (prevent default and reload page cleanly) - if ((e.ctrlKey || e.metaKey) && e.key === 'r') { - e.preventDefault(); - window.location.reload(); - } -}); - -// Touch device optimizations -if ('ontouchstart' in window) { - // Add touch-friendly classes - document.body.classList.add('touch-device'); - - // Prevent zoom on input focus for iOS - const inputs = document.querySelectorAll('input[type="text"]'); - inputs.forEach(input => { - input.addEventListener('focus', function() { - const viewport = document.querySelector('meta[name="viewport"]'); - if (viewport) { - viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'); - } - }); - - input.addEventListener('blur', function() { - const viewport = document.querySelector('meta[name="viewport"]'); - if (viewport) { - viewport.setAttribute('content', 'width=device-width, initial-scale=1.0'); - } - }); - }); + // Reload page + window.location.reload(); } -// Auto-refresh page if idle for too long (optional) -let idleTimer; -const IDLE_TIME = 30 * 60 * 1000; // 30 minutes - -function resetIdleTimer() { - clearTimeout(idleTimer); - idleTimer = setTimeout(() => { - if (confirm('This page has been idle for 30 minutes. Would you like to refresh it?')) { - window.location.reload(); - } else { - resetIdleTimer(); // Reset timer if user chooses not to refresh - } - }, IDLE_TIME); +// NEW: Cleanup function for page unload +function cleanup() { + stopLocationWatching(); + console.log('🧹 Cleaned up geolocation resources'); } -// Track user activity -['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'].forEach(event => { - document.addEventListener(event, resetIdleTimer, true); -}); +// NEW: Setup cleanup handlers +window.addEventListener('beforeunload', cleanup); +window.addEventListener('pagehide', cleanup); -// Initialize idle timer -resetIdleTimer(); \ No newline at end of file +// NEW: Export geolocation functions for global use +window.requestUserLocation = requestUserLocation; +window.getCurrentLocationData = getCurrentLocationData; +window.retryLocationRequest = retryLocationRequest; +window.toggleLocationInfo = toggleLocationInfo; +window.stopLocationWatching = stopLocationWatching; +window.startLocationWatching = startLocationWatching; + +console.log('📍 QR Destination with Geolocation loaded successfully!'); +console.log('🔧 Available functions: requestUserLocation(), getCurrentLocationData(), retryLocationRequest(), toggleLocationInfo()'); +console.log('📊 Location tracking ready for check-ins!'); \ No newline at end of file diff --git a/templates/qr_destination.html b/templates/qr_destination.html index 04a71b0..3eb0cf2 100644 --- a/templates/qr_destination.html +++ b/templates/qr_destination.html @@ -7,6 +7,91 @@ +
@@ -74,6 +159,43 @@
+ +
+ + Getting your location... +
+ + +
+

Location Details

+
+ Latitude: + - +
+
+ Longitude: + - +
+
+ Accuracy: + - +
+
+ Address: + - +
+
+ + +
+ + +
+
@@ -85,6 +207,14 @@
+ + + + + + + +
+ +
@@ -177,10 +312,238 @@ \ No newline at end of file