Files
GOV_QR_Codes_Management/static/js/attendance_report.js
T
2025-07-29 17:02:08 -04:00

723 lines
22 KiB
JavaScript

/**
* 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 = `
<td colspan="9" style="text-align: center; padding: 3rem;">
<div style="color: #6b7280;">
<i class="fas fa-search" style="font-size: 2rem; margin-bottom: 1rem; opacity: 0.5;"></i>
<h3>No Records Found</h3>
<p>No attendance records match your current filters.</p>
<button onclick="clearFilters()" class="btn btn-primary" style="margin-top: 1rem;">
<i class="fas fa-refresh"></i> Clear Filters
</button>
</div>
</td>
`;
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 = '<div class="pagination">';
// Previous button
paginationHTML += `
<button onclick="goToPage(${currentPage - 1})"
class="pagination-btn"
${currentPage === 1 ? 'disabled' : ''}>
<i class="fas fa-chevron-left"></i>
</button>
`;
// Page numbers
const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
if (startPage > 1) {
paginationHTML += `<button onclick="goToPage(1)" class="pagination-btn">1</button>`;
if (startPage > 2) {
paginationHTML += '<span class="pagination-ellipsis">...</span>';
}
}
for (let i = startPage; i <= endPage; i++) {
paginationHTML += `
<button onclick="goToPage(${i})"
class="pagination-btn ${i === currentPage ? 'active' : ''}">
${i}
</button>
`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
paginationHTML += '<span class="pagination-ellipsis">...</span>';
}
paginationHTML += `<button onclick="goToPage(${totalPages})" class="pagination-btn">${totalPages}</button>`;
}
// Next button
paginationHTML += `
<button onclick="goToPage(${currentPage + 1})"
class="pagination-btn"
${currentPage === totalPages ? 'disabled' : ''}>
<i class="fas fa-chevron-right"></i>
</button>
`;
paginationHTML += '</div>';
// Add pagination info
const startRecord = (currentPage - 1) * entriesPerPage + 1;
const endRecord = Math.min(currentPage * entriesPerPage, filteredData.length);
paginationHTML += `
<div class="pagination-info">
Showing ${startRecord} to ${endRecord} of ${filteredData.length} entries
${filteredData.length !== attendanceData.length ?
`(filtered from ${attendanceData.length} total entries)` : ''}
</div>
`;
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 = `
<div class="record-details">
<div class="detail-grid">
<div class="detail-item">
<strong>Employee ID:</strong>
<span>${record.employeeId}</span>
</div>
<div class="detail-item">
<strong>Location:</strong>
<span>${record.location}</span>
</div>
<div class="detail-item">
<strong>Event:</strong>
<span>${record.event}</span>
</div>
<div class="detail-item">
<strong>Date:</strong>
<span>${record.date}</span>
</div>
<div class="detail-item">
<strong>Time:</strong>
<span>${record.time}</span>
</div>
<div class="detail-item">
<strong>Device:</strong>
<span>${record.device}</span>
</div>
<div class="detail-item">
<strong>Status:</strong>
<span class="status-badge ${record.status.toLowerCase()}">
${record.status}
</span>
</div>
</div>
</div>
`;
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 = `
<div class="toast-content">
<i class="fas ${getToastIcon(type)}"></i>
<span>${message}</span>
</div>
`;
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;