diff --git a/app.py b/app.py index 78f76cc..4d381f7 100644 --- a/app.py +++ b/app.py @@ -1617,18 +1617,86 @@ def toggle_qr_status_api(qr_id): @app.route('/attendance') @admin_required def attendance_report(): - """Attendance report page (Admin only)""" + """Enhanced attendance report page with date range filtering and location data (Admin only)""" try: # Get filter parameters - date_filter = request.args.get('date', '') + date_from = request.args.get('date_from', '') + date_to = request.args.get('date_to', '') location_filter = request.args.get('location', '') employee_filter = request.args.get('employee', '') - # Base query using the view - query = db.session.execute(text("SELECT * FROM attendance_report WHERE 1=1")) + # Build base query with enhanced location data + base_query = """ + SELECT + ad.id, + ad.employee_id, + ad.check_in_date, + ad.check_in_time, + ad.location_name, + qc.location_event, + qc.location_address as qr_address, + ad.address as checked_in_address, + ad.latitude, + ad.longitude, + ad.accuracy, + ad.device_info + FROM attendance_data ad + LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id + WHERE 1=1 + """ - # Apply filters (you can enhance this with proper SQLAlchemy filtering) - attendance_records = query.fetchall() + conditions = [] + params = {} + + # Apply date range filter + if date_from: + conditions.append("ad.check_in_date >= :date_from") + params['date_from'] = date_from + + if date_to: + conditions.append("ad.check_in_date <= :date_to") + params['date_to'] = date_to + + # Apply location filter + if location_filter: + conditions.append("ad.location_name ILIKE :location") + params['location'] = f"%{location_filter}%" + + # Apply employee filter + if employee_filter: + conditions.append("ad.employee_id ILIKE :employee") + params['employee'] = f"%{employee_filter}%" + + # Add conditions to query + if conditions: + base_query += " AND " + " AND ".join(conditions) + + # Add ordering + base_query += " ORDER BY ad.check_in_date DESC, ad.check_in_time DESC" + + # Execute query + query_result = db.session.execute(text(base_query), params) + attendance_records = query_result.fetchall() + + # Process records to add calculated fields + processed_records = [] + for record in attendance_records: + record_dict = { + 'id': record.id, + 'employee_id': record.employee_id, + 'check_in_date': record.check_in_date, + 'check_in_time': record.check_in_time, + 'location_name': record.location_name, + 'location_event': record.location_event, + 'qr_address': record.qr_address or 'Not available', + 'checked_in_address': record.checked_in_address or 'Location not captured', + 'device_info': record.device_info, + 'accuracy': record.accuracy, + 'accuracy_level': get_accuracy_level(record.accuracy), + 'has_location_data': record.latitude is not None and record.longitude is not None, + 'coordinates': f"{record.latitude:.6f}, {record.longitude:.6f}" if record.latitude and record.longitude else "No GPS data" + } + processed_records.append(record_dict) # Get unique locations for filter dropdown locations_query = db.session.execute(text(""" @@ -1644,7 +1712,9 @@ def attendance_report(): COUNT(*) as total_checkins, COUNT(DISTINCT employee_id) as unique_employees, COUNT(DISTINCT qr_code_id) as active_locations, - COUNT(CASE WHEN check_in_date = CURRENT_DATE THEN 1 END) as today_checkins + COUNT(CASE WHEN check_in_date = CURRENT_DATE THEN 1 END) as today_checkins, + COUNT(CASE WHEN latitude IS NOT NULL AND longitude IS NOT NULL THEN 1 END) as records_with_gps, + AVG(accuracy) as avg_accuracy FROM attendance_data """)) stats = stats_query.fetchone() @@ -1654,10 +1724,11 @@ def attendance_report(): current_date_formatted = datetime.now().strftime('%B %d') return render_template('attendance_report.html', - attendance_records=attendance_records, + attendance_records=processed_records, locations=locations, stats=stats, - date_filter=date_filter, + date_from=date_from, + date_to=date_to, location_filter=location_filter, employee_filter=employee_filter, today_date=today_date, @@ -1665,8 +1736,19 @@ def attendance_report(): except Exception as e: print(f"Error loading attendance report: {e}") - flash('Error loading attendance report.', 'error') + flash('Error loading attendance report. Please try again.', 'error') return redirect(url_for('dashboard')) + +def get_accuracy_level(accuracy): + """Get human-readable accuracy level""" + if not accuracy: + return 'unknown' + elif accuracy <= 50: + return 'high' + elif accuracy <= 100: + return 'medium' + else: + return 'low' @app.route('/api/attendance/stats') @admin_required diff --git a/static/css/attendance.css b/static/css/attendance.css index db4447d..c076d73 100644 --- a/static/css/attendance.css +++ b/static/css/attendance.css @@ -97,7 +97,11 @@ } .stat-card.primary::before { - background: linear-gradient(90deg, var(--primary-color), var(--primary-hover)); + background: linear-gradient( + 90deg, + var(--primary-color), + var(--primary-hover) + ); } .stat-card.success::before { @@ -130,7 +134,11 @@ } .stat-card.primary .stat-icon { - background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); + background: linear-gradient( + 135deg, + var(--primary-color), + var(--primary-hover) + ); } .stat-card.success .stat-icon { @@ -378,7 +386,7 @@ .employee-id { font-weight: 600; color: var(--gray-900); - font-family: 'Courier New', monospace; + font-family: "Courier New", monospace; font-size: var(--font-size-sm); background: var(--gray-100); padding: var(--spacing-1) var(--spacing-2); @@ -403,13 +411,13 @@ } .date-info { - font-family: 'Courier New', monospace; + font-family: "Courier New", monospace; color: var(--gray-900); font-weight: 500; } .time-info { - font-family: 'Courier New', monospace; + font-family: "Courier New", monospace; color: var(--gray-900); font-weight: 600; background: var(--primary-light); @@ -706,6 +714,275 @@ background: var(--gray-50); } +.address-info { + display: flex; + align-items: flex-start; + gap: var(--spacing-2); + color: var(--gray-700); + font-size: var(--font-size-sm); + max-width: 250px; +} + +.address-info i { + color: var(--primary-color); + font-size: var(--font-size-xs); + margin-top: 2px; + flex-shrink: 0; +} + +.address-info span { + line-height: 1.4; + word-break: break-word; +} + +.address-info.qr-address i { + color: var(--success-color); +} + +.address-info.checkin-address i { + color: var(--info-color); +} + +/* GPS Accuracy badges */ +.accuracy-info { + display: flex; + align-items: center; + justify-content: center; +} + +.accuracy-badge { + display: inline-flex; + align-items: center; + gap: var(--spacing-1); + padding: var(--spacing-1) var(--spacing-3); + border-radius: var(--radius-full); + font-size: var(--font-size-xs); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.025em; + flex-direction: column; + text-align: center; + min-width: 80px; +} + +.accuracy-badge small { + font-size: 0.6rem; + font-weight: 500; + margin-top: 2px; + opacity: 0.8; +} + +.accuracy-badge.accuracy-high { + background: var(--success-light); + color: var(--success-color); + border: 1px solid rgba(5, 150, 105, 0.3); +} + +.accuracy-badge.accuracy-medium { + background: var(--warning-light); + color: var(--warning-color); + border: 1px solid rgba(245, 158, 11, 0.3); +} + +.accuracy-badge.accuracy-low { + background: var(--danger-light); + color: var(--danger-color); + border: 1px solid rgba(220, 38, 38, 0.3); +} + +.accuracy-badge.accuracy-unknown { + background: var(--gray-100); + color: var(--gray-500); + border: 1px solid rgba(107, 114, 128, 0.3); +} + +/* Filter indicator */ +.filter-indicator { + background: var(--primary-light); + color: var(--primary-color); + font-size: var(--font-size-xs); + padding: var(--spacing-1) var(--spacing-2); + border-radius: var(--radius); + margin-left: var(--spacing-2); + font-weight: 500; +} + +/* Enhanced action buttons */ +.btn-map { + background: var(--success-light); + color: var(--success-color); +} + +.btn-map:hover { + background: var(--success-color); + color: var(--white); +} + +.btn-map:disabled { + background: var(--gray-100); + color: var(--gray-400); + cursor: not-allowed; +} + +/* Modal enhancements */ +.modal-large .modal-content { + max-width: 800px; + width: 90%; +} + +.record-details-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--spacing-6); +} + +.detail-section { + background: var(--gray-50); + padding: var(--spacing-4); + border-radius: var(--radius-lg); + border: 1px solid var(--gray-200); +} + +.detail-section h4 { + color: var(--gray-800); + font-size: var(--font-size-base); + font-weight: 600; + margin-bottom: var(--spacing-3); + display: flex; + align-items: center; + gap: var(--spacing-2); +} + +.detail-section h4 i { + color: var(--primary-color); + font-size: var(--font-size-sm); +} + +.detail-item { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: var(--spacing-2) 0; + border-bottom: 1px solid var(--gray-200); + gap: var(--spacing-3); +} + +.detail-item:last-child { + border-bottom: none; +} + +.detail-item strong { + color: var(--gray-700); + font-weight: 500; + flex-shrink: 0; + min-width: 120px; +} + +.detail-item span { + color: var(--gray-900); + text-align: right; + word-break: break-word; +} + +/* Enhanced date range filters */ +.filter-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-4); + align-items: end; +} + +.filter-group input[type="date"] { + appearance: none; + -webkit-appearance: none; + position: relative; +} + +.filter-group input[type="date"]::-webkit-calendar-picker-indicator { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor'%3e%3cpath fill-rule='evenodd' d='M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z' clip-rule='evenodd'/%3e%3c/svg%3e"); + background-size: 16px; + background-repeat: no-repeat; + background-position: center; + cursor: pointer; +} + +/* Enhanced table responsive design */ +@media (max-width: 1200px) { + .attendance-table th:nth-child(7), + .attendance-table td:nth-child(7), + .attendance-table th:nth-child(8), + .attendance-table td:nth-child(8) { + display: none; + } +} + +@media (max-width: 768px) { + .address-info { + max-width: 150px; + } + + .accuracy-badge { + min-width: 60px; + font-size: 0.625rem; + } + + .accuracy-badge small { + display: none; + } + + .filter-row { + grid-template-columns: 1fr; + } + + .record-details-grid { + grid-template-columns: 1fr; + } + + /* Hide address columns on mobile */ + .attendance-table th:nth-child(7), + .attendance-table td:nth-child(7), + .attendance-table th:nth-child(8), + .attendance-table td:nth-child(8), + .attendance-table th:nth-child(9), + .attendance-table td:nth-child(9) { + display: none; + } +} + +/* Enhanced empty state */ +.empty-state .btn { + margin-top: var(--spacing-4); +} + +/* Location map placeholder styles */ +#locationMap { + background: var(--gray-50); + border: 2px dashed var(--gray-300); + border-radius: var(--radius-lg); + display: flex; + align-items: center; + justify-content: center; + color: var(--gray-600); +} + +/* Print styles for new columns */ +@media print { + .address-info, + .accuracy-info { + font-size: 0.7rem; + } + + .accuracy-badge { + background: transparent !important; + border: 1px solid #ccc !important; + color: #000 !important; + } + + .btn-map { + display: none; + } +} + /* Responsive Design */ @media (max-width: 768px) { .attendance-page { @@ -818,4 +1095,4 @@ .pagination-container { display: none; } -} \ No newline at end of file +} diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index 6f7ccf3..17a7e56 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -1,13 +1,13 @@ /** - * Attendance Report JavaScript - * Handles filtering, sorting, pagination, and analytics for attendance data + * Enhanced Attendance Report JavaScript + * Handles filtering, sorting, pagination, and new location/accuracy features */ // Global variables let currentPage = 1; let entriesPerPage = 50; let sortColumn = -1; -let sortDirection = 'asc'; +let sortDirection = "asc"; let attendanceData = []; let filteredData = []; @@ -16,708 +16,544 @@ let dailyChart = null; let locationChart = null; // Initialize page when DOM is loaded -document.addEventListener('DOMContentLoaded', function() { - console.log('Attendance Report page initialized'); - - initializeReport(); - loadAttendanceData(); - initializeCharts(); - setupEventListeners(); +document.addEventListener("DOMContentLoaded", function () { + console.log("Enhanced Attendance Report page initialized"); + + initializeReport(); + loadAttendanceData(); + initializeCharts(); + setupEventListeners(); + initializeDateRangeFilters(); }); function initializeReport() { - // Load data from table - loadTableData(); - - // Initialize pagination - updatePagination(); - - // Apply initial filters if any - applyFilters(); + // Load data from table + loadTableData(); + + // Initialize pagination + updatePagination(); + + // Apply initial filters if any + applyFilters(); } function loadTableData() { - const table = document.getElementById('attendanceTable'); - if (table) { - const rows = table.querySelectorAll('tbody tr'); - attendanceData = Array.from(rows).map((row, index) => { - const cells = row.querySelectorAll('td'); - return { - id: row.dataset.recordId, - index: index + 1, - employeeId: cells[1] ? cells[1].textContent.trim() : '', - location: cells[2] ? cells[2].textContent.trim() : '', - event: cells[3] ? cells[3].textContent.trim() : '', - date: cells[4] ? cells[4].textContent.trim() : '', - time: cells[5] ? cells[5].textContent.trim() : '', - device: cells[6] ? cells[6].getAttribute('title') || cells[6].textContent.trim() : '', - status: cells[7] ? cells[7].textContent.trim() : '', - element: row - }; - }); - - filteredData = [...attendanceData]; - } + const table = document.getElementById("attendanceTable"); + if (table) { + const rows = table.querySelectorAll("tbody tr"); + attendanceData = Array.from(rows).map((row, index) => { + const cells = row.querySelectorAll("td"); + return { + id: row.dataset.recordId, + index: index + 1, + employeeId: cells[1] ? cells[1].textContent.trim() : "", + location: cells[2] ? cells[2].textContent.trim() : "", + event: cells[3] ? cells[3].textContent.trim() : "", + date: cells[4] ? cells[4].textContent.trim() : "", + time: cells[5] ? cells[5].textContent.trim() : "", + qr_address: cells[6] + ? cells[6].getAttribute("title") || cells[6].textContent.trim() + : "", + checked_in_address: cells[7] + ? cells[7].getAttribute("title") || cells[7].textContent.trim() + : "", + accuracy: cells[8] ? extractAccuracyValue(cells[8]) : null, + accuracy_level: cells[8] ? extractAccuracyLevel(cells[8]) : "unknown", + device: cells[9] + ? cells[9].getAttribute("title") || cells[9].textContent.trim() + : "", + has_location_data: cells[8] + ? !cells[8].textContent.includes("No GPS") + : false, + coordinates: extractCoordinates(cells[8]), + }; + }); + + filteredData = [...attendanceData]; + console.log(`Loaded ${attendanceData.length} attendance records`); + } +} + +function extractAccuracyValue(cell) { + const text = cell.textContent; + const match = text.match(/(\d+\.?\d*)m/); + return match ? parseFloat(match[1]) : null; +} + +function extractAccuracyLevel(cell) { + const text = cell.textContent; + if (text.includes("high")) return "high"; + if (text.includes("medium")) return "medium"; + if (text.includes("low")) return "low"; + return "unknown"; +} + +function extractCoordinates(cell) { + // This would need to be enhanced based on actual data structure + // For now, return placeholder + return "Coordinates available"; +} + +function initializeDateRangeFilters() { + const dateFromInput = document.getElementById("date_from"); + const dateToInput = document.getElementById("date_to"); + + if (dateFromInput && dateToInput) { + // Set max date to today + const today = new Date().toISOString().split("T")[0]; + dateFromInput.max = today; + dateToInput.max = today; + + // Add validation to ensure 'from' date is not after 'to' date + dateFromInput.addEventListener("change", function () { + if (dateToInput.value && this.value > dateToInput.value) { + dateToInput.value = this.value; + } + }); + + dateToInput.addEventListener("change", function () { + if (dateFromInput.value && this.value < dateFromInput.value) { + dateFromInput.value = this.value; + } + }); + } } function setupEventListeners() { - // Entries per page change - const entriesSelect = document.getElementById('entriesPerPage'); - if (entriesSelect) { - entriesSelect.addEventListener('change', changeEntriesPerPage); - } - - // Filter form - const filtersForm = document.getElementById('filtersForm'); - if (filtersForm) { - filtersForm.addEventListener('submit', function(e) { - e.preventDefault(); - applyFilters(); - }); - } - - // Real-time employee filter - const employeeFilter = document.getElementById('employee'); - if (employeeFilter) { - employeeFilter.addEventListener('input', debounce(applyFilters, 300)); - } - - // Date and location filters - const dateFilter = document.getElementById('date'); - const locationFilter = document.getElementById('location'); - - if (dateFilter) { - dateFilter.addEventListener('change', applyFilters); - } - - if (locationFilter) { - locationFilter.addEventListener('change', applyFilters); - } -} + // Enhanced search and filter listeners + const searchInput = document.getElementById("searchInput"); + const locationFilter = document.getElementById("location"); + const employeeFilter = document.getElementById("employee"); -function changeEntriesPerPage() { - const select = document.getElementById('entriesPerPage'); - entriesPerPage = select.value === 'all' ? filteredData.length : parseInt(select.value); - currentPage = 1; - updateTable(); - updatePagination(); + if (searchInput) { + searchInput.addEventListener("input", debounce(applyFilters, 300)); + } + + if (locationFilter) { + locationFilter.addEventListener("change", applyFilters); + } + + if (employeeFilter) { + employeeFilter.addEventListener("input", debounce(applyFilters, 300)); + } + + // Entries per page listener + const entriesSelect = document.getElementById("entriesPerPage"); + if (entriesSelect) { + entriesSelect.addEventListener("change", changeEntriesPerPage); + } + + // Modal close listeners + window.addEventListener("click", function (event) { + const recordModal = document.getElementById("recordModal"); + const mapModal = document.getElementById("mapModal"); + + if (event.target === recordModal) { + closeModal(); + } + if (event.target === mapModal) { + closeMapModal(); + } + }); + + // Keyboard shortcuts + document.addEventListener("keydown", function (event) { + if (event.key === "Escape") { + closeModal(); + closeMapModal(); + } + }); } function applyFilters() { - const dateFilter = document.getElementById('date')?.value || ''; - const locationFilter = document.getElementById('location')?.value || ''; - const employeeFilter = document.getElementById('employee')?.value.toLowerCase() || ''; - - filteredData = attendanceData.filter(record => { - const matchesDate = !dateFilter || record.date === dateFilter; - const matchesLocation = !locationFilter || record.location === locationFilter; - const matchesEmployee = !employeeFilter || - record.employeeId.toLowerCase().includes(employeeFilter); - - return matchesDate && matchesLocation && matchesEmployee; - }); - - currentPage = 1; - updateTable(); - updatePagination(); - updateFilterStats(); -} + const searchTerm = + document.getElementById("searchInput")?.value.toLowerCase() || ""; + const locationFilter = document.getElementById("location")?.value || ""; + const employeeFilter = + document.getElementById("employee")?.value.toLowerCase() || ""; -function clearFilters() { - // Clear form inputs - const form = document.getElementById('filtersForm'); - if (form) { - form.reset(); - } - - // Reset filtered data - filteredData = [...attendanceData]; - currentPage = 1; - - // Update display - updateTable(); - updatePagination(); - updateFilterStats(); - - // Update URL without filters - const url = new URL(window.location); - url.search = ''; - window.history.pushState({}, '', url); + filteredData = attendanceData.filter((record) => { + const matchesSearch = + !searchTerm || + record.employeeId.toLowerCase().includes(searchTerm) || + record.location.toLowerCase().includes(searchTerm) || + record.event.toLowerCase().includes(searchTerm); + + const matchesLocation = + !locationFilter || record.location === locationFilter; + const matchesEmployee = + !employeeFilter || + record.employeeId.toLowerCase().includes(employeeFilter); + + return matchesSearch && matchesLocation && matchesEmployee; + }); + + currentPage = 1; + updateTable(); + updatePagination(); + updateFilterStats(); } function sortTable(columnIndex) { - const headers = ['index', 'employeeId', 'location', 'event', 'date', 'time', 'device', 'status']; - const column = headers[columnIndex]; - - if (sortColumn === columnIndex) { - sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; - } else { - sortColumn = columnIndex; - sortDirection = 'asc'; + if (sortColumn === columnIndex) { + sortDirection = sortDirection === "asc" ? "desc" : "asc"; + } else { + sortColumn = columnIndex; + sortDirection = "asc"; + } + + const sortKey = getSortKey(columnIndex); + + filteredData.sort((a, b) => { + let aVal = a[sortKey]; + let bVal = b[sortKey]; + + // Handle numeric values for accuracy + if (columnIndex === 8 && aVal !== null && bVal !== null) { + aVal = parseFloat(aVal); + bVal = parseFloat(bVal); } - - filteredData.sort((a, b) => { - let aVal = a[column]; - let bVal = b[column]; - - // Handle different data types - if (column === 'date' || column === 'time') { - aVal = new Date(column === 'date' ? aVal : `2000-01-01 ${aVal}`); - bVal = new Date(column === 'date' ? bVal : `2000-01-01 ${bVal}`); - } else if (column === 'index') { - aVal = parseInt(aVal); - bVal = parseInt(bVal); - } else { - aVal = aVal.toString().toLowerCase(); - bVal = bVal.toString().toLowerCase(); - } - - if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; - if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; - return 0; - }); - - updateTable(); - updateSortIndicators(columnIndex); + + // Handle null values + if (aVal === null || aVal === undefined) aVal = ""; + if (bVal === null || bVal === undefined) bVal = ""; + + if (typeof aVal === "string") { + aVal = aVal.toLowerCase(); + bVal = bVal.toLowerCase(); + } + + let result; + if (aVal < bVal) result = -1; + else if (aVal > bVal) result = 1; + else result = 0; + + return sortDirection === "asc" ? result : -result; + }); + + updateTable(); + updateSortIndicators(columnIndex); +} + +function getSortKey(columnIndex) { + const sortKeys = [ + "index", + "employeeId", + "location", + "event", + "date", + "time", + "qr_address", + "checked_in_address", + "accuracy", + "device", + ]; + return sortKeys[columnIndex] || "index"; } function updateSortIndicators(activeColumn) { - const headers = document.querySelectorAll('th[onclick]'); - headers.forEach((header, index) => { - const icon = header.querySelector('i'); - if (icon) { - if (index === activeColumn) { - icon.className = `fas fa-sort-${sortDirection === 'asc' ? 'up' : 'down'}`; - } else { - icon.className = 'fas fa-sort'; - } - } - }); + // Update sort indicators in table headers + const headers = document.querySelectorAll(".attendance-table th"); + headers.forEach((header, index) => { + const icon = header.querySelector("i"); + if (icon) { + icon.className = "fas fa-sort"; + if (index === activeColumn) { + icon.className = + sortDirection === "asc" ? "fas fa-sort-up" : "fas fa-sort-down"; + } + } + }); } function updateTable() { - const tbody = document.querySelector('#attendanceTable tbody'); - if (!tbody) return; - - // Calculate pagination - const startIndex = (currentPage - 1) * entriesPerPage; - const endIndex = entriesPerPage === filteredData.length ? - filteredData.length : - Math.min(startIndex + entriesPerPage, filteredData.length); - - // Hide all rows first - attendanceData.forEach(record => { - if (record.element) { - record.element.style.display = 'none'; - } - }); - - // Show filtered and paginated rows - const visibleData = filteredData.slice(startIndex, endIndex); - visibleData.forEach((record, index) => { - if (record.element) { - record.element.style.display = ''; - // Update row number - const firstCell = record.element.querySelector('td:first-child'); - if (firstCell) { - firstCell.textContent = startIndex + index + 1; - } - } - }); - - // Show empty state if no data - showEmptyStateIfNeeded(); + const table = document.getElementById("attendanceTable"); + if (!table) return; + + const tbody = table.querySelector("tbody"); + const startIndex = (currentPage - 1) * entriesPerPage; + const endIndex = + entriesPerPage === "all" + ? filteredData.length + : startIndex + entriesPerPage; + const pageData = filteredData.slice(startIndex, endIndex); + + tbody.innerHTML = ""; + + pageData.forEach((record, index) => { + const row = createTableRow(record, startIndex + index + 1); + tbody.appendChild(row); + }); + + // Update any dynamic elements + updateFilterStats(); } -function showEmptyStateIfNeeded() { - const tbody = document.querySelector('#attendanceTable tbody'); - let emptyRow = tbody.querySelector('.empty-row'); - - if (filteredData.length === 0) { - if (!emptyRow) { - emptyRow = document.createElement('tr'); - emptyRow.className = 'empty-row'; - emptyRow.innerHTML = ` -
No attendance records match your current filters.
- -Monitor and analyze staff attendance across all locations
+Monitor and analyze staff attendance with location accuracy tracking
Active Locations
- With check-ins + QR codes deployedToday's Check-ins
- {{ current_date_formatted or 'Today' }} + {{ current_date_formatted }} +GPS Records
+ Location captured +Avg. Accuracy
+ Location precision