464 lines
13 KiB
HTML
464 lines
13 KiB
HTML
{% extends "base_authenticated.html" %}
|
|
{% block title %}Time Attendance Dashboard - {{ COMPANY_NAME }}{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/attendance.css') }}">
|
|
{% endblock %}
|
|
|
|
{% block page_title %}Time Attendance{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="attendance-page">
|
|
<!-- Page Header -->
|
|
<div class="attendance-header">
|
|
<div class="header-content">
|
|
<h1>
|
|
<i class="fas fa-clock"></i>
|
|
Time Attendance Dashboard
|
|
</h1>
|
|
<p>Manage and analyze employee time attendance data from Excel imports</p>
|
|
</div>
|
|
|
|
<div class="header-actions">
|
|
{% if session.role == 'admin' %}
|
|
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary">
|
|
<i class="fas fa-upload"></i>
|
|
Import Excel Data
|
|
</a>
|
|
{% endif %}
|
|
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-secondary">
|
|
<i class="fas fa-search"></i>
|
|
View All Records
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Section -->
|
|
<div class="stats-section">
|
|
<div class="stats-grid">
|
|
<div class="stat-card primary">
|
|
<div class="stat-icon">
|
|
<i class="fas fa-database"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<h3>{{ "{:,}".format(total_records) }}</h3>
|
|
<p>Total Records</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card success">
|
|
<div class="stat-icon">
|
|
<i class="fas fa-users"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<h3>{{ "{:,}".format(unique_employees) }}</h3>
|
|
<p>Employees</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card info">
|
|
<div class="stat-icon">
|
|
<i class="fas fa-map-marker-alt"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<h3>{{ "{:,}".format(unique_locations) }}</h3>
|
|
<p>Locations</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card warning">
|
|
<div class="stat-icon">
|
|
<i class="fas fa-file-import"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<h3>{{ "{:,}".format(recent_imports|length) }}</h3>
|
|
<p>Import Batches</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Records Table -->
|
|
{% if recent_records %}
|
|
<div class="attendance-table-section">
|
|
<div class="table-header">
|
|
<h3>
|
|
<i class="fas fa-table"></i>
|
|
Recent Time Attendance Records
|
|
</h3>
|
|
<div class="table-controls">
|
|
<div class="entries-per-page">
|
|
<span>Showing latest {{ recent_records|length }} records</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table class="attendance-table">
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Employee ID</th>
|
|
<th>Employee Name</th>
|
|
<th>Location</th>
|
|
<th>Action</th>
|
|
<th>Date</th>
|
|
<th>Time</th>
|
|
<th>Platform</th>
|
|
<th>Address</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for record in recent_records %}
|
|
<tr>
|
|
<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 or '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="action-info">
|
|
{% if record.action_description.lower() == 'check in' %}
|
|
<i class="fas fa-sign-in-alt" style="color: #059669;"></i>
|
|
{% elif record.action_description.lower() == 'check out' %}
|
|
<i class="fas fa-sign-out-alt" style="color: #d97706;"></i>
|
|
{% else %}
|
|
<i class="fas fa-clock" style="color: #6b7280;"></i>
|
|
{% endif %}
|
|
{{ record.action_description }}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="date-info">
|
|
<i class="fas fa-calendar"></i>
|
|
{{ record.attendance_date.strftime('%Y-%m-%d') }}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="time-info">
|
|
<i class="fas fa-clock"></i>
|
|
{{ record.attendance_time.strftime('%H:%M:%S') }}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="platform-info">
|
|
{% if record.platform %}
|
|
<i class="fas fa-mobile-alt"></i>
|
|
{{ record.platform[:25] }}{% if record.platform|length > 25 %}...{% endif %}
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="address-info">
|
|
{% if record.recorded_address %}
|
|
<i class="fas fa-map-pin"></i>
|
|
<span title="{{ record.recorded_address }}">
|
|
{{ record.recorded_address[:35] }}{% if record.recorded_address|length > 35 %}...{% endif %}
|
|
</span>
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="record-actions">
|
|
<button class="action-btn btn-view" onclick="viewRecord({{ record.id }})" title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
{% if session.role == 'admin' %}
|
|
<button class="action-btn btn-delete" onclick="deleteRecord({{ record.id }}, '{{ record.employee_name }}', '{{ record.attendance_date }}')" title="Delete">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Table Footer -->
|
|
<div class="table-footer">
|
|
<div class="table-info">
|
|
<span>Showing {{ recent_records|length }} of {{ "{:,}".format(total_records) }} total records</span>
|
|
</div>
|
|
<div class="table-actions">
|
|
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-primary">
|
|
<i class="fas fa-list"></i>
|
|
View All Records
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Recent Imports Table -->
|
|
{% if recent_imports %}
|
|
<div class="attendance-table-section">
|
|
<div class="table-header">
|
|
<h3>
|
|
<i class="fas fa-history"></i>
|
|
Recent Import Batches
|
|
</h3>
|
|
<div class="table-controls">
|
|
<span>Last {{ recent_imports|length }} import batches</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table class="attendance-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Import Date</th>
|
|
<th>Source</th>
|
|
<th>Records</th>
|
|
<th>Batch ID</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for import_batch in recent_imports %}
|
|
<tr>
|
|
<td>
|
|
<div class="date-info">
|
|
<i class="fas fa-calendar"></i>
|
|
{{ import_batch.import_date.strftime('%Y-%m-%d %H:%M') if import_batch.import_date else 'Unknown' }}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="source-info">
|
|
<i class="fas fa-file-excel" style="color: #059669;"></i>
|
|
{{ import_batch.import_source or 'Excel Import' }}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="record-count">{{ "{:,}".format(import_batch.record_count) }}</span>
|
|
</td>
|
|
<td>
|
|
<code class="batch-id">{{ import_batch.import_batch_id[:8] }}...</code>
|
|
</td>
|
|
<td>
|
|
<div class="record-actions">
|
|
<a href="{{ url_for('time_attendance_records', import_batch=import_batch.import_batch_id) }}"
|
|
class="action-btn btn-view" title="View Records">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
<a href="{{ url_for('view_import_batch', batch_id=import_batch.import_batch_id) }}"
|
|
class="action-btn btn-info" title="Batch Details">
|
|
<i class="fas fa-info-circle"></i>
|
|
</a>
|
|
{% if session.role == 'admin' %}
|
|
<button onclick="deleteBatch('{{ import_batch.import_batch_id }}')"
|
|
class="action-btn btn-delete" title="Delete Batch">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Empty State -->
|
|
{% if total_records == 0 %}
|
|
<div class="empty-state">
|
|
<div class="empty-icon">
|
|
<i class="fas fa-clock"></i>
|
|
</div>
|
|
<h3>No Time Attendance Data</h3>
|
|
<p>
|
|
Get started by importing your first Excel file with time attendance data.
|
|
The system supports various Excel formats (.xlsx, .xls) and will automatically process your data.
|
|
</p>
|
|
{% if session.role == 'admin' %}
|
|
<div class="empty-actions">
|
|
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary">
|
|
<i class="fas fa-upload"></i>
|
|
Import Your First File
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<style>
|
|
/* Additional styles specific to this page */
|
|
.table-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem 1.5rem;
|
|
background: var(--gray-50);
|
|
border-top: 1px solid var(--gray-200);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.table-info {
|
|
color: var(--gray-600);
|
|
}
|
|
|
|
.record-count {
|
|
font-weight: 600;
|
|
color: var(--gray-900);
|
|
}
|
|
|
|
.batch-id {
|
|
background: var(--gray-100);
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: var(--radius);
|
|
font-family: monospace;
|
|
font-size: var(--font-size-xs);
|
|
color: var(--gray-600);
|
|
}
|
|
|
|
.text-muted {
|
|
color: var(--gray-400);
|
|
font-style: italic;
|
|
}
|
|
|
|
.date-info,
|
|
.time-info,
|
|
.action-info,
|
|
.platform-info,
|
|
.address-info,
|
|
.source-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.date-info i,
|
|
.time-info i,
|
|
.action-info i {
|
|
width: 14px;
|
|
text-align: center;
|
|
}
|
|
|
|
.platform-info,
|
|
.address-info {
|
|
max-width: 200px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
background: var(--white);
|
|
border-radius: var(--radius-xl);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.empty-icon {
|
|
width: 120px;
|
|
height: 120px;
|
|
margin: 0 auto 1.5rem;
|
|
background: var(--gray-100);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--gray-500);
|
|
font-size: 3rem;
|
|
}
|
|
|
|
.empty-state h3 {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: 600;
|
|
color: var(--gray-900);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.empty-state p {
|
|
color: var(--gray-600);
|
|
font-size: var(--font-size-lg);
|
|
margin-bottom: 2rem;
|
|
max-width: 500px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.empty-actions {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.table-footer {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Hide some columns on mobile */
|
|
.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;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// View record details
|
|
function viewRecord(recordId) {
|
|
window.location.href = "{{ url_for('time_attendance_record_detail', record_id=0) }}".replace('0', recordId);
|
|
}
|
|
|
|
// Delete record with confirmation
|
|
function deleteRecord(recordId, employeeName, date) {
|
|
if (confirm(`Are you sure you want to delete the attendance record for ${employeeName} on ${date}?`)) {
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = "{{ url_for('delete_time_attendance_record', record_id=0) }}".replace('0', recordId);
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
}
|
|
|
|
function deleteBatch(batchId) {
|
|
if (confirm('Are you sure you want to delete this entire import batch? This action cannot be undone.')) {
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = `/time-attendance/import/batch/${batchId}/delete`;
|
|
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]');
|
|
if (csrfToken) {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'csrf_token';
|
|
input.value = csrfToken.content;
|
|
form.appendChild(input);
|
|
}
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %} |