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
+649 -283
View File
File diff suppressed because it is too large Load Diff
+176
View File
@@ -965,6 +965,182 @@
color: var(--gray-600); 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 */ /* Print styles for new columns */
@media print { @media print {
.address-info, .address-info,
+438
View File
@@ -557,3 +557,441 @@ function debounce(func, wait) {
timeout = setTimeout(later, 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
View File
@@ -18,7 +18,7 @@
<i class="fas fa-chart-line"></i> <i class="fas fa-chart-line"></i>
Attendance Report Attendance Report
</h1> </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>
<div class="header-actions"> <div class="header-actions">
@@ -80,7 +80,7 @@
</div> </div>
</div> </div>
<!-- New GPS accuracy stats --> <!-- GPS tracking stats -->
<div class="stat-card info"> <div class="stat-card info">
<div class="stat-icon"> <div class="stat-icon">
<i class="fas fa-satellite"></i> <i class="fas fa-satellite"></i>
@@ -92,88 +92,110 @@
</div> </div>
</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-card success">
<div class="stat-icon"> <div class="stat-icon">
<i class="fas fa-crosshairs"></i> <i class="fas fa-crosshairs"></i>
</div> </div>
<div class="stat-content"> <div class="stat-content">
<h3>{{ "%.1f"|format(stats.avg_accuracy or 0) }}m</h3> <h3>GPS</h3>
<p>Avg. Accuracy</p> <p>Tracking Mode</p>
<span class="stat-trend">Location precision</span> <span class="stat-trend">Current system</span>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
<!-- Enhanced Filters Section with Date Range --> <!-- Enhanced Filters Section with Date Range -->
<div class="filters-section"> <div class="filters-section">
<div class="filters-container"> <div class="filters-card">
<form method="GET" action="{{ url_for('attendance_report') }}"> <div class="filters-header">
<div class="filter-row"> <h3>
<!-- Date Range Filters --> <i class="fas fa-filter"></i>
<div class="filter-group"> Filter Records
<label for="date_from"> </h3>
<i class="fas fa-calendar-alt"></i> </div>
From Date <div class="filters-form">
</label> <form method="GET" action="{{ url_for('attendance_report') }}">
<input type="date" <div class="filter-row">
id="date_from" <!-- Date Range Filters -->
name="date_from" <div class="filter-group">
value="{{ date_from }}" <label for="date_from">
max="{{ today_date }}"> <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>
</form>
<div class="filter-group"> </div>
<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>
</div> </div>
</div> </div>
@@ -212,9 +234,12 @@
<th onclick="sortTable(3)">Event <i class="fas fa-sort"></i></th> <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(4)">Date <i class="fas fa-sort"></i></th>
<th onclick="sortTable(5)">Time <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(6)" class="address-column">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(7)" class="address-column">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(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 onclick="sortTable(9)">Device <i class="fas fa-sort"></i></th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
@@ -249,7 +274,7 @@
{{ record.check_in_time.strftime('%H:%M') }} {{ record.check_in_time.strftime('%H:%M') }}
</div> </div>
</td> </td>
<td> <td class="address-column">
<div class="address-info qr-address"> <div class="address-info qr-address">
<i class="fas fa-qrcode"></i> <i class="fas fa-qrcode"></i>
<span title="{{ record.qr_address }}"> <span title="{{ record.qr_address }}">
@@ -257,7 +282,7 @@
</span> </span>
</div> </div>
</td> </td>
<td> <td class="address-column">
<div class="address-info checkin-address"> <div class="address-info checkin-address">
<i class="fas fa-location-arrow"></i> <i class="fas fa-location-arrow"></i>
<span title="{{ record.checked_in_address }}"> <span title="{{ record.checked_in_address }}">
@@ -266,21 +291,35 @@
</div> </div>
</td> </td>
<td> <td>
<div class="accuracy-info"> {% if has_location_accuracy_feature and record.location_accuracy %}
{% if record.accuracy %} <!-- Show location accuracy in miles -->
<span class="accuracy-badge accuracy-{{ record.accuracy_level }}" <div class="location-accuracy-info">
title="GPS accuracy: {{ record.accuracy }}m"> <span class="location-accuracy-badge accuracy-{{ record.accuracy_level }}"
<i class="fas fa-crosshairs"></i> title="Distance between QR location and check-in location: {{ record.location_accuracy }} miles">
{{ "%.1f"|format(record.accuracy) }}m <i class="fas fa-ruler"></i>
<small>({{ record.accuracy_level }})</small> {{ "%.3f"|format(record.location_accuracy) }} mi
</span> <small>({{ record.accuracy_level }})</small>
{% else %} </span>
<span class="accuracy-badge accuracy-unknown" title="No GPS data available"> </div>
<i class="fas fa-question-circle"></i> {% elif record.gps_accuracy %}
No GPS <!-- Fallback to GPS accuracy in meters -->
</span> <div class="accuracy-info">
{% endif %} <span class="accuracy-badge accuracy-{{ record.accuracy_level }}"
</div> 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>
<td> <td>
<div class="device-info"> <div class="device-info">
@@ -297,12 +336,19 @@
title="View Details"> title="View Details">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> </button>
{% if record.has_location_data %}
<button onclick="showLocationMap('{{ record.id }}')" <button onclick="showLocationMap('{{ record.id }}')"
class="action-btn btn-map" class="action-btn btn-map"
title="Show on Map" title="Show on Map">
{% if not record.has_location_data %}disabled{% endif %}>
<i class="fas fa-map"></i> <i class="fas fa-map"></i>
</button> </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 }}')" <button onclick="editRecord('{{ record.id }}')"
class="action-btn btn-edit" class="action-btn btn-edit"
title="Edit Record"> title="Edit Record">
@@ -372,7 +418,13 @@
</button> </button>
</div> </div>
<div class="modal-body" id="mapModalBody"> <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>
<div class="modal-footer"> <div class="modal-footer">
<button onclick="closeMapModal()" class="btn btn-secondary">Close</button> <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 src="{{ url_for('static', filename='js/attendance_report.js') }}"></script>
<script> <script>
// Enhanced JavaScript for new functionality // Enhanced JavaScript for new functionality
const hasLocationAccuracy = {{ 'true' if has_location_accuracy_feature else 'false' }};
function clearFilters() { function clearFilters() {
window.location.href = "{{ url_for('attendance_report') }}"; window.location.href = "{{ url_for('attendance_report') }}";
} }
@@ -413,18 +467,24 @@ function showLocationMap(recordId) {
return; return;
} }
// Show map modal (requires Google Maps API or similar) // Show map modal
document.getElementById('mapModal').style.display = 'block'; document.getElementById('mapModal').style.display = 'block';
document.getElementById('mapModalTitle').textContent = `Location - ${record.employeeId}`; 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'); const mapContainer = document.getElementById('locationMap');
mapContainer.innerHTML = ` mapContainer.innerHTML = `
<div style="text-align: center; padding: 100px;"> <div style="text-align: center; padding: 100px;">
<i class="fas fa-map-marked-alt" style="font-size: 3rem; color: #6b7280; margin-bottom: 1rem;"></i> <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>GPS Coordinates:</strong> ${record.coordinates}</p>
<p><strong>Accuracy:</strong> ${record.accuracy}m (${record.accuracy_level})</p> <p><strong>Employee:</strong> ${record.employeeId}</p>
<p><em>Map integration requires Google Maps API setup</em></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> </div>
`; `;
} }
@@ -433,6 +493,10 @@ function closeMapModal() {
document.getElementById('mapModal').style.display = 'none'; document.getElementById('mapModal').style.display = 'none';
} }
function closeModal() {
document.getElementById('recordModal').style.display = 'none';
}
// Enhanced record details view // Enhanced record details view
function viewRecordDetails(recordId) { function viewRecordDetails(recordId) {
const record = attendanceData.find(r => r.id == recordId); const record = attendanceData.find(r => r.id == recordId);
@@ -446,6 +510,58 @@ function viewRecordDetails(recordId) {
modalTitle.textContent = `Attendance Record - ${record.employeeId}`; 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 = ` modalBody.innerHTML = `
<div class="record-details-grid"> <div class="record-details-grid">
<div class="detail-section"> <div class="detail-section">
@@ -474,45 +590,43 @@ function viewRecordDetails(recordId) {
<strong>Event:</strong> <strong>Event:</strong>
<span>${record.event}</span> <span>${record.event}</span>
</div> </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"> <div class="detail-item">
<strong>Device:</strong> <strong>Device:</strong>
<span>${record.device}</span> <span>${record.device}</span>
</div> </div>
</div> </div>
${accuracySection}
</div> </div>
`; `;
modal.style.display = 'block'; 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> </script>
{% endblock %} {% endblock %}