diff --git a/app.py b/app.py index 4d381f7..9171d90 100644 --- a/app.py +++ b/app.py @@ -11,6 +11,9 @@ from sqlalchemy import text import re import uuid from user_agents import parse +import requests +import json +from math import radians, cos, sin, asin, sqrt # Initialize Flask application app = Flask(__name__) @@ -144,6 +147,113 @@ class AttendanceData(db.Model): 'address': self.address, 'location_source': self.location_source } + +def get_coordinates_from_address(address): + """ + Get latitude and longitude from address using geocoding service + Returns (lat, lng) tuple or (None, None) if failed + """ + if not address or address.strip() == '': + return None, None + + try: + # Using a free geocoding service (Nominatim/OpenStreetMap) + # In production, consider using Google Maps Geocoding API for better accuracy + url = "https://nominatim.openstreetmap.org/search" + params = { + 'q': address, + 'format': 'json', + 'limit': 1, + 'addressdetails': 1 + } + + headers = { + 'User-Agent': 'QR-Attendance-System/1.0' + } + + response = requests.get(url, params=params, headers=headers, timeout=10) + + if response.status_code == 200: + data = response.json() + if data and len(data) > 0: + lat = float(data[0]['lat']) + lng = float(data[0]['lon']) + print(f"āœ… Geocoded address '{address[:50]}...' to coordinates: {lat}, {lng}") + return lat, lng + + print(f"āš ļø Could not geocode address: {address}") + return None, None + + except Exception as e: + print(f"āŒ Error geocoding address '{address}': {e}") + return None, None + +def calculate_distance_miles(lat1, lng1, lat2, lng2): + """ + Calculate the great circle distance between two points on Earth in miles + Using the Haversine formula + """ + if any(coord is None for coord in [lat1, lng1, lat2, lng2]): + return None + + try: + # Convert decimal degrees to radians + lat1, lng1, lat2, lng2 = map(radians, [lat1, lng1, lat2, lng2]) + + # Haversine formula + dlng = lng2 - lng1 + dlat = lat2 - lat1 + a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlng/2)**2 + c = 2 * asin(sqrt(a)) + + # Radius of Earth in miles + r_miles = 3959 + + # Calculate the result + distance = c * r_miles + + print(f"šŸ“ Calculated distance: {distance:.3f} miles") + return round(distance, 3) + + except Exception as e: + print(f"āŒ Error calculating distance: {e}") + return None + +def calculate_location_accuracy(qr_address, checkin_address, checkin_lat=None, checkin_lng=None): + """ + Calculate location accuracy by comparing QR code address with check-in location + Returns distance in miles between the two locations + """ + print(f"\nšŸ“ CALCULATING LOCATION ACCURACY:") + print(f" QR Address: {qr_address}") + print(f" Check-in Address: {checkin_address}") + print(f" Check-in Coordinates: {checkin_lat}, {checkin_lng}") + + # Get QR code coordinates from address + qr_lat, qr_lng = get_coordinates_from_address(qr_address) + + if qr_lat is None or qr_lng is None: + print(f"āš ļø Could not geocode QR address, cannot calculate accuracy") + return None + + # Use check-in coordinates if available, otherwise geocode check-in address + if checkin_lat is not None and checkin_lng is not None: + checkin_coords_lat, checkin_coords_lng = checkin_lat, checkin_lng + print(f"āœ… Using GPS coordinates for check-in location") + else: + checkin_coords_lat, checkin_coords_lng = get_coordinates_from_address(checkin_address) + if checkin_coords_lat is None or checkin_coords_lng is None: + print(f"āš ļø Could not geocode check-in address, cannot calculate accuracy") + return None + print(f"āœ… Using geocoded coordinates for check-in address") + + # Calculate distance + distance = calculate_distance_miles(qr_lat, qr_lng, checkin_coords_lat, checkin_coords_lng) + + if distance is not None: + print(f"āœ… Location accuracy calculated: {distance} miles") + + return distance def generate_qr_url(name, qr_id): """Generate a unique URL for QR code destination""" @@ -977,6 +1087,125 @@ def is_admin_user(user_id): except: return False +def get_coordinates_from_address(address): + """ + Get latitude and longitude from address using geocoding service + Returns (lat, lng) tuple or (None, None) if failed + """ + if not address or address.strip() == '': + return None, None + + try: + # Using Nominatim/OpenStreetMap for free geocoding + url = "https://nominatim.openstreetmap.org/search" + params = { + 'q': address, + 'format': 'json', + 'limit': 1, + 'addressdetails': 1 + } + + headers = { + 'User-Agent': 'QR-Attendance-System/1.0' + } + + response = requests.get(url, params=params, headers=headers, timeout=10) + + if response.status_code == 200: + data = response.json() + if data and len(data) > 0: + lat = float(data[0]['lat']) + lng = float(data[0]['lon']) + print(f"āœ… Geocoded address '{address[:50]}...' to coordinates: {lat}, {lng}") + return lat, lng + + print(f"āš ļø Could not geocode address: {address}") + return None, None + + except Exception as e: + print(f"āŒ Error geocoding address '{address}': {e}") + return None, None + +def calculate_distance_miles(lat1, lng1, lat2, lng2): + """ + Calculate the great circle distance between two points on Earth in miles + Using the Haversine formula + """ + if any(coord is None for coord in [lat1, lng1, lat2, lng2]): + return None + + try: + # Convert decimal degrees to radians + lat1, lng1, lat2, lng2 = map(radians, [lat1, lng1, lat2, lng2]) + + # Haversine formula + dlng = lng2 - lng1 + dlat = lat2 - lat1 + a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlng/2)**2 + c = 2 * asin(sqrt(a)) + + # Radius of Earth in miles + r_miles = 3959 + + # Calculate the result + distance = c * r_miles + + print(f"šŸ“ Calculated distance: {distance:.3f} miles") + return round(distance, 3) + + except Exception as e: + print(f"āŒ Error calculating distance: {e}") + return None + +def calculate_location_accuracy(qr_address, checkin_address, checkin_lat=None, checkin_lng=None): + """ + Calculate location accuracy by comparing QR code address with check-in location + Returns distance in miles between the two locations + """ + print(f"\nšŸ“ CALCULATING LOCATION ACCURACY:") + print(f" QR Address: {qr_address}") + print(f" Check-in Address: {checkin_address}") + print(f" Check-in Coordinates: {checkin_lat}, {checkin_lng}") + + # Get QR code coordinates from address + qr_lat, qr_lng = get_coordinates_from_address(qr_address) + + if qr_lat is None or qr_lng is None: + print(f"āš ļø Could not geocode QR address, cannot calculate accuracy") + return None + + # Use check-in coordinates if available, otherwise geocode check-in address + if checkin_lat is not None and checkin_lng is not None: + checkin_coords_lat, checkin_coords_lng = checkin_lat, checkin_lng + print(f"āœ… Using GPS coordinates for check-in location") + else: + checkin_coords_lat, checkin_coords_lng = get_coordinates_from_address(checkin_address) + if checkin_coords_lat is None or checkin_coords_lng is None: + print(f"āš ļø Could not geocode check-in address, cannot calculate accuracy") + return None + print(f"āœ… Using geocoded coordinates for check-in address") + + # Calculate distance + distance = calculate_distance_miles(qr_lat, qr_lng, checkin_coords_lat, checkin_coords_lng) + + if distance is not None: + print(f"āœ… Location accuracy calculated: {distance} miles") + + return distance + +def get_location_accuracy_level(location_accuracy): + """Get human-readable location accuracy level based on distance""" + if not location_accuracy: + return 'unknown' + elif location_accuracy <= 0.1: # Within 0.1 mile (528 feet) + return 'excellent' + elif location_accuracy <= 0.5: # Within 0.5 mile + return 'good' + elif location_accuracy <= 1.0: # Within 1 mile + return 'fair' + else: + return 'poor' + @app.route('/qr-codes/create', methods=['GET', 'POST']) @login_required def create_qr_code(): @@ -1149,172 +1378,117 @@ def qr_destination(qr_url): @app.route('/qr//checkin', methods=['POST']) def qr_checkin(qr_url): - """Enhanced staff check-in with guaranteed location saving""" + """Enhanced staff check-in with location accuracy calculation""" try: + print(f"\nšŸš€ STARTING ENHANCED CHECK-IN PROCESS") + print(f" QR URL: {qr_url}") + print(f" Time: {datetime.now()}") + # Find QR code by URL qr_code = QRCode.query.filter_by(qr_url=qr_url, active_status=True).first() if not qr_code: + print(f"āŒ QR code not found or inactive: {qr_url}") return jsonify({ 'success': False, 'message': 'QR code not found or inactive.' }), 404 + print(f"āœ… Found QR code: {qr_code.name} (ID: {qr_code.id})") + print(f" Location: {qr_code.location}") + print(f" Address: {qr_code.location_address}") + # Get form data employee_id = request.form.get('employee_id', '').strip() - # CRITICAL: Get location data with debug logging - 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() - - # COMPREHENSIVE DEBUG: Log all received form data - print(f"\n{'='*60}") - print(f"šŸ” QR CHECK-IN DEBUG - FORM DATA RECEIVED") - print(f"{'='*60}") - print(f"Employee ID: '{employee_id}'") - print(f"Latitude: '{latitude}' (length: {len(latitude)}, type: {type(latitude)})") - print(f"Longitude: '{longitude}' (length: {len(longitude)}, type: {type(longitude)})") - print(f"Accuracy: '{accuracy}' (length: {len(accuracy)}, type: {type(accuracy)})") - print(f"Altitude: '{altitude}' (length: {len(altitude)}, type: {type(altitude)})") - print(f"Location Source: '{location_source}' (type: {type(location_source)})") - print(f"Address: '{address}' (length: {len(address) if address else 0})") - print(f"QR Code ID: {qr_code.id}") - print(f"QR Location: {qr_code.location}") - print(f"{'='*60}\n") - if not employee_id: return jsonify({ 'success': False, 'message': 'Employee ID is required.' }), 400 - # Validate employee ID format - if not re.match(r'^[A-Za-z0-9]{3,20}$', employee_id): - return jsonify({ - 'success': False, - 'message': 'Invalid employee ID format. Use 3-20 alphanumeric characters.' - }), 400 + print(f"āœ… Employee ID: {employee_id}") - # Check for duplicate check-ins today - today = datetime.today() - existing_checkin = AttendanceData.query.filter_by( - qr_code_id=qr_code.id, - employee_id=employee_id.upper(), - check_in_date=today - ).first() + # 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 existing_checkin: - return jsonify({ - 'success': False, - 'message': f'You have already checked in today at {existing_checkin.check_in_time.strftime("%H:%M")}.' - }), 409 + print(f"\nšŸ“ RECEIVED LOCATION DATA:") + print(f" Latitude: '{latitude}'") + print(f" Longitude: '{longitude}'") + print(f" Accuracy: '{accuracy}'") + print(f" Address: '{address}'") + print(f" Source: '{location_source}'") - # Parse user agent for device info - user_agent = request.headers.get('User-Agent', '') - try: - parsed_agent = parse(user_agent) - device_info = f"{parsed_agent.browser.family} on {parsed_agent.os.family}" - except: - device_info = "Unknown device" - - # 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() - - # Enhanced location data processing with validation + # Process location data with validation lat_value = None lng_value = None acc_value = None alt_value = None - print(f"šŸ”„ PROCESSING LOCATION DATA:") - - # Process latitude with comprehensive validation - if latitude and latitude.strip() and latitude not in ['null', '', 'undefined', 'NaN']: - try: + try: + if latitude and latitude not in ['', 'null', 'undefined']: lat_value = float(latitude) - if -90 <= lat_value <= 90: - print(f"āœ… Valid latitude: {lat_value}") - else: - print(f"āš ļø Invalid latitude range: {lat_value} (must be -90 to 90)") + if not (-90 <= lat_value <= 90): lat_value = None - except (ValueError, TypeError) as e: - print(f"āŒ Latitude parsing error: {e}") - lat_value = None - else: - print(f"šŸ“ No latitude data: '{latitude}'") - - # Process longitude with comprehensive validation - if longitude and longitude.strip() and longitude not in ['null', '', 'undefined', 'NaN']: - try: + print(f"āš ļø Invalid latitude range: {latitude}") + + if longitude and longitude not in ['', 'null', 'undefined']: lng_value = float(longitude) - if -180 <= lng_value <= 180: - print(f"āœ… Valid longitude: {lng_value}") - else: - print(f"āš ļø Invalid longitude range: {lng_value} (must be -180 to 180)") + if not (-180 <= lng_value <= 180): lng_value = None - except (ValueError, TypeError) as e: - print(f"āŒ Longitude parsing error: {e}") - lng_value = None - else: - print(f"šŸ“ No longitude data: '{longitude}'") - - # Process accuracy - if accuracy and accuracy.strip() and accuracy not in ['null', '', 'undefined', 'NaN']: - try: + print(f"āš ļø Invalid longitude range: {longitude}") + + if accuracy and accuracy not in ['', 'null', 'undefined']: acc_value = float(accuracy) - if acc_value >= 0: - print(f"āœ… Valid accuracy: {acc_value}m") - else: - print(f"āš ļø Invalid accuracy (negative): {acc_value}") + if acc_value < 0: acc_value = None - except (ValueError, TypeError) as e: - print(f"āŒ Accuracy parsing error: {e}") - acc_value = None - else: - print(f"šŸ“ No accuracy data: '{accuracy}'") - - # Process altitude - if altitude and altitude.strip() and altitude not in ['null', '', 'undefined', 'NaN']: - try: + + if altitude and altitude not in ['', 'null', 'undefined']: alt_value = float(altitude) - print(f"āœ… Valid altitude: {alt_value}m") - except (ValueError, TypeError) as e: - print(f"āŒ Altitude parsing error: {e}") - alt_value = None - else: - print(f"šŸ“ No altitude data: '{altitude}'") + + except (ValueError, TypeError) as e: + print(f"āš ļø Error parsing location data: {e}") - # Validate and clean location source - valid_sources = ['gps', 'network', 'manual'] - if location_source not in valid_sources: - print(f"āš ļø Invalid location source '{location_source}', defaulting to 'manual'") - location_source = 'manual' - else: - print(f"āœ… Valid location source: {location_source}") - - # Truncate address if too long - if address and len(address) > 500: - address = address[:500] - print(f"āš ļø Address truncated to 500 characters") - - print(f"\nšŸ“Š FINAL PROCESSED LOCATION DATA:") + print(f"\nāœ… PROCESSED LOCATION DATA:") print(f" Latitude: {lat_value}") print(f" Longitude: {lng_value}") print(f" Accuracy: {acc_value}") - print(f" Altitude: {alt_value}") - print(f" Source: {location_source}") - print(f" Address: {address[:50]}..." if address and len(address) > 50 else f" Address: {address}") + print(f" Address: {address}") - has_coordinates = lat_value is not None and lng_value is not None - print(f" Has Valid Coordinates: {has_coordinates}") + # Get device and request info + user_agent_string = request.headers.get('User-Agent', '') + try: + user_agent_obj = parse(user_agent_string) + device_info = f"{user_agent_obj.device.family} - {user_agent_obj.os.family} {user_agent_obj.os.version_string}" + except: + device_info = "Unknown device" - # Create attendance record with explicit location field assignment + client_ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.environ.get('REMOTE_ADDR', 'Unknown')) + if client_ip and ',' in client_ip: + client_ip = client_ip.split(',')[0].strip() + + today = datetime.now().date() + + # Check for duplicate check-in + existing_checkin = AttendanceData.query.filter_by( + employee_id=employee_id.upper(), + qr_code_id=qr_code.id, + check_in_date=today + ).first() + + if existing_checkin: + print(f"āŒ Duplicate check-in attempt") + return jsonify({ + 'success': False, + 'message': f'Employee {employee_id.upper()} has already checked in at {qr_code.location} today at {existing_checkin.check_in_time.strftime("%H:%M")}.' + }), 400 + + # Create attendance record print(f"\nšŸ’¾ CREATING ATTENDANCE RECORD:") attendance = AttendanceData( @@ -1323,132 +1497,86 @@ def qr_checkin(qr_url): check_in_date=today, check_in_time=datetime.now().time(), device_info=device_info, - user_agent=user_agent, + user_agent=user_agent_string, ip_address=client_ip, location_name=qr_code.location, + latitude=lat_value, + longitude=lng_value, + accuracy=acc_value, + altitude=alt_value, + location_source=location_source, + address=address, status='present' ) - # EXPLICIT LOCATION FIELD ASSIGNMENT - if lat_value is not None: - attendance.latitude = lat_value - print(f"āœ… Set latitude: {attendance.latitude}") + print(f"āœ… Created base attendance record") - if lng_value is not None: - attendance.longitude = lng_value - print(f"āœ… Set longitude: {attendance.longitude}") - - if acc_value is not None: - attendance.accuracy = acc_value - print(f"āœ… Set accuracy: {attendance.accuracy}") - - if alt_value is not None: - attendance.altitude = alt_value - print(f"āœ… Set altitude: {attendance.altitude}") - - if location_source: - attendance.location_source = location_source - print(f"āœ… Set location_source: {attendance.location_source}") - - if address: - attendance.address = address - print(f"āœ… Set address: {attendance.address[:50]}...") - - print(f"\nšŸ’¾ SAVING TO DATABASE:") - print(f" Record ID will be generated...") - print(f" Employee: {attendance.employee_id}") - print(f" Location Name: {attendance.location_name}") - print(f" Coordinates: {attendance.latitude}, {attendance.longitude}") - print(f" Accuracy: {attendance.accuracy}") - print(f" Source: {attendance.location_source}") - - # Add to session and commit - db.session.add(attendance) + # Calculate location accuracy + print(f"\nšŸŽÆ CALCULATING LOCATION ACCURACY...") + location_accuracy = None try: - db.session.commit() - print(f"āœ… Database commit successful!") - except Exception as commit_error: - print(f"āŒ Database commit failed: {commit_error}") - db.session.rollback() - raise - - # VERIFICATION: Re-fetch the saved record to confirm data persistence - print(f"\nšŸ” VERIFICATION - Re-fetching saved record:") - - try: - saved_record = AttendanceData.query.get(attendance.id) - if saved_record: - print(f"āœ… Record found with ID: {saved_record.id}") - print(f"āœ… Employee ID: {saved_record.employee_id}") - print(f"āœ… Saved latitude: {saved_record.latitude}") - print(f"āœ… Saved longitude: {saved_record.longitude}") - print(f"āœ… Saved accuracy: {saved_record.accuracy}") - print(f"āœ… Saved altitude: {saved_record.altitude}") - print(f"āœ… Saved location_source: {saved_record.location_source}") - print(f"āœ… Saved address: {saved_record.address}") - - has_location_final = saved_record.latitude is not None and saved_record.longitude is not None - print(f"āœ… Final has_location status: {has_location_final}") - - # DATABASE VERIFICATION QUERY - verification_query = f""" - SELECT id, employee_id, latitude, longitude, accuracy, altitude, location_source, address - FROM attendance_data - WHERE id = {saved_record.id} - """ - print(f"\nšŸ” Database verification query:") - print(f" {verification_query}") - + location_accuracy = calculate_location_accuracy( + qr_address=qr_code.location_address, + checkin_address=address, + checkin_lat=lat_value, + checkin_lng=lng_value + ) + + if location_accuracy is not None: + attendance.location_accuracy = location_accuracy + print(f"āœ… Set location accuracy: {location_accuracy} miles") else: - print(f"āŒ ERROR: Could not re-fetch saved record!") - saved_record = attendance # Fallback to original object + print(f"āš ļø Could not calculate location accuracy") - except Exception as verify_error: - print(f"āŒ Verification error: {verify_error}") - saved_record = attendance # Fallback to original object + except Exception as e: + print(f"āŒ Error calculating location accuracy: {e}") - # Build response with verification data + # Save to database + try: + db.session.add(attendance) + db.session.commit() + print(f"āœ… Successfully saved attendance record with ID: {attendance.id}") + + except Exception as e: + print(f"āŒ Database error: {e}") + db.session.rollback() + return jsonify({ + 'success': False, + 'message': 'Database error occurred. Please try again.' + }), 500 + + # Build success response response_data = { 'success': True, 'message': 'Check-in successful!', - 'employee_id': employee_id.upper(), - 'location': qr_code.location, - 'event': qr_code.location_event, - 'time': attendance.check_in_time.strftime('%H:%M'), - 'date': attendance.check_in_date.strftime('%Y-%m-%d'), - 'has_location': saved_record.latitude is not None and saved_record.longitude is not None, - 'location_coordinates': f"{saved_record.latitude},{saved_record.longitude}" if saved_record.latitude and saved_record.longitude else None, - 'location_accuracy': saved_record.accuracy, - 'location_address': saved_record.address, - 'location_source': saved_record.location_source + 'data': { + 'employee_id': attendance.employee_id, + 'location': attendance.location_name, + 'check_in_time': attendance.check_in_time.strftime('%H:%M'), + 'has_gps': attendance.latitude is not None and attendance.longitude is not None, + 'location_accuracy': f"{location_accuracy:.3f} miles" if location_accuracy else "Not calculated", + 'accuracy_level': get_location_accuracy_level(location_accuracy) if location_accuracy else "unknown" + } } - print(f"\nšŸ“¤ SENDING RESPONSE:") - print(f" Success: {response_data['success']}") - print(f" Has Location: {response_data['has_location']}") - print(f" Coordinates: {response_data['location_coordinates']}") - print(f" Accuracy: {response_data['location_accuracy']}") - print(f"{'='*60}\n") + print(f"āœ… CHECK-IN COMPLETED SUCCESSFULLY!") + print(f" Employee: {attendance.employee_id}") + print(f" Location: {attendance.location_name}") + print(f" GPS: {'Yes' if attendance.latitude else 'No'}") + print(f" Location Accuracy: {location_accuracy or 'Not calculated'}") return jsonify(response_data) except Exception as e: - print(f"\nāŒ CRITICAL ERROR in qr_checkin:") - print(f"āŒ Error type: {type(e).__name__}") - print(f"āŒ Error message: {str(e)}") - - # Print full traceback for debugging - import traceback - print(f"āŒ Full traceback:") - print(traceback.format_exc()) - db.session.rollback() + print(f"āŒ Unexpected error in check-in process: {e}") + import traceback + print(f"āŒ Traceback: {traceback.format_exc()}") return jsonify({ 'success': False, - 'message': 'Check-in failed due to server error. Please try again.', - 'error': str(e) if app.debug else None + 'message': 'An unexpected error occurred during check-in. Please try again.' }), 500 def process_location_data(location_data): @@ -1617,33 +1745,63 @@ def toggle_qr_status_api(qr_id): @app.route('/attendance') @admin_required def attendance_report(): - """Enhanced attendance report page with date range filtering and location data (Admin only)""" + """Safe attendance report with backward compatibility for location_accuracy""" try: + print("šŸ“Š Loading attendance report...") + + # Check if location_accuracy column exists + has_location_accuracy = check_location_accuracy_column_exists() + print(f"šŸ” Location accuracy column exists: {has_location_accuracy}") + # Get filter parameters date_from = request.args.get('date_from', '') date_to = request.args.get('date_to', '') location_filter = request.args.get('location', '') employee_filter = request.args.get('employee', '') - # Build base query with enhanced location data - base_query = """ - SELECT - ad.id, - ad.employee_id, - ad.check_in_date, - ad.check_in_time, - ad.location_name, - qc.location_event, - qc.location_address as qr_address, - ad.address as checked_in_address, - ad.latitude, - ad.longitude, - ad.accuracy, - ad.device_info - FROM attendance_data ad - LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id - WHERE 1=1 - """ + # Build base query - conditional based on column existence + if has_location_accuracy: + # New query with location accuracy + base_query = """ + SELECT + ad.id, + ad.employee_id, + ad.check_in_date, + ad.check_in_time, + ad.location_name, + qc.location_event, + qc.location_address as qr_address, + ad.address as checked_in_address, + ad.latitude, + ad.longitude, + ad.location_accuracy, + ad.accuracy as gps_accuracy, + ad.device_info + FROM attendance_data ad + LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id + WHERE 1=1 + """ + else: + # Fallback query without location accuracy + base_query = """ + SELECT + ad.id, + ad.employee_id, + ad.check_in_date, + ad.check_in_time, + ad.location_name, + qc.location_event, + qc.location_address as qr_address, + ad.address as checked_in_address, + ad.latitude, + ad.longitude, + NULL as location_accuracy, + ad.accuracy as gps_accuracy, + ad.device_info + FROM attendance_data ad + LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id + WHERE 1=1 + """ conditions = [] params = {} @@ -1674,55 +1832,104 @@ def attendance_report(): # Add ordering base_query += " ORDER BY ad.check_in_date DESC, ad.check_in_time DESC" + print(f"šŸ” Executing query with {len(params)} parameters") + # Execute query query_result = db.session.execute(text(base_query), params) attendance_records = query_result.fetchall() + print(f"āœ… Found {len(attendance_records)} attendance records") + # Process records to add calculated fields processed_records = [] for record in attendance_records: + # Safe attribute access with fallbacks + location_accuracy = getattr(record, 'location_accuracy', None) + gps_accuracy = getattr(record, 'gps_accuracy', None) + record_dict = { 'id': record.id, 'employee_id': record.employee_id, 'check_in_date': record.check_in_date, 'check_in_time': record.check_in_time, 'location_name': record.location_name, - 'location_event': record.location_event, - 'qr_address': record.qr_address or 'Not available', - 'checked_in_address': record.checked_in_address or 'Location not captured', - 'device_info': record.device_info, - 'accuracy': record.accuracy, - 'accuracy_level': get_accuracy_level(record.accuracy), + 'location_event': getattr(record, 'location_event', ''), + 'qr_address': getattr(record, 'qr_address', None) or 'Not available', + 'checked_in_address': getattr(record, 'checked_in_address', None) or 'Location not captured', + 'device_info': getattr(record, 'device_info', ''), + 'location_accuracy': location_accuracy, + 'gps_accuracy': gps_accuracy, + 'accuracy_level': get_location_accuracy_level(location_accuracy) if location_accuracy else 'unknown', 'has_location_data': record.latitude is not None and record.longitude is not None, - 'coordinates': f"{record.latitude:.6f}, {record.longitude:.6f}" if record.latitude and record.longitude else "No GPS data" + 'coordinates': f"{record.latitude:.6f}, {record.longitude:.6f}" if record.latitude and record.longitude else "No GPS data", + 'has_location_accuracy_feature': has_location_accuracy } processed_records.append(record_dict) + print(f"āœ… Processed {len(processed_records)} records") + # Get unique locations for filter dropdown - locations_query = db.session.execute(text(""" - SELECT DISTINCT location_name - FROM attendance_data - ORDER BY location_name - """)) - locations = [row[0] for row in locations_query.fetchall()] + try: + locations_query = db.session.execute(text(""" + SELECT DISTINCT location_name + FROM attendance_data + WHERE location_name IS NOT NULL + ORDER BY location_name + """)) + locations = [row[0] for row in locations_query.fetchall()] + print(f"āœ… Found {len(locations)} unique locations") + except Exception as e: + print(f"āš ļø Error loading locations: {e}") + locations = [] # Get attendance statistics - stats_query = db.session.execute(text(""" - SELECT - COUNT(*) as total_checkins, - COUNT(DISTINCT employee_id) as unique_employees, - COUNT(DISTINCT qr_code_id) as active_locations, - COUNT(CASE WHEN check_in_date = CURRENT_DATE THEN 1 END) as today_checkins, - COUNT(CASE WHEN latitude IS NOT NULL AND longitude IS NOT NULL THEN 1 END) as records_with_gps, - AVG(accuracy) as avg_accuracy - FROM attendance_data - """)) - stats = stats_query.fetchone() + try: + if has_location_accuracy: + stats_query = db.session.execute(text(""" + SELECT + COUNT(*) as total_checkins, + COUNT(DISTINCT employee_id) as unique_employees, + COUNT(DISTINCT qr_code_id) as active_locations, + COUNT(CASE WHEN check_in_date = CURRENT_DATE THEN 1 END) as today_checkins, + COUNT(CASE WHEN latitude IS NOT NULL AND longitude IS NOT NULL THEN 1 END) as records_with_gps, + COUNT(CASE WHEN location_accuracy IS NOT NULL THEN 1 END) as records_with_accuracy, + AVG(location_accuracy) as avg_location_accuracy + FROM attendance_data + """)) + else: + stats_query = db.session.execute(text(""" + SELECT + COUNT(*) as total_checkins, + COUNT(DISTINCT employee_id) as unique_employees, + COUNT(DISTINCT qr_code_id) as active_locations, + COUNT(CASE WHEN check_in_date = CURRENT_DATE THEN 1 END) as today_checkins, + COUNT(CASE WHEN latitude IS NOT NULL AND longitude IS NOT NULL THEN 1 END) as records_with_gps, + 0 as records_with_accuracy, + 0 as avg_location_accuracy + FROM attendance_data + """)) + + stats = stats_query.fetchone() + print(f"āœ… Loaded statistics: {stats.total_checkins} total check-ins") + except Exception as e: + print(f"āš ļø Error loading statistics: {e}") + # Fallback stats + stats = type('Stats', (), { + 'total_checkins': 0, + 'unique_employees': 0, + 'active_locations': 0, + 'today_checkins': 0, + 'records_with_gps': 0, + 'records_with_accuracy': 0, + 'avg_location_accuracy': 0 + })() # Add today's date for template today_date = datetime.now().strftime('%Y-%m-%d') current_date_formatted = datetime.now().strftime('%B %d') + print("āœ… Rendering attendance report template") + return render_template('attendance_report.html', attendance_records=processed_records, locations=locations, @@ -1732,23 +1939,182 @@ def attendance_report(): location_filter=location_filter, employee_filter=employee_filter, today_date=today_date, - current_date_formatted=current_date_formatted) + current_date_formatted=current_date_formatted, + has_location_accuracy_feature=has_location_accuracy) except Exception as e: - print(f"Error loading attendance report: {e}") - flash('Error loading attendance report. Please try again.', 'error') + print(f"āŒ Error loading attendance report: {e}") + print(f"āŒ Exception type: {type(e)}") + import traceback + print(f"āŒ Traceback: {traceback.format_exc()}") + + flash('Error loading attendance report. Please check the server logs for details.', 'error') return redirect(url_for('dashboard')) -def get_accuracy_level(accuracy): - """Get human-readable accuracy level""" - if not accuracy: +def check_location_accuracy_column_exists(): + """Check if the location_accuracy column exists in the attendance_data table""" + try: + result = db.session.execute(text(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name='attendance_data' AND column_name='location_accuracy' + """)) + + column_exists = result.fetchone() is not None + return column_exists + + except Exception as e: + print(f"āš ļø Error checking location_accuracy column: {e}") + return False + +def get_location_accuracy_level(location_accuracy): + """Get human-readable location accuracy level based on distance""" + if not location_accuracy: return 'unknown' - elif accuracy <= 50: - return 'high' - elif accuracy <= 100: - return 'medium' + elif location_accuracy <= 0.1: # Within 0.1 mile (528 feet) + return 'excellent' + elif location_accuracy <= 0.5: # Within 0.5 mile + return 'good' + elif location_accuracy <= 1.0: # Within 1 mile + return 'fair' else: - return 'low' + return 'poor' + +# Safe migration function +def safely_add_location_accuracy_column(): + """Safely add location_accuracy column if it doesn't exist""" + try: + # Check if column already exists + if not check_location_accuracy_column_exists(): + # Add the column + db.session.execute(text(""" + ALTER TABLE attendance_data + ADD COLUMN location_accuracy FLOAT + """)) + db.session.commit() + print("āœ… Added location_accuracy column to attendance_data table") + return True + else: + print("āœ… location_accuracy column already exists") + return True + + except Exception as e: + print(f"āŒ Error adding location_accuracy column: {e}") + db.session.rollback() + return False + +# Test database connection +def test_database_connection(): + """Test if database connection is working""" + try: + result = db.session.execute(text("SELECT 1")) + test = result.fetchone() + print("āœ… Database connection successful") + return True + except Exception as e: + print(f"āŒ Database connection failed: {e}") + return False + +# Test attendance table structure +def test_attendance_table(): + """Test if attendance_data table exists and get its structure""" + try: + result = db.session.execute(text(""" + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name='attendance_data' + ORDER BY ordinal_position + """)) + + columns = result.fetchall() + print(f"āœ… attendance_data table has {len(columns)} columns:") + for col in columns: + print(f" - {col.column_name}: {col.data_type}") + + return True + + except Exception as e: + print(f"āŒ Error checking attendance_data table: {e}") + return False + +def get_location_accuracy_level(location_accuracy): + """Get human-readable location accuracy level based on distance""" + if not location_accuracy: + return 'unknown' + elif location_accuracy <= 0.1: # Within 0.1 mile (528 feet) + return 'excellent' + elif location_accuracy <= 0.5: # Within 0.5 mile + return 'good' + elif location_accuracy <= 1.0: # Within 1 mile + return 'fair' + else: + return 'poor' + +# Database migration function to add location_accuracy column +def add_location_accuracy_column(): + """Add location_accuracy column to existing attendance_data table""" + try: + # Check if column already exists + result = db.session.execute(text(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name='attendance_data' AND column_name='location_accuracy' + """)) + + if not result.fetchone(): + # Add the column + db.session.execute(text(""" + ALTER TABLE attendance_data + ADD COLUMN location_accuracy FLOAT + """)) + db.session.commit() + print("āœ… Added location_accuracy column to attendance_data table") + else: + print("āœ… location_accuracy column already exists") + + except Exception as e: + print(f"āŒ Error adding location_accuracy column: {e}") + db.session.rollback() + +# Function to recalculate location accuracy for existing records +def recalculate_existing_location_accuracy(): + """Recalculate location accuracy for all existing attendance records""" + try: + records = db.session.execute(text(""" + SELECT ad.id, qc.location_address, ad.address, ad.latitude, ad.longitude + FROM attendance_data ad + LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id + WHERE ad.location_accuracy IS NULL + AND qc.location_address IS NOT NULL + """)).fetchall() + + updated_count = 0 + + for record in records: + location_accuracy = calculate_location_accuracy( + qr_address=record.location_address, + checkin_address=record.address, + checkin_lat=record.latitude, + checkin_lng=record.longitude + ) + + if location_accuracy is not None: + db.session.execute(text(""" + UPDATE attendance_data + SET location_accuracy = :accuracy + WHERE id = :record_id + """), { + 'accuracy': location_accuracy, + 'record_id': record.id + }) + updated_count += 1 + + db.session.commit() + print(f"āœ… Updated location accuracy for {updated_count} records") + + except Exception as e: + print(f"āŒ Error recalculating location accuracy: {e}") + db.session.rollback() @app.route('/api/attendance/stats') @admin_required diff --git a/static/css/attendance.css b/static/css/attendance.css index c076d73..e277f45 100644 --- a/static/css/attendance.css +++ b/static/css/attendance.css @@ -965,6 +965,182 @@ color: var(--gray-600); } +.location-accuracy-info { + display: flex; + align-items: center; + justify-content: center; +} + +.location-accuracy-badge { + display: inline-flex; + align-items: center; + gap: var(--spacing-1); + padding: var(--spacing-1) var(--spacing-3); + border-radius: var(--radius-full); + font-size: var(--font-size-xs); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.025em; + flex-direction: column; + text-align: center; + min-width: 80px; +} + +.location-accuracy-badge small { + font-size: 0.6rem; + font-weight: 500; + margin-top: 2px; + opacity: 0.8; +} + +/* Location accuracy level colors - based on distance */ +.location-accuracy-badge.accuracy-excellent { + background: var(--success-light); + color: var(--success-color); + border: 1px solid rgba(5, 150, 105, 0.3); +} + +.location-accuracy-badge.accuracy-good { + background: #ecfdf5; + color: #059669; + border: 1px solid rgba(5, 150, 105, 0.2); +} + +.location-accuracy-badge.accuracy-fair { + background: var(--warning-light); + color: var(--warning-color); + border: 1px solid rgba(245, 158, 11, 0.3); +} + +.location-accuracy-badge.accuracy-poor { + background: var(--danger-light); + color: var(--danger-color); + border: 1px solid rgba(220, 38, 38, 0.3); +} + +.location-accuracy-badge.accuracy-unknown { + background: var(--gray-100); + color: var(--gray-500); + border: 1px solid rgba(107, 114, 128, 0.3); +} + +/* Distance ruler icon styling */ +.location-accuracy-badge .fa-ruler { + font-size: var(--font-size-xs); +} + +/* Enhanced table header for location accuracy */ +.attendance-table th:nth-child(9) { + min-width: 120px; + text-align: center; +} + +/* Responsive adjustments for location accuracy column */ +@media (max-width: 1200px) { + .location-accuracy-badge { + min-width: 70px; + font-size: 0.625rem; + } + + .location-accuracy-badge small { + display: none; + } +} + +@media (max-width: 768px) { + /* Hide location accuracy column on mobile to save space */ + .attendance-table th:nth-child(9), + .attendance-table td:nth-child(9) { + display: none; + } +} + +/* Enhanced modal details for location accuracy */ +.location-accuracy-details { + background: var(--gray-50); + padding: var(--spacing-4); + border-radius: var(--radius-lg); + margin: var(--spacing-3) 0; +} + +.location-accuracy-details h5 { + color: var(--gray-800); + margin-bottom: var(--spacing-2); + display: flex; + align-items: center; + gap: var(--spacing-2); +} + +.location-accuracy-details .fa-ruler { + color: var(--primary-color); +} + +.accuracy-comparison { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-4); + margin-top: var(--spacing-3); +} + +.accuracy-comparison .location-point { + text-align: center; + padding: var(--spacing-3); + background: var(--white); + border-radius: var(--radius); + border: 1px solid var(--gray-200); +} + +.accuracy-comparison .location-point i { + font-size: 1.5rem; + margin-bottom: var(--spacing-2); +} + +.accuracy-comparison .qr-point i { + color: var(--success-color); +} + +.accuracy-comparison .checkin-point i { + color: var(--info-color); +} + +.distance-display { + text-align: center; + margin: var(--spacing-4) 0; + padding: var(--spacing-4); + background: linear-gradient( + 135deg, + var(--primary-light), + var(--success-light) + ); + border-radius: var(--radius-lg); + border: 2px dashed var(--primary-color); +} + +.distance-display .distance-value { + font-size: var(--font-size-2xl); + font-weight: 700; + color: var(--primary-color); + margin-bottom: var(--spacing-1); +} + +.distance-display .distance-label { + color: var(--gray-600); + font-weight: 500; +} + +/* Print styles for location accuracy */ +@media print { + .location-accuracy-badge { + background: transparent !important; + border: 1px solid #ccc !important; + color: #000 !important; + } + + .location-accuracy-badge small { + display: inline; + } +} + /* Print styles for new columns */ @media print { .address-info, diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index 17a7e56..7415ff5 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -557,3 +557,441 @@ function debounce(func, wait) { timeout = setTimeout(later, wait); }; } + +// Enhanced JavaScript functions for location accuracy features + +function loadTableData() { + const table = document.getElementById("attendanceTable"); + if (table) { + const rows = table.querySelectorAll("tbody tr"); + attendanceData = Array.from(rows).map((row, index) => { + const cells = row.querySelectorAll("td"); + return { + id: row.dataset.recordId, + index: index + 1, + employeeId: cells[1] ? cells[1].textContent.trim() : "", + location: cells[2] ? cells[2].textContent.trim() : "", + event: cells[3] ? cells[3].textContent.trim() : "", + date: cells[4] ? cells[4].textContent.trim() : "", + time: cells[5] ? cells[5].textContent.trim() : "", + qr_address: cells[6] + ? cells[6].getAttribute("title") || cells[6].textContent.trim() + : "", + checked_in_address: cells[7] + ? cells[7].getAttribute("title") || cells[7].textContent.trim() + : "", + location_accuracy: cells[8] ? extractLocationAccuracy(cells[8]) : null, + accuracy_level: cells[8] + ? extractLocationAccuracyLevel(cells[8]) + : "unknown", + device: cells[9] + ? cells[9].getAttribute("title") || cells[9].textContent.trim() + : "", + has_location_data: cells[8] + ? !cells[8].textContent.includes("Unknown") + : false, + coordinates: extractCoordinates(cells[8]), + }; + }); + + filteredData = [...attendanceData]; + console.log( + `Loaded ${attendanceData.length} attendance records with location accuracy` + ); + } +} + +function extractLocationAccuracy(cell) { + const text = cell.textContent; + const match = text.match(/(\d+\.?\d*)\s*mi/); + return match ? parseFloat(match[1]) : null; +} + +function extractLocationAccuracyLevel(cell) { + const text = cell.textContent; + if (text.includes("excellent")) return "excellent"; + if (text.includes("good")) return "good"; + if (text.includes("fair")) return "fair"; + if (text.includes("poor")) return "poor"; + return "unknown"; +} + +function createTableRow(record, displayIndex) { + const row = document.createElement("tr"); + row.dataset.recordId = record.id; + + // Create location accuracy badge HTML + const locationAccuracyBadge = + record.location_accuracy !== null + ? ` + + ${record.location_accuracy.toFixed(3)} mi + (${record.accuracy_level}) + ` + : ` + + Unknown + `; + + row.innerHTML = ` + ${displayIndex} + +
+ ${record.employeeId} +
+ + +
+ + ${record.location} +
+ + +
+ ${record.event} +
+ + +
+ ${record.date} +
+ + +
+ ${record.time} +
+ + +
+ + + ${ + record.qr_address.length > 50 + ? record.qr_address.substring(0, 50) + "..." + : record.qr_address + } + +
+ + +
+ + + ${ + record.checked_in_address.length > 50 + ? record.checked_in_address.substring(0, 50) + "..." + : record.checked_in_address + } + +
+ + +
+ ${locationAccuracyBadge} +
+ + +
+ + + ${ + record.device.length > 20 + ? record.device.substring(0, 20) + "..." + : record.device + } + +
+ + +
+ + + +
+ + `; + + return row; +} + +function getSortKey(columnIndex) { + const sortKeys = [ + "index", + "employeeId", + "location", + "event", + "date", + "time", + "qr_address", + "checked_in_address", + "location_accuracy", + "device", + ]; + return sortKeys[columnIndex] || "index"; +} + +// Enhanced record details view with location accuracy +function viewRecordDetails(recordId) { + const record = attendanceData.find((r) => r.id == recordId); + if (!record) return; + + const modal = document.getElementById("recordModal"); + const modalTitle = document.getElementById("modalTitle"); + const modalBody = document.getElementById("modalBody"); + + if (!modal || !modalTitle || !modalBody) return; + + modalTitle.textContent = `Attendance Record - ${record.employeeId}`; + + // Build location accuracy details + const locationAccuracySection = + record.location_accuracy !== null + ? ` +
+
Location Accuracy Analysis
+
+
+ +
QR Code Location
+

