/** * Attendance Report JavaScript * Handles filtering, sorting, pagination, and analytics for attendance data */ // Global variables let currentPage = 1; let entriesPerPage = 50; let sortColumn = -1; let sortDirection = 'asc'; let attendanceData = []; let filteredData = []; // Charts 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(); }); function initializeReport() { // 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]; } } 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); } } function changeEntriesPerPage() { const select = document.getElementById('entriesPerPage'); entriesPerPage = select.value === 'all' ? filteredData.length : parseInt(select.value); currentPage = 1; updateTable(); updatePagination(); } 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(); } 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); } 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'; } 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); } 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'; } } }); } 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(); } 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 Records Found

No attendance records match your current filters.

`; tbody.appendChild(emptyRow); } emptyRow.style.display = ''; } else if (emptyRow) { emptyRow.style.display = 'none'; } } function updatePagination() { const container = document.getElementById('paginationContainer'); if (!container) return; const totalPages = Math.ceil(filteredData.length / entriesPerPage); if (totalPages <= 1) { container.innerHTML = ''; return; } let paginationHTML = ''; // Add pagination info const startRecord = (currentPage - 1) * entriesPerPage + 1; const endRecord = Math.min(currentPage * entriesPerPage, filteredData.length); paginationHTML += `
Showing ${startRecord} to ${endRecord} of ${filteredData.length} entries ${filteredData.length !== attendanceData.length ? `(filtered from ${attendanceData.length} total entries)` : ''}
`; container.innerHTML = paginationHTML; } function goToPage(page) { const totalPages = Math.ceil(filteredData.length / entriesPerPage); if (page < 1 || page > totalPages) return; currentPage = page; updateTable(); updatePagination(); // Scroll to top of table const table = document.getElementById('attendanceTable'); if (table) { table.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } function updateFilterStats() { // Update stats display if needed const totalRecords = filteredData.length; console.log(`Filtered records: ${totalRecords}`); } // Record actions function viewRecordDetails(recordId) { const record = attendanceData.find(r => r.id == recordId); if (!record) return; const modal = document.getElementById('recordModal'); const modalTitle = document.getElementById('modalTitle'); const modalBody = document.getElementById('modalBody'); if (!modal || !modalTitle || !modalBody) return; modalTitle.textContent = `Attendance Record - ${record.employeeId}`; modalBody.innerHTML = `
Employee ID: ${record.employeeId}
Location: ${record.location}
Event: ${record.event}
Date: ${record.date}
Time: ${record.time}
Device: ${record.device}
Status: ${record.status}
`; modal.style.display = 'flex'; setTimeout(() => modal.classList.add('show'), 10); } function editRecord(recordId) { // Placeholder for edit functionality alert(`Edit functionality for record ${recordId} would be implemented here.`); } function deleteRecord(recordId) { const record = attendanceData.find(r => r.id == recordId); if (!record) return; const confirmed = confirm( `Are you sure you want to delete the attendance record for ${record.employeeId}?\n\n` + `Date: ${record.date}\n` + `Time: ${record.time}\n` + `Location: ${record.location}\n\n` + 'This action cannot be undone.' ); if (confirmed) { // Here you would make an API call to delete the record console.log(`Deleting record ${recordId}`); // For demo purposes, just remove from current data const index = attendanceData.findIndex(r => r.id == recordId); if (index > -1) { // Remove from DOM if (attendanceData[index].element) { attendanceData[index].element.remove(); } // Remove from data arrays attendanceData.splice(index, 1); const filteredIndex = filteredData.findIndex(r => r.id == recordId); if (filteredIndex > -1) { filteredData.splice(filteredIndex, 1); } // Update display updateTable(); updatePagination(); showToast('Record deleted successfully', 'success'); } } } function closeRecordModal() { const modal = document.getElementById('recordModal'); if (modal) { modal.classList.remove('show'); setTimeout(() => { modal.style.display = 'none'; }, 200); } } // Export functionality function exportAttendance() { const exportData = filteredData.map(record => ({ 'Employee ID': record.employeeId, 'Location': record.location, 'Event': record.event, 'Date': record.date, 'Time': record.time, 'Device': record.device, 'Status': record.status })); const csv = convertToCSV(exportData); downloadCSV(csv, `attendance_report_${new Date().toISOString().split('T')[0]}.csv`); showToast('Attendance data exported successfully', 'success'); } function convertToCSV(data) { if (!data.length) return ''; const headers = Object.keys(data[0]); const csvContent = [ headers.join(','), ...data.map(row => headers.map(header => { const value = row[header]; // Escape commas and quotes return typeof value === 'string' && (value.includes(',') || value.includes('"')) ? `"${value.replace(/"/g, '""')}"` : value; }).join(',') ) ].join('\n'); return csvContent; } function downloadCSV(csv, filename) { const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } function refreshReport() { showToast('Refreshing report...', 'info'); setTimeout(() => { window.location.reload(); }, 500); } // Charts initialization function initializeCharts() { loadAttendanceStats(); } function loadAttendanceStats() { fetch('/api/attendance/stats') .then(response => response.json()) .then(data => { createDailyChart(data.daily_stats || []); createLocationChart(data.location_stats || []); }) .catch(error => { console.error('Error loading attendance stats:', error); }); } function createDailyChart(dailyStats) { const ctx = document.getElementById('dailyChart'); if (!ctx) return; if (dailyChart) { dailyChart.destroy(); } dailyChart = new Chart(ctx, { type: 'line', data: { labels: dailyStats.map(stat => stat.date), datasets: [{ label: 'Check-ins', data: dailyStats.map(stat => stat.checkins), borderColor: '#2563eb', backgroundColor: 'rgba(37, 99, 235, 0.1)', tension: 0.4, fill: true }, { label: 'Unique Employees', data: dailyStats.map(stat => stat.employees), borderColor: '#059669', backgroundColor: 'rgba(5, 150, 105, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'top' } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } } }); } function createLocationChart(locationStats) { const ctx = document.getElementById('locationChart'); if (!ctx) return; if (locationChart) { locationChart.destroy(); } locationChart = new Chart(ctx, { type: 'doughnut', data: { labels: locationStats.map(stat => stat.location), datasets: [{ data: locationStats.map(stat => stat.checkins), backgroundColor: [ '#2563eb', '#059669', '#d97706', '#dc2626', '#7c3aed', '#0891b2', '#65a30d', '#c2410c' ] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'right' } } } }); } // Utility functions function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.innerHTML = `
${message}
`; toast.style.cssText = ` position: fixed; top: 20px; right: 20px; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); padding: 1rem; z-index: 1000; opacity: 0; transform: translateX(100%); transition: all 0.3s ease; border-left: 4px solid ${getToastColor(type)}; max-width: 400px; `; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '1'; toast.style.transform = 'translateX(0)'; }, 100); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(100%)'; setTimeout(() => { if (document.body.contains(toast)) { document.body.removeChild(toast); } }, 300); }, 3000); } function getToastIcon(type) { const icons = { success: 'fa-check-circle', error: 'fa-exclamation-circle', warning: 'fa-exclamation-triangle', info: 'fa-info-circle' }; return icons[type] || icons.info; } function getToastColor(type) { const colors = { success: '#059669', error: '#dc2626', warning: '#d97706', info: '#0891b2' }; return colors[type] || colors.info; } // Global function exports window.sortTable = sortTable; window.changeEntriesPerPage = changeEntriesPerPage; window.clearFilters = clearFilters; window.goToPage = goToPage; window.viewRecordDetails = viewRecordDetails; window.editRecord = editRecord; window.deleteRecord = deleteRecord; window.closeRecordModal = closeRecordModal; window.exportAttendance = exportAttendance; window.refreshReport = refreshReport;