Mar 06 2026: fixed time attendance records page, update search by id
This commit is contained in:
@@ -10949,12 +10949,31 @@ def time_attendance_records():
|
|||||||
unique_locations = TimeAttendance.get_unique_locations()
|
unique_locations = TimeAttendance.get_unique_locations()
|
||||||
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
|
||||||
|
# Resolve display name for the employee filter input
|
||||||
|
employee_display_name = ''
|
||||||
|
if employee_filter:
|
||||||
|
try:
|
||||||
|
import re as _re
|
||||||
|
numeric_only = _re.search(r'\d+', str(employee_filter))
|
||||||
|
if numeric_only:
|
||||||
|
emp = Employee.query.filter_by(id=int(numeric_only.group(0))).first()
|
||||||
|
if emp:
|
||||||
|
employee_display_name = f"{emp.lastName}, {emp.firstName}"
|
||||||
|
else:
|
||||||
|
employee_display_name = f"ID: {employee_filter}"
|
||||||
|
else:
|
||||||
|
employee_display_name = f"ID: {employee_filter}"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
employee_display_name = employee_filter
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'time_attendance_records.html',
|
'time_attendance_records.html',
|
||||||
records=records,
|
records=records,
|
||||||
unique_employees=unique_employees,
|
unique_employees=unique_employees,
|
||||||
unique_locations=unique_locations,
|
unique_locations=unique_locations,
|
||||||
projects=projects
|
projects=projects,
|
||||||
|
employee_display_name=employee_display_name,
|
||||||
|
employee_filter=employee_filter or ''
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -3,6 +3,53 @@
|
|||||||
|
|
||||||
{% 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>
|
||||||
|
/* ── Employee autocomplete for time-attendance records filter ── */
|
||||||
|
.ta-autocomplete-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.ta-autocomplete-results {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,.12);
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.ta-autocomplete-results.show { display: block; }
|
||||||
|
.ta-autocomplete-item {
|
||||||
|
padding: 10px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.ta-autocomplete-item:last-child { border-bottom: none; }
|
||||||
|
.ta-autocomplete-item:hover { background: #f8fafc; }
|
||||||
|
.ta-emp-name { font-weight: 500; color: #0f172a; font-size: .875rem; }
|
||||||
|
.ta-emp-id { font-size: .75rem; color: #64748b; white-space: nowrap; }
|
||||||
|
.ta-clear-btn {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: .85rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.ta-autocomplete-container:hover .ta-clear-btn,
|
||||||
|
.ta-clear-btn:focus { display: block; }
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -55,15 +102,28 @@
|
|||||||
<div class="filter-body" id="filterBody">
|
<div class="filter-body" id="filterBody">
|
||||||
<form method="GET" action="{{ url_for('time_attendance_records') }}" class="filter-form">
|
<form method="GET" action="{{ url_for('time_attendance_records') }}" class="filter-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="employee_id" class="form-label">Employee</label>
|
<label for="ta_employee_search" class="form-label">Employee</label>
|
||||||
<select id="employee_id" name="employee_id" class="form-select">
|
<div class="ta-autocomplete-container">
|
||||||
<option value="">All Employees</option>
|
<input type="text"
|
||||||
{% for employee in unique_employees %}
|
id="ta_employee_search"
|
||||||
<option value="{{ employee.employee_id }}" {% if request.args.get('employee_id') == employee.employee_id %}selected{% endif %}>
|
class="form-input"
|
||||||
{{ employee.employee_name }} ({{ employee.employee_id }})
|
placeholder="Search by ID or Name..."
|
||||||
</option>
|
value="{{ employee_display_name }}"
|
||||||
{% endfor %}
|
autocomplete="off">
|
||||||
</select>
|
<input type="hidden"
|
||||||
|
id="employee_id"
|
||||||
|
name="employee_id"
|
||||||
|
value="{{ employee_filter }}">
|
||||||
|
<div id="ta_autocomplete_results" class="ta-autocomplete-results"></div>
|
||||||
|
{% if employee_filter %}
|
||||||
|
<button type="button"
|
||||||
|
class="ta-clear-btn"
|
||||||
|
onclick="taClearEmployee()"
|
||||||
|
title="Clear employee filter">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -346,8 +406,8 @@ function confirmDelete(recordId, employeeName, date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportRecords(format) {
|
function exportRecords(format) {
|
||||||
// Get current filter values
|
// Get current filter values — employee_id is now a hidden field (autocomplete widget)
|
||||||
const employeeId = document.querySelector('select[name="employee_id"]')?.value || '';
|
const employeeId = document.getElementById('employee_id')?.value || '';
|
||||||
const locationName = document.querySelector('select[name="location_name"]')?.value || '';
|
const locationName = document.querySelector('select[name="location_name"]')?.value || '';
|
||||||
const projectId = document.querySelector('select[name="project_id"]')?.value || '';
|
const projectId = document.querySelector('select[name="project_id"]')?.value || '';
|
||||||
const startDate = document.querySelector('input[name="start_date"]')?.value || '';
|
const startDate = document.querySelector('input[name="start_date"]')?.value || '';
|
||||||
@@ -367,8 +427,8 @@ function exportRecords(format) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportByBuilding() {
|
function exportByBuilding() {
|
||||||
// Get current filter values
|
// Get current filter values — employee_id is now a hidden field (autocomplete widget)
|
||||||
const employeeId = document.querySelector('select[name="employee_id"]')?.value || '';
|
const employeeId = document.getElementById('employee_id')?.value || '';
|
||||||
const locationName = document.querySelector('select[name="location_name"]')?.value || '';
|
const locationName = document.querySelector('select[name="location_name"]')?.value || '';
|
||||||
const projectId = document.querySelector('select[name="project_id"]')?.value || '';
|
const projectId = document.querySelector('select[name="project_id"]')?.value || '';
|
||||||
const startDate = document.querySelector('input[name="start_date"]')?.value || '';
|
const startDate = document.querySelector('input[name="start_date"]')?.value || '';
|
||||||
@@ -412,6 +472,102 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Always ensure the filter section is visible
|
// Always ensure the filter section is visible
|
||||||
filterBody.style.display = 'block';
|
filterBody.style.display = 'block';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Employee autocomplete ──────────────────────────────────────────────────
|
||||||
|
(function () {
|
||||||
|
const searchInput = document.getElementById('ta_employee_search');
|
||||||
|
const hiddenInput = document.getElementById('employee_id');
|
||||||
|
const dropdown = document.getElementById('ta_autocomplete_results');
|
||||||
|
let debounceTimer;
|
||||||
|
|
||||||
|
if (!searchInput) return;
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', function () {
|
||||||
|
const term = this.value.trim();
|
||||||
|
|
||||||
|
// Keep hidden field in sync when typing a plain numeric ID so the user
|
||||||
|
// can submit without selecting from the dropdown (handles IDs not in Employee table).
|
||||||
|
if (/^\d+$/.test(term)) {
|
||||||
|
hiddenInput.value = term;
|
||||||
|
} else {
|
||||||
|
hiddenInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (term.length === 0) {
|
||||||
|
hiddenInput.value = '';
|
||||||
|
dropdown.classList.remove('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (term.length < 2) {
|
||||||
|
dropdown.classList.remove('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(function () {
|
||||||
|
fetch('/api/search_employees?q=' + encodeURIComponent(term))
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (data) { renderDropdown(data.employees || []); })
|
||||||
|
.catch(function (err) { console.error('Employee search error:', err); });
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (!e.target.closest('.ta-autocomplete-container')) {
|
||||||
|
dropdown.classList.remove('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderDropdown(employees) {
|
||||||
|
if (employees.length === 0) {
|
||||||
|
dropdown.innerHTML = '<div class="ta-autocomplete-item" style="cursor:default;color:#999;">No employees found</div>';
|
||||||
|
} else {
|
||||||
|
dropdown.innerHTML = employees.map(function (emp) {
|
||||||
|
const label = (emp.lastName === '(no record)')
|
||||||
|
? 'ID: ' + emp.id + ' <em style="color:#94a3b8">(no record)</em>'
|
||||||
|
: emp.lastName + ', ' + emp.firstName;
|
||||||
|
const safeLabel = (emp.lastName === '(no record)')
|
||||||
|
? 'ID: ' + emp.id
|
||||||
|
: emp.lastName + ', ' + emp.firstName;
|
||||||
|
return '<div class="ta-autocomplete-item" onclick="taSelectEmployee('' +
|
||||||
|
emp.id + '', '' + safeLabel.replace(/'/g, "\\'") + '')">' +
|
||||||
|
'<span class="ta-emp-name">' + label + '</span>' +
|
||||||
|
'<span class="ta-emp-id">ID: ' + emp.id + '</span>' +
|
||||||
|
'</div>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
positionDropdown();
|
||||||
|
dropdown.classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionDropdown() {
|
||||||
|
const rect = searchInput.getBoundingClientRect();
|
||||||
|
dropdown.style.top = (rect.bottom + window.scrollY) + 'px';
|
||||||
|
dropdown.style.left = rect.left + 'px';
|
||||||
|
dropdown.style.width = rect.width + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.taSelectEmployee = function (id, displayName) {
|
||||||
|
hiddenInput.value = id;
|
||||||
|
searchInput.value = displayName;
|
||||||
|
dropdown.classList.remove('show');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.taClearEmployee = function () {
|
||||||
|
searchInput.value = '';
|
||||||
|
hiddenInput.value = '';
|
||||||
|
searchInput.closest('form').submit();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', function () {
|
||||||
|
if (dropdown.classList.contains('show')) positionDropdown();
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
window.addEventListener('resize', function () {
|
||||||
|
if (dropdown.classList.contains('show')) positionDropdown();
|
||||||
|
});
|
||||||
|
}());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Reference in New Issue
Block a user