Updated import recorded address
This commit is contained in:
@@ -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/<int:record_id>')
|
||||
|
||||
@@ -3,94 +3,6 @@
|
||||
|
||||
{% block extra_head %}
|
||||
<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 %}
|
||||
|
||||
{% block page_title %}Time Attendance Records{% endblock %}
|
||||
@@ -123,40 +35,10 @@
|
||||
Import Data
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- Export Dropdown -->
|
||||
<div class="export-dropdown">
|
||||
<button class="btn btn-secondary" onclick="toggleExportMenu()">
|
||||
<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>
|
||||
<button class="btn btn-secondary" onclick="exportRecords()">
|
||||
<i class="fas fa-download"></i>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -177,9 +59,8 @@
|
||||
<label for="employee_id" class="form-label">Employee</label>
|
||||
<select id="employee_id" name="employee_id" class="form-select">
|
||||
<option value="">All Employees</option>
|
||||
{% for employee in employees %}
|
||||
<option value="{{ employee.employee_id }}"
|
||||
{% if current_filters.employee_id == employee.employee_id %}selected{% endif %}>
|
||||
{% for employee in unique_employees %}
|
||||
<option value="{{ employee.employee_id }}" {% if request.args.get('employee_id') == employee.employee_id %}selected{% endif %}>
|
||||
{{ employee.employee_name }} ({{ employee.employee_id }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
@@ -190,10 +71,9 @@
|
||||
<label for="location_name" class="form-label">Location</label>
|
||||
<select id="location_name" name="location_name" class="form-select">
|
||||
<option value="">All Locations</option>
|
||||
{% for location in locations %}
|
||||
<option value="{{ location.location_name }}"
|
||||
{% if current_filters.location_name == location.location_name %}selected{% endif %}>
|
||||
{{ location.location_name }}
|
||||
{% for location in unique_locations %}
|
||||
<option value="{{ location[0] }}" {% if request.args.get('location_name') == location[0] %}selected{% endif %}>
|
||||
{{ location[0] }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
@@ -201,20 +81,14 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start_date" class="form-label">Start Date</label>
|
||||
<input type="date"
|
||||
id="start_date"
|
||||
name="start_date"
|
||||
class="form-input"
|
||||
value="{{ current_filters.start_date or '' }}">
|
||||
<input type="date" id="start_date" name="start_date" class="form-input"
|
||||
value="{{ request.args.get('start_date', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="end_date" class="form-label">End Date</label>
|
||||
<input type="date"
|
||||
id="end_date"
|
||||
name="end_date"
|
||||
class="form-input"
|
||||
value="{{ current_filters.end_date or '' }}">
|
||||
<input type="date" id="end_date" name="end_date" class="form-input"
|
||||
value="{{ request.args.get('end_date', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
@@ -222,79 +96,135 @@
|
||||
<i class="fas fa-search"></i>
|
||||
Apply Filters
|
||||
</button>
|
||||
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-outline">
|
||||
<i class="fas fa-undo"></i>
|
||||
Clear Filters
|
||||
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i>
|
||||
Clear
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</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 -->
|
||||
{% if records and records.items %}
|
||||
<div class="table-section">
|
||||
<div class="table-container">
|
||||
<div class="records-section">
|
||||
<div class="records-header">
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Employee</th>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Location</th>
|
||||
<th>Employee ID</th>
|
||||
<th>Name</th>
|
||||
<th>Date & Time</th>
|
||||
<th>Action</th>
|
||||
<th>Location</th>
|
||||
<th>Platform</th>
|
||||
<th>Import Source</th>
|
||||
<th>QR Address</th>
|
||||
<th>Check-in Address</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for record in records.items %}
|
||||
<tr>
|
||||
<td>{{ record.id }}</td>
|
||||
<td>
|
||||
<div class="employee-info">
|
||||
<strong>{{ record.employee_name }}</strong>
|
||||
<small>ID: {{ record.employee_id }}</small>
|
||||
<div class="employee-id-cell">
|
||||
<span class="employee-id">{{ record.employee_id }}</span>
|
||||
</div>
|
||||
</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>
|
||||
<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 }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ record.platform or 'N/A' }}</td>
|
||||
<td>
|
||||
<div class="import-info">
|
||||
{{ record.import_source or 'Unknown' }}
|
||||
<small>{{ record.import_date.strftime('%Y-%m-%d') if record.import_date else '' }}</small>
|
||||
<td class="location-cell">
|
||||
<div class="location-info">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
{{ record.location_name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="record-actions">
|
||||
<button class="action-btn btn-view" onclick="viewRecord({{ record.id }})" title="View Details">
|
||||
<td class="platform-cell">
|
||||
{{ record.platform or 'Unknown' }}
|
||||
</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>
|
||||
</button>
|
||||
View
|
||||
</a>
|
||||
{% 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>
|
||||
Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -304,123 +234,77 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if records.pages > 1 %}
|
||||
<div class="pagination-section">
|
||||
<div class="pagination">
|
||||
<div class="pagination-info">
|
||||
Page {{ records.page }} of {{ records.pages }}
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
{% 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">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
Previous
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="pagination-btn disabled">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
Previous
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% 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 != records.page %}
|
||||
<a href="{{ url_for('time_attendance_records', page=page_num, **current_filters) }}"
|
||||
class="pagination-btn">{{ page_num }}</a>
|
||||
<a href="{{ url_for('time_attendance_records', page=page_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">
|
||||
{{ page_num }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="pagination-btn active">{{ page_num }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="pagination-btn disabled">…</span>
|
||||
<span class="pagination-ellipsis">...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{% 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">
|
||||
Next
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="pagination-btn disabled">
|
||||
Next
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
<h3>No Records Found</h3>
|
||||
<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 %}
|
||||
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<i class="fas fa-inbox"></i>
|
||||
</div>
|
||||
<h3>No Records Found</h3>
|
||||
<p>No time attendance records match your current filters.</p>
|
||||
<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
|
||||
</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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle filters
|
||||
function toggleFilters() {
|
||||
const filterBody = document.getElementById('filterBody');
|
||||
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) {
|
||||
document.getElementById('deleteEmployeeName').textContent = employeeName;
|
||||
document.getElementById('deleteDate').textContent = date;
|
||||
document.getElementById('deleteForm').action = `{{ url_for('delete_time_attendance_record', record_id=0) }}`.replace('0', recordId);
|
||||
document.getElementById('deleteModal').style.display = 'flex';
|
||||
if (confirm(`Are you sure you want to delete the attendance record for ${employeeName} on ${date}?`)) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = `/time-attendance/record/${recordId}/delete`;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
document.getElementById('deleteModal').style.display = 'none';
|
||||
function exportRecords() {
|
||||
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
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Show filters if any are active
|
||||
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' }};
|
||||
if (hasActiveFilters) {
|
||||
toggleFilters();
|
||||
const filterBody = document.getElementById('filterBody');
|
||||
const hasFilters = {{ 'true' if request.args else 'false' }};
|
||||
|
||||
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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user