Files
LT_QR_Codes_Management/templates/attendance_report.html
T
2026-09-11 22:42:45 -04:00

1109 lines
48 KiB
HTML

{% extends "base_authenticated.html" %}
{% block title %}Attendance Report - QR Code Management{% endblock %}
{% block extra_head %}
<!-- Attendance-specific CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/attendance.css') }}">
<style>
/* Dynamic QR record row — subtle left accent to distinguish from standard records */
tr.dynamic-qr-record {
border-left: 3px solid #3b5bdb;
background-color: rgba(59, 91, 219, 0.03);
}
tr.dynamic-qr-record:hover {
background-color: rgba(59, 91, 219, 0.07) !important;
}
</style>
<!-- Fullscreen CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/attendance_fullscreen.css') }}">
<!-- Chart.js for analytics -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<!-- Fullscreen JavaScript -->
<script src="{{ url_for('static', filename='js/attendance_fullscreen.js') }}" defer></script>
{% endblock %}
{% block content %}
<!-- Fullscreen Toggle Button -->
<button id="fullscreenToggle" class="fullscreen-toggle-btn" onclick="toggleFullscreen()" title="Toggle Fullscreen">
<i id="fullscreenIcon" class="fas fa-expand"></i>
</button>
<!-- Attendance Report Container with Fullscreen Support -->
<div id="attendanceReportContainer" class="attendance-page" data-user-role="{{ session.role }}">
<!-- Header Section -->
<div class="attendance-header">
<div class="header-content">
<h1>
<i class="fas fa-chart-line"></i>
Attendance Report
</h1>
<p>Monitor and analyze staff attendance with enhanced location tracking</p>
</div>
<div class="header-actions">
{% if session.role in ['admin', 'payroll', 'accounting'] %}
<button onclick="exportAttendance()" class="btn btn-success">
<i class="fas fa-download"></i>
Export Data
</button>
{% endif %}
<!-- NEW: Manual Add Record Button -->
{% if session.role in ['admin', 'accounting'] %}
<button onclick="window.location.href='{{ url_for('attendance.add_manual_attendance') }}'" class="btn btn-primary">
<i class="fas fa-plus-circle"></i>
Add Record
</button>
{% endif %}
<button onclick="refreshReport()" class="btn btn-secondary">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
</div>
</div>
<!-- Enhanced Statistics Cards -->
<!-- Enhanced Filters Section with Date Range -->
<div class="filters-section">
<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.attendance_report') }}">
<!-- Hidden field to preserve fullscreen state -->
<input type="hidden" id="fullscreenState" name="fullscreen" value="">
<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="project">
<i class="fas fa-project-diagram"></i>
Project
</label>
<select id="project" name="project">
<option value="">All Projects</option>
{% for project in projects %}
<option value="{{ project.id }}" {{ 'selected' if project.id|string == project_filter else '' }}>
{{ project.name }} ({{ project.attendance_count }} records)
</option>
{% endfor %}
</select>
</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_chip_input">
<i class="fas fa-user-search"></i>
Employee
</label>
<div class="autocomplete-container-filter">
<input type="hidden" id="employee" name="employee" value="{{ employee_filter }}">
<div class="employee-chips-wrapper" id="employeeChipsWrapper">
{% for emp in employee_display_names %}
<span class="employee-chip" data-id="{{ emp.id }}">
<span title="{{ emp.name }}">{{ emp.name }}</span>
<button type="button" class="employee-chip-remove" title="Remove">
<i class="fas fa-times"></i>
</button>
</span>
{% endfor %}
<input type="text"
id="employee_chip_input"
class="employee-chip-input"
placeholder="{% if employee_display_names %}Add more...{% else %}Search by ID or Name...{% endif %}"
autocomplete="off">
{% if employee_filter %}
<button type="button" class="employee-chips-clear-all" id="clearAllEmployees" title="Clear all">
<i class="fas fa-times"></i>
</button>
{% endif %}
</div>
<div id="employee_autocomplete_results" class="autocomplete-results-filter"></div>
</div>
</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>
<!-- Enhanced Attendance Table -->
<div class="attendance-table-section">
<div class="table-header">
<h3>
<i class="fas fa-list"></i>
Attendance Records
{% if date_from or date_to or location_filter or employee_filter or project_filter %}
<span class="filter-indicator">(Filtered)</span>
{% endif %}
</h3>
<div class="table-controls">
<div class="entries-per-page">
<label>Show:</label>
<select id="entriesPerPage" onchange="changeEntriesPerPage()">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="all">All</option>
</select>
<span>entries</span>
</div>
</div>
</div>
<div class="table-container">
{% if records_truncated %}
<div class="alert alert-warning" style="margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-exclamation-triangle"></i>
<span>
Showing the most recent <strong>{{ records_limit }}</strong> records.
Your current filters match more than {{ records_limit }} entries —
please narrow the date range or apply additional filters to see all results.
</span>
</div>
{% endif %}
{% if attendance_records %}
<table class="attendance-table" id="attendanceTable">
<thead>
<tr>
<th onclick="sortTable(0)">#</th>
<th onclick="sortTable(1)">Employee ID <i class="fas fa-sort"></i></th>
<th onclick="sortTable(2)">Employee Name <i class="fas fa-sort"></i></th>
<th onclick="sortTable(3)">Location <i class="fas fa-sort"></i></th>
<th onclick="sortTable(4)">Event <i class="fas fa-sort"></i></th>
<th onclick="sortTable(5)">Date <i class="fas fa-sort"></i></th>
<th onclick="sortTable(6)">Time <i class="fas fa-sort"></i></th>
<th onclick="sortTable(7)" class="address-column">QR Address <i class="fas fa-sort"></i></th>
<th onclick="sortTable(8)" class="address-column">Check-in Address <i class="fas fa-sort"></i></th>
<th onclick="sortTable(9)">
{% if has_location_accuracy_feature %}Location Accuracy{% else %}GPS Accuracy{% endif %}
<i class="fas fa-sort"></i>
</th>
<th onclick="sortTable(10)">Device <i class="fas fa-sort"></i></th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for record in attendance_records %}
<tr data-record-id="{{ record.id }}"
data-is-dynamic="{{ '1' if (record.get('is_dynamic_qr') or record.location_name == 'Dynamic') else '0' }}"
class="{% if record.updated_timestamp > record.created_timestamp %}modified-record{% endif %}{% if record.get('is_dynamic_qr') or record.location_name == 'Dynamic' %} dynamic-qr-record{% endif %}">
<td>{{ loop.index }}</td>
<td>
<div class="employee-info">
<span class="employee-id">{{ record.employee_id }}</span>
</div>
</td>
<td>
<div class="employee-name">
<i class="fas fa-user"></i>
<span>{{ record.employee_name if record.employee_name else 'Unknown' }}</span>
</div>
</td>
<td>
<div class="location-info">
<i class="fas fa-map-marker-alt"></i>
{{ record.location_name if record.location_name != 'Dynamic' else '(No location recorded)' }}
</div>
</td>
<td>
<div class="event-info">
{{ record.location_event }}
</div>
</td>
<td>
<div class="date-info">
{{ record.check_in_date.strftime('%m/%d/%Y') }}
</div>
</td>
<td>
<div class="time-info">
{% if record.check_in_time %}
{% set check_in_time = record.check_in_time %}
{% if check_in_time is string %}
{{ check_in_time }}
{% else %}
{% if check_in_time.strftime is defined %}
{{ check_in_time.strftime('%H:%M') }}
{% elif check_in_time.total_seconds is defined %}
{% set total_seconds = check_in_time.total_seconds() | int %}
{% set hours = (total_seconds // 3600) % 24 %}
{% set minutes = (total_seconds % 3600) // 60 %}
{{ "%02d:%02d"|format(hours, minutes) }}
{% else %}
{{ check_in_time }}
{% endif %}
{% endif %}
{% else %}
N/A
{% endif %}
</div>
</td>
<td>
<div class="address-info qr-address">
<i class="fas fa-qrcode" style="color: #6366f1; margin-right: 4px;" title="QR Code Address (Fixed)"></i>
<span title="QR Address: {{ record.qr_address or 'N/A' }}">
{% if record.qr_address %}
{{ record.qr_address[:50] }}{{ '...' if record.qr_address|length > 50 else '' }}
{% else %}
N/A
{% endif %}
</span>
</div>
</td>
<td class="address-column">
<div class="address-info checkin-address">
<i class="fas fa-location-arrow"></i>
<span title="{{ record.checked_in_address }}">
{{ (record.checked_in_address or 'N/A')[:50] }}{% if (record.checked_in_address or '')|length > 50 %}...{% endif %}
</span>
</div>
</td>
<td>
{% if record.verification_required and record.verification_status == 'pending' %}
<!-- Show Review Needed badge with link to review page -->
<div class="location-accuracy-info">
<a href="{{ url_for('attendance.verification_review_detail', record_id=record.id) }}"
class="location-accuracy-badge badge-review-needed"
style="cursor: pointer; text-decoration: none;"
title="Click to review verification photo - Distance: {{ '%.3f'|format(record.location_accuracy) }} miles">
<i class="fas fa-exclamation-triangle"></i>
Review Needed
<small>({{ '%.3f'|format(record.location_accuracy) }} mi)</small>
</a>
</div>
{% elif record.verification_status == 'approved' %}
<!-- Show Verified badge -->
<div class="location-accuracy-info">
<span class="location-accuracy-badge badge-verified"
title="Verification approved - Distance: {{ '%.3f'|format(record.location_accuracy) }} miles">
<i class="fas fa-check-circle"></i>
Verified
<small>({{ '%.3f'|format(record.location_accuracy) }} mi)</small>
</span>
</div>
{% elif record.verification_status == 'rejected' %}
<!-- Show Rejected badge -->
<div class="location-accuracy-info">
<span class="location-accuracy-badge badge-rejected"
title="Verification rejected - Distance: {{ '%.3f'|format(record.location_accuracy) }} miles">
<i class="fas fa-times-circle"></i>
Rejected
<small>({{ '%.3f'|format(record.location_accuracy) }} mi)</small>
</span>
</div>
{% elif has_location_accuracy_feature and record.location_accuracy %}
<!-- Show location accuracy in miles (normal case) -->
<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">
<i class="fas fa-mobile-alt"></i>
<span title="{{ record.device_info }}">
{{ record.device_info[:20] }}{% if record.device_info|length > 20 %}...{% endif %}
</span>
</div>
</td>
<td>
<div class="record-actions">
{% if record.verification_required and record.verification_status == 'pending' %}
<!-- Show Review button linking to review page -->
<a href="{{ url_for('attendance.verification_review_detail', record_id=record.id) }}"
class="action-btn btn-review"
title="Review Verification Photo">
<i class="fas fa-camera"></i>
</a>
{% endif %}
{% if session.role in ['admin', 'payroll', 'accounting'] %}
<button onclick="editRecord('{{ record.id }}')"
class="action-btn btn-edit"
title="Edit Record">
<i class="fas fa-edit"></i>
</button>
<button onclick="deleteRecord('{{ record.id }}', '{{ record.employee_id }}')"
class="action-btn btn-delete"
title="Delete Record">
<i class="fas fa-trash"></i>
</button>
{% else %}
<span class="text-muted" title="Admin access required">
<i class="fas fa-lock"></i>
</span>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-clipboard-list"></i>
</div>
<h3>No Attendance Records Found</h3>
<p>{% if date_from or date_to or location_filter or employee_filter %}
No records match your current filters. Try adjusting the filter criteria.
{% else %}
No staff have checked in yet. QR codes need to be scanned to generate attendance data.
{% endif %}</p>
{% if date_from or date_to or location_filter or employee_filter or project_filter %}
<button onclick="clearFilters()" class="btn btn-primary">
<i class="fas fa-times"></i>
Clear All Filters
</button>
{% endif %}
</div>
{% endif %}
</div>
<!-- Pagination -->
{% if attendance_records %}
<div class="pagination-container" id="paginationContainer">
<!-- Pagination will be generated by JavaScript -->
</div>
{% endif %}
</div>
</div>
<!-- Enhanced Record Details Modal - KEPT FOR OTHER FEATURES -->
<div id="recordModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">Record Details</h3>
<button onclick="closeModal()" class="modal-close">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body" id="modalBody">
<!-- Content will be populated by JavaScript -->
</div>
<div class="modal-footer">
<button onclick="closeModal()" class="btn btn-secondary">Close</button>
</div>
</div>
</div>
<!-- Map Modal for Location Viewing - KEPT FOR OTHER FEATURES -->
<div id="mapModal" class="modal">
<div class="modal-content modal-large">
<div class="modal-header">
<h3 id="mapModalTitle">Location Map</h3>
<button onclick="closeMapModal()" class="modal-close">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body" id="mapModalBody">
<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>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', filename='js/attendance_report.js') }}"></script>
<script>
// Template variables (processed by Flask)
const userRole = '{{ session.role }}';
const hasEditPermission = ['admin', 'payroll', 'accounting'].includes(userRole);
const hasDeletePermission = ['admin'].includes(userRole);
// Enhanced JavaScript for new functionality
const hasLocationAccuracy = {{ 'true' if has_location_accuracy_feature else 'false' }};
function clearFilters() {
// Get current fullscreen state
const fullscreenState = window.isAttendanceFullscreen && window.isAttendanceFullscreen() ? '1' : '';
// Redirect to clean URL with only fullscreen parameter if active
if (fullscreenState === '1') {
window.location.href = '{{ url_for('attendance.attendance_report') }}?fullscreen=1';
} else {
window.location.href = '{{ url_for('attendance.attendance_report') }}';
}
}
function refreshReport() {
// Get current fullscreen state
const fullscreenState = window.isAttendanceFullscreen && window.isAttendanceFullscreen() ? '1' : '';
// Build URL with current filters and fullscreen state
const params = new URLSearchParams(window.location.search);
// Add or update fullscreen parameter
if (fullscreenState === '1') {
params.set('fullscreen', '1');
} else {
params.delete('fullscreen');
}
// Reload with preserved state
window.location.href = window.location.pathname + '?' + params.toString();
}
function showLocationMap(recordId) {
// Find the record data
const record = attendanceData.find(r => r.id == recordId);
if (!record || !record.has_location_data) {
alert('No GPS data available for this record');
return;
}
// Show map modal
document.getElementById('mapModal').style.display = 'block';
document.getElementById('mapModalTitle').textContent = `Location - ${record.employeeId}`;
// 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>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>
`;
}
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);
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 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>
`;
}
modalBody.innerHTML = `
<div class="record-details">
<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>Device:</strong>
<span>${record.device}</span>
</div>
</div>
${accuracySection}
</div>
`;
modal.style.display = 'block';
}
// Edit and Delete functions
function editRecord(recordId) {
console.log('Edit function called for record:', recordId);
// Check permissions before allowing edit
if (!hasEditPermission) {
alert('Access denied. Only administrators, accounting staff can edit attendance records.');
return;
}
console.log(`[LOG] User attempting to edit attendance record: ${recordId}`);
// Redirect to edit page
window.location.href = `/attendance/${recordId}/edit`;
}
function deleteRecord(recordId, employeeId) {
console.log('Delete function called for record:', recordId);
// Check permissions before allowing delete
if (!hasDeletePermission) {
alert('Access denied. Only administrators can delete attendance records.');
return;
}
console.log(`Delete record: ${recordId}`);
// Confirmation dialog
const confirmMessage = `Are you sure you want to delete the attendance record for employee "${employeeId}"?\n\nThis action cannot be undone.`;
if (confirm(confirmMessage)) {
console.log(`[LOG] User confirmed deletion of attendance record: ${recordId}`);
// Send delete request
fetch(`/attendance/${recordId}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || ''
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log(`[LOG] Successfully deleted attendance record: ${recordId}`);
alert('Attendance record deleted successfully!');
window.location.reload();
} else {
console.error(`[LOG] Failed to delete attendance record: ${recordId} - ${data.message}`);
alert(data.message || 'Error deleting record. Please try again.');
}
})
.catch(error => {
console.error(`[LOG] Error during attendance record deletion: ${recordId}`, error);
alert('Error deleting record. Please try again.');
});
}
}
// 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();
}
});
// Force update buttons on page load for admin users
document.addEventListener('DOMContentLoaded', function() {
if (hasEditPermission) {
// Find all record action containers and update them
const recordActions = document.querySelectorAll('.record-actions');
recordActions.forEach(function(actionDiv) {
// Check if it contains a lock icon (meaning user is locked out)
const lockIcon = actionDiv.querySelector('.fa-lock');
if (lockIcon) {
const recordRow = actionDiv.closest('tr');
const recordId = recordRow.dataset.recordId;
const employeeCell = recordRow.querySelector('.employee-id');
const employeeId = employeeCell ? employeeCell.textContent.trim() : 'Unknown';
// Replace lock icon with edit/delete buttons
actionDiv.innerHTML = `
<button onclick="editRecord('${recordId}')"
class="action-btn btn-edit"
title="Edit Record">
<i class="fas fa-edit"></i>
</button>
<button onclick="deleteRecord('${recordId}', '${employeeId}')"
class="action-btn btn-delete"
title="Delete Record">
<i class="fas fa-trash"></i>
</button>
`;
}
});
console.log('✅ Admin edit/delete buttons have been activated');
}
});
window.userRole = '{{ session.role }}';
</script>
<script>
// Initialize fullscreen functionality when page loads
document.addEventListener('DOMContentLoaded', function() {
if (typeof initializeFullscreen === 'function') {
initializeFullscreen();
console.log('Fullscreen functionality initialized for attendance report');
}
// Check if we should restore fullscreen from URL parameter
const urlParams = new URLSearchParams(window.location.search);
const shouldBeFullscreen = urlParams.get('fullscreen') === '1';
if (shouldBeFullscreen) {
console.log('URL indicates fullscreen should be active');
// The initializeFullscreen function will handle restoration
}
});
</script>
<script>
// ─── Multi-Employee Chip Filter Autocomplete ──────────────────────────────
(function () {
const chipInput = document.getElementById('employee_chip_input');
const employeeHidden = document.getElementById('employee');
const autocomplete = document.getElementById('employee_autocomplete_results');
const chipsWrapper = document.getElementById('employeeChipsWrapper');
let searchTimeout;
// ── State: map of id -> displayName for currently selected employees
const selected = {};
{% for emp in employee_display_names %}
selected['{{ emp.id }}'] = '{{ emp.name | e }}';
{% endfor %}
function syncHiddenField() {
employeeHidden.value = Object.keys(selected).join(',');
// Show/hide the clear-all button dynamically
const hasChips = Object.keys(selected).length > 0;
let btn = document.getElementById('clearAllEmployees');
if (hasChips && !btn) {
btn = document.createElement('button');
btn.type = 'button';
btn.id = 'clearAllEmployees';
btn.className = 'employee-chips-clear-all';
btn.title = 'Clear all';
btn.innerHTML = '<i class="fas fa-times"></i>';
btn.addEventListener('click', clearAll);
chipsWrapper.appendChild(btn);
} else if (!hasChips && btn) {
btn.remove();
}
}
function addChip(id, name) {
id = String(id);
if (selected[id]) return; // already added
selected[id] = name;
const chip = document.createElement('span');
chip.className = 'employee-chip';
chip.dataset.id = id;
const nameSpan = document.createElement('span');
nameSpan.title = name;
nameSpan.textContent = name;
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'employee-chip-remove';
removeBtn.title = 'Remove';
removeBtn.innerHTML = '<i class="fas fa-times"></i>';
removeBtn.addEventListener('click', function () {
delete selected[chip.dataset.id];
chip.remove();
syncHiddenField();
updateInputPlaceholder();
});
chip.appendChild(nameSpan);
chip.appendChild(removeBtn);
// Insert chip before the text input
chipsWrapper.insertBefore(chip, chipInput);
syncHiddenField();
updateInputPlaceholder();
}
function updateInputPlaceholder() {
chipInput.placeholder = Object.keys(selected).length > 0
? 'Add more...'
: 'Search by ID or Name...';
}
function clearAll() {
Object.keys(selected).forEach(function(k) { delete selected[k]; });
chipsWrapper.querySelectorAll('.employee-chip').forEach(function(c) { c.remove(); });
syncHiddenField();
updateInputPlaceholder();
}
// Wire remove buttons for server-rendered chips on page load
chipsWrapper.querySelectorAll('.employee-chip-remove').forEach(function (btn) {
btn.addEventListener('click', function () {
const chip = btn.closest('.employee-chip');
delete selected[chip.dataset.id];
chip.remove();
syncHiddenField();
updateInputPlaceholder();
});
});
const clearAllBtn = document.getElementById('clearAllEmployees');
if (clearAllBtn) {
clearAllBtn.addEventListener('click', clearAll);
}
// Clicking the wrapper focuses the text input
chipsWrapper.addEventListener('click', function (e) {
if (!e.target.closest('.employee-chip') && !e.target.closest('.employee-chips-clear-all')) {
chipInput.focus();
}
});
// ── Autocomplete input handler
chipInput.addEventListener('input', function () {
const q = this.value.trim();
if (q.length === 0) {
autocomplete.classList.remove('show');
return;
}
if (q.length < 2) {
autocomplete.classList.remove('show');
return;
}
clearTimeout(searchTimeout);
searchTimeout = setTimeout(function () {
fetch('/api/search_employees?q=' + encodeURIComponent(q))
.then(function(r) { return r.json(); })
.then(function(data) { renderDropdown(data.employees); })
.catch(function(err) { console.error('Employee search error:', err); });
}, 300);
});
// Keyboard shortcuts: Enter to add numeric ID, Backspace to remove last chip
chipInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
const q = chipInput.value.trim();
if (/^\d+$/.test(q) && !selected[q]) {
addChip(q, 'ID: ' + q);
chipInput.value = '';
autocomplete.classList.remove('show');
}
}
if (e.key === 'Backspace' && chipInput.value === '') {
const chips = chipsWrapper.querySelectorAll('.employee-chip');
if (chips.length > 0) {
const last = chips[chips.length - 1];
delete selected[last.dataset.id];
last.remove();
syncHiddenField();
updateInputPlaceholder();
}
}
});
function renderDropdown(employees) {
autocomplete.innerHTML = '';
const unselected = employees.filter(function(emp) {
return !selected[String(emp.id)];
});
if (unselected.length === 0) {
const noResult = document.createElement('div');
noResult.className = 'autocomplete-item-filter';
noResult.style.cssText = 'cursor:default;color:#999;';
noResult.textContent = employees.length === 0 ? 'No employees found' : 'All matching employees already selected';
autocomplete.appendChild(noResult);
} else {
unselected.forEach(function (emp) {
const isUnregistered = (emp.lastName === '(no record)');
const displayName = isUnregistered
? 'ID: ' + emp.id
: emp.lastName + ', ' + emp.firstName;
const item = document.createElement('div');
item.className = 'autocomplete-item-filter';
const info = document.createElement('div');
info.className = 'employee-info-filter';
const nameSpan = document.createElement('span');
nameSpan.className = 'employee-name-filter';
if (isUnregistered) {
nameSpan.textContent = 'ID: ' + emp.id + ' ';
const em = document.createElement('em');
em.style.color = '#94a3b8';
em.textContent = '(no record)';
nameSpan.appendChild(em);
} else {
nameSpan.textContent = displayName;
}
const idSpan = document.createElement('span');
idSpan.className = 'employee-id-filter';
idSpan.textContent = 'ID: ' + emp.id;
info.appendChild(nameSpan);
info.appendChild(idSpan);
item.appendChild(info);
item.addEventListener('click', (function (id, name) {
return function () {
addChip(id, name);
chipInput.value = '';
autocomplete.classList.remove('show');
};
}(String(emp.id), displayName)));
autocomplete.appendChild(item);
});
}
positionDropdown();
autocomplete.classList.add('show');
}
function positionDropdown() {
const wrapperRect = chipsWrapper.getBoundingClientRect();
autocomplete.style.top = (wrapperRect.bottom + window.scrollY) + 'px';
autocomplete.style.left = wrapperRect.left + 'px';
autocomplete.style.width = wrapperRect.width + 'px';
}
document.addEventListener('click', function (e) {
if (!e.target.closest('.autocomplete-container-filter')) {
autocomplete.classList.remove('show');
}
});
window.addEventListener('scroll', function () {
if (autocomplete.classList.contains('show')) positionDropdown();
}, true);
window.addEventListener('resize', function () {
if (autocomplete.classList.contains('show')) positionDropdown();
});
// ── Legacy globals for backward compatibility
window.selectEmployeeFilter = function (id, name) {
addChip(String(id), name);
chipInput.value = '';
autocomplete.classList.remove('show');
};
window.clearEmployeeFilter = function () {
clearAll();
chipsWrapper.closest('form').submit();
};
}());
</script>
<script>
// ─── Dynamic Location Dropdown (scoped by selected project) ───────────────
(function () {
const projectSelect = document.getElementById('project');
const locationSelect = document.getElementById('location');
if (!projectSelect || !locationSelect) return;
// Capture the full server-rendered location list on page load so we can
// restore it when the project filter is cleared.
const allLocationOptions = Array.from(locationSelect.options).map(function (opt) {
return { value: opt.value, text: opt.text };
});
// The location value that was active when the page loaded (from URL param).
const initialLocationValue = locationSelect.value;
function rebuildLocationDropdown(locations, preserveValue) {
// Remove all options except the first "All Locations" placeholder
while (locationSelect.options.length > 1) {
locationSelect.remove(1);
}
locations.forEach(function (locName) {
const opt = document.createElement('option');
opt.value = locName;
opt.textContent = locName;
if (locName === preserveValue) {
opt.selected = true;
}
locationSelect.appendChild(opt);
});
}
function loadLocationsForProject(projectId) {
const url = '/api/attendance/locations?project_id=' + encodeURIComponent(projectId);
fetch(url)
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.success) {
// Only preserve current location selection if it exists in the new list
const currentLoc = locationSelect.value;
const validLoc = data.locations.includes(currentLoc) ? currentLoc : '';
rebuildLocationDropdown(data.locations, validLoc);
} else {
console.error('Failed to load locations:', data.error);
}
})
.catch(function (err) {
console.error('Error fetching locations:', err);
});
}
function restoreAllLocations() {
while (locationSelect.options.length > 1) {
locationSelect.remove(1);
}
// Re-add all server-rendered options (skip index 0, the "All Locations" option)
for (let i = 1; i < allLocationOptions.length; i++) {
const opt = document.createElement('option');
opt.value = allLocationOptions[i].value;
opt.textContent = allLocationOptions[i].text;
if (allLocationOptions[i].value === initialLocationValue) {
opt.selected = true;
}
locationSelect.appendChild(opt);
}
}
projectSelect.addEventListener('change', function () {
const projectId = this.value;
if (projectId) {
loadLocationsForProject(projectId);
} else {
restoreAllLocations();
}
});
}());
</script>
{% endblock %}