Update calculating location accuracy
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+242
-128
@@ -18,7 +18,7 @@
|
||||
<i class="fas fa-chart-line"></i>
|
||||
Attendance Report
|
||||
</h1>
|
||||
<p>Monitor and analyze staff attendance with location accuracy tracking</p>
|
||||
<p>Monitor and analyze staff attendance with enhanced location tracking</p>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
@@ -80,7 +80,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New GPS accuracy stats -->
|
||||
<!-- GPS tracking stats -->
|
||||
<div class="stat-card info">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-satellite"></i>
|
||||
@@ -92,88 +92,110 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location accuracy stats (conditional) -->
|
||||
{% if has_location_accuracy_feature %}
|
||||
<div class="stat-card success">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-ruler"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ "%.2f"|format(stats.avg_location_accuracy or 0) }}mi</h3>
|
||||
<p>Avg. Location Accuracy</p>
|
||||
<span class="stat-trend">Distance precision</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="stat-card success">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-crosshairs"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ "%.1f"|format(stats.avg_accuracy or 0) }}m</h3>
|
||||
<p>Avg. Accuracy</p>
|
||||
<span class="stat-trend">Location precision</span>
|
||||
<h3>GPS</h3>
|
||||
<p>Tracking Mode</p>
|
||||
<span class="stat-trend">Current system</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced Filters Section with Date Range -->
|
||||
<div class="filters-section">
|
||||
<div class="filters-container">
|
||||
<form method="GET" action="{{ url_for('attendance_report') }}">
|
||||
<div class="filter-row">
|
||||
<!-- Date Range Filters -->
|
||||
<div class="filter-group">
|
||||
<label for="date_from">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
From Date
|
||||
</label>
|
||||
<input type="date"
|
||||
id="date_from"
|
||||
name="date_from"
|
||||
value="{{ date_from }}"
|
||||
max="{{ today_date }}">
|
||||
<div class="filters-card">
|
||||
<div class="filters-header">
|
||||
<h3>
|
||||
<i class="fas fa-filter"></i>
|
||||
Filter Records
|
||||
</h3>
|
||||
</div>
|
||||
<div class="filters-form">
|
||||
<form method="GET" action="{{ url_for('attendance_report') }}">
|
||||
<div class="filter-row">
|
||||
<!-- Date Range Filters -->
|
||||
<div class="filter-group">
|
||||
<label for="date_from">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
From Date
|
||||
</label>
|
||||
<input type="date"
|
||||
id="date_from"
|
||||
name="date_from"
|
||||
value="{{ date_from }}"
|
||||
max="{{ today_date }}">
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="date_to">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
To Date
|
||||
</label>
|
||||
<input type="date"
|
||||
id="date_to"
|
||||
name="date_to"
|
||||
value="{{ date_to }}"
|
||||
max="{{ today_date }}">
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="location">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
Location
|
||||
</label>
|
||||
<select id="location" name="location">
|
||||
<option value="">All Locations</option>
|
||||
{% for location in locations %}
|
||||
<option value="{{ location }}" {{ 'selected' if location == location_filter else '' }}>
|
||||
{{ location }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="employee">
|
||||
<i class="fas fa-id-badge"></i>
|
||||
Employee ID
|
||||
</label>
|
||||
<input type="text"
|
||||
id="employee"
|
||||
name="employee"
|
||||
value="{{ employee_filter }}"
|
||||
placeholder="Enter Employee ID">
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-search"></i>
|
||||
Apply Filters
|
||||
</button>
|
||||
<button type="button" onclick="clearFilters()" class="btn btn-outline">
|
||||
<i class="fas fa-times"></i>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="date_to">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
To Date
|
||||
</label>
|
||||
<input type="date"
|
||||
id="date_to"
|
||||
name="date_to"
|
||||
value="{{ date_to }}"
|
||||
max="{{ today_date }}">
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="location">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
Location
|
||||
</label>
|
||||
<select id="location" name="location">
|
||||
<option value="">All Locations</option>
|
||||
{% for location in locations %}
|
||||
<option value="{{ location }}" {{ 'selected' if location == location_filter else '' }}>
|
||||
{{ location }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="employee">
|
||||
<i class="fas fa-id-badge"></i>
|
||||
Employee ID
|
||||
</label>
|
||||
<input type="text"
|
||||
id="employee"
|
||||
name="employee"
|
||||
value="{{ employee_filter }}"
|
||||
placeholder="Enter Employee ID">
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-search"></i>
|
||||
Apply Filters
|
||||
</button>
|
||||
<button type="button" onclick="clearFilters()" class="btn btn-outline">
|
||||
<i class="fas fa-times"></i>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -212,9 +234,12 @@
|
||||
<th onclick="sortTable(3)">Event <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(4)">Date <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(5)">Time <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(6)">QR Address <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(7)">Check-in Address <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(8)">GPS Accuracy <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(6)" class="address-column">QR Address <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(7)" class="address-column">Check-in Address <i class="fas fa-sort"></i></th>
|
||||
<th onclick="sortTable(8)">
|
||||
{% if has_location_accuracy_feature %}Location Accuracy{% else %}GPS Accuracy{% endif %}
|
||||
<i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th onclick="sortTable(9)">Device <i class="fas fa-sort"></i></th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
@@ -249,7 +274,7 @@
|
||||
{{ record.check_in_time.strftime('%H:%M') }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<td class="address-column">
|
||||
<div class="address-info qr-address">
|
||||
<i class="fas fa-qrcode"></i>
|
||||
<span title="{{ record.qr_address }}">
|
||||
@@ -257,7 +282,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<td class="address-column">
|
||||
<div class="address-info checkin-address">
|
||||
<i class="fas fa-location-arrow"></i>
|
||||
<span title="{{ record.checked_in_address }}">
|
||||
@@ -266,21 +291,35 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="accuracy-info">
|
||||
{% if record.accuracy %}
|
||||
<span class="accuracy-badge accuracy-{{ record.accuracy_level }}"
|
||||
title="GPS accuracy: {{ record.accuracy }}m">
|
||||
<i class="fas fa-crosshairs"></i>
|
||||
{{ "%.1f"|format(record.accuracy) }}m
|
||||
<small>({{ record.accuracy_level }})</small>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="accuracy-badge accuracy-unknown" title="No GPS data available">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
No GPS
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if has_location_accuracy_feature and record.location_accuracy %}
|
||||
<!-- Show location accuracy in miles -->
|
||||
<div class="location-accuracy-info">
|
||||
<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>
|
||||
{{ "%.3f"|format(record.location_accuracy) }} mi
|
||||
<small>({{ record.accuracy_level }})</small>
|
||||
</span>
|
||||
</div>
|
||||
{% elif record.gps_accuracy %}
|
||||
<!-- Fallback to GPS accuracy in meters -->
|
||||
<div class="accuracy-info">
|
||||
<span class="accuracy-badge accuracy-{{ record.accuracy_level }}"
|
||||
title="GPS accuracy: {{ record.gps_accuracy }}m">
|
||||
<i class="fas fa-crosshairs"></i>
|
||||
{{ "%.1f"|format(record.gps_accuracy) }}m
|
||||
<small>(gps)</small>
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- No accuracy data -->
|
||||
<div class="accuracy-info">
|
||||
<span class="accuracy-badge accuracy-unknown" title="No accuracy data available">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
Unknown
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="device-info">
|
||||
@@ -297,12 +336,19 @@
|
||||
title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
{% if record.has_location_data %}
|
||||
<button onclick="showLocationMap('{{ record.id }}')"
|
||||
class="action-btn btn-map"
|
||||
title="Show on Map"
|
||||
{% if not record.has_location_data %}disabled{% endif %}>
|
||||
title="Show on Map">
|
||||
<i class="fas fa-map"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="action-btn btn-map"
|
||||
title="No GPS data available"
|
||||
disabled>
|
||||
<i class="fas fa-map"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button onclick="editRecord('{{ record.id }}')"
|
||||
class="action-btn btn-edit"
|
||||
title="Edit Record">
|
||||
@@ -372,7 +418,13 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="mapModalBody">
|
||||
<div id="locationMap" style="height: 400px; width: 100%;"></div>
|
||||
<div id="locationMap" style="height: 400px; width: 100%;">
|
||||
<div style="text-align: center; padding: 100px; color: #6b7280;">
|
||||
<i class="fas fa-map-marked-alt" style="font-size: 3rem; margin-bottom: 1rem;"></i>
|
||||
<p><strong>GPS Coordinates will be displayed here</strong></p>
|
||||
<p><em>Map integration requires additional setup</em></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button onclick="closeMapModal()" class="btn btn-secondary">Close</button>
|
||||
@@ -385,6 +437,8 @@
|
||||
<script src="{{ url_for('static', filename='js/attendance_report.js') }}"></script>
|
||||
<script>
|
||||
// Enhanced JavaScript for new functionality
|
||||
const hasLocationAccuracy = {{ 'true' if has_location_accuracy_feature else 'false' }};
|
||||
|
||||
function clearFilters() {
|
||||
window.location.href = "{{ url_for('attendance_report') }}";
|
||||
}
|
||||
@@ -413,18 +467,24 @@ function showLocationMap(recordId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show map modal (requires Google Maps API or similar)
|
||||
// Show map modal
|
||||
document.getElementById('mapModal').style.display = 'block';
|
||||
document.getElementById('mapModalTitle').textContent = `Location - ${record.employeeId}`;
|
||||
|
||||
// Initialize map (placeholder - requires actual map implementation)
|
||||
// Update map container with coordinates info
|
||||
const mapContainer = document.getElementById('locationMap');
|
||||
mapContainer.innerHTML = `
|
||||
<div style="text-align: center; padding: 100px;">
|
||||
<i class="fas fa-map-marked-alt" style="font-size: 3rem; color: #6b7280; margin-bottom: 1rem;"></i>
|
||||
<p><strong>GPS Coordinates:</strong> ${record.coordinates}</p>
|
||||
<p><strong>Accuracy:</strong> ${record.accuracy}m (${record.accuracy_level})</p>
|
||||
<p><em>Map integration requires Google Maps API setup</em></p>
|
||||
<p><strong>Employee:</strong> ${record.employeeId}</p>
|
||||
<p><strong>Location:</strong> ${record.location}</p>
|
||||
<p><strong>Check-in Time:</strong> ${record.date} ${record.time}</p>
|
||||
${hasLocationAccuracy && record.location_accuracy ?
|
||||
`<p><strong>Location Accuracy:</strong> ${record.location_accuracy.toFixed(3)} miles (${record.accuracy_level})</p>` :
|
||||
''
|
||||
}
|
||||
<p style="margin-top: 2rem; color: #6b7280;"><em>Full map integration requires Google Maps API setup</em></p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -433,6 +493,10 @@ function closeMapModal() {
|
||||
document.getElementById('mapModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('recordModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Enhanced record details view
|
||||
function viewRecordDetails(recordId) {
|
||||
const record = attendanceData.find(r => r.id == recordId);
|
||||
@@ -446,6 +510,58 @@ function viewRecordDetails(recordId) {
|
||||
|
||||
modalTitle.textContent = `Attendance Record - ${record.employeeId}`;
|
||||
|
||||
// Build accuracy section based on available data
|
||||
let accuracySection = '';
|
||||
if (hasLocationAccuracy && record.location_accuracy) {
|
||||
accuracySection = `
|
||||
<div class="detail-section">
|
||||
<h4><i class="fas fa-ruler"></i> Location Accuracy</h4>
|
||||
<div class="detail-item">
|
||||
<strong>Distance:</strong>
|
||||
<span>${record.location_accuracy.toFixed(3)} miles</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Accuracy Level:</strong>
|
||||
<span class="location-accuracy-badge accuracy-${record.accuracy_level}">
|
||||
${record.accuracy_level.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>QR 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>
|
||||
`;
|
||||
} else if (record.gps_accuracy) {
|
||||
accuracySection = `
|
||||
<div class="detail-section">
|
||||
<h4><i class="fas fa-crosshairs"></i> GPS Information</h4>
|
||||
<div class="detail-item">
|
||||
<strong>GPS Accuracy:</strong>
|
||||
<span>${record.gps_accuracy}m</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Coordinates:</strong>
|
||||
<span>${record.coordinates}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
accuracySection = `
|
||||
<div class="detail-section">
|
||||
<h4><i class="fas fa-question-circle"></i> Location Information</h4>
|
||||
<div class="detail-item">
|
||||
<strong>Status:</strong>
|
||||
<span>No location data available</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<div class="record-details-grid">
|
||||
<div class="detail-section">
|
||||
@@ -474,45 +590,43 @@ function viewRecordDetails(recordId) {
|
||||
<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-satellite"></i> GPS Information</h4>
|
||||
<div class="detail-item">
|
||||
<strong>Coordinates:</strong>
|
||||
<span>${record.coordinates}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Accuracy:</strong>
|
||||
<span>${record.accuracy ? record.accuracy + 'm (' + record.accuracy_level + ')' : 'No GPS data'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Location Status:</strong>
|
||||
<span class="status-badge ${record.has_location_data ? 'success' : 'warning'}">
|
||||
${record.has_location_data ? 'GPS Available' : 'No GPS Data'}
|
||||
</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>
|
||||
|
||||
${accuracySection}
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
function editRecord(recordId) {
|
||||
console.log(`Edit record: ${recordId}`);
|
||||
alert('Edit functionality to be implemented');
|
||||
}
|
||||
|
||||
// Close modals when clicking outside
|
||||
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();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user