Updated import recorded address
This commit is contained in:
@@ -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>')
|
||||||
|
|||||||
@@ -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 -->
|
|
||||||
<div class="export-dropdown">
|
|
||||||
<button class="btn btn-secondary" onclick="toggleExportMenu()">
|
|
||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
Export
|
Export
|
||||||
<i class="fas fa-chevron-down" style="margin-left: 0.5rem; font-size: 0.75rem;"></i>
|
|
||||||
</button>
|
</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-search"></i>
|
<i class="fas fa-inbox"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3>No Records Found</h3>
|
<h3>No Records Found</h3>
|
||||||
<p>
|
<p>No time attendance records match your current filters.</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>
|
</div>
|
||||||
{% endif %}
|
{% 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>
|
</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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user