Updated import recorded address

This commit is contained in:
2025-10-09 11:01:56 -04:00
parent aa1540c9dd
commit f83f032a60
3 changed files with 280 additions and 361 deletions
+41 -21
View File
@@ -7329,30 +7329,50 @@ def time_attendance_records():
) )
# Paginate results # Paginate results
records = query.paginate( records = query.paginate(page=page, per_page=per_page, error_out=False)
page=page,
per_page=per_page, # Enhance records with QR address and location accuracy
error_out=False 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: except Exception as e:
logger_handler.logger.error(f"Error in time attendance records view: {e}") logger_handler.logger.error(f"Error displaying time attendance records: {e}")
flash('Error loading time attendance records.', 'error') flash('Error loading attendance records.', 'error')
return redirect(url_for('time_attendance_dashboard')) return redirect(url_for('time_attendance_dashboard'))
@app.route('/time-attendance/record/<int:record_id>') @app.route('/time-attendance/record/<int:record_id>')
+159 -334
View File
@@ -3,94 +3,6 @@
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/time_attendance.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/time_attendance.css') }}">
<style>
/* Export dropdown styles */
.export-dropdown {
position: relative;
display: inline-block;
}
.export-menu {
display: none;
position: absolute;
right: 0;
top: 100%;
margin-top: 0.5rem;
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
z-index: 9999;
min-width: 220px;
}
.export-menu.show {
display: block;
}
.export-menu-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border: none;
background: none;
width: 100%;
text-align: left;
cursor: pointer;
transition: background 0.2s;
font-size: 0.875rem;
color: #2d3748;
}
.export-menu-item:first-child {
border-radius: 8px 8px 0 0;
}
.export-menu-item:last-child {
border-radius: 0 0 8px 8px;
}
.export-menu-item:hover {
background: #f7fafc;
}
.export-menu-item i {
font-size: 1.125rem;
width: 20px;
text-align: center;
}
.export-menu-item .fa-file-csv {
color: #4299e1;
}
.export-menu-item .fa-file-excel {
color: #48bb78;
}
.export-menu-divider {
height: 1px;
background: #e2e8f0;
margin: 0.25rem 0;
}
.export-menu-info {
padding: 0.75rem 1rem;
font-size: 0.75rem;
color: #718096;
background: #f7fafc;
border-radius: 0 0 8px 8px;
}
.time-attendance-page {
overflow: visible !important;
}
.time-attendance-header {
overflow: visible !important;
}
</style>
{% endblock %} {% endblock %}
{% block page_title %}Time Attendance Records{% endblock %} {% block page_title %}Time Attendance Records{% endblock %}
@@ -123,40 +35,10 @@
Import Data Import Data
</a> </a>
{% endif %} {% endif %}
<button class="btn btn-secondary" onclick="exportRecords()">
<!-- Export Dropdown --> <i class="fas fa-download"></i>
<div class="export-dropdown"> Export
<button class="btn btn-secondary" onclick="toggleExportMenu()"> </button>
<i class="fas fa-download"></i>
Export
<i class="fas fa-chevron-down" style="margin-left: 0.5rem; font-size: 0.75rem;"></i>
</button>
<div class="export-menu" id="exportMenu">
<button class="export-menu-item" onclick="exportToCSV()">
<i class="fas fa-file-csv"></i>
<div>
<strong>Export to CSV</strong>
<div style="font-size: 0.75rem; color: #718096;">Quick download, all data</div>
</div>
</button>
<button class="export-menu-item" onclick="exportToExcel()">
<i class="fas fa-file-excel"></i>
<div>
<strong>Export to Excel</strong>
<div style="font-size: 0.75rem; color: #718096;">Formatted with summary sheet</div>
</div>
</button>
<div class="export-menu-divider"></div>
<div class="export-menu-info">
<i class="fas fa-info-circle"></i>
Export will include current filters
</div>
</div>
</div>
</div> </div>
</div> </div>
@@ -177,9 +59,8 @@
<label for="employee_id" class="form-label">Employee</label> <label for="employee_id" class="form-label">Employee</label>
<select id="employee_id" name="employee_id" class="form-select"> <select id="employee_id" name="employee_id" class="form-select">
<option value="">All Employees</option> <option value="">All Employees</option>
{% for employee in employees %} {% for employee in unique_employees %}
<option value="{{ employee.employee_id }}" <option value="{{ employee.employee_id }}" {% if request.args.get('employee_id') == employee.employee_id %}selected{% endif %}>
{% if current_filters.employee_id == employee.employee_id %}selected{% endif %}>
{{ employee.employee_name }} ({{ employee.employee_id }}) {{ employee.employee_name }} ({{ employee.employee_id }})
</option> </option>
{% endfor %} {% endfor %}
@@ -190,10 +71,9 @@
<label for="location_name" class="form-label">Location</label> <label for="location_name" class="form-label">Location</label>
<select id="location_name" name="location_name" class="form-select"> <select id="location_name" name="location_name" class="form-select">
<option value="">All Locations</option> <option value="">All Locations</option>
{% for location in locations %} {% for location in unique_locations %}
<option value="{{ location.location_name }}" <option value="{{ location[0] }}" {% if request.args.get('location_name') == location[0] %}selected{% endif %}>
{% if current_filters.location_name == location.location_name %}selected{% endif %}> {{ location[0] }}
{{ location.location_name }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
@@ -201,20 +81,14 @@
<div class="form-group"> <div class="form-group">
<label for="start_date" class="form-label">Start Date</label> <label for="start_date" class="form-label">Start Date</label>
<input type="date" <input type="date" id="start_date" name="start_date" class="form-input"
id="start_date" value="{{ request.args.get('start_date', '') }}">
name="start_date"
class="form-input"
value="{{ current_filters.start_date or '' }}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="end_date" class="form-label">End Date</label> <label for="end_date" class="form-label">End Date</label>
<input type="date" <input type="date" id="end_date" name="end_date" class="form-input"
id="end_date" value="{{ request.args.get('end_date', '') }}">
name="end_date"
class="form-input"
value="{{ current_filters.end_date or '' }}">
</div> </div>
<div class="form-actions"> <div class="form-actions">
@@ -222,79 +96,135 @@
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
Apply Filters Apply Filters
</button> </button>
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-outline"> <a href="{{ url_for('time_attendance_records') }}" class="btn btn-secondary">
<i class="fas fa-undo"></i> <i class="fas fa-times"></i>
Clear Filters Clear
</a> </a>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Records Count Info -->
{% if records %}
<div class="records-info">
<div class="info-card">
<i class="fas fa-database"></i>
<span>Showing {{ records.items|length }} of {{ records.total }} records</span>
{% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %}
<span class="filter-badge">Filtered</span>
{% endif %}
</div>
</div>
{% endif %}
<!-- Records Table --> <!-- Records Table -->
{% if records and records.items %} <div class="records-section">
<div class="table-section"> <div class="records-header">
<div class="table-container"> <h2>
<i class="fas fa-table"></i>
Attendance Records
</h2>
<div class="records-meta">
{% 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 %}
</div>
</div>
{% if records.items %}
<div class="records-table-container">
<table class="records-table"> <table class="records-table">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>Employee ID</th>
<th>Employee</th> <th>Name</th>
<th>Date</th> <th>Date & Time</th>
<th>Time</th>
<th>Location</th>
<th>Action</th> <th>Action</th>
<th>Location</th>
<th>Platform</th> <th>Platform</th>
<th>Import Source</th> <th>QR Address</th>
<th>Check-in Address</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for record in records.items %} {% for record in records.items %}
<tr> <tr>
<td>{{ record.id }}</td>
<td> <td>
<div class="employee-info"> <div class="employee-id-cell">
<strong>{{ record.employee_name }}</strong> <span class="employee-id">{{ record.employee_id }}</span>
<small>ID: {{ record.employee_id }}</small>
</div> </div>
</td> </td>
<td>{{ record.attendance_date.strftime('%Y-%m-%d') if record.attendance_date else 'N/A' }}</td>
<td>{{ record.attendance_time.strftime('%H:%M:%S') if record.attendance_time else 'N/A' }}</td>
<td>{{ record.location_name }}</td>
<td> <td>
<span class="action-badge {% if 'Check In' in record.action_description %}check-in{% else %}check-out{% endif %}"> <div class="employee-name-cell">
<i class="fas fa-user"></i>
<span>{{ record.employee_name }}</span>
</div>
</td>
<td class="datetime-cell">
<div class="datetime-info">
<div class="date">{{ record.attendance_date.strftime('%Y-%m-%d') }}</div>
<div class="time">{{ record.attendance_time.strftime('%H:%M:%S') }}</div>
</div>
</td>
<td>
<span class="action-badge {{ record.action_description.lower().replace(' ', '-') }}">
{% if record.action_description.lower() == 'check in' %}
<i class="fas fa-sign-in-alt"></i>
{% elif record.action_description.lower() == 'check out' %}
<i class="fas fa-sign-out-alt"></i>
{% else %}
<i class="fas fa-clock"></i>
{% endif %}
{{ record.action_description }} {{ record.action_description }}
</span> </span>
</td> </td>
<td>{{ record.platform or 'N/A' }}</td> <td class="location-cell">
<td> <div class="location-info">
<div class="import-info"> <i class="fas fa-map-marker-alt"></i>
{{ record.import_source or 'Unknown' }} {{ record.location_name }}
<small>{{ record.import_date.strftime('%Y-%m-%d') if record.import_date else '' }}</small>
</div> </div>
</td> </td>
<td> <td class="platform-cell">
<div class="record-actions"> {{ record.platform or 'Unknown' }}
<button class="action-btn btn-view" onclick="viewRecord({{ record.id }})" title="View Details"> </td>
<td class="address-cell qr-address-cell">
{% if record.qr_address %}
<span title="{{ record.qr_address }}">
<i class="fas fa-qrcode" style="color: #6366f1;"></i>
{{ record.qr_address[:40] }}{% if record.qr_address|length > 40 %}...{% endif %}
</span>
{% else %}
<span class="text-muted">No QR address</span>
{% endif %}
</td>
<td class="address-cell checkin-address-cell">
{% 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 %}
<span title="QR Address (High Accuracy ≤ 0.3 mi): {{ record.qr_address }}">
<i class="fas fa-check-circle" style="color: #059669;"></i>
{{ record.qr_address[:40] }}{% if record.qr_address|length > 40 %}...{% endif %}
</span>
{% else %}
<span class="text-muted">No address</span>
{% endif %}
{% else %}
{# Beyond 0.3 mile threshold or no accuracy - show actual check-in address #}
{% if record.recorded_address %}
<span title="Check-in Address: {{ record.recorded_address }}">
<i class="fas fa-location-arrow" style="color: #f59e0b;"></i>
{{ record.recorded_address[:40] }}{% if record.recorded_address|length > 40 %}...{% endif %}
</span>
{% else %}
<span class="text-muted">No address</span>
{% endif %}
{% endif %}
</td>
<td class="actions-cell">
<div class="action-buttons">
<a href="{{ url_for('time_attendance_record_detail', record_id=record.id) }}"
class="action-btn view">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> View
</a>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<button class="action-btn btn-delete" onclick="confirmDelete({{ record.id }}, '{{ record.employee_name }}', '{{ record.attendance_date.strftime('%Y-%m-%d') if record.attendance_date else 'N/A' }}')" title="Delete"> <button class="action-btn delete"
onclick="confirmDelete({{ record.id }}, '{{ record.employee_name }}', '{{ record.attendance_date }}')">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
Delete
</button> </button>
{% endif %} {% endif %}
</div> </div>
@@ -307,120 +237,74 @@
<!-- Pagination --> <!-- Pagination -->
{% if records.pages > 1 %} {% if records.pages > 1 %}
<div class="pagination-section"> <div class="pagination">
<div class="pagination-info"> <div class="pagination-info">
Page {{ records.page }} of {{ records.pages }} Page {{ records.page }} of {{ records.pages }}
</div> </div>
<div class="pagination-controls"> <div class="pagination-controls">
{% if records.has_prev %} {% if records.has_prev %}
<a href="{{ url_for('time_attendance_records', page=records.prev_num, **current_filters) }}" <a href="{{ url_for('time_attendance_records', page=records.prev_num,
employee_id=request.args.get('employee_id', ''),
location_name=request.args.get('location_name', ''),
start_date=request.args.get('start_date', ''),
end_date=request.args.get('end_date', '')) }}"
class="pagination-btn"> class="pagination-btn">
<i class="fas fa-chevron-left"></i> <i class="fas fa-chevron-left"></i>
Previous Previous
</a> </a>
{% else %}
<span class="pagination-btn disabled">
<i class="fas fa-chevron-left"></i>
Previous
</span>
{% endif %} {% endif %}
{% for page_num in records.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %} {% for page_num in records.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
{% if page_num %} {% if page_num %}
{% if page_num != records.page %} {% if page_num != records.page %}
<a href="{{ url_for('time_attendance_records', page=page_num, **current_filters) }}" <a href="{{ url_for('time_attendance_records', page=page_num,
class="pagination-btn">{{ page_num }}</a> employee_id=request.args.get('employee_id', ''),
location_name=request.args.get('location_name', ''),
start_date=request.args.get('start_date', ''),
end_date=request.args.get('end_date', '')) }}"
class="pagination-btn">
{{ page_num }}
</a>
{% else %} {% else %}
<span class="pagination-btn active">{{ page_num }}</span> <span class="pagination-btn active">{{ page_num }}</span>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="pagination-btn disabled"></span> <span class="pagination-ellipsis">...</span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if records.has_next %} {% if records.has_next %}
<a href="{{ url_for('time_attendance_records', page=records.next_num, **current_filters) }}" <a href="{{ url_for('time_attendance_records', page=records.next_num,
employee_id=request.args.get('employee_id', ''),
location_name=request.args.get('location_name', ''),
start_date=request.args.get('start_date', ''),
end_date=request.args.get('end_date', '')) }}"
class="pagination-btn"> class="pagination-btn">
Next Next
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
</a> </a>
{% else %}
<span class="pagination-btn disabled">
Next
<i class="fas fa-chevron-right"></i>
</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div>
{% else %} {% else %}
<!-- Empty State --> <div class="empty-state">
<div class="empty-state"> <div class="empty-icon">
<div class="empty-icon"> <i class="fas fa-inbox"></i>
<i class="fas fa-search"></i> </div>
</div> <h3>No Records Found</h3>
<h3>No Records Found</h3> <p>No time attendance records match your current filters.</p>
<p>
{% 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 %}
</p>
<div class="empty-actions">
{% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %}
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-primary"> <a href="{{ url_for('time_attendance_records') }}" class="btn btn-primary">
<i class="fas fa-undo"></i> <i class="fas fa-refresh"></i>
Clear Filters Clear Filters
</a> </a>
{% endif %}
{% if session.role == 'admin' %}
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-secondary">
<i class="fas fa-upload"></i>
Import Data
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- Delete Confirmation Modal -->
<div id="deleteModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>
<i class="fas fa-exclamation-triangle"></i>
Confirm Deletion
</h3>
<button class="modal-close" onclick="closeDeleteModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this attendance record?</p>
<p><strong>Employee:</strong> <span id="deleteEmployeeName"></span></p>
<p><strong>Date:</strong> <span id="deleteDate"></span></p>
<p class="text-danger">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>
<form id="deleteForm" method="POST" style="display: inline;">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash"></i>
Delete Record
</button>
</form>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
<script> <script>
// Toggle filters
function toggleFilters() { function toggleFilters() {
const filterBody = document.getElementById('filterBody'); const filterBody = document.getElementById('filterBody');
const filterIcon = document.getElementById('filterIcon'); const filterIcon = document.getElementById('filterIcon');
@@ -436,90 +320,31 @@ function toggleFilters() {
} }
} }
// Toggle export menu
function toggleExportMenu() {
const menu = document.getElementById('exportMenu');
menu.classList.toggle('show');
}
// Close export menu when clicking outside
document.addEventListener('click', function(e) {
const exportDropdown = document.querySelector('.export-dropdown');
if (exportDropdown && !exportDropdown.contains(e.target)) {
document.getElementById('exportMenu').classList.remove('show');
}
});
// Get current filters as URL parameters
function getCurrentFilters() {
const params = new URLSearchParams(window.location.search);
return {
employee_id: params.get('employee_id') || '',
location_name: params.get('location_name') || '',
start_date: params.get('start_date') || '',
end_date: params.get('end_date') || '',
import_batch: params.get('import_batch') || ''
};
}
// Export to CSV
function exportToCSV() {
const filters = getCurrentFilters();
const params = new URLSearchParams(filters);
params.set('format', 'csv');
window.location.href = `{{ url_for('export_time_attendance') }}?${params.toString()}`;
toggleExportMenu();
}
// Export to Excel
function exportToExcel() {
const filters = getCurrentFilters();
const params = new URLSearchParams(filters);
params.set('format', 'excel');
window.location.href = `{{ url_for('export_time_attendance') }}?${params.toString()}`;
toggleExportMenu();
}
// View record details
function viewRecord(recordId) {
// Implement view details functionality
alert(`View details for record ID: ${recordId}\nThis feature can be implemented to show a modal with full record details.`);
}
// Delete confirmation
function confirmDelete(recordId, employeeName, date) { function confirmDelete(recordId, employeeName, date) {
document.getElementById('deleteEmployeeName').textContent = employeeName; if (confirm(`Are you sure you want to delete the attendance record for ${employeeName} on ${date}?`)) {
document.getElementById('deleteDate').textContent = date; const form = document.createElement('form');
document.getElementById('deleteForm').action = `{{ url_for('delete_time_attendance_record', record_id=0) }}`.replace('0', recordId); form.method = 'POST';
document.getElementById('deleteModal').style.display = 'flex'; form.action = `/time-attendance/record/${recordId}/delete`;
document.body.appendChild(form);
form.submit();
}
} }
function closeDeleteModal() { function exportRecords() {
document.getElementById('deleteModal').style.display = 'none'; alert('Export functionality coming soon!');
} }
// Close modal when clicking outside
document.getElementById('deleteModal').addEventListener('click', function(e) {
if (e.target === this) {
closeDeleteModal();
}
});
// Escape key to close modal
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeDeleteModal();
}
});
// Initialize filters state // Initialize filters state
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Show filters if any are active const filterBody = document.getElementById('filterBody');
const hasActiveFilters = {{ 'true' if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date else 'false' }}; const hasFilters = {{ 'true' if request.args else 'false' }};
if (hasActiveFilters) {
toggleFilters(); if (hasFilters) {
filterBody.style.display = 'block';
document.getElementById('filterIcon').classList.remove('fa-chevron-down');
document.getElementById('filterIcon').classList.add('fa-chevron-up');
} else {
filterBody.style.display = 'none';
} }
}); });
</script> </script>
+76 -2
View File
@@ -21,6 +21,80 @@ class TimeAttendanceImportService:
self.db = db self.db = db
self.logger = logger_handler 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]: def analyze_for_duplicates(self, file_path: str) -> Dict[str, Any]:
""" """
Analyze file for potential duplicates WITHOUT importing Analyze file for potential duplicates WITHOUT importing
@@ -85,7 +159,7 @@ class TimeAttendanceImportService:
'location_name': str(row['Location Name']).strip(), 'location_name': str(row['Location Name']).strip(),
'action_description': str(row['Action Description']).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, '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 # Check for duplicates
@@ -242,7 +316,7 @@ class TimeAttendanceImportService:
'location_name': str(row['Location Name']).strip(), 'location_name': str(row['Location Name']).strip(),
'action_description': str(row['Action Description']).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, '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 # Check for duplicates