From f83f032a603ce917cd3fb2519b8ecbe109a47a16 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Thu, 9 Oct 2025 11:01:56 -0400 Subject: [PATCH] Updated import recorded address --- app.py | 62 +-- templates/time_attendance_records.html | 501 ++++++++----------------- time_attendance_import_service.py | 78 +++- 3 files changed, 280 insertions(+), 361 deletions(-) diff --git a/app.py b/app.py index a60a0fe..0923227 100644 --- a/app.py +++ b/app.py @@ -7329,30 +7329,50 @@ def time_attendance_records(): ) # Paginate results - records = query.paginate( - page=page, - per_page=per_page, - error_out=False + records = query.paginate(page=page, per_page=per_page, error_out=False) + + # Enhance records with QR address and location accuracy + for record in records.items: + # Find matching QR code by location name + qr_code = QRCode.query.filter_by(location=record.location_name).first() + + if qr_code: + record.qr_address = qr_code.location_address + + # Calculate location accuracy if coordinates are available + if record.recorded_address and qr_code.location_address: + try: + # Try to calculate location accuracy + location_accuracy = calculate_location_accuracy_enhanced( + qr_address=qr_code.location_address, + checkin_address=record.recorded_address, + checkin_lat=None, # TimeAttendance doesn't have GPS coords + checkin_lng=None + ) + record.location_accuracy = location_accuracy + except Exception as e: + logger_handler.logger.warning(f"Could not calculate location accuracy for record {record.id}: {e}") + record.location_accuracy = None + else: + record.location_accuracy = None + else: + record.qr_address = None + record.location_accuracy = None + + # Get unique employees and locations for filters + unique_employees = TimeAttendance.get_unique_employees() + unique_locations = TimeAttendance.get_unique_locations() + + return render_template( + 'time_attendance_records.html', + records=records, + unique_employees=unique_employees, + unique_locations=unique_locations ) - # Get filter options for dropdown menus - employees = TimeAttendance.get_unique_employees() - locations = TimeAttendance.get_unique_locations() - - return render_template('time_attendance_records.html', - records=records, - employees=employees, - locations=locations, - current_filters={ - 'employee_id': employee_filter, - 'location_name': location_filter, - 'start_date': start_date, - 'end_date': end_date - }) - except Exception as e: - logger_handler.logger.error(f"Error in time attendance records view: {e}") - flash('Error loading time attendance records.', 'error') + logger_handler.logger.error(f"Error displaying time attendance records: {e}") + flash('Error loading attendance records.', 'error') return redirect(url_for('time_attendance_dashboard')) @app.route('/time-attendance/record/') diff --git a/templates/time_attendance_records.html b/templates/time_attendance_records.html index d765e4b..28416b5 100644 --- a/templates/time_attendance_records.html +++ b/templates/time_attendance_records.html @@ -3,94 +3,6 @@ {% block extra_head %} - {% endblock %} {% block page_title %}Time Attendance Records{% endblock %} @@ -123,40 +35,10 @@ Import Data {% endif %} - - -
- - -
- - - - -
- -
- - Export will include current filters -
-
-
+ @@ -177,9 +59,8 @@ - {% for location in locations %} - {% endfor %} @@ -201,20 +81,14 @@
- +
- +
@@ -222,79 +96,135 @@ Apply Filters - - - Clear Filters + + + Clear
- - {% if records %} -
-
- - Showing {{ records.items|length }} of {{ records.total }} records - {% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %} - Filtered - {% endif %} -
-
- {% endif %} - - {% if records and records.items %} -
-
+
+
+

+ + Attendance Records +

