diff --git a/app.py b/app.py index a9509c1..7b6ef66 100644 --- a/app.py +++ b/app.py @@ -2,7 +2,7 @@ from flask import Flask, render_template, request, redirect, url_for, flash, ses from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash from functools import wraps -from datetime import datetime +from datetime import datetime, date, time, timedelta import qrcode import io import base64 @@ -212,6 +212,71 @@ def get_coordinates_from_address(address): print(f"āŒ Error geocoding address '{address}': {e}") return None, None +def get_coordinates_from_address_enhanced(address): + """ + Enhanced geocoding function with better error handling + Returns (latitude, longitude, accuracy_level) + """ + if not address or address.strip() == "": + print("āš ļø Empty address provided for geocoding") + return None, None, None + + address = address.strip() + print(f"šŸŒ Enhanced geocoding for: {address}") + + try: + # Primary geocoding using Nominatim (OpenStreetMap) + nominatim_url = "https://nominatim.openstreetmap.org/search" + params = { + 'q': address, + 'format': 'json', + 'limit': 1, + 'addressdetails': 1, + 'extratags': 1 + } + + headers = { + 'User-Agent': 'QR-Attendance-System/1.0 (Enhanced Location Accuracy)' + } + + response = requests.get(nominatim_url, params=params, headers=headers, timeout=10) + + if response.status_code == 200: + results = response.json() + + if results: + result = results[0] + lat = float(result['lat']) + lng = float(result['lon']) + + # Enhanced accuracy assessment + place_type = result.get('type', 'unknown') + osm_type = result.get('osm_type', 'unknown') + importance = float(result.get('importance', 0)) + + # More sophisticated accuracy determination + if place_type in ['house', 'building', 'shop', 'office'] or osm_type == 'way': + accuracy = 'excellent' + elif place_type in ['neighbourhood', 'suburb', 'quarter', 'residential']: + accuracy = 'good' + elif place_type in ['city', 'town', 'village'] and importance > 0.5: + accuracy = 'fair' + else: + accuracy = 'poor' + + print(f"āœ… Enhanced geocoding successful:") + print(f" Coordinates: {lat:.6f}, {lng:.6f}") + print(f" Accuracy: {accuracy}") + + return lat, lng, accuracy + + print(f"āš ļø No results from enhanced geocoding for: {address}") + return None, None, None + + except Exception as e: + print(f"āŒ Enhanced geocoding error: {e}") + return None, None, None + def geocode_address_enhanced(address): """ Enhanced geocoding function for new coordinate features @@ -270,33 +335,52 @@ def geocode_address_enhanced(address): def calculate_distance_miles(lat1, lng1, lat2, lng2): """ - Calculate the great circle distance between two points on Earth in miles - Using the Haversine formula + Enhanced Haversine formula to calculate distance between two points in miles + Improved with better precision and error handling """ if any(coord is None for coord in [lat1, lng1, lat2, lng2]): + print("āš ļø Missing coordinates for distance calculation") return None try: - # Convert decimal degrees to radians - lat1, lng1, lat2, lng2 = map(radians, [lat1, lng1, lat2, lng2]) + # Validate coordinate ranges + if not (-90 <= lat1 <= 90) or not (-90 <= lat2 <= 90): + print(f"āš ļø Invalid latitude values: {lat1}, {lat2}") + return None - # Haversine formula + if not (-180 <= lng1 <= 180) or not (-180 <= lng2 <= 180): + print(f"āš ļø Invalid longitude values: {lng1}, {lng2}") + return None + + # Convert decimal degrees to radians + lat1, lng1, lat2, lng2 = map(radians, [float(lat1), float(lng1), float(lat2), float(lng2)]) + + # Enhanced Haversine formula for better precision dlng = lng2 - lng1 dlat = lat2 - lat1 + + # Haversine calculation 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 + # Earth's radius in miles (more precise value) + r_miles = 3959.87433 - # Calculate the result + # Calculate distance with enhanced precision distance = c * r_miles - print(f"šŸ“ Calculated distance: {distance:.3f} miles") - return round(distance, 3) + # Round to 4 decimal places for better precision + distance = round(distance, 4) + + print(f"šŸ“ Enhanced distance calculation:") + print(f" Point 1: {lat1*180/3.14159:.6f}, {lng1*180/3.14159:.6f}") + print(f" Point 2: {lat2*180/3.14159:.6f}, {lng2*180/3.14159:.6f}") + print(f" Distance: {distance:.4f} miles") + + return distance except Exception as e: - print(f"āŒ Error calculating distance: {e}") + print(f"āŒ Error in enhanced distance calculation: {e}") return None def calculate_location_accuracy(qr_address, checkin_address, checkin_lat=None, checkin_lng=None): @@ -335,6 +419,96 @@ def calculate_location_accuracy(qr_address, checkin_address, checkin_lat=None, c return distance +def calculate_location_accuracy_enhanced(qr_address, checkin_address, checkin_lat=None, checkin_lng=None): + """ + ENHANCED location accuracy calculation comparing QR address with check-in location + This function provides improved precision and better error handling + + Parameters: + - qr_address: Address associated with the QR code + - checkin_address: Address where user checked in (from reverse geocoding) + - checkin_lat: GPS latitude from check-in (if available) + - checkin_lng: GPS longitude from check-in (if available) + + Returns: + - Distance in miles between QR location and check-in location + """ + print(f"\nšŸŽÆ ENHANCED LOCATION ACCURACY CALCULATION:") + print(f" QR Address: {qr_address}") + print(f" Check-in Address: {checkin_address}") + print(f" Check-in GPS: {checkin_lat}, {checkin_lng}") + print(f" Timestamp: {datetime.now()}") + + # Validate input parameters + if not qr_address or qr_address.strip() == "": + print(f"āŒ QR address is empty or invalid") + return None + + # Step 1: Get coordinates for QR address using enhanced geocoding + print(f"\nšŸ“ Step 1: Geocoding QR address...") + qr_lat, qr_lng, qr_accuracy = get_coordinates_from_address_enhanced(qr_address) + + if qr_lat is None or qr_lng is None: + print(f"āŒ Could not geocode QR address: {qr_address}") + return None + + print(f"āœ… QR location coordinates: {qr_lat:.6f}, {qr_lng:.6f} (accuracy: {qr_accuracy})") + + # Step 2: Determine check-in coordinates + print(f"\nšŸ“± Step 2: Determining check-in coordinates...") + + checkin_coords_lat = None + checkin_coords_lng = None + checkin_source = "unknown" + + # Priority 1: Use GPS coordinates if available and valid + if checkin_lat is not None and checkin_lng is not None: + try: + lat_val = float(checkin_lat) + lng_val = float(checkin_lng) + + # Validate GPS coordinates + if -90 <= lat_val <= 90 and -180 <= lng_val <= 180: + checkin_coords_lat = lat_val + checkin_coords_lng = lng_val + checkin_source = "gps" + print(f"āœ… Using GPS coordinates: {lat_val:.6f}, {lng_val:.6f}") + else: + print(f"āš ļø Invalid GPS coordinates: {lat_val}, {lng_val}") + except (ValueError, TypeError): + print(f"āš ļø Could not parse GPS coordinates") + + # Priority 2: Fallback to geocoding check-in address + if checkin_coords_lat is None and checkin_address: + print(f"šŸŒ Falling back to geocoding check-in address...") + checkin_coords_lat, checkin_coords_lng, checkin_accuracy = get_coordinates_from_address_enhanced(checkin_address) + if checkin_coords_lat is not None: + checkin_source = "address" + print(f"āœ… Using geocoded coordinates: {checkin_coords_lat:.6f}, {checkin_coords_lng:.6f} (accuracy: {checkin_accuracy})") + + # Check if we have valid coordinates for both locations + if checkin_coords_lat is None or checkin_coords_lng is None: + print(f"āŒ Could not determine check-in coordinates") + print(f" GPS: {checkin_lat}, {checkin_lng}") + print(f" Address: {checkin_address}") + return None + + # Step 3: Calculate enhanced distance + print(f"\nšŸ“ Step 3: Calculating enhanced distance...") + distance = calculate_distance_miles(qr_lat, qr_lng, checkin_coords_lat, checkin_coords_lng) + + if distance is not None: + print(f"āœ… Enhanced location accuracy calculated successfully!") + print(f" QR Location: {qr_lat:.6f}, {qr_lng:.6f}") + print(f" Check-in Location: {checkin_coords_lat:.6f}, {checkin_coords_lng:.6f}") + print(f" Source: {checkin_source}") + print(f" Distance: {distance:.4f} miles") + print(f" Accuracy Level: {get_location_accuracy_level_enhanced(distance)}") + else: + print(f"āŒ Failed to calculate distance") + + return distance + def generate_qr_url(name, qr_id): """Generate a unique URL for QR code destination""" # Clean the name for URL use @@ -1390,7 +1564,28 @@ def get_location_accuracy_level(location_accuracy): return 'fair' else: return 'poor' + +def get_location_accuracy_level_enhanced(location_accuracy): + """ + Enhanced function to categorize location accuracy with more granular levels + """ + if not location_accuracy or location_accuracy is None: + return 'unknown' + # More precise accuracy thresholds + if location_accuracy <= 0.05: # Within 264 feet (50 meters) + return 'excellent' + elif location_accuracy <= 0.1: # Within 528 feet (100 meters) + return 'very_good' + elif location_accuracy <= 0.25: # Within 0.25 mile (1320 feet) + return 'good' + elif location_accuracy <= 0.5: # Within 0.5 mile + return 'fair' + elif location_accuracy <= 1.0: # Within 1 mile + return 'poor' + else: # Greater than 1 mile + return 'very_poor' + @app.route('/qr-codes/create', methods=['GET', 'POST']) @login_required def create_qr_code(): @@ -1609,12 +1804,14 @@ def qr_destination(qr_url): return render_template('qr_not_found.html'), 500 @app.route('/qr//checkin', methods=['POST']) -def qr_checkin(qr_url): - """Enhanced staff check-in with location accuracy calculation""" +def qr_checkin_enhanced(qr_url): + """ + Enhanced staff check-in with improved location accuracy calculation + """ try: print(f"\nšŸš€ STARTING ENHANCED CHECK-IN PROCESS") print(f" QR URL: {qr_url}") - print(f" Time: {datetime.now()}") + print(f" Timestamp: {datetime.now()}") # Find QR code by URL qr_code = QRCode.query.filter_by(qr_url=qr_url, active_status=True).first() @@ -1628,7 +1825,7 @@ def qr_checkin(qr_url): 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}") + print(f" QR Address: {qr_code.location_address}") # Get form data employee_id = request.form.get('employee_id', '').strip() @@ -1639,89 +1836,35 @@ def qr_checkin(qr_url): 'message': 'Employee ID is required.' }), 400 - print(f"āœ… Employee ID: {employee_id}") - - # 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() - - 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}'") - - # Process location data with validation - lat_value = None - lng_value = None - acc_value = None - alt_value = None - - try: - if latitude and latitude not in ['', 'null', 'undefined']: - lat_value = float(latitude) - if not (-90 <= lat_value <= 90): - lat_value = None - print(f"āš ļø Invalid latitude range: {latitude}") - - if longitude and longitude not in ['', 'null', 'undefined']: - lng_value = float(longitude) - if not (-180 <= lng_value <= 180): - lng_value = None - print(f"āš ļø Invalid longitude range: {longitude}") - - if accuracy and accuracy not in ['', 'null', 'undefined']: - acc_value = float(accuracy) - if acc_value < 0: - acc_value = None - - if altitude and altitude not in ['', 'null', 'undefined']: - alt_value = float(altitude) - - except (ValueError, TypeError) as e: - print(f"āš ļø Error parsing location data: {e}") - - print(f"\nāœ… PROCESSED LOCATION DATA:") - print(f" Latitude: {lat_value}") - print(f" Longitude: {lng_value}") - print(f" Accuracy: {acc_value}") - print(f" Address: {address}") - - # 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" - - 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 + # Check for duplicate check-ins + today = date.today() existing_checkin = AttendanceData.query.filter_by( - employee_id=employee_id.upper(), qr_code_id=qr_code.id, + employee_id=employee_id.upper(), check_in_date=today ).first() if existing_checkin: - print(f"āŒ Duplicate check-in attempt") + print(f"āš ļø Duplicate check-in attempt for {employee_id}") 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")}.' + 'message': f'You have already checked in today at {existing_checkin.check_in_time.strftime("%H:%M")}.' }), 400 + # Process location data with enhanced validation + location_data = process_location_data_enhanced(request.form) + + # Get device and network info + user_agent_string = request.headers.get('User-Agent', '') + device_info = detect_device_info(user_agent_string) + client_ip = get_client_ip() + + print(f"šŸ“± Device Info: {device_info}") + print(f"🌐 IP Address: {client_ip}") + print(f"šŸ“ Location Data: {location_data}") + # Create attendance record - print(f"\nšŸ’¾ CREATING ATTENDANCE RECORD:") + print(f"\nšŸ’¾ CREATING ENHANCED ATTENDANCE RECORD:") attendance = AttendanceData( qr_code_id=qr_code.id, @@ -1732,43 +1875,44 @@ def qr_checkin(qr_url): 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, + latitude=location_data['latitude'], + longitude=location_data['longitude'], + accuracy=location_data['accuracy'], + altitude=location_data['altitude'], + location_source=location_data['source'], + address=location_data['address'], status='present' ) print(f"āœ… Created base attendance record") - # Calculate location accuracy - print(f"\nšŸŽÆ CALCULATING LOCATION ACCURACY...") + # ENHANCED LOCATION ACCURACY CALCULATION + print(f"\nšŸŽÆ CALCULATING ENHANCED LOCATION ACCURACY...") location_accuracy = None try: - location_accuracy = calculate_location_accuracy( + location_accuracy = calculate_location_accuracy_enhanced( qr_address=qr_code.location_address, - checkin_address=address, - checkin_lat=lat_value, - checkin_lng=lng_value + checkin_address=location_data['address'], + checkin_lat=location_data['latitude'], + checkin_lng=location_data['longitude'] ) if location_accuracy is not None: attendance.location_accuracy = location_accuracy - print(f"āœ… Set location accuracy: {location_accuracy} miles") + accuracy_level = get_location_accuracy_level_enhanced(location_accuracy) + print(f"āœ… Enhanced location accuracy set: {location_accuracy:.4f} miles ({accuracy_level})") else: - print(f"āš ļø Could not calculate location accuracy") + print(f"āš ļø Could not calculate enhanced location accuracy") except Exception as e: - print(f"āŒ Error calculating location accuracy: {e}") + print(f"āŒ Error in enhanced location accuracy calculation: {e}") # Save to database try: db.session.add(attendance) db.session.commit() - print(f"āœ… Successfully saved attendance record with ID: {attendance.id}") + print(f"āœ… Successfully saved enhanced attendance record with ID: {attendance.id}") except Exception as e: print(f"āŒ Database error: {e}") @@ -1778,37 +1922,35 @@ def qr_checkin(qr_url): 'message': 'Database error occurred. Please try again.' }), 500 - # Build success response + # Build enhanced success response response_data = { 'success': True, - 'message': 'Check-in successful!', + 'message': 'Enhanced check-in successful!', '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" + 'location_accuracy': f"{location_accuracy:.4f} miles" if location_accuracy else "Not calculated", + 'accuracy_level': get_location_accuracy_level_enhanced(location_accuracy) if location_accuracy else "unknown", + 'location_source': location_data['source'], + 'enhanced_features': True } } - print(f"āœ… CHECK-IN COMPLETED SUCCESSFULLY!") + print(f"āœ… ENHANCED 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'}") + print(f" Accuracy: {location_accuracy:.4f} miles" if location_accuracy else "Not calculated") + print(f" Time: {attendance.check_in_time}") return jsonify(response_data) except Exception as e: - db.session.rollback() - print(f"āŒ Unexpected error in check-in process: {e}") - import traceback - print(f"āŒ Traceback: {traceback.format_exc()}") - + print(f"āŒ Critical error in enhanced check-in: {e}") return jsonify({ 'success': False, - 'message': 'An unexpected error occurred during check-in. Please try again.' + 'message': 'An unexpected error occurred. Please try again.' }), 500 def process_location_data(location_data): @@ -1860,7 +2002,132 @@ def process_location_data(location_data): print(f"āš ļø Error processing location data: {e}") return processed + +def process_location_data_enhanced(form_data): + """ + Enhanced location data processing with better validation and error handling + """ + processed = { + 'latitude': None, + 'longitude': None, + 'accuracy': None, + 'altitude': None, + 'source': form_data.get('location_source', 'manual'), + 'address': None + } + try: + # Enhanced latitude validation + if form_data.get('latitude') and form_data['latitude'] not in ['null', '', 'undefined']: + lat = float(form_data['latitude']) + if -90 <= lat <= 90: + processed['latitude'] = round(lat, 6) # 6 decimal precision + else: + print(f"āš ļø Invalid latitude range: {lat}") + + # Enhanced longitude validation + if form_data.get('longitude') and form_data['longitude'] not in ['null', '', 'undefined']: + lng = float(form_data['longitude']) + if -180 <= lng <= 180: + processed['longitude'] = round(lng, 6) # 6 decimal precision + else: + print(f"āš ļø Invalid longitude range: {lng}") + + # Enhanced accuracy validation + if form_data.get('accuracy') and form_data['accuracy'] not in ['null', '', 'undefined']: + acc = float(form_data['accuracy']) + if 0 <= acc <= 50000: # Reasonable accuracy range in meters + processed['accuracy'] = round(acc, 1) + else: + print(f"āš ļø Invalid accuracy value: {acc}") + + # Enhanced altitude validation + if form_data.get('altitude') and form_data['altitude'] not in ['null', '', 'undefined']: + alt = float(form_data['altitude']) + if -1000 <= alt <= 10000: # Reasonable altitude range in meters + processed['altitude'] = round(alt, 1) + else: + print(f"āš ļø Invalid altitude value: {alt}") + + # Enhanced address processing + if form_data.get('address'): + address = form_data['address'].strip() + if len(address) > 0: + processed['address'] = address[:500] # Limit to 500 characters + + print(f"āœ… Enhanced location data processed: {processed}") + + except (ValueError, TypeError) as e: + print(f"āš ļø Error in enhanced location data processing: {e}") + + return processed + +def migrate_to_enhanced_location_accuracy(): + """ + Migration function to recalculate all existing records with enhanced accuracy + """ + try: + print("šŸ”„ Starting enhanced location accuracy migration...") + + # Get all records that need recalculation + records = db.session.execute(text(""" + SELECT ad.id, qc.location_address, ad.address, ad.latitude, ad.longitude, ad.location_accuracy + FROM attendance_data ad + LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id + WHERE qc.location_address IS NOT NULL + """)).fetchall() + + print(f"šŸ“Š Found {len(records)} records to process") + + updated_count = 0 + improved_count = 0 + + for record in records: + try: + # Calculate enhanced location accuracy + new_accuracy = calculate_location_accuracy_enhanced( + qr_address=record.location_address, + checkin_address=record.address, + checkin_lat=record.latitude, + checkin_lng=record.longitude + ) + + if new_accuracy is not None: + # Update the record + db.session.execute(text(""" + UPDATE attendance_data + SET location_accuracy = :accuracy + WHERE id = :record_id + """), { + 'accuracy': new_accuracy, + 'record_id': record.id + }) + + updated_count += 1 + + # Check if this is an improvement + if record.location_accuracy is None or abs(new_accuracy - (record.location_accuracy or 0)) > 0.001: + improved_count += 1 + print(f" āœ… Updated record {record.id}: {record.location_accuracy} → {new_accuracy:.4f} miles") + + except Exception as e: + print(f" āš ļø Error processing record {record.id}: {e}") + + # Commit all changes + db.session.commit() + + print(f"āœ… Enhanced migration completed!") + print(f" šŸ“Š Records processed: {len(records)}") + print(f" āœ… Records updated: {updated_count}") + print(f" šŸ“ˆ Records improved: {improved_count}") + + return True + + except Exception as e: + print(f"āŒ Enhanced migration failed: {e}") + db.session.rollback() + return False + @app.route('/qr-codes//toggle-status', methods=['POST']) @login_required def toggle_qr_status(qr_id):