Updated location address accuracy
This commit is contained in:
@@ -663,10 +663,55 @@ def process_location_data(location_data):
|
||||
|
||||
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):
|
||||
"""
|
||||
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 = {
|
||||
'latitude': None,
|
||||
@@ -707,13 +752,33 @@ def process_location_data_enhanced(form_data):
|
||||
alt = float(form_data['altitude'])
|
||||
processed['altitude'] = alt
|
||||
|
||||
# Process address (limit length for database storage)
|
||||
# Process address - First check if address was provided
|
||||
if form_data.get('address'):
|
||||
address = form_data['address'].strip()
|
||||
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
|
||||
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" GPS Accuracy: {processed['accuracy']}m")
|
||||
print(f" Source: {processed['source']}")
|
||||
@@ -1932,6 +1997,7 @@ def qr_checkin(qr_url):
|
||||
"""
|
||||
Enhanced staff check-in with location accuracy calculation
|
||||
Allows multiple check-ins with minimum 30-minute intervals between them
|
||||
PRESERVES coordinate-to-address conversion functionality
|
||||
"""
|
||||
try:
|
||||
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
|
||||
if recent_checkin_datetime > thirty_minutes_ago:
|
||||
minutes_remaining = 30 - int((current_time - recent_checkin_datetime).total_seconds() / 60)
|
||||
print(f"⚠️ Too soon for another submission for {employee_id}")
|
||||
print(f" Last submission: {recent_checkin.check_in_time.strftime('%H:%M')}")
|
||||
print(f"⚠️ Too soon for another check-in for {employee_id}")
|
||||
print(f" Last check-in: {recent_checkin.check_in_time.strftime('%H:%M')}")
|
||||
print(f" Minutes remaining: {minutes_remaining}")
|
||||
|
||||
return jsonify({
|
||||
'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
|
||||
else:
|
||||
print(f"✅ 30-minute interval satisfied. Allowing new check-in for {employee_id}")
|
||||
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)
|
||||
|
||||
# Get device and network info
|
||||
# PRESERVED: 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()
|
||||
@@ -2005,7 +2071,7 @@ def qr_checkin(qr_url):
|
||||
print(f"🌐 IP Address: {client_ip}")
|
||||
print(f"📍 Location Data: {location_data}")
|
||||
|
||||
# Create attendance record
|
||||
# PRESERVED: Create attendance record
|
||||
print(f"\n💾 CREATING ATTENDANCE RECORD:")
|
||||
|
||||
attendance = AttendanceData(
|
||||
@@ -2022,13 +2088,13 @@ def qr_checkin(qr_url):
|
||||
accuracy=location_data['accuracy'],
|
||||
altitude=location_data['altitude'],
|
||||
location_source=location_data['source'],
|
||||
address=location_data['address'],
|
||||
address=location_data['address'], # This now includes converted address
|
||||
status='present'
|
||||
)
|
||||
|
||||
print(f"✅ Created base attendance record")
|
||||
|
||||
# Calculate location accuracy
|
||||
# PRESERVED: Calculate location accuracy
|
||||
print(f"\n🎯 CALCULATING LOCATION ACCURACY...")
|
||||
location_accuracy = None
|
||||
|
||||
@@ -2050,13 +2116,13 @@ def qr_checkin(qr_url):
|
||||
except Exception as e:
|
||||
print(f"❌ Error in location accuracy calculation: {e}")
|
||||
|
||||
# Save to database
|
||||
# PRESERVED: Save to database
|
||||
try:
|
||||
db.session.add(attendance)
|
||||
db.session.commit()
|
||||
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(
|
||||
qr_code_id=qr_code.id,
|
||||
employee_id=employee_id.upper(),
|
||||
@@ -2073,7 +2139,7 @@ def qr_checkin(qr_url):
|
||||
'message': 'Database error occurred.'
|
||||
}), 500
|
||||
|
||||
# Return success response
|
||||
# ENHANCED: Return success response with sequence information
|
||||
response_data = {
|
||||
'success': True,
|
||||
'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" Time: {attendance.check_in_time}")
|
||||
print(f" Location: {attendance.location_name}")
|
||||
print(f" Address: {attendance.address}")
|
||||
print(f" Today's count: {today_checkin_count}")
|
||||
|
||||
return jsonify(response_data), 200
|
||||
|
||||
@@ -45,7 +45,7 @@ const translations = {
|
||||
error: "Error en el registro. Por favor intente de nuevo.",
|
||||
duplicate: "Ya se ha registrado hoy.",
|
||||
tooSoon: "Por favor espere antes de registrarse nuevamente.",
|
||||
multipleSuccess: "Submitted successful!!",
|
||||
multipleSuccess: "Submitted successfully!!",
|
||||
invalidId: "Por favor ingrese un ID de empleado válido.",
|
||||
locationError: "No se pudo obtener datos de ubicación.",
|
||||
networkError: "Error de red. Verifique su conexión.",
|
||||
@@ -225,7 +225,7 @@ function handleCheckinResponse(data) {
|
||||
|
||||
// ENHANCED SUCCESS HANDLING WITH MULTIPLE CHECK-IN INFO
|
||||
function handleCheckinSuccess(data) {
|
||||
console.log("✅ Submitted successful!");
|
||||
console.log("✅ Submitted successfully!");
|
||||
|
||||
const responseData = data.data || data || {};
|
||||
const checkinCount = responseData.checkin_count_today || 1;
|
||||
@@ -515,9 +515,32 @@ function handleLocationError(error) {
|
||||
}
|
||||
|
||||
function reverseGeocode(lat, lng) {
|
||||
// This would typically use a geocoding service
|
||||
// For now, just set a placeholder
|
||||
console.log(`🌍 Starting reverse geocoding for: ${lat}, ${lng}`);
|
||||
|
||||
// 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)}`;
|
||||
}
|
||||
})
|
||||
.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
|
||||
|
||||
+123
-40
@@ -1,10 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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>
|
||||
/* PRESERVED: All existing styles remain unchanged */
|
||||
:root {
|
||||
@@ -30,7 +33,7 @@
|
||||
}
|
||||
|
||||
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%);
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
@@ -93,7 +96,11 @@
|
||||
.header-icon {
|
||||
width: 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%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -333,9 +340,17 @@
|
||||
}
|
||||
|
||||
@keyframes successPulse {
|
||||
0% { transform: scale(0.8); opacity: 0; }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.success-title {
|
||||
@@ -380,7 +395,11 @@
|
||||
|
||||
/* NEW: Enhanced styles for multiple check-in information */
|
||||
.checkin-sequence {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-color),
|
||||
var(--primary-hover)
|
||||
);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 1rem;
|
||||
@@ -465,12 +484,17 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
<body>
|
||||
<div class="destination-container">
|
||||
<!-- 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>
|
||||
<span id="languageText">EN</span>
|
||||
</button>
|
||||
@@ -501,26 +525,43 @@
|
||||
<div class="checkin-header">
|
||||
<h2>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<form id="checkinForm" class="checkin-form">
|
||||
<!-- HIDDEN LOCATION FIELDS -->
|
||||
<input type="hidden" id="latitude" name="latitude">
|
||||
<input type="hidden" id="longitude" name="longitude">
|
||||
<input type="hidden" id="accuracy" name="accuracy">
|
||||
<input type="hidden" id="altitude" name="altitude">
|
||||
<input type="hidden" id="locationSource" name="location_source" value="manual">
|
||||
<input type="hidden" id="address" name="address">
|
||||
<input type="hidden" id="latitude" name="latitude" />
|
||||
<input type="hidden" id="longitude" name="longitude" />
|
||||
<input type="hidden" id="accuracy" name="accuracy" />
|
||||
<input type="hidden" id="altitude" name="altitude" />
|
||||
<input
|
||||
type="hidden"
|
||||
id="locationSource"
|
||||
name="location_source"
|
||||
value="manual"
|
||||
/>
|
||||
<input type="hidden" id="address" name="address" />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="employee_id">
|
||||
<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>
|
||||
<input type="text"
|
||||
<input
|
||||
type="text"
|
||||
id="employee_id"
|
||||
name="employee_id"
|
||||
class="form-control"
|
||||
@@ -528,7 +569,8 @@
|
||||
data-es-placeholder="Ingrese su ID de Empleado"
|
||||
placeholder="Enter your Employee ID"
|
||||
autocomplete="off"
|
||||
required>
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitCheckin" class="btn btn-primary">
|
||||
@@ -545,10 +587,18 @@
|
||||
<i class="fas fa-check"></i>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -559,39 +609,72 @@
|
||||
|
||||
<div class="success-details">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="detail-row" style="display: none;" id="deviceInfoRow">
|
||||
<span class="detail-label" data-en="Device" data-es="Dispositivo">Device</span>
|
||||
<div class="detail-row" style="display: none" id="deviceInfoRow">
|
||||
<span class="detail-label" data-en="Device" data-es="Dispositivo"
|
||||
>Device</span
|
||||
>
|
||||
<span class="detail-value" id="successDeviceInfo">-</span>
|
||||
</div>
|
||||
<div class="detail-row" style="display: none;" id="coordinatesRow">
|
||||
<span class="detail-label" data-en="Coordinates" data-es="Coordenadas">Coordinates</span>
|
||||
<div class="detail-row" style="display: none" id="coordinatesRow">
|
||||
<span
|
||||
class="detail-label"
|
||||
data-en="Coordinates"
|
||||
data-es="Coordenadas"
|
||||
>Coordinates</span
|
||||
>
|
||||
<span class="detail-value" id="successCoordinates">-</span>
|
||||
</div>
|
||||
<div class="detail-row" style="display: none;" id="addressRow">
|
||||
<span class="detail-label" data-en="Address" data-es="Dirección">Address</span>
|
||||
<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-value" id="successAddress">-</span>
|
||||
</div>
|
||||
<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>
|
||||
<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-value" id="successLocationAccuracy">-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -602,5 +685,5 @@
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="{{ url_for('static', filename='js/qr_destination.js') }}"></script>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user