Updated attendance report page

This commit is contained in:
2025-08-02 12:56:46 -04:00
parent 5cb8bc8b89
commit fc8326be92
4 changed files with 1113 additions and 734 deletions
+92 -10
View File
@@ -1617,18 +1617,86 @@ def toggle_qr_status_api(qr_id):
@app.route('/attendance') @app.route('/attendance')
@admin_required @admin_required
def attendance_report(): def attendance_report():
"""Attendance report page (Admin only)""" """Enhanced attendance report page with date range filtering and location data (Admin only)"""
try: try:
# Get filter parameters # Get filter parameters
date_filter = request.args.get('date', '') date_from = request.args.get('date_from', '')
date_to = request.args.get('date_to', '')
location_filter = request.args.get('location', '') location_filter = request.args.get('location', '')
employee_filter = request.args.get('employee', '') employee_filter = request.args.get('employee', '')
# Base query using the view # Build base query with enhanced location data
query = db.session.execute(text("SELECT * FROM attendance_report WHERE 1=1")) base_query = """
SELECT
ad.id,
ad.employee_id,
ad.check_in_date,
ad.check_in_time,
ad.location_name,
qc.location_event,
qc.location_address as qr_address,
ad.address as checked_in_address,
ad.latitude,
ad.longitude,
ad.accuracy,
ad.device_info
FROM attendance_data ad
LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id
WHERE 1=1
"""
# Apply filters (you can enhance this with proper SQLAlchemy filtering) conditions = []
attendance_records = query.fetchall() params = {}
# Apply date range filter
if date_from:
conditions.append("ad.check_in_date >= :date_from")
params['date_from'] = date_from
if date_to:
conditions.append("ad.check_in_date <= :date_to")
params['date_to'] = date_to
# Apply location filter
if location_filter:
conditions.append("ad.location_name ILIKE :location")
params['location'] = f"%{location_filter}%"
# Apply employee filter
if employee_filter:
conditions.append("ad.employee_id ILIKE :employee")
params['employee'] = f"%{employee_filter}%"
# Add conditions to query
if conditions:
base_query += " AND " + " AND ".join(conditions)
# Add ordering
base_query += " ORDER BY ad.check_in_date DESC, ad.check_in_time DESC"
# Execute query
query_result = db.session.execute(text(base_query), params)
attendance_records = query_result.fetchall()
# Process records to add calculated fields
processed_records = []
for record in attendance_records:
record_dict = {
'id': record.id,
'employee_id': record.employee_id,
'check_in_date': record.check_in_date,
'check_in_time': record.check_in_time,
'location_name': record.location_name,
'location_event': record.location_event,
'qr_address': record.qr_address or 'Not available',
'checked_in_address': record.checked_in_address or 'Location not captured',
'device_info': record.device_info,
'accuracy': record.accuracy,
'accuracy_level': get_accuracy_level(record.accuracy),
'has_location_data': record.latitude is not None and record.longitude is not None,
'coordinates': f"{record.latitude:.6f}, {record.longitude:.6f}" if record.latitude and record.longitude else "No GPS data"
}
processed_records.append(record_dict)
# Get unique locations for filter dropdown # Get unique locations for filter dropdown
locations_query = db.session.execute(text(""" locations_query = db.session.execute(text("""
@@ -1644,7 +1712,9 @@ def attendance_report():
COUNT(*) as total_checkins, COUNT(*) as total_checkins,
COUNT(DISTINCT employee_id) as unique_employees, COUNT(DISTINCT employee_id) as unique_employees,
COUNT(DISTINCT qr_code_id) as active_locations, COUNT(DISTINCT qr_code_id) as active_locations,
COUNT(CASE WHEN check_in_date = CURRENT_DATE THEN 1 END) as today_checkins COUNT(CASE WHEN check_in_date = CURRENT_DATE THEN 1 END) as today_checkins,
COUNT(CASE WHEN latitude IS NOT NULL AND longitude IS NOT NULL THEN 1 END) as records_with_gps,
AVG(accuracy) as avg_accuracy
FROM attendance_data FROM attendance_data
""")) """))
stats = stats_query.fetchone() stats = stats_query.fetchone()
@@ -1654,10 +1724,11 @@ def attendance_report():
current_date_formatted = datetime.now().strftime('%B %d') current_date_formatted = datetime.now().strftime('%B %d')
return render_template('attendance_report.html', return render_template('attendance_report.html',
attendance_records=attendance_records, attendance_records=processed_records,
locations=locations, locations=locations,
stats=stats, stats=stats,
date_filter=date_filter, date_from=date_from,
date_to=date_to,
location_filter=location_filter, location_filter=location_filter,
employee_filter=employee_filter, employee_filter=employee_filter,
today_date=today_date, today_date=today_date,
@@ -1665,9 +1736,20 @@ def attendance_report():
except Exception as e: except Exception as e:
print(f"Error loading attendance report: {e}") print(f"Error loading attendance report: {e}")
flash('Error loading attendance report.', 'error') flash('Error loading attendance report. Please try again.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
def get_accuracy_level(accuracy):
"""Get human-readable accuracy level"""
if not accuracy:
return 'unknown'
elif accuracy <= 50:
return 'high'
elif accuracy <= 100:
return 'medium'
else:
return 'low'
@app.route('/api/attendance/stats') @app.route('/api/attendance/stats')
@admin_required @admin_required
def attendance_stats_api(): def attendance_stats_api():
+282 -5
View File
@@ -97,7 +97,11 @@
} }
.stat-card.primary::before { .stat-card.primary::before {
background: linear-gradient(90deg, var(--primary-color), var(--primary-hover)); background: linear-gradient(
90deg,
var(--primary-color),
var(--primary-hover)
);
} }
.stat-card.success::before { .stat-card.success::before {
@@ -130,7 +134,11 @@
} }
.stat-card.primary .stat-icon { .stat-card.primary .stat-icon {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); background: linear-gradient(
135deg,
var(--primary-color),
var(--primary-hover)
);
} }
.stat-card.success .stat-icon { .stat-card.success .stat-icon {
@@ -378,7 +386,7 @@
.employee-id { .employee-id {
font-weight: 600; font-weight: 600;
color: var(--gray-900); color: var(--gray-900);
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
background: var(--gray-100); background: var(--gray-100);
padding: var(--spacing-1) var(--spacing-2); padding: var(--spacing-1) var(--spacing-2);
@@ -403,13 +411,13 @@
} }
.date-info { .date-info {
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
color: var(--gray-900); color: var(--gray-900);
font-weight: 500; font-weight: 500;
} }
.time-info { .time-info {
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
color: var(--gray-900); color: var(--gray-900);
font-weight: 600; font-weight: 600;
background: var(--primary-light); background: var(--primary-light);
@@ -706,6 +714,275 @@
background: var(--gray-50); background: var(--gray-50);
} }
.address-info {
display: flex;
align-items: flex-start;
gap: var(--spacing-2);
color: var(--gray-700);
font-size: var(--font-size-sm);
max-width: 250px;
}
.address-info i {
color: var(--primary-color);
font-size: var(--font-size-xs);
margin-top: 2px;
flex-shrink: 0;
}
.address-info span {
line-height: 1.4;
word-break: break-word;
}
.address-info.qr-address i {
color: var(--success-color);
}
.address-info.checkin-address i {
color: var(--info-color);
}
/* GPS Accuracy badges */
.accuracy-info {
display: flex;
align-items: center;
justify-content: center;
}
.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;
}
.accuracy-badge small {
font-size: 0.6rem;
font-weight: 500;
margin-top: 2px;
opacity: 0.8;
}
.accuracy-badge.accuracy-high {
background: var(--success-light);
color: var(--success-color);
border: 1px solid rgba(5, 150, 105, 0.3);
}
.accuracy-badge.accuracy-medium {
background: var(--warning-light);
color: var(--warning-color);
border: 1px solid rgba(245, 158, 11, 0.3);
}
.accuracy-badge.accuracy-low {
background: var(--danger-light);
color: var(--danger-color);
border: 1px solid rgba(220, 38, 38, 0.3);
}
.accuracy-badge.accuracy-unknown {
background: var(--gray-100);
color: var(--gray-500);
border: 1px solid rgba(107, 114, 128, 0.3);
}
/* Filter indicator */
.filter-indicator {
background: var(--primary-light);
color: var(--primary-color);
font-size: var(--font-size-xs);
padding: var(--spacing-1) var(--spacing-2);
border-radius: var(--radius);
margin-left: var(--spacing-2);
font-weight: 500;
}
/* Enhanced action buttons */
.btn-map {
background: var(--success-light);
color: var(--success-color);
}
.btn-map:hover {
background: var(--success-color);
color: var(--white);
}
.btn-map:disabled {
background: var(--gray-100);
color: var(--gray-400);
cursor: not-allowed;
}
/* Modal enhancements */
.modal-large .modal-content {
max-width: 800px;
width: 90%;
}
.record-details-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--spacing-6);
}
.detail-section {
background: var(--gray-50);
padding: var(--spacing-4);
border-radius: var(--radius-lg);
border: 1px solid var(--gray-200);
}
.detail-section h4 {
color: var(--gray-800);
font-size: var(--font-size-base);
font-weight: 600;
margin-bottom: var(--spacing-3);
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.detail-section h4 i {
color: var(--primary-color);
font-size: var(--font-size-sm);
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: var(--spacing-2) 0;
border-bottom: 1px solid var(--gray-200);
gap: var(--spacing-3);
}
.detail-item:last-child {
border-bottom: none;
}
.detail-item strong {
color: var(--gray-700);
font-weight: 500;
flex-shrink: 0;
min-width: 120px;
}
.detail-item span {
color: var(--gray-900);
text-align: right;
word-break: break-word;
}
/* Enhanced date range filters */
.filter-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-4);
align-items: end;
}
.filter-group input[type="date"] {
appearance: none;
-webkit-appearance: none;
position: relative;
}
.filter-group input[type="date"]::-webkit-calendar-picker-indicator {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor'%3e%3cpath fill-rule='evenodd' d='M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z' clip-rule='evenodd'/%3e%3c/svg%3e");
background-size: 16px;
background-repeat: no-repeat;
background-position: center;
cursor: pointer;
}
/* Enhanced table responsive design */
@media (max-width: 1200px) {
.attendance-table th:nth-child(7),
.attendance-table td:nth-child(7),
.attendance-table th:nth-child(8),
.attendance-table td:nth-child(8) {
display: none;
}
}
@media (max-width: 768px) {
.address-info {
max-width: 150px;
}
.accuracy-badge {
min-width: 60px;
font-size: 0.625rem;
}
.accuracy-badge small {
display: none;
}
.filter-row {
grid-template-columns: 1fr;
}
.record-details-grid {
grid-template-columns: 1fr;
}
/* Hide address columns on mobile */
.attendance-table th:nth-child(7),
.attendance-table td:nth-child(7),
.attendance-table th:nth-child(8),
.attendance-table td:nth-child(8),
.attendance-table th:nth-child(9),
.attendance-table td:nth-child(9) {
display: none;
}
}
/* Enhanced empty state */
.empty-state .btn {
margin-top: var(--spacing-4);
}
/* Location map placeholder styles */
#locationMap {
background: var(--gray-50);
border: 2px dashed var(--gray-300);
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
color: var(--gray-600);
}
/* Print styles for new columns */
@media print {
.address-info,
.accuracy-info {
font-size: 0.7rem;
}
.accuracy-badge {
background: transparent !important;
border: 1px solid #ccc !important;
color: #000 !important;
}
.btn-map {
display: none;
}
}
/* Responsive Design */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.attendance-page { .attendance-page {
File diff suppressed because it is too large Load Diff
+263 -79
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 across all locations</p> <p>Monitor and analyze staff attendance with location accuracy tracking</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
@@ -33,7 +33,7 @@
</div> </div>
</div> </div>
<!-- Statistics Cards --> <!-- Enhanced Statistics Cards -->
<div class="stats-section"> <div class="stats-section">
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card primary"> <div class="stat-card primary">
@@ -65,7 +65,7 @@
<div class="stat-content"> <div class="stat-content">
<h3>{{ stats.active_locations or 0 }}</h3> <h3>{{ stats.active_locations or 0 }}</h3>
<p>Active Locations</p> <p>Active Locations</p>
<span class="stat-trend">With check-ins</span> <span class="stat-trend">QR codes deployed</span>
</div> </div>
</div> </div>
@@ -76,46 +76,74 @@
<div class="stat-content"> <div class="stat-content">
<h3>{{ stats.today_checkins or 0 }}</h3> <h3>{{ stats.today_checkins or 0 }}</h3>
<p>Today's Check-ins</p> <p>Today's Check-ins</p>
<span class="stat-trend">{{ current_date_formatted or 'Today' }}</span> <span class="stat-trend">{{ current_date_formatted }}</span>
</div>
</div>
<!-- New GPS accuracy stats -->
<div class="stat-card info">
<div class="stat-icon">
<i class="fas fa-satellite"></i>
</div>
<div class="stat-content">
<h3>{{ stats.records_with_gps or 0 }}</h3>
<p>GPS Records</p>
<span class="stat-trend">Location captured</span>
</div>
</div>
<div class="stat-card success">
<div class="stat-icon">
<i class="fas fa-crosshairs"></i>
</div>
<div class="stat-content">
<h3>{{ "%.1f"|format(stats.avg_accuracy or 0) }}m</h3>
<p>Avg. Accuracy</p>
<span class="stat-trend">Location precision</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Filters Section --> <!-- Enhanced Filters Section with Date Range -->
<div class="filters-section"> <div class="filters-section">
<div class="filters-card"> <div class="filters-container">
<div class="filters-header"> <form method="GET" action="{{ url_for('attendance_report') }}">
<h3>
<i class="fas fa-filter"></i>
Filter Records
</h3>
</div>
<form method="GET" class="filters-form" id="filtersForm">
<div class="filter-row"> <div class="filter-row">
<!-- Date Range Filters -->
<div class="filter-group"> <div class="filter-group">
<label for="date"> <label for="date_from">
<i class="fas fa-calendar"></i> <i class="fas fa-calendar-alt"></i>
Date From Date
</label> </label>
<input type="date" <input type="date"
id="date" id="date_from"
name="date" name="date_from"
value="{{ date_filter }}" value="{{ date_from }}"
max="{{ today_date or '' }}"> 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>
<div class="filter-group"> <div class="filter-group">
<label for="location"> <label for="location">
<i class="fas fa-building"></i> <i class="fas fa-map-marker-alt"></i>
Location Location
</label> </label>
<select id="location" name="location"> <select id="location" name="location">
<option value="">All Locations</option> <option value="">All Locations</option>
{% for location in locations %} {% for location in locations %}
<option value="{{ location }}" <option value="{{ location }}" {{ 'selected' if location == location_filter else '' }}>
{{ 'selected' if location == location_filter else '' }}>
{{ location }} {{ location }}
</option> </option>
{% endfor %} {% endfor %}
@@ -149,12 +177,15 @@
</div> </div>
</div> </div>
<!-- Attendance Table --> <!-- Enhanced Attendance Table -->
<div class="attendance-table-section"> <div class="attendance-table-section">
<div class="table-header"> <div class="table-header">
<h3> <h3>
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
Attendance Records Attendance Records
{% if date_from or date_to or location_filter or employee_filter %}
<span class="filter-indicator">(Filtered)</span>
{% endif %}
</h3> </h3>
<div class="table-controls"> <div class="table-controls">
<div class="entries-per-page"> <div class="entries-per-page">
@@ -181,8 +212,10 @@
<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)">Device <i class="fas fa-sort"></i></th> <th onclick="sortTable(6)">QR Address <i class="fas fa-sort"></i></th>
<th onclick="sortTable(7)">Status <i class="fas fa-sort"></i></th> <th onclick="sortTable(7)">Check-in Address <i class="fas fa-sort"></i></th>
<th onclick="sortTable(8)">GPS Accuracy <i class="fas fa-sort"></i></th>
<th onclick="sortTable(9)">Device <i class="fas fa-sort"></i></th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
@@ -216,6 +249,39 @@
{{ record.check_in_time.strftime('%H:%M') }} {{ record.check_in_time.strftime('%H:%M') }}
</div> </div>
</td> </td>
<td>
<div class="address-info qr-address">
<i class="fas fa-qrcode"></i>
<span title="{{ record.qr_address }}">
{{ record.qr_address[:50] }}{% if record.qr_address|length > 50 %}...{% endif %}
</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[:50] }}{% if record.checked_in_address|length > 50 %}...{% endif %}
</span>
</div>
</td>
<td>
<div class="accuracy-info">
{% if record.accuracy %}
<span class="accuracy-badge accuracy-{{ record.accuracy_level }}"
title="GPS accuracy: {{ record.accuracy }}m">
<i class="fas fa-crosshairs"></i>
{{ "%.1f"|format(record.accuracy) }}m
<small>({{ record.accuracy_level }})</small>
</span>
{% else %}
<span class="accuracy-badge accuracy-unknown" title="No GPS data available">
<i class="fas fa-question-circle"></i>
No GPS
</span>
{% endif %}
</div>
</td>
<td> <td>
<div class="device-info"> <div class="device-info">
<i class="fas fa-mobile-alt"></i> <i class="fas fa-mobile-alt"></i>
@@ -224,29 +290,24 @@
</span> </span>
</div> </div>
</td> </td>
<td>
<span class="status-badge {{ record.status }}">
<i class="fas {{ 'fa-check-circle' if record.status == 'present' else 'fa-times-circle' }}"></i>
{{ record.status.title() }}
</span>
</td>
<td> <td>
<div class="record-actions"> <div class="record-actions">
<button onclick="viewRecordDetails({{ record.id }})" <button onclick="viewRecordDetails('{{ record.id }}')"
class="action-btn btn-view" class="action-btn btn-view"
title="View Details"> title="View Details">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> </button>
<button onclick="editRecord({{ record.id }})" <button onclick="showLocationMap('{{ record.id }}')"
class="action-btn btn-map"
title="Show on Map"
{% if not record.has_location_data %}disabled{% endif %}>
<i class="fas fa-map"></i>
</button>
<button onclick="editRecord('{{ record.id }}')"
class="action-btn btn-edit" class="action-btn btn-edit"
title="Edit Record"> title="Edit Record">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</button> </button>
<button onclick="deleteRecord({{ record.id }})"
class="action-btn btn-delete"
title="Delete Record">
<i class="fas fa-trash"></i>
</button>
</div> </div>
</td> </td>
</tr> </tr>
@@ -259,76 +320,199 @@
<i class="fas fa-clipboard-list"></i> <i class="fas fa-clipboard-list"></i>
</div> </div>
<h3>No Attendance Records Found</h3> <h3>No Attendance Records Found</h3>
<p>{% if date_filter or location_filter or employee_filter %} <p>{% if date_from or date_to or location_filter or employee_filter %}
No records match your current filters. Try adjusting the filter criteria. No records match your current filters. Try adjusting the filter criteria.
{% else %} {% else %}
No staff have checked in yet. QR codes need to be scanned to generate attendance data. No staff have checked in yet. QR codes need to be scanned to generate attendance data.
{% endif %}</p> {% endif %}</p>
{% if date_from or date_to or location_filter or employee_filter %}
<button onclick="clearFilters()" class="btn btn-primary"> <button onclick="clearFilters()" class="btn btn-primary">
<i class="fas fa-refresh"></i> <i class="fas fa-times"></i>
Clear Filters Clear All Filters
</button> </button>
{% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Pagination --> <!-- Pagination -->
{% if attendance_records %}
<div class="pagination-container" id="paginationContainer"> <div class="pagination-container" id="paginationContainer">
<!-- Pagination will be dynamically generated --> <!-- Pagination will be generated by JavaScript -->
</div>
</div>
<!-- Charts Section -->
<div class="charts-section">
<div class="charts-grid">
<div class="chart-card">
<div class="chart-header">
<h3>
<i class="fas fa-chart-bar"></i>
Daily Check-ins (Last 7 Days)
</h3>
</div>
<div class="chart-container">
<canvas id="dailyChart"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-header">
<h3>
<i class="fas fa-chart-pie"></i>
Check-ins by Location
</h3>
</div>
<div class="chart-container">
<canvas id="locationChart"></canvas>
</div>
</div>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
<!-- Record Details Modal --> <!-- Enhanced Record Details Modal -->
<div id="recordModal" class="modal"> <div id="recordModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3 id="modalTitle">Attendance Record Details</h3> <h3 id="modalTitle">Record Details</h3>
<button class="modal-close" onclick="closeRecordModal()"> <button onclick="closeModal()" class="modal-close">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<div class="modal-body" id="modalBody"> <div class="modal-body" id="modalBody">
<!-- Dynamic content will be loaded here --> <!-- Content will be populated by JavaScript -->
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button onclick="closeRecordModal()" class="btn btn-secondary"> <button onclick="closeModal()" class="btn btn-secondary">Close</button>
Close </div>
</div>
</div>
<!-- Map Modal for Location Viewing -->
<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> </button>
</div> </div>
<div class="modal-body" id="mapModalBody">
<div id="locationMap" style="height: 400px; width: 100%;"></div>
</div>
<div class="modal-footer">
<button onclick="closeMapModal()" class="btn btn-secondary">Close</button>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_scripts %} {% block extra_scripts %}
<script src="{{ url_for('static', filename='js/attendance_report.js') }}"></script> <script src="{{ url_for('static', filename='js/attendance_report.js') }}"></script>
<script>
// Enhanced JavaScript for new functionality
function clearFilters() {
window.location.href = "{{ url_for('attendance_report') }}";
}
function exportAttendance() {
// Build export URL with current filters
const params = new URLSearchParams();
if ('{{ date_from }}') params.append('date_from', '{{ date_from }}');
if ('{{ date_to }}') params.append('date_to', '{{ date_to }}');
if ('{{ location_filter }}') params.append('location', '{{ location_filter }}');
if ('{{ employee_filter }}') params.append('employee', '{{ employee_filter }}');
params.append('export', 'csv');
window.open(`{{ url_for('attendance_report') }}?${params.toString()}`);
}
function refreshReport() {
window.location.reload();
}
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 (requires Google Maps API or similar)
document.getElementById('mapModal').style.display = 'block';
document.getElementById('mapModalTitle').textContent = `Location - ${record.employeeId}`;
// Initialize map (placeholder - requires actual map implementation)
const mapContainer = document.getElementById('locationMap');
mapContainer.innerHTML = `
<div style="text-align: center; padding: 100px;">
<i class="fas fa-map-marked-alt" style="font-size: 3rem; color: #6b7280; margin-bottom: 1rem;"></i>
<p><strong>GPS Coordinates:</strong> ${record.coordinates}</p>
<p><strong>Accuracy:</strong> ${record.accuracy}m (${record.accuracy_level})</p>
<p><em>Map integration requires Google Maps API setup</em></p>
</div>
`;
}
function closeMapModal() {
document.getElementById('mapModal').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}`;
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-satellite"></i> GPS Information</h4>
<div class="detail-item">
<strong>Coordinates:</strong>
<span>${record.coordinates}</span>
</div>
<div class="detail-item">
<strong>Accuracy:</strong>
<span>${record.accuracy ? record.accuracy + 'm (' + record.accuracy_level + ')' : 'No GPS data'}</span>
</div>
<div class="detail-item">
<strong>Location Status:</strong>
<span class="status-badge ${record.has_location_data ? 'success' : 'warning'}">
${record.has_location_data ? 'GPS Available' : 'No GPS Data'}
</span>
</div>
</div>
<div class="detail-section">
<h4><i class="fas fa-mobile-alt"></i> Device Information</h4>
<div class="detail-item">
<strong>Device:</strong>
<span>${record.device}</span>
</div>
</div>
</div>
`;
modal.style.display = 'block';
}
</script>
{% endblock %} {% endblock %}