+
+ {% if records.items %} + Showing {{ records.per_page * (records.page - 1) + 1 }} - + {{ records.per_page * (records.page - 1) + records.items|length }} + of {{ records.total }} records + {% else %} + No records found + {% endif %} +
+
+ + {% if records.items %} +
- - - - - + + + + - + + {% for record in records.items %} - - - - + + - - - + + +
IDEmployeeDateTimeLocationEmployee IDNameDate & Time ActionLocation PlatformImport SourceQR AddressCheck-in Address Actions
{{ record.id }} -
- {{ record.employee_name }} - ID: {{ record.employee_id }} +
+ {{ record.employee_id }}
{{ record.attendance_date.strftime('%Y-%m-%d') if record.attendance_date else 'N/A' }}{{ record.attendance_time.strftime('%H:%M:%S') if record.attendance_time else 'N/A' }}{{ record.location_name }} - +
+ + {{ record.employee_name }} +
+
+
+
{{ record.attendance_date.strftime('%Y-%m-%d') }}
+
{{ record.attendance_time.strftime('%H:%M:%S') }}
+
+
+ + {% if record.action_description.lower() == 'check in' %} + + {% elif record.action_description.lower() == 'check out' %} + + {% else %} + + {% endif %} {{ record.action_description }} {{ record.platform or 'N/A' }} -
- {{ record.import_source or 'Unknown' }} - {{ record.import_date.strftime('%Y-%m-%d') if record.import_date else '' }} +
+
+ + {{ record.location_name }}
-
-
+ {{ record.platform or 'Unknown' }} + + {% if record.qr_address %} + + + {{ record.qr_address[:40] }}{% if record.qr_address|length > 40 %}...{% endif %} + + {% else %} + No QR address + {% endif %} + + {% if record.location_accuracy is not none and record.location_accuracy <= 0.3 %} + {# Within 0.3 mile threshold - show QR address #} + {% if record.qr_address %} + + + {{ record.qr_address[:40] }}{% if record.qr_address|length > 40 %}...{% endif %} + + {% else %} + No address + {% endif %} + {% else %} + {# Beyond 0.3 mile threshold or no accuracy - show actual check-in address #} + {% if record.recorded_address %} + + + {{ record.recorded_address[:40] }}{% if record.recorded_address|length > 40 %}...{% endif %} + + {% else %} + No address + {% endif %} + {% endif %} + +
+ - + View + {% if session.role == 'admin' %} - {% endif %}
@@ -304,123 +234,77 @@
- + {% if records.pages > 1 %} -
+ {% endif %} -
- - {% else %} - -
-
- -
-

No Records Found

-

- {% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %} - No attendance records match your current filter criteria. - Try adjusting your filters or clearing them to see all records. - {% else %} - No time attendance records have been imported yet. - Import your first Excel file to get started. - {% endif %} -

-
- {% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %} + + {% else %} +
+
+ +
+

No Records Found

+

No time attendance records match your current filters.

- + Clear Filters - {% endif %} - {% if session.role == 'admin' %} - - - Import Data - - {% endif %} -
-
- {% endif %} -
- - - diff --git a/time_attendance_import_service.py b/time_attendance_import_service.py index 18514d2..7243d2b 100644 --- a/time_attendance_import_service.py +++ b/time_attendance_import_service.py @@ -20,6 +20,80 @@ class TimeAttendanceImportService: """Initialize the import service with database and logger""" self.db = db self.logger = logger_handler + + def _parse_excel_hyperlink(self, cell_value: str) -> str: + """ + Parse Excel HYPERLINK formula to extract the display text (address) + + Handles formats like: + - =HYPERLINK("http://maps.google.com/maps?q=[lat],[long]","123 Maint Street") + - Regular text (no formula) + + Args: + cell_value: The cell value which may contain a HYPERLINK formula + + Returns: + Extracted address text or original value if not a hyperlink + """ + if not cell_value or not isinstance(cell_value, str): + return cell_value + + # Check if it's a HYPERLINK formula + if cell_value.strip().startswith('=HYPERLINK('): + try: + # Extract content between HYPERLINK parentheses + # Pattern: =HYPERLINK("url","display_text") + import re + + # Match the display text (second quoted string) + pattern = r'=HYPERLINK\s*\(\s*"[^"]*"\s*,\s*"([^"]*)"\s*\)' + match = re.search(pattern, cell_value) + + if match: + address_text = match.group(1) + if self.logger: + self.logger.logger.debug(f"Parsed HYPERLINK: {cell_value[:50]}... -> {address_text}") + return address_text.strip() + else: + # If pattern doesn't match, try to extract any quoted text after comma + parts = cell_value.split(',', 1) + if len(parts) > 1: + # Get text between last quotes + text_part = parts[1].strip().rstrip(')') + if '"' in text_part: + # Extract text between quotes + address_text = text_part.split('"')[1] if text_part.count('"') >= 2 else text_part + if self.logger: + self.logger.logger.debug(f"Parsed HYPERLINK (fallback): {cell_value[:50]}... -> {address_text}") + return address_text.strip() + except Exception as e: + if self.logger: + self.logger.logger.warning(f"Failed to parse HYPERLINK formula: {cell_value[:50]}... Error: {e}") + # Return original if parsing fails + return cell_value + + # Not a hyperlink formula, return as-is + return cell_value.strip() if isinstance(cell_value, str) else cell_value + + def _process_recorded_address(self, row): + """ + Process recorded address field, handling Excel HYPERLINK formulas + + Args: + row: DataFrame row containing the 'Recorded Address' column + + Returns: + Parsed address string or None + """ + recorded_address_value = row.get('Recorded Address', '') + + if pd.notna(recorded_address_value): + # Convert to string and parse if it's a HYPERLINK formula + address_str = str(recorded_address_value).strip() + parsed_address = self._parse_excel_hyperlink(address_str) + return parsed_address if parsed_address else None + + return None def analyze_for_duplicates(self, file_path: str) -> Dict[str, Any]: """ @@ -85,7 +159,7 @@ class TimeAttendanceImportService: 'location_name': str(row['Location Name']).strip(), 'action_description': str(row['Action Description']).strip(), 'event_description': str(row.get('Event Description', '')).strip() if pd.notna(row.get('Event Description')) else None, - 'recorded_address': str(row.get('Recorded Address', '')).strip() if pd.notna(row.get('Recorded Address')) else None, + 'recorded_address': self._process_recorded_address(row), } # Check for duplicates @@ -242,7 +316,7 @@ class TimeAttendanceImportService: 'location_name': str(row['Location Name']).strip(), 'action_description': str(row['Action Description']).strip(), 'event_description': str(row.get('Event Description', '')).strip() if pd.notna(row.get('Event Description')) else None, - 'recorded_address': str(row.get('Recorded Address', '')).strip() if pd.notna(row.get('Recorded Address')) else None, + 'recorded_address': self._process_recorded_address(row), } # Check for duplicates