Update calculating location accuracy

This commit is contained in:
2025-08-02 14:18:44 -04:00
parent fc8326be92
commit 4024e02a76
4 changed files with 1505 additions and 411 deletions
+176
View File
@@ -965,6 +965,182 @@
color: var(--gray-600);
}
.location-accuracy-info {
display: flex;
align-items: center;
justify-content: center;
}
.location-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;
}
.location-accuracy-badge small {
font-size: 0.6rem;
font-weight: 500;
margin-top: 2px;
opacity: 0.8;
}
/* Location accuracy level colors - based on distance */
.location-accuracy-badge.accuracy-excellent {
background: var(--success-light);
color: var(--success-color);
border: 1px solid rgba(5, 150, 105, 0.3);
}
.location-accuracy-badge.accuracy-good {
background: #ecfdf5;
color: #059669;
border: 1px solid rgba(5, 150, 105, 0.2);
}
.location-accuracy-badge.accuracy-fair {
background: var(--warning-light);
color: var(--warning-color);
border: 1px solid rgba(245, 158, 11, 0.3);
}
.location-accuracy-badge.accuracy-poor {
background: var(--danger-light);
color: var(--danger-color);
border: 1px solid rgba(220, 38, 38, 0.3);
}
.location-accuracy-badge.accuracy-unknown {
background: var(--gray-100);
color: var(--gray-500);
border: 1px solid rgba(107, 114, 128, 0.3);
}
/* Distance ruler icon styling */
.location-accuracy-badge .fa-ruler {
font-size: var(--font-size-xs);
}
/* Enhanced table header for location accuracy */
.attendance-table th:nth-child(9) {
min-width: 120px;
text-align: center;
}
/* Responsive adjustments for location accuracy column */
@media (max-width: 1200px) {
.location-accuracy-badge {
min-width: 70px;
font-size: 0.625rem;
}
.location-accuracy-badge small {
display: none;
}
}
@media (max-width: 768px) {
/* Hide location accuracy column on mobile to save space */
.attendance-table th:nth-child(9),
.attendance-table td:nth-child(9) {
display: none;
}
}
/* Enhanced modal details for location accuracy */
.location-accuracy-details {
background: var(--gray-50);
padding: var(--spacing-4);
border-radius: var(--radius-lg);
margin: var(--spacing-3) 0;
}
.location-accuracy-details h5 {
color: var(--gray-800);
margin-bottom: var(--spacing-2);
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.location-accuracy-details .fa-ruler {
color: var(--primary-color);
}
.accuracy-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-4);
margin-top: var(--spacing-3);
}
.accuracy-comparison .location-point {
text-align: center;
padding: var(--spacing-3);
background: var(--white);
border-radius: var(--radius);
border: 1px solid var(--gray-200);
}
.accuracy-comparison .location-point i {
font-size: 1.5rem;
margin-bottom: var(--spacing-2);
}
.accuracy-comparison .qr-point i {
color: var(--success-color);
}
.accuracy-comparison .checkin-point i {
color: var(--info-color);
}
.distance-display {
text-align: center;
margin: var(--spacing-4) 0;
padding: var(--spacing-4);
background: linear-gradient(
135deg,
var(--primary-light),
var(--success-light)
);
border-radius: var(--radius-lg);
border: 2px dashed var(--primary-color);
}
.distance-display .distance-value {
font-size: var(--font-size-2xl);
font-weight: 700;
color: var(--primary-color);
margin-bottom: var(--spacing-1);
}
.distance-display .distance-label {
color: var(--gray-600);
font-weight: 500;
}
/* Print styles for location accuracy */
@media print {
.location-accuracy-badge {
background: transparent !important;
border: 1px solid #ccc !important;
color: #000 !important;
}
.location-accuracy-badge small {
display: inline;
}
}
/* Print styles for new columns */
@media print {
.address-info,
+438
View File
@@ -557,3 +557,441 @@ function debounce(func, wait) {
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")) return "excellent";
if (text.includes("good")) return "good";
if (text.includes("fair")) return "fair";
if (text.includes("poor")) return "poor";
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
? `<span class="location-accuracy-badge accuracy-${
record.accuracy_level
}"
title="Distance between QR location and check-in location: ${
record.location_accuracy
} miles">
<i class="fas fa-ruler"></i>
${record.location_accuracy.toFixed(3)} mi
<small>(${record.accuracy_level})</small>
</span>`
: `<span class="location-accuracy-badge accuracy-unknown" title="Location accuracy could not be calculated">
<i class="fas fa-question-circle"></i>
Unknown
</span>`;
row.innerHTML = `
<td>${displayIndex}</td>
<td>
<div class="employee-info">
<span class="employee-id">${record.employeeId}</span>
</div>
</td>
<td>
<div class="location-info">
<i class="fas fa-map-marker-alt"></i>
${record.location}
</div>
</td>
<td>
<div class="event-info">
${record.event}
</div>
</td>
<td>
<div class="date-info">
${record.date}
</div>
</td>
<td>
<div class="time-info">
${record.time}
</div>
</td>
<td>
<div class="address-info qr-address">
<i class="fas fa-qrcode"></i>
<span title="${record.qr_address}">
${
record.qr_address.length > 50
? record.qr_address.substring(0, 50) + "..."
: record.qr_address
}
</span>
</div>
</td>
<td>
<div class="address-info checkin-address">
<i class="fas fa-location-arrow"></i>
<span title="${record.checked_in_address}">
${
record.checked_in_address.length > 50
? record.checked_in_address.substring(0, 50) + "..."
: record.checked_in_address
}
</span>
</div>
</td>
<td>
<div class="location-accuracy-info">
${locationAccuracyBadge}
</div>
</td>
<td>
<div class="device-info">
<i class="fas fa-mobile-alt"></i>
<span title="${record.device}">
${
record.device.length > 20
? record.device.substring(0, 20) + "..."
: record.device
}
</span>
</div>
</td>
<td>
<div class="record-actions">
<button onclick="viewRecordDetails('${record.id}')"
class="action-btn btn-view"
title="View Details">
<i class="fas fa-eye"></i>
</button>
<button onclick="showLocationMap('${record.id}')"
class="action-btn btn-map"
title="Show on Map"
${!record.has_location_data ? "disabled" : ""}>
<i class="fas fa-map"></i>
</button>
<button onclick="editRecord('${record.id}')"
class="action-btn btn-edit"
title="Edit Record">
<i class="fas fa-edit"></i>
</button>
</div>
</td>
`;
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 record details view with location accuracy
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}`;
// Build location accuracy details
const locationAccuracySection =
record.location_accuracy !== null
? `
<div class="location-accuracy-details">
<h5><i class="fas fa-ruler"></i> Location Accuracy Analysis</h5>
<div class="accuracy-comparison">
<div class="location-point qr-point">
<i class="fas fa-qrcode"></i>
<h6>QR Code Location</h6>
<p>${record.qr_address}</p>
</div>
<div class="location-point checkin-point">
<i class="fas fa-location-arrow"></i>
<h6>Check-in Location</h6>
<p>${record.checked_in_address}</p>
</div>
</div>
<div class="distance-display">
<div class="distance-value">${record.location_accuracy.toFixed(
3
)} miles</div>
<div class="distance-label">Distance between locations</div>
<div class="accuracy-level-display">
<span class="location-accuracy-badge accuracy-${
record.accuracy_level
}">
<i class="fas fa-ruler"></i>
${record.accuracy_level.toUpperCase()} ACCURACY
</span>
</div>
</div>
</div>
`
: `
<div class="location-accuracy-details">
<h5><i class="fas fa-ruler"></i> Location Accuracy Analysis</h5>
<div class="distance-display">
<div class="distance-value">Unable to Calculate</div>
<div class="distance-label">Location accuracy could not be determined</div>
<p style="color: var(--gray-600); margin-top: var(--spacing-2);">
This may be due to missing address information or geocoding limitations.
</p>
</div>
</div>
`;
modalBody.innerHTML = `
<div class="record-details-grid">
<div class="detail-section">
<h4><i class="fas fa-user"></i> Employee Information</h4>
<div class="detail-item">
<strong>Employee ID:</strong>
<span>${record.employeeId}</span>
</div>
<div class="detail-item">
<strong>Check-in Date:</strong>
<span>${record.date}</span>
</div>
<div class="detail-item">
<strong>Check-in Time:</strong>
<span>${record.time}</span>
</div>
</div>
<div class="detail-section">
<h4><i class="fas fa-map-marker-alt"></i> Location Information</h4>
<div class="detail-item">
<strong>Location Name:</strong>
<span>${record.location}</span>
</div>
<div class="detail-item">
<strong>Event:</strong>
<span>${record.event}</span>
</div>
<div class="detail-item">
<strong>QR Code Address:</strong>
<span>${record.qr_address}</span>
</div>
<div class="detail-item">
<strong>Check-in Address:</strong>
<span>${record.checked_in_address}</span>
</div>
</div>
<div class="detail-section">
<h4><i class="fas fa-mobile-alt"></i> Device Information</h4>
<div class="detail-item">
<strong>Device:</strong>
<span>${record.device}</span>
</div>
<div class="detail-item">
<strong>GPS Coordinates:</strong>
<span>${record.coordinates}</span>
</div>
</div>
</div>
${locationAccuracySection}
`;
modal.style.display = "block";
}
// 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 = {
excellent: "#059669", // green
good: "#10b981", // lighter green
fair: "#f59e0b", // yellow
poor: "#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);
}