/**
* 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 attendanceData = [];
let filteredData = [];
// Charts
let dailyChart = null;
let locationChart = null;
// Initialize page when DOM is loaded
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();
}
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() : "",
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() {
// Enhanced search and filter listeners
const searchInput = document.getElementById("searchInput");
const locationFilter = document.getElementById("location");
const employeeFilter = document.getElementById("employee");
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 searchTerm =
document.getElementById("searchInput")?.value.toLowerCase() || "";
const locationFilter = document.getElementById("location")?.value || "";
const employeeFilter =
document.getElementById("employee")?.value.toLowerCase() || "";
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) {
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);
}
// 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) {
// 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 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 createTableRow(record, displayIndex) {
const row = document.createElement("tr");
row.dataset.recordId = record.id;
// Create accuracy badge HTML
const accuracyBadge =
record.accuracy !== null
? `
${record.accuracy.toFixed(1)}m
(${record.accuracy_level})
`
: `
No GPS
`;
row.innerHTML = `
${displayIndex} |
${record.employeeId}
|
${record.location}
|
${record.event}
|
${record.date}
|
${record.time}
|
${
record.qr_address.length > 50
? record.qr_address.substring(0, 50) + "..."
: record.qr_address
}
|
${
record.checked_in_address.length > 50
? record.checked_in_address.substring(0, 50) + "..."
: record.checked_in_address
}
|
${accuracyBadge}
|
${
record.device.length > 20
? record.device.substring(0, 20) + "..."
: record.device
}
|
|
`;
return row;
}
function changeEntriesPerPage() {
const select = document.getElementById("entriesPerPage");
entriesPerPage = select.value === "all" ? "all" : parseInt(select.value);
currentPage = 1;
updateTable();
updatePagination();
}
function updatePagination() {
const container = document.getElementById("paginationContainer");
if (!container || entriesPerPage === "all") {
if (container) container.innerHTML = "";
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 += `
`;
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}`);
}
// Enhanced record actions
function editRecord(recordId) {
console.log(`Edit record: ${recordId}`);
// Implement edit functionality
alert("Edit functionality to be implemented");
}
function closeModal() {
const modal = document.getElementById("recordModal");
if (modal) {
modal.style.display = "none";
}
}
// Chart initialization (placeholder)
function initializeCharts() {
console.log("Initializing charts...");
// Chart implementation would go here
}
function loadAttendanceData() {
console.log("Loading attendance data for charts...");
// Additional data loading for charts would go here
}
// Utility function
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Enhanced JavaScript functions for location accuracy features
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() : "",
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()
: "",
location_accuracy: cells[8] ? extractLocationAccuracy(cells[8]) : null,
accuracy_level: cells[8]
? extractLocationAccuracyLevel(cells[8])
: "unknown",
device: cells[9]
? cells[9].getAttribute("title") || cells[9].textContent.trim()
: "",
has_location_data: cells[8]
? !cells[8].textContent.includes("Unknown")
: false,
coordinates: extractCoordinates(cells[8]),
};
});
filteredData = [...attendanceData];
console.log(
`Loaded ${attendanceData.length} attendance records with location accuracy`
);
}
}
function extractLocationAccuracy(cell) {
const text = cell.textContent;
const match = text.match(/(\d+\.?\d*)\s*mi/);
return match ? parseFloat(match[1]) : null;
}
function extractLocationAccuracyLevel(cell) {
const text = cell.textContent;
if (text.includes("excellent") || text.includes("good")) return "accurate";
if (text.includes("fair") || text.includes("poor")) return "unaccurate";
return "unknown";
}
function createTableRow(record, displayIndex) {
const row = document.createElement("tr");
row.dataset.recordId = record.id;
// Create location accuracy badge HTML
const locationAccuracyBadge =
record.location_accuracy !== null
? `
${record.location_accuracy.toFixed(3)} mi
(${record.accuracy_level})
`
: `
Unknown
`;
row.innerHTML = `
${displayIndex} |
${record.employeeId}
|
${record.location}
|
${record.event}
|
${record.date}
|
${record.time}
|
${
record.qr_address.length > 50
? record.qr_address.substring(0, 50) + "..."
: record.qr_address
}
|
${
record.checked_in_address.length > 50
? record.checked_in_address.substring(0, 50) + "..."
: record.checked_in_address
}
|
${locationAccuracyBadge}
|
${
record.device.length > 20
? record.device.substring(0, 20) + "..."
: record.device
}
|
|
`;
return row;
}
function getSortKey(columnIndex) {
const sortKeys = [
"index",
"employeeId",
"location",
"event",
"date",
"time",
"qr_address",
"checked_in_address",
"location_accuracy",
"device",
];
return sortKeys[columnIndex] || "index";
}
// Enhanced sorting for location accuracy (numeric sorting)
function sortTable(columnIndex) {
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 location accuracy
if (columnIndex === 8 && aVal !== null && bVal !== null) {
aVal = parseFloat(aVal);
bVal = parseFloat(bVal);
}
// Handle null values - put them at the end
if (aVal === null || aVal === undefined) {
return sortDirection === "asc" ? 1 : -1;
}
if (bVal === null || bVal === undefined) {
return sortDirection === "asc" ? -1 : 1;
}
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);
}
// Enhanced statistics display for location accuracy
function updateFilterStats() {
const totalRecords = filteredData.length;
const recordsWithAccuracy = filteredData.filter(
(r) => r.location_accuracy !== null
).length;
const avgAccuracy =
recordsWithAccuracy > 0
? filteredData
.filter((r) => r.location_accuracy !== null)
.reduce((sum, r) => sum + r.location_accuracy, 0) /
recordsWithAccuracy
: 0;
console.log(`Filtered records: ${totalRecords}`);
console.log(`Records with location accuracy: ${recordsWithAccuracy}`);
console.log(`Average location accuracy: ${avgAccuracy.toFixed(3)} miles`);
}
// Function to get accuracy level color for charts or displays
function getAccuracyLevelColor(level) {
const colors = {
accurate: "#059669", // green
unaccurate: "#dc2626", // red
unknown: "#6b7280", // gray
};
return colors[level] || colors["unknown"];
}
// Enhanced export function to include location accuracy
function exportAttendanceWithAccuracy() {
// Build CSV header with location accuracy
const headers = [
"Employee ID",
"Location",
"Event",
"Date",
"Time",
"QR Address",
"Check-in Address",
"Location Accuracy (miles)",
"Accuracy Level",
"Device",
];
// Build CSV rows
const rows = filteredData.map((record) => [
record.employeeId,
record.location,
record.event,
record.date,
record.time,
record.qr_address,
record.checked_in_address,
record.location_accuracy !== null
? record.location_accuracy.toFixed(3)
: "Unknown",
record.accuracy_level,
record.device,
]);
// Create CSV content
const csvContent = [headers, ...rows]
.map((row) => row.map((field) => `"${field}"`).join(","))
.join("\n");
// Download CSV
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute(
"download",
`attendance_report_with_accuracy_${
new Date().toISOString().split("T")[0]
}.csv`
);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}