${record.qr_address}

+
+
+ +
Check-in Location
+

${record.checked_in_address}

+
+
+
+
${record.location_accuracy.toFixed( + 3 + )} miles
+
Distance between locations
+
+ + + ${record.accuracy_level.toUpperCase()} ACCURACY + +
+
+
+ ` + : ` +
+
Location Accuracy Analysis
+
+
Unable to Calculate
+
Location accuracy could not be determined
+

+ This may be due to missing address information or geocoding limitations. +

+
+
+ `; + + modalBody.innerHTML = ` +
+
+

Employee Information

+
+ Employee ID: + ${record.employeeId} +
+
+ Check-in Date: + ${record.date} +
+
+ Check-in Time: + ${record.time} +
+
+ +
+

Location Information

+
+ Location Name: + ${record.location} +
+
+ Event: + ${record.event} +
+
+ QR Code Address: + ${record.qr_address} +
+
+ Check-in Address: + ${record.checked_in_address} +
+
+ +
+

Device Information

+
+ Device: + ${record.device} +
+
+ GPS Coordinates: + ${record.coordinates} +
+
+
+ + ${locationAccuracySection} + `; + + modal.style.display = "block"; +} + +// Enhanced sorting for location accuracy (numeric sorting) +function sortTable(columnIndex) { + if (sortColumn === columnIndex) { + sortDirection = sortDirection === "asc" ? "desc" : "asc"; + } else { + sortColumn = columnIndex; + sortDirection = "asc"; + } + + const sortKey = getSortKey(columnIndex); + + filteredData.sort((a, b) => { + let aVal = a[sortKey]; + let bVal = b[sortKey]; + + // Handle numeric values for location accuracy + if (columnIndex === 8 && aVal !== null && bVal !== null) { + aVal = parseFloat(aVal); + bVal = parseFloat(bVal); + } + + // Handle null values - put them at the end + if (aVal === null || aVal === undefined) { + return sortDirection === "asc" ? 1 : -1; + } + if (bVal === null || bVal === undefined) { + return sortDirection === "asc" ? -1 : 1; + } + + if (typeof aVal === "string") { + aVal = aVal.toLowerCase(); + bVal = bVal.toLowerCase(); + } + + let result; + if (aVal < bVal) result = -1; + else if (aVal > bVal) result = 1; + else result = 0; + + return sortDirection === "asc" ? result : -result; + }); + + updateTable(); + updateSortIndicators(columnIndex); +} + +// Enhanced statistics display for location accuracy +function updateFilterStats() { + const totalRecords = filteredData.length; + const recordsWithAccuracy = filteredData.filter( + (r) => r.location_accuracy !== null + ).length; + const avgAccuracy = + recordsWithAccuracy > 0 + ? filteredData + .filter((r) => r.location_accuracy !== null) + .reduce((sum, r) => sum + r.location_accuracy, 0) / + recordsWithAccuracy + : 0; + + console.log(`Filtered records: ${totalRecords}`); + console.log(`Records with location accuracy: ${recordsWithAccuracy}`); + console.log(`Average location accuracy: ${avgAccuracy.toFixed(3)} miles`); +} + +// Function to get accuracy level color for charts or displays +function getAccuracyLevelColor(level) { + const colors = { + excellent: "#059669", // green + good: "#10b981", // lighter green + fair: "#f59e0b", // yellow + poor: "#dc2626", // red + unknown: "#6b7280", // gray + }; + return colors[level] || colors["unknown"]; +} + +// Enhanced export function to include location accuracy +function exportAttendanceWithAccuracy() { + // Build CSV header with location accuracy + const headers = [ + "Employee ID", + "Location", + "Event", + "Date", + "Time", + "QR Address", + "Check-in Address", + "Location Accuracy (miles)", + "Accuracy Level", + "Device", + ]; + + // Build CSV rows + const rows = filteredData.map((record) => [ + record.employeeId, + record.location, + record.event, + record.date, + record.time, + record.qr_address, + record.checked_in_address, + record.location_accuracy !== null + ? record.location_accuracy.toFixed(3) + : "Unknown", + record.accuracy_level, + record.device, + ]); + + // Create CSV content + const csvContent = [headers, ...rows] + .map((row) => row.map((field) => `"${field}"`).join(",")) + .join("\n"); + + // Download CSV + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute( + "download", + `attendance_report_with_accuracy_${ + new Date().toISOString().split("T")[0] + }.csv` + ); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} diff --git a/templates/attendance_report.html b/templates/attendance_report.html index 39693df..165b433 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -18,7 +18,7 @@ Attendance Report -

Monitor and analyze staff attendance with location accuracy tracking

+

Monitor and analyze staff attendance with enhanced location tracking

@@ -80,7 +80,7 @@
- +
@@ -92,88 +92,110 @@
+ + {% if has_location_accuracy_feature %} +
+
+ +
+
+

{{ "%.2f"|format(stats.avg_location_accuracy or 0) }}mi

+

Avg. Location Accuracy

+ Distance precision +
+
+ {% else %}
-

{{ "%.1f"|format(stats.avg_accuracy or 0) }}m

-

Avg. Accuracy

- Location precision +

GPS

+

Tracking Mode

+ Current system
+ {% endif %}
-
-
-
- -
- - +
+
+

+ + Filter Records +

+
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- + +
@@ -212,9 +234,12 @@ Event Date Time - QR Address - Check-in Address - GPS Accuracy + QR Address + Check-in Address + + {% if has_location_accuracy_feature %}Location Accuracy{% else %}GPS Accuracy{% endif %} + + Device Actions @@ -249,7 +274,7 @@ {{ record.check_in_time.strftime('%H:%M') }}
- +
@@ -257,7 +282,7 @@
- +
@@ -266,21 +291,35 @@
-
- {% if record.accuracy %} - - - {{ "%.1f"|format(record.accuracy) }}m - ({{ record.accuracy_level }}) - - {% else %} - - - No GPS - - {% endif %} -
+ {% if has_location_accuracy_feature and record.location_accuracy %} + +
+ + + {{ "%.3f"|format(record.location_accuracy) }} mi + ({{ record.accuracy_level }}) + +
+ {% elif record.gps_accuracy %} + +
+ + + {{ "%.1f"|format(record.gps_accuracy) }}m + (gps) + +
+ {% else %} + +
+ + + Unknown + +
+ {% endif %}
@@ -297,12 +336,19 @@ title="View Details"> + {% if record.has_location_data %} + {% else %} + + {% endif %}