Updated location address accuracy

This commit is contained in:
2025-08-07 21:29:50 -04:00
parent 75291274c3
commit 80a72d3458
3 changed files with 761 additions and 588 deletions
+82 -15
View File
@@ -663,10 +663,55 @@ def process_location_data(location_data):
return processed return processed
def reverse_geocode_coordinates(latitude, longitude):
"""
Convert GPS coordinates to human-readable address using reverse geocoding
Returns address string or None if failed
"""
if not latitude or not longitude:
return None
try:
print(f"🌍 Reverse geocoding coordinates: {latitude}, {longitude}")
# Using Nominatim (OpenStreetMap) reverse geocoding service
url = "https://nominatim.openstreetmap.org/reverse"
params = {
'lat': latitude,
'lon': longitude,
'format': 'json',
'addressdetails': 1,
'zoom': 18 # High detail level
}
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 'display_name' in data:
address = data['display_name']
print(f"✅ Reverse geocoded address: {address}")
return address
else:
print(f"⚠️ No address found for coordinates")
return None
else:
print(f"⚠️ Reverse geocoding API returned status: {response.status_code}")
return None
except Exception as e:
print(f"❌ Error in reverse geocoding: {e}")
return None
def process_location_data_enhanced(form_data): def process_location_data_enhanced(form_data):
""" """
Enhanced processing of location data from form submission Enhanced processing of location data from form submission
Validates and cleans location data for storage Validates and cleans location data for storage, including reverse geocoding
""" """
processed = { processed = {
'latitude': None, 'latitude': None,
@@ -707,13 +752,33 @@ def process_location_data_enhanced(form_data):
alt = float(form_data['altitude']) alt = float(form_data['altitude'])
processed['altitude'] = alt processed['altitude'] = alt
# Process address (limit length for database storage) # Process address - First check if address was provided
if form_data.get('address'): if form_data.get('address'):
address = form_data['address'].strip() address = form_data['address'].strip()
if address and address not in ['null', '', 'undefined']: if address and address not in ['null', '', 'undefined']:
# Check if the address is just coordinates (like "38.8104192000, -77.1850240000")
if re.match(r'^-?\d+\.\d+,?\s*-?\d+\.\d+$', address.replace(' ', '')):
print(f"🔍 Detected coordinate-format address: {address}")
# This is just coordinates, we need to reverse geocode
processed['address'] = None # Reset so reverse geocoding will trigger
else:
# This is a real address
processed['address'] = address[:500] # Limit to 500 characters processed['address'] = address[:500] # Limit to 500 characters
print(f"✅ Using provided address: {processed['address'][:100]}...")
print(f"📍 Processed location data:") # CRITICAL: If we have coordinates but no real address, perform reverse geocoding
if (processed['latitude'] is not None and processed['longitude'] is not None
and not processed['address']):
print(f"🌍 Performing reverse geocoding for coordinates: {processed['latitude']}, {processed['longitude']}")
reverse_geocoded_address = reverse_geocode_coordinates(processed['latitude'], processed['longitude'])
if reverse_geocoded_address:
processed['address'] = reverse_geocoded_address[:500]
print(f"✅ Reverse geocoded address: {processed['address']}")
else:
print(f"⚠️ Could not reverse geocode coordinates, keeping coordinates as fallback")
processed['address'] = f"{processed['latitude']:.10f}, {processed['longitude']:.10f}"
print(f"📍 Final processed location data:")
print(f" Coordinates: {processed['latitude']}, {processed['longitude']}") print(f" Coordinates: {processed['latitude']}, {processed['longitude']}")
print(f" GPS Accuracy: {processed['accuracy']}m") print(f" GPS Accuracy: {processed['accuracy']}m")
print(f" Source: {processed['source']}") print(f" Source: {processed['source']}")
@@ -1932,6 +1997,7 @@ def qr_checkin(qr_url):
""" """
Enhanced staff check-in with location accuracy calculation Enhanced staff check-in with location accuracy calculation
Allows multiple check-ins with minimum 30-minute intervals between them Allows multiple check-ins with minimum 30-minute intervals between them
PRESERVES coordinate-to-address conversion functionality
""" """
try: try:
print(f"\n🚀 STARTING ENHANCED CHECK-IN PROCESS") print(f"\n🚀 STARTING ENHANCED CHECK-IN PROCESS")
@@ -1980,23 +2046,23 @@ def qr_checkin(qr_url):
# Check if 30 minutes have passed since the last check-in # Check if 30 minutes have passed since the last check-in
if recent_checkin_datetime > thirty_minutes_ago: if recent_checkin_datetime > thirty_minutes_ago:
minutes_remaining = 30 - int((current_time - recent_checkin_datetime).total_seconds() / 60) minutes_remaining = 30 - int((current_time - recent_checkin_datetime).total_seconds() / 60)
print(f"⚠️ Too soon for another submission for {employee_id}") print(f"⚠️ Too soon for another check-in for {employee_id}")
print(f" Last submission: {recent_checkin.check_in_time.strftime('%H:%M')}") print(f" Last check-in: {recent_checkin.check_in_time.strftime('%H:%M')}")
print(f" Minutes remaining: {minutes_remaining}") print(f" Minutes remaining: {minutes_remaining}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': f'You can submit again in {minutes_remaining} minutes. Last submission was at {recent_checkin.check_in_time.strftime("%H:%M")}.' 'message': f'You can check in again in {minutes_remaining} minutes. Last check-in was at {recent_checkin.check_in_time.strftime("%H:%M")}.'
}), 400 }), 400
else: else:
print(f"✅ 30-minute interval satisfied. Allowing new check-in for {employee_id}") print(f"✅ 30-minute interval satisfied. Allowing new check-in for {employee_id}")
else: else:
print(f"✅ First submission today for {employee_id}") print(f"✅ First check-in today for {employee_id}")
# Process location data # PRESERVED: Process location data with coordinate-to-address conversion
location_data = process_location_data_enhanced(request.form) location_data = process_location_data_enhanced(request.form)
# Get device and network info # PRESERVED: Get device and network info
user_agent_string = request.headers.get('User-Agent', '') user_agent_string = request.headers.get('User-Agent', '')
device_info = detect_device_info(user_agent_string) device_info = detect_device_info(user_agent_string)
client_ip = get_client_ip() client_ip = get_client_ip()
@@ -2005,7 +2071,7 @@ def qr_checkin(qr_url):
print(f"🌐 IP Address: {client_ip}") print(f"🌐 IP Address: {client_ip}")
print(f"📍 Location Data: {location_data}") print(f"📍 Location Data: {location_data}")
# Create attendance record # PRESERVED: Create attendance record
print(f"\n💾 CREATING ATTENDANCE RECORD:") print(f"\n💾 CREATING ATTENDANCE RECORD:")
attendance = AttendanceData( attendance = AttendanceData(
@@ -2022,13 +2088,13 @@ def qr_checkin(qr_url):
accuracy=location_data['accuracy'], accuracy=location_data['accuracy'],
altitude=location_data['altitude'], altitude=location_data['altitude'],
location_source=location_data['source'], location_source=location_data['source'],
address=location_data['address'], address=location_data['address'], # This now includes converted address
status='present' status='present'
) )
print(f"✅ Created base attendance record") print(f"✅ Created base attendance record")
# Calculate location accuracy # PRESERVED: Calculate location accuracy
print(f"\n🎯 CALCULATING LOCATION ACCURACY...") print(f"\n🎯 CALCULATING LOCATION ACCURACY...")
location_accuracy = None location_accuracy = None
@@ -2050,13 +2116,13 @@ def qr_checkin(qr_url):
except Exception as e: except Exception as e:
print(f"❌ Error in location accuracy calculation: {e}") print(f"❌ Error in location accuracy calculation: {e}")
# Save to database # PRESERVED: Save to database
try: try:
db.session.add(attendance) db.session.add(attendance)
db.session.commit() db.session.commit()
print(f"✅ Successfully saved attendance record with ID: {attendance.id}") print(f"✅ Successfully saved attendance record with ID: {attendance.id}")
# Count total check-ins for today for this employee at this location # NEW: Count total check-ins for today for this employee at this location
today_checkin_count = AttendanceData.query.filter_by( today_checkin_count = AttendanceData.query.filter_by(
qr_code_id=qr_code.id, qr_code_id=qr_code.id,
employee_id=employee_id.upper(), employee_id=employee_id.upper(),
@@ -2073,7 +2139,7 @@ def qr_checkin(qr_url):
'message': 'Database error occurred.' 'message': 'Database error occurred.'
}), 500 }), 500
# Return success response # ENHANCED: Return success response with sequence information
response_data = { response_data = {
'success': True, 'success': True,
'message': f'Check-in successful! {checkin_sequence_text} for today.', 'message': f'Check-in successful! {checkin_sequence_text} for today.',
@@ -2101,6 +2167,7 @@ def qr_checkin(qr_url):
print(f" Employee: {attendance.employee_id}") print(f" Employee: {attendance.employee_id}")
print(f" Time: {attendance.check_in_time}") print(f" Time: {attendance.check_in_time}")
print(f" Location: {attendance.location_name}") print(f" Location: {attendance.location_name}")
print(f" Address: {attendance.address}")
print(f" Today's count: {today_checkin_count}") print(f" Today's count: {today_checkin_count}")
return jsonify(response_data), 200 return jsonify(response_data), 200
+27 -4
View File
@@ -45,7 +45,7 @@ const translations = {
error: "Error en el registro. Por favor intente de nuevo.", error: "Error en el registro. Por favor intente de nuevo.",
duplicate: "Ya se ha registrado hoy.", duplicate: "Ya se ha registrado hoy.",
tooSoon: "Por favor espere antes de registrarse nuevamente.", tooSoon: "Por favor espere antes de registrarse nuevamente.",
multipleSuccess: "Submitted successful!!", multipleSuccess: "Submitted successfully!!",
invalidId: "Por favor ingrese un ID de empleado válido.", invalidId: "Por favor ingrese un ID de empleado válido.",
locationError: "No se pudo obtener datos de ubicación.", locationError: "No se pudo obtener datos de ubicación.",
networkError: "Error de red. Verifique su conexión.", networkError: "Error de red. Verifique su conexión.",
@@ -225,7 +225,7 @@ function handleCheckinResponse(data) {
// ENHANCED SUCCESS HANDLING WITH MULTIPLE CHECK-IN INFO // ENHANCED SUCCESS HANDLING WITH MULTIPLE CHECK-IN INFO
function handleCheckinSuccess(data) { function handleCheckinSuccess(data) {
console.log("✅ Submitted successful!"); console.log("✅ Submitted successfully!");
const responseData = data.data || data || {}; const responseData = data.data || data || {};
const checkinCount = responseData.checkin_count_today || 1; const checkinCount = responseData.checkin_count_today || 1;
@@ -515,9 +515,32 @@ function handleLocationError(error) {
} }
function reverseGeocode(lat, lng) { function reverseGeocode(lat, lng) {
// This would typically use a geocoding service console.log(`🌍 Starting reverse geocoding for: ${lat}, ${lng}`);
// For now, just set a placeholder
// Using Nominatim (OpenStreetMap) reverse geocoding service
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&addressdetails=1&zoom=18`;
fetch(url, {
method: "GET",
headers: {
"User-Agent": "QR-Attendance-System/1.0",
},
})
.then((response) => response.json())
.then((data) => {
if (data && data.display_name) {
userLocation.address = data.display_name;
console.log(`✅ Reverse geocoded address: ${userLocation.address}`);
} else {
console.log(`⚠️ No address found, using coordinates as fallback`);
userLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(10)}`; userLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(10)}`;
}
})
.catch((error) => {
console.error(`❌ Reverse geocoding error:`, error);
// Fallback to coordinates if reverse geocoding fails
userLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(10)}`;
});
} }
// Language functionality remains unchanged // Language functionality remains unchanged
+123 -40
View File
@@ -1,10 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ qr_code.location_event }} - Check In</title> <title>{{ qr_code.location_event }} - Check In</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> <link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
rel="stylesheet"
/>
<style> <style>
/* PRESERVED: All existing styles remain unchanged */ /* PRESERVED: All existing styles remain unchanged */
:root { :root {
@@ -30,7 +33,7 @@
} }
body { body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
padding: 1rem; padding: 1rem;
@@ -93,7 +96,11 @@
.header-icon { .header-icon {
width: 80px; width: 80px;
height: 80px; height: 80px;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); background: linear-gradient(
135deg,
var(--primary-color),
var(--primary-hover)
);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -333,9 +340,17 @@
} }
@keyframes successPulse { @keyframes successPulse {
0% { transform: scale(0.8); opacity: 0; } 0% {
50% { transform: scale(1.1); } transform: scale(0.8);
100% { transform: scale(1); opacity: 1; } opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;
}
} }
.success-title { .success-title {
@@ -380,7 +395,11 @@
/* NEW: Enhanced styles for multiple check-in information */ /* NEW: Enhanced styles for multiple check-in information */
.checkin-sequence { .checkin-sequence {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); background: linear-gradient(
135deg,
var(--primary-color),
var(--primary-hover)
);
color: white; color: white;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 1rem; border-radius: 1rem;
@@ -465,12 +484,17 @@
transform: translateY(0); transform: translateY(0);
} }
</style> </style>
</head> </head>
<body> <body>
<div class="destination-container"> <div class="destination-container">
<!-- Language Selector --> <!-- Language Selector -->
<div class="language-selector"> <div class="language-selector">
<button id="languageToggle" class="language-toggle" type="button" aria-label="Switch Language"> <button
id="languageToggle"
class="language-toggle"
type="button"
aria-label="Switch Language"
>
<i class="fas fa-globe"></i> <i class="fas fa-globe"></i>
<span id="languageText">EN</span> <span id="languageText">EN</span>
</button> </button>
@@ -501,26 +525,43 @@
<div class="checkin-header"> <div class="checkin-header">
<h2> <h2>
<i class="fas fa-user-check"></i> <i class="fas fa-user-check"></i>
<span data-en="Attendance Tracking" data-es="Seguimiento de Asistencia">Attendance Tracking</span> <span
data-en="Attendance Tracking"
data-es="Seguimiento de Asistencia"
>Attendance Tracking</span
>
</h2> </h2>
<p data-en="Please enter your Employee ID to check in/out" data-es="Por favor ingrese su ID de empleado para registrar su entrada/salida">Please enter your Employee ID to check in/out</p> <p
data-en="Please enter your Employee ID to check in/out"
data-es="Por favor ingrese su ID de empleado para registrar su entrada/salida"
>
Please enter your Employee ID to check in/out
</p>
</div> </div>
<form id="checkinForm" class="checkin-form"> <form id="checkinForm" class="checkin-form">
<!-- HIDDEN LOCATION FIELDS --> <!-- HIDDEN LOCATION FIELDS -->
<input type="hidden" id="latitude" name="latitude"> <input type="hidden" id="latitude" name="latitude" />
<input type="hidden" id="longitude" name="longitude"> <input type="hidden" id="longitude" name="longitude" />
<input type="hidden" id="accuracy" name="accuracy"> <input type="hidden" id="accuracy" name="accuracy" />
<input type="hidden" id="altitude" name="altitude"> <input type="hidden" id="altitude" name="altitude" />
<input type="hidden" id="locationSource" name="location_source" value="manual"> <input
<input type="hidden" id="address" name="address"> type="hidden"
id="locationSource"
name="location_source"
value="manual"
/>
<input type="hidden" id="address" name="address" />
<div class="form-group"> <div class="form-group">
<label for="employee_id"> <label for="employee_id">
<i class="fas fa-id-badge"></i> <i class="fas fa-id-badge"></i>
<span data-en="Employee ID" data-es="ID de Empleado">Employee ID</span> <span data-en="Employee ID" data-es="ID de Empleado"
>Employee ID</span
>
</label> </label>
<input type="text" <input
type="text"
id="employee_id" id="employee_id"
name="employee_id" name="employee_id"
class="form-control" class="form-control"
@@ -528,7 +569,8 @@
data-es-placeholder="Ingrese su ID de Empleado" data-es-placeholder="Ingrese su ID de Empleado"
placeholder="Enter your Employee ID" placeholder="Enter your Employee ID"
autocomplete="off" autocomplete="off"
required> required
/>
</div> </div>
<button type="submit" id="submitCheckin" class="btn btn-primary"> <button type="submit" id="submitCheckin" class="btn btn-primary">
@@ -545,10 +587,18 @@
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
</div> </div>
<h2 class="success-title"> <h2 class="success-title">
<span data-en="Submitted Successful!" data-es="Enviado exitosamente!">Submitted Successful!</span> <span
data-en="Submitted Successfully!"
data-es="Enviado exitosamente!"
>Submitted Successfully!</span
>
</h2> </h2>
<p class="success-subtitle"> <p class="success-subtitle">
<span data-en="Your attendance has been recorded" data-es="Su asistencia ha sido registrada">Your attendance has been recorded</span> <span
data-en="Your attendance has been recorded"
data-es="Su asistencia ha sido registrada"
>Your attendance has been recorded</span
>
</p> </p>
</div> </div>
@@ -559,39 +609,72 @@
<div class="success-details"> <div class="success-details">
<div class="detail-row"> <div class="detail-row">
<span class="detail-label" data-en="Employee ID" data-es="ID Empleado">Employee ID</span> <span
class="detail-label"
data-en="Employee ID"
data-es="ID Empleado"
>Employee ID</span
>
<span class="detail-value" id="successEmployeeId">-</span> <span class="detail-value" id="successEmployeeId">-</span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span class="detail-label" data-en="Location/Building" data-es="Ubicación/Edificio">Location/Building</span> <span
class="detail-label"
data-en="Location/Building"
data-es="Ubicación/Edificio"
>Location/Building</span
>
<span class="detail-value" id="successLocation">-</span> <span class="detail-value" id="successLocation">-</span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span class="detail-label" data-en="Event" data-es="Evento">Event</span> <span class="detail-label" data-en="Event" data-es="Evento"
>Event</span
>
<span class="detail-value" id="successEvent">-</span> <span class="detail-value" id="successEvent">-</span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span class="detail-label" data-en="Submission Time" data-es="Hora de presentación">Submission Time</span> <span
class="detail-label"
data-en="Submission Time"
data-es="Hora de presentación"
>Submission Time</span
>
<span class="detail-value" id="successCheckInTime">-</span> <span class="detail-value" id="successCheckInTime">-</span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span class="detail-label" data-en="Date" data-es="Fecha">Date</span> <span class="detail-label" data-en="Date" data-es="Fecha"
>Date</span
>
<span class="detail-value" id="successCheckInDate">-</span> <span class="detail-value" id="successCheckInDate">-</span>
</div> </div>
<div class="detail-row" style="display: none;" id="deviceInfoRow"> <div class="detail-row" style="display: none" id="deviceInfoRow">
<span class="detail-label" data-en="Device" data-es="Dispositivo">Device</span> <span class="detail-label" data-en="Device" data-es="Dispositivo"
>Device</span
>
<span class="detail-value" id="successDeviceInfo">-</span> <span class="detail-value" id="successDeviceInfo">-</span>
</div> </div>
<div class="detail-row" style="display: none;" id="coordinatesRow"> <div class="detail-row" style="display: none" id="coordinatesRow">
<span class="detail-label" data-en="Coordinates" data-es="Coordenadas">Coordinates</span> <span
class="detail-label"
data-en="Coordinates"
data-es="Coordenadas"
>Coordinates</span
>
<span class="detail-value" id="successCoordinates">-</span> <span class="detail-value" id="successCoordinates">-</span>
</div> </div>
<div class="detail-row" style="display: none;" id="addressRow"> <div class="detail-row" style="display: none" id="addressRow">
<span class="detail-label" data-en="Address" data-es="Dirección">Address</span> <span class="detail-label" data-en="Address" data-es="Dirección"
>Address</span
>
<span class="detail-value" id="successAddress">-</span> <span class="detail-value" id="successAddress">-</span>
</div> </div>
<div class="detail-row" style="display: none;" id="accuracyRow"> <div class="detail-row" style="display: none" id="accuracyRow">
<span class="detail-label" data-en="Location Accuracy" data-es="Precisión de Ubicación">Location Accuracy</span> <span
class="detail-label"
data-en="Location Accuracy"
data-es="Precisión de Ubicación"
>Location Accuracy</span
>
<span class="detail-value" id="successLocationAccuracy">-</span> <span class="detail-value" id="successLocationAccuracy">-</span>
</div> </div>
</div> </div>
@@ -602,5 +685,5 @@
<!-- JavaScript --> <!-- JavaScript -->
<script src="{{ url_for('static', filename='js/qr_destination.js') }}"></script> <script src="{{ url_for('static', filename='js/qr_destination.js') }}"></script>
</body> </body>
</html> </html>