871 lines
40 KiB
HTML
871 lines
40 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') }}">
|
|
<!-- 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('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_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_search_filter">
|
|
<i class="fas fa-user-search"></i>
|
|
Employee
|
|
</label>
|
|
<div class="autocomplete-container-filter">
|
|
<input type="text"
|
|
id="employee_search_filter"
|
|
class="filter-input"
|
|
placeholder="Search by ID or Name..."
|
|
value="{{ employee_display_name }}"
|
|
autocomplete="off">
|
|
<input type="hidden"
|
|
id="employee"
|
|
name="employee"
|
|
value="{{ employee_filter }}">
|
|
<div id="employee_autocomplete_results" class="autocomplete-results-filter"></div>
|
|
{% if employee_filter %}
|
|
<button type="button"
|
|
class="clear-employee-filter"
|
|
onclick="clearEmployeeFilter()"
|
|
title="Clear employee filter">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
{% endif %}
|
|
</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 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 }}" class="{% if record.updated_timestamp > record.created_timestamp %}modified-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 }}
|
|
</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('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('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_report') }}?fullscreen=1';
|
|
} else {
|
|
window.location.href = '{{ url_for('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'
|
|
}
|
|
})
|
|
.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>
|
|
// Employee Filter Autocomplete
|
|
(function() {
|
|
const employeeSearchFilter = document.getElementById('employee_search_filter');
|
|
const employeeHiddenFilter = document.getElementById('employee');
|
|
const autocompleteResultsFilter = document.getElementById('employee_autocomplete_results');
|
|
let searchFilterTimeout;
|
|
|
|
if (employeeSearchFilter) {
|
|
employeeSearchFilter.addEventListener('input', function() {
|
|
const searchTerm = this.value.trim();
|
|
|
|
// Clear hidden field if input is cleared
|
|
if (searchTerm.length === 0) {
|
|
employeeHiddenFilter.value = '';
|
|
autocompleteResultsFilter.classList.remove('show');
|
|
return;
|
|
}
|
|
|
|
// If the visible input is a plain numeric ID, keep the hidden field
|
|
// in sync immediately so the user can just type an ID and submit
|
|
// without having to pick from the dropdown (handles IDs not in Employee table).
|
|
if (/^\d+$/.test(searchTerm)) {
|
|
employeeHiddenFilter.value = searchTerm;
|
|
} else {
|
|
// Non-numeric input — clear hidden field until a suggestion is picked
|
|
employeeHiddenFilter.value = '';
|
|
}
|
|
|
|
if (searchTerm.length < 2) {
|
|
autocompleteResultsFilter.classList.remove('show');
|
|
return;
|
|
}
|
|
|
|
// Debounce search
|
|
clearTimeout(searchFilterTimeout);
|
|
searchFilterTimeout = setTimeout(() => {
|
|
fetch(`/api/search_employees?q=${encodeURIComponent(searchTerm)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
displayEmployeeAutocompleteResults(data.employees);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error searching employees:', error);
|
|
});
|
|
}, 300);
|
|
});
|
|
|
|
// Close autocomplete when clicking outside
|
|
document.addEventListener('click', function(e) {
|
|
if (!e.target.closest('.autocomplete-container-filter')) {
|
|
autocompleteResultsFilter.classList.remove('show');
|
|
}
|
|
});
|
|
}
|
|
|
|
function displayEmployeeAutocompleteResults(employees) {
|
|
if (employees.length === 0) {
|
|
autocompleteResultsFilter.innerHTML = '<div class="autocomplete-item-filter" style="cursor: default; color: #999;">No employees found</div>';
|
|
positionDropdown();
|
|
autocompleteResultsFilter.classList.add('show');
|
|
return;
|
|
}
|
|
|
|
const html = employees.map(emp => `
|
|
<div class="autocomplete-item-filter" onclick="selectEmployeeFilter(${emp.id}, '${emp.lastName}, ${emp.firstName}')">
|
|
<div class="employee-info-filter">
|
|
<span class="employee-name-filter">${emp.lastName}, ${emp.firstName}</span>
|
|
<span class="employee-id-filter">ID: ${emp.id}</span>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
autocompleteResultsFilter.innerHTML = html;
|
|
positionDropdown();
|
|
autocompleteResultsFilter.classList.add('show');
|
|
}
|
|
|
|
// Helper function to position the dropdown correctly
|
|
function positionDropdown() {
|
|
const inputRect = employeeSearchFilter.getBoundingClientRect();
|
|
autocompleteResultsFilter.style.top = (inputRect.bottom + window.scrollY) + 'px';
|
|
autocompleteResultsFilter.style.left = inputRect.left + 'px';
|
|
autocompleteResultsFilter.style.width = inputRect.width + 'px';
|
|
}
|
|
|
|
// Make this function globally accessible
|
|
window.selectEmployeeFilter = function(id, name) {
|
|
employeeHiddenFilter.value = id;
|
|
employeeSearchFilter.value = name;
|
|
autocompleteResultsFilter.classList.remove('show');
|
|
};
|
|
|
|
window.clearEmployeeFilter = function() {
|
|
employeeSearchFilter.value = '';
|
|
employeeHiddenFilter.value = '';
|
|
// Submit the form to refresh with cleared filter
|
|
employeeSearchFilter.closest('form').submit();
|
|
};
|
|
})();
|
|
|
|
// Reposition dropdown on scroll or resize
|
|
window.addEventListener('scroll', function() {
|
|
if (autocompleteResultsFilter.classList.contains('show')) {
|
|
positionDropdown();
|
|
}
|
|
}, true);
|
|
|
|
window.addEventListener('resize', function() {
|
|
if (autocompleteResultsFilter.classList.contains('show')) {
|
|
positionDropdown();
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |