From 455105f8327fff45a2eeca772049e68038c8d5df Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Thu, 14 Aug 2025 22:05:06 -0400 Subject: [PATCH] Fix attendance report filter button issues --- app.py | 159 +++++++------- static/js/attendance_report.js | 344 +++++++++++++++++++------------ templates/attendance_report.html | 26 ++- 3 files changed, 314 insertions(+), 215 deletions(-) diff --git a/app.py b/app.py index 12ba0e5..b2436d4 100644 --- a/app.py +++ b/app.py @@ -1233,7 +1233,7 @@ def login(): try: # Find user (case-insensitive username) user = User.query.filter( - User.username.ilike(username), + User.username.like(username), User.active_status == True ).first() @@ -2405,7 +2405,7 @@ def create_qr_code(): 'create', additional_info={'location_event': location_event} ) - + # Success message with coordinates info project_info = f" in project '{project.name}'" if project else "" coord_info = f" with coordinates ({new_qr_code.coordinates_display})" if has_coordinates else "" @@ -2997,6 +2997,12 @@ def attendance_report(): try: print("📊 Loading attendance report...") + # Log attendance report access + try: + logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed attendance report") + except Exception as log_error: + print(f"⚠️ Logging error (non-critical): {log_error}") + # Check if location_accuracy column exists has_location_accuracy = check_location_accuracy_column_exists() print(f"🔍 Location accuracy column exists: {has_location_accuracy}") @@ -3066,17 +3072,18 @@ def attendance_report(): # Apply location filter if location_filter: - conditions.append("ad.location_name ILIKE :location") + conditions.append("ad.location_name LIKE :location") params['location'] = f"%{location_filter}%" # Apply employee filter if employee_filter: - conditions.append("ad.employee_id ILIKE :employee") + conditions.append("ad.employee_id LIKE :employee") params['employee'] = f"%{employee_filter}%" + # FIXED: Apply project filter using SQL approach (not ORM) if project_filter: - # Join with QRCode and filter by project_id - query = query.join(QRCode, AttendanceData.qr_code_id == QRCode.id).filter(QRCode.project_id == int(project_filter)) + conditions.append("qc.project_id = :project_id") + params['project_id'] = int(project_filter) print(f"📊 Applied project filter: {project_filter}") # Add conditions to query @@ -3100,80 +3107,78 @@ def attendance_report(): # Safe attribute access with fallbacks location_accuracy = getattr(record, 'location_accuracy', None) gps_accuracy = getattr(record, 'gps_accuracy', None) - qr_address = getattr(record, 'qr_address', None) - # Handle location accuracy for address display logic - if location_accuracy is not None and location_accuracy != "None": - try: - accuracy_value = float(location_accuracy) if isinstance(location_accuracy, str) else location_accuracy - checked_in_address = qr_address if (accuracy_value <= 0.5) else getattr(record, 'checked_in_address', None) - except (ValueError, TypeError): - checked_in_address = getattr(record, 'checked_in_address', None) - else: - checked_in_address = getattr(record, 'checked_in_address', None) - - # CRITICAL FIX: Properly handle check_in_time formatting - check_in_time_value = record.check_in_time - - # Handle different possible types for check_in_time - if isinstance(check_in_time_value, timedelta): - # Convert timedelta to time object - total_seconds = int(check_in_time_value.total_seconds()) - hours = total_seconds // 3600 - minutes = (total_seconds % 3600) // 60 - seconds = total_seconds % 60 - formatted_time = time(hours % 24, minutes, seconds) - print(f"⚠️ Converted timedelta to time: {check_in_time_value} -> {formatted_time}") - elif isinstance(check_in_time_value, time): - # Already a time object, use as-is - formatted_time = check_in_time_value - elif isinstance(check_in_time_value, datetime): - # Extract time component from datetime - formatted_time = check_in_time_value.time() - elif isinstance(check_in_time_value, str): - # Try to parse string to time - try: - formatted_time = datetime.strptime(check_in_time_value, '%H:%M:%S').time() - except ValueError: - try: - formatted_time = datetime.strptime(check_in_time_value, '%H:%M').time() - except ValueError: - # Fallback to current time if parsing fails - formatted_time = datetime.now().time() - print(f"⚠️ Could not parse time string: {check_in_time_value}, using current time") - else: - # Fallback to current time for any other type - formatted_time = datetime.now().time() - print(f"⚠️ Unexpected check_in_time type: {type(check_in_time_value)}, using current time") - - # Create the record dictionary with properly formatted time - record_dict = { + # Create processed record with calculated fields + processed_record = { 'id': record.id, - 'employee_id': record.employee_id, + 'employee_id': record.employee_id or 'Unknown', 'check_in_date': record.check_in_date, - 'check_in_time': formatted_time, # Now guaranteed to be a time object - 'location_name': record.location_name, - 'location_event': getattr(record, 'location_event', ''), - 'qr_address': qr_address or 'Not available', - 'checked_in_address': checked_in_address or 'Location not captured', - 'device_info': getattr(record, 'device_info', ''), + 'check_in_time': record.check_in_time, + 'location_name': record.location_name or 'Unknown Location', + 'location_event': getattr(record, 'location_event', None) or 'Check In', + 'qr_address': getattr(record, 'qr_address', None) or 'N/A', + 'checked_in_address': getattr(record, 'checked_in_address', None) or 'N/A', + 'latitude': record.latitude, + 'longitude': record.longitude, '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:.10f}, {record.longitude:.10f}" if record.latitude and record.longitude else "No GPS data", - 'has_location_accuracy_feature': has_location_accuracy + 'device_info': getattr(record, 'device_info', None) or 'Unknown Device', } - processed_records.append(record_dict) - - print(f"✅ Processed {len(processed_records)} records") + + # FIXED: Add address display logic based on location accuracy + # If location accuracy <= 0.5 miles, display QR address; otherwise display actual check-in address + if location_accuracy is not None: + try: + accuracy_value = float(location_accuracy) if isinstance(location_accuracy, str) else location_accuracy + if accuracy_value <= 0.5: + # High accuracy - use QR code address + processed_record['display_address'] = getattr(record, 'qr_address', None) or 'N/A' + processed_record['address_source'] = 'qr' + print(f"📍 Using QR address for employee {record.employee_id} (accuracy: {accuracy_value:.3f} miles)") + else: + # Lower accuracy - use actual check-in address + processed_record['display_address'] = getattr(record, 'checked_in_address', None) or 'N/A' + processed_record['address_source'] = 'checkin' + print(f"📍 Using check-in address for employee {record.employee_id} (accuracy: {accuracy_value:.3f} miles)") + except (ValueError, TypeError): + # If accuracy can't be converted to float, use check-in address + processed_record['display_address'] = getattr(record, 'checked_in_address', None) or 'N/A' + processed_record['address_source'] = 'checkin' + else: + # No location accuracy data - use actual check-in address + processed_record['display_address'] = getattr(record, 'checked_in_address', None) or 'N/A' + processed_record['address_source'] = 'checkin' + + # Add accuracy level calculation if location_accuracy exists + if location_accuracy is not None: + if location_accuracy <= 10: + processed_record['accuracy_level'] = 'High' + elif location_accuracy <= 50: + processed_record['accuracy_level'] = 'Medium' + else: + processed_record['accuracy_level'] = 'Low' + else: + processed_record['accuracy_level'] = 'Unknown' + + # Add formatted datetime for display + try: + if record.check_in_date and record.check_in_time: + datetime_obj = datetime.combine(record.check_in_date, record.check_in_time) + processed_record['formatted_datetime'] = datetime_obj.strftime('%m/%d/%Y %I:%M %p') + else: + processed_record['formatted_datetime'] = 'Invalid Date/Time' + except Exception as e: + print(f"⚠️ Error formatting datetime for record {record.id}: {e}") + processed_record['formatted_datetime'] = 'Error' + + processed_records.append(processed_record) # Get unique locations for filter dropdown try: locations_query = db.session.execute(text(""" SELECT DISTINCT location_name FROM attendance_data - WHERE location_name IS NOT NULL + WHERE location_name IS NOT NULL ORDER BY location_name """)) locations = [row[0] for row in locations_query.fetchall()] @@ -3182,7 +3187,7 @@ def attendance_report(): print(f"⚠️ Error loading locations: {e}") locations = [] - # Update the locations query to get only active projects for the dropdown + # Update the projects query to get only active projects for the dropdown try: projects = db.session.execute(text(""" SELECT p.id, p.name, COUNT(DISTINCT ad.id) as attendance_count @@ -3242,7 +3247,7 @@ def attendance_report(): })() # Add today's date for template - today_date = datetime.now().strftime('%m/%d/%Y') + today_date = datetime.now().strftime('%Y-%m-%d') current_date_formatted = datetime.now().strftime('%B %d') print("✅ Rendering attendance report template") @@ -3267,6 +3272,12 @@ def attendance_report(): import traceback print(f"❌ Traceback: {traceback.format_exc()}") + # Log the error + try: + logger_handler.log_database_error('attendance_report', e) + except Exception as log_error: + print(f"⚠️ Additional logging error: {log_error}") + flash('Error loading attendance report. Please check the server logs for details.', 'error') return redirect(url_for('dashboard')) @@ -3693,12 +3704,12 @@ def create_excel_export(selected_columns, column_names, filters): # Apply location filter if filters.get('location_filter'): - query = query.filter(AttendanceData.location_name.ilike(f"%{filters['location_filter']}%")) + query = query.filter(AttendanceData.location_name.like(f"%{filters['location_filter']}%")) print(f"📊 Applied location filter: {filters['location_filter']}") # Apply employee filter if filters.get('employee_filter'): - query = query.filter(AttendanceData.employee_id.ilike(f"%{filters['employee_filter']}%")) + query = query.filter(AttendanceData.employee_id.like(f"%{filters['employee_filter']}%")) print(f"📊 Applied employee filter: {filters['employee_filter']}") # Apply project filter @@ -3856,12 +3867,12 @@ def create_excel_export_ordered(selected_columns, column_names, filters): # Apply location filter if filters.get('location_filter'): - query = query.filter(AttendanceData.location_name.ilike(f"%{filters['location_filter']}%")) + query = query.filter(AttendanceData.location_name.like(f"%{filters['location_filter']}%")) print(f"📊 Applied location filter: {filters['location_filter']}") # Apply employee filter if filters.get('employee_filter'): - query = query.filter(AttendanceData.employee_id.ilike(f"%{filters['employee_filter']}%")) + query = query.filter(AttendanceData.employee_id.like(f"%{filters['employee_filter']}%")) print(f"📊 Applied employee filter: {filters['employee_filter']}") # Execute query and get results diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index a4c8a8b..69a692a 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -43,6 +43,31 @@ function loadTableData() { const rows = table.querySelectorAll("tbody tr"); attendanceData = Array.from(rows).map((row, index) => { const cells = row.querySelectorAll("td"); + + // Debug: Log the actual cell content + if (index < 3) { + // Only log first 3 rows for debugging + console.log(`=== DEBUGGING ROW ${index + 1} ===`); + console.log( + `Cell 7 (check-in address):`, + cells[7] ? cells[7].innerHTML : "NOT FOUND" + ); + console.log( + `Cell 8 (accuracy):`, + cells[8] ? cells[8].innerHTML : "NOT FOUND" + ); + + if (cells[8]) { + const accuracyText = cells[8].textContent; + console.log(`Accuracy text:`, accuracyText); + + const milesMatch = accuracyText.match(/(\d+\.?\d*)\s*mi/); + const metersMatch = accuracyText.match(/(\d+\.?\d*)\s*m/); + console.log(`Miles match:`, milesMatch); + console.log(`Meters match:`, metersMatch); + } + } + return { id: row.dataset.recordId, index: index + 1, @@ -57,13 +82,16 @@ function loadTableData() { checked_in_address: cells[7] ? cells[7].getAttribute("title") || cells[7].textContent.trim() : "", - accuracy: cells[8] ? extractAccuracyValue(cells[8]) : null, - accuracy_level: cells[8] ? extractAccuracyLevel(cells[8]) : "unknown", + // FIXED: Extract location accuracy for address display logic + 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("No GPS") + ? !cells[8].textContent.includes("Unknown") : false, coordinates: extractCoordinates(cells[8]), }; @@ -71,6 +99,29 @@ function loadTableData() { filteredData = [...attendanceData]; console.log(`Loaded ${attendanceData.length} attendance records`); + + // Debug log for location accuracy data + const recordsWithAccuracy = attendanceData.filter( + (r) => r.location_accuracy !== null + ); + console.log( + `Records with location accuracy: ${recordsWithAccuracy.length}` + ); + if (recordsWithAccuracy.length > 0) { + console.log( + `Sample records with accuracy:`, + recordsWithAccuracy.slice(0, 3).map((r) => ({ + employeeId: r.employeeId, + location_accuracy: r.location_accuracy, + accuracy_level: r.accuracy_level, + qr_address: r.qr_address, + checked_in_address: r.checked_in_address, + })) + ); + } + + // Log all first 3 records for debugging + console.log("First 3 attendance records:", attendanceData.slice(0, 3)); } } @@ -289,123 +340,6 @@ function updateTable() { updateFilterStats(); } -function createTableRow(record, displayIndex) { - const row = document.createElement("tr"); - row.dataset.recordId = record.id; - - // Create accuracy badge HTML - const accuracyBadge = - record.accuracy !== null - ? ` - - ${record.accuracy.toFixed(1)}m - (${record.accuracy_level}) - ` - : ` - - No GPS - `; - - 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 - } - -
- - -
- ${accuracyBadge} -
- - -
- - - ${ - record.device.length > 20 - ? record.device.substring(0, 20) + "..." - : record.device - } - -
- - -
- ${ - hasEditPermission - ? ` - - - ` - : ` - - - - ` - } -
- - `; - - return row; -} - function changeEntriesPerPage() { const select = document.getElementById("entriesPerPage"); entriesPerPage = select.value === "all" ? "all" : parseInt(select.value); @@ -647,6 +581,7 @@ function loadTableData() { checked_in_address: cells[7] ? cells[7].getAttribute("title") || cells[7].textContent.trim() : "", + // FIXED: Extract location accuracy for address display logic location_accuracy: cells[8] ? extractLocationAccuracy(cells[8]) : null, accuracy_level: cells[8] ? extractLocationAccuracyLevel(cells[8]) @@ -662,29 +597,91 @@ function loadTableData() { }); filteredData = [...attendanceData]; - console.log( - `Loaded ${attendanceData.length} attendance records with location accuracy` + console.log(`Loaded ${attendanceData.length} attendance records`); + + // Debug log for location accuracy data + const recordsWithAccuracy = attendanceData.filter( + (r) => r.location_accuracy !== null ); + console.log( + `Records with location accuracy: ${recordsWithAccuracy.length}` + ); + if (recordsWithAccuracy.length > 0) { + console.log( + `Sample location accuracy values:`, + recordsWithAccuracy.slice(0, 3).map((r) => r.location_accuracy) + ); + } } } function extractLocationAccuracy(cell) { const text = cell.textContent; - const match = text.match(/(\d+\.?\d*)\s*mi/); - return match ? parseFloat(match[1]) : null; + console.log(`Extracting accuracy from: "${text}"`); + + // Look for miles pattern (e.g., "0.003 mi", "1.234 mi") + const milesMatch = text.match(/(\d+\.?\d*)\s*mi/); + if (milesMatch) { + const value = parseFloat(milesMatch[1]); + console.log(`Found miles: ${value}`); + return value; + } + + // Look for specific accuracy patterns in the HTML + const accuracyMatch = text.match(/accuracy[:\s]*(\d+\.?\d*)/i); + if (accuracyMatch) { + const value = parseFloat(accuracyMatch[1]); + console.log(`Found accuracy: ${value}`); + return value; + } + + // Check for data attributes + const dataAccuracy = cell.getAttribute("data-accuracy"); + if (dataAccuracy) { + const value = parseFloat(dataAccuracy); + console.log(`Found data-accuracy: ${value}`); + return value; + } + + // Fallback: look for GPS accuracy in meters and convert to miles (approximate) + const metersMatch = text.match(/(\d+\.?\d*)\s*m/); + if (metersMatch) { + const meters = parseFloat(metersMatch[1]); + const miles = meters * 0.000621371; // Convert meters to miles (approximate) + console.log(`Found meters: ${meters}, converted to miles: ${miles}`); + return miles; + } + + console.log(`No accuracy found in: "${text}"`); + return null; } function extractLocationAccuracyLevel(cell) { - const text = cell.textContent; - if (text.includes("excellent") || text.includes("good")) return "accurate"; - if (text.includes("fair") || text.includes("poor")) return "inaccurate"; - return "unknown"; + const text = cell.textContent.toLowerCase(); + if ( + text.includes("high") || + text.includes("excellent") || + text.includes("good") + ) + return "High"; + if (text.includes("medium") || text.includes("fair")) return "Medium"; + if (text.includes("low") || text.includes("poor")) return "Low"; + return "Unknown"; } function createTableRow(record, displayIndex) { const row = document.createElement("tr"); row.dataset.recordId = record.id; + // Debug logging for first few records + if (displayIndex <= 3) { + console.log(`=== CREATING ROW ${displayIndex} ===`); + console.log(`Employee: ${record.employeeId}`); + console.log(`Location accuracy: ${record.location_accuracy}`); + console.log(`QR address: ${record.qr_address}`); + console.log(`Check-in address: ${record.checked_in_address}`); + } + // Create location accuracy badge HTML const locationAccuracyBadge = record.location_accuracy !== null @@ -703,6 +700,88 @@ function createTableRow(record, displayIndex) { Unknown `; + // FIXED: Address display logic based on location accuracy + let addressDisplayHTML = ""; + let addressToShow = record.checked_in_address; + let addressIcon = "fas fa-location-arrow"; + let addressClass = "address-normal-accuracy"; + let addressTitle = `Check-in Address: ${record.checked_in_address}`; + + // Apply 0.5-mile threshold logic + if ( + record.location_accuracy !== null && + record.location_accuracy !== undefined + ) { + const accuracy = parseFloat(record.location_accuracy); + + if (displayIndex <= 3) { + console.log(`Applying address logic for ${record.employeeId}:`); + console.log(` Accuracy value: ${accuracy}`); + console.log(` Is <= 0.5? ${accuracy <= 0.5}`); + } + + if (!isNaN(accuracy) && accuracy <= 0.5) { + // High accuracy - use QR address + addressToShow = record.qr_address; + addressIcon = "fas fa-check-circle"; + addressClass = "address-high-accuracy"; + addressTitle = `QR Address (High Accuracy ≤ 0.5 mi): ${record.qr_address}`; + + if (displayIndex <= 3) { + console.log(` → Using QR address: ${addressToShow}`); + } + + addressDisplayHTML = ` + + + ${ + addressToShow.length > 45 + ? addressToShow.substring(0, 45) + "..." + : addressToShow + } + + `; + } else { + // Lower accuracy - use check-in address + if (displayIndex <= 3) { + console.log(` → Using check-in address: ${addressToShow}`); + } + + addressDisplayHTML = ` + + + ${ + addressToShow.length > 45 + ? addressToShow.substring(0, 45) + "..." + : addressToShow + } + + `; + } + } else { + // No accuracy data - use check-in address + if (displayIndex <= 3) { + console.log( + ` → No accuracy data, using check-in address: ${addressToShow}` + ); + } + + addressDisplayHTML = ` + + + ${ + addressToShow.length > 45 + ? addressToShow.substring(0, 45) + "..." + : addressToShow + } + + `; + } + row.innerHTML = ` ${displayIndex} @@ -745,14 +824,7 @@ function createTableRow(record, displayIndex) {
- - - ${ - record.checked_in_address.length > 50 - ? record.checked_in_address.substring(0, 50) + "..." - : record.checked_in_address - } - + ${addressDisplayHTML}
diff --git a/templates/attendance_report.html b/templates/attendance_report.html index 30c457d..aa907d7 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -224,11 +224,27 @@ -
- - - {{ record.qr_address[:50] }}{% if record.qr_address|length > 50 %}...{% endif %} - +
+ + {% if record.address_source == 'qr' %} + + + + {{ record.qr_address[:45] }}{% if record.qr_address|length > 45 %}...{% endif %} + + {% else %} + + + {% if record.location_accuracy and record.location_accuracy > 0.5 %} + + {% endif %} + {{ record.checked_in_address[:45] }}{% if record.checked_in_address|length > 45 %}...{% endif %} + + {% endif %}