Time Attendance dashboard layout changed

This commit is contained in:
Nguyen Ngo
2025-09-18 17:18:06 -04:00
parent b9d40460f0
commit 8a0f4e8f4b
2 changed files with 353 additions and 311 deletions
+31 -18
View File
@@ -6449,34 +6449,38 @@ def employee_detail(employee_index):
flash('Error loading employee details. Please try again.', 'error')
return redirect(url_for('employees'))
"""
Time Attendance Routes for QR Attendance Management System
==========================================================
Routes for managing time attendance data import and display functionality.
Add these routes to your main app.py file.
"""
from flask import render_template, request, redirect, url_for, flash, jsonify
from werkzeug.utils import secure_filename
import os
from datetime import datetime, date
from models.time_attendance import TimeAttendance
from time_attendance_import_service import TimeAttendanceImportService
# Add this to your existing app.py imports and route definitions
@app.route('/time-attendance')
@login_required
@log_user_activity('time_attendance_view')
def time_attendance_dashboard():
"""Display time attendance dashboard"""
"""Display time attendance dashboard with table layout"""
try:
# Initialize default values
total_records = 0
unique_employees = 0
unique_locations = 0
recent_imports = []
recent_records = []
employees = []
locations = []
# Try to get data from TimeAttendance model if it exists
try:
from models.time_attendance import TimeAttendance
# Get summary statistics
total_records = TimeAttendance.query.count()
if total_records > 0:
unique_employees = db.session.query(TimeAttendance.employee_id).distinct().count()
unique_locations = db.session.query(TimeAttendance.location_name).distinct().count()
# Get recent records (last 20 records for table display)
recent_records = TimeAttendance.query.order_by(
TimeAttendance.attendance_date.desc(),
TimeAttendance.attendance_time.desc()
).limit(20).all()
# Get recent imports (last 10 import batches)
recent_imports = db.session.query(
TimeAttendance.import_batch_id,
@@ -6497,11 +6501,20 @@ def time_attendance_dashboard():
employees = TimeAttendance.get_unique_employees()
locations = TimeAttendance.get_unique_locations()
except ImportError:
# TimeAttendance model doesn't exist yet - use defaults
pass
except Exception as e:
# Database table doesn't exist yet or other error - use defaults
print(f"TimeAttendance query error: {e}")
pass
return render_template('time_attendance_dashboard.html',
total_records=total_records,
unique_employees=unique_employees,
unique_locations=unique_locations,
recent_imports=recent_imports,
recent_records=recent_records,
employees=employees,
locations=locations)
+261 -232
View File
@@ -2,30 +2,21 @@
{% block title %}Time Attendance Dashboard - {{ COMPANY_NAME }}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/time_attendance.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/attendance.css') }}">
{% endblock %}
{% block page_title %}Time Attendance{% endblock %}
{% block content %}
<div class="time-attendance-page">
<div class="attendance-page">
<!-- Page Header -->
<div class="time-attendance-header">
<div class="header-navigation">
<a href="{{ url_for('dashboard') }}" class="back-button">
<i class="fas fa-arrow-left"></i>
Back to Dashboard
</a>
</div>
<div class="attendance-header">
<div class="header-content">
<h1>
<i class="fas fa-clock"></i>
Time Attendance Management
Time Attendance Dashboard
</h1>
<p class="header-description">
Import, manage, and analyze employee time attendance data from Excel files
</p>
<p>Manage and analyze employee time attendance data from Excel imports</p>
</div>
<div class="header-actions">
@@ -36,121 +27,203 @@
</a>
{% endif %}
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-secondary">
<i class="fas fa-list"></i>
View Records
<i class="fas fa-search"></i>
View All Records
</a>
</div>
</div>
<!-- Statistics Grid -->
<!-- Statistics Section -->
<div class="stats-section">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon records">
<div class="stat-card primary">
<div class="stat-icon">
<i class="fas fa-database"></i>
</div>
<div class="stat-info">
<div class="stat-content">
<h3>{{ "{:,}".format(total_records) }}</h3>
<p>Total Records</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon employees">
<div class="stat-card success">
<div class="stat-icon">
<i class="fas fa-users"></i>
</div>
<div class="stat-info">
<div class="stat-content">
<h3>{{ "{:,}".format(unique_employees) }}</h3>
<p>Unique Employees</p>
<p>Employees</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon locations">
<div class="stat-card info">
<div class="stat-icon">
<i class="fas fa-map-marker-alt"></i>
</div>
<div class="stat-info">
<div class="stat-content">
<h3>{{ "{:,}".format(unique_locations) }}</h3>
<p>Locations</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon imports">
<div class="stat-card warning">
<div class="stat-icon">
<i class="fas fa-file-import"></i>
</div>
<div class="stat-info">
<div class="stat-content">
<h3>{{ "{:,}".format(recent_imports|length) }}</h3>
<p>Recent Imports</p>
<p>Import Batches</p>
</div>
</div>
</div>
</div>
<!-- Quick Actions Section -->
<div class="import-section">
<div class="import-header">
<h2>
<i class="fas fa-bolt"></i>
Quick Actions
</h2>
</div>
<div class="import-body">
<div class="form-grid">
<div class="action-card">
<!-- Recent Records Table -->
{% if recent_records %}
<div class="attendance-table-section">
<div class="table-header">
<h3>
<i class="fas fa-search"></i>
Search Records
<i class="fas fa-table"></i>
Recent Time Attendance Records
</h3>
<p>Find specific attendance records by employee, date, or location</p>
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-primary">
<i class="fas fa-search"></i>
Search Now
</a>
<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' %}
<div class="action-card">
<h3>
<i class="fas fa-upload"></i>
Import Excel File
</h3>
<p>Upload and import time attendance data from Excel spreadsheets</p>
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-success">
<i class="fas fa-file-excel"></i>
Import Data
<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 %}
<div class="action-card">
<h3>
<i class="fas fa-chart-bar"></i>
Generate Reports
</h3>
<p>Create detailed attendance reports and analytics</p>
<a href="#" class="btn btn-warning">
<i class="fas fa-chart-line"></i>
View Reports
</a>
</div>
</div>
</div>
</div>
<!-- Recent Imports Section -->
<!-- Recent Imports Table -->
{% if recent_imports %}
<div class="records-section">
<div class="records-header">
<h2>
<div class="attendance-table-section">
<div class="table-header">
<h3>
<i class="fas fa-history"></i>
Recent Imports
</h2>
<div class="records-meta">
Last {{ recent_imports|length }} import batches
Recent Import Batches
</h3>
<div class="table-controls">
<span>Last {{ recent_imports|length }} import batches</span>
</div>
</div>
<div class="records-table-container">
<table class="records-table">
<div class="table-container">
<table class="attendance-table">
<thead>
<tr>
<th>Import Date</th>
@@ -163,29 +236,31 @@
<tbody>
{% for import_batch in recent_imports %}
<tr>
<td class="datetime-cell">
<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 text-success"></i>
<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) }} records
</span>
<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 class="actions-cell">
<td>
<div class="record-actions">
<a href="{{ url_for('time_attendance_records', import_batch=import_batch.import_batch_id) }}"
class="action-btn view">
class="action-btn btn-view" title="View Records">
<i class="fas fa-eye"></i>
View
</a>
</div>
</td>
</tr>
{% endfor %}
@@ -195,52 +270,6 @@
</div>
{% endif %}
<!-- Employee Quick Access -->
{% if employees %}
<div class="records-section">
<div class="records-header">
<h2>
<i class="fas fa-users"></i>
Employee Quick Access
</h2>
<div class="records-meta">
Click to view employee attendance records
</div>
</div>
<div class="employee-grid">
{% for employee in employees[:12] %}
<div class="employee-card">
<div class="employee-avatar">
{{ employee.employee_name[0].upper() if employee.employee_name else employee.employee_id[0].upper() }}
</div>
<div class="employee-info">
<div class="employee-name">{{ employee.employee_name }}</div>
<div class="employee-id">ID: {{ employee.employee_id }}</div>
</div>
<a href="{{ url_for('time_attendance_records', employee_id=employee.employee_id) }}"
class="btn btn-sm btn-outline">
<i class="fas fa-eye"></i>
View Records
</a>
</div>
{% endfor %}
{% if employees|length > 12 %}
<div class="employee-card view-all">
<div class="view-all-content">
<i class="fas fa-plus-circle"></i>
<span>{{ employees|length - 12 }} more employees</span>
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-sm btn-primary">
View All
</a>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Empty State -->
{% if total_records == 0 %}
<div class="empty-state">
@@ -250,156 +279,156 @@
<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 and will automatically process your 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>
.action-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
padding: 1.5rem;
text-align: center;
transition: all 0.2s ease-in-out;
}
.action-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, 0.1);
}
.action-card h3 {
/* Additional styles specific to this page */
.table-footer {
display: flex;
justify-content: space-between;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 1.125rem;
font-weight: 600;
color: #0f172a;
margin-bottom: 0.5rem;
padding: 1rem 1.5rem;
background: var(--gray-50);
border-top: 1px solid var(--gray-200);
font-size: var(--font-size-sm);
}
.action-card p {
color: #64748b;
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.source-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.text-success {
color: #10b981;
.table-info {
color: var(--gray-600);
}
.record-count {
font-weight: 600;
color: #0f172a;
color: var(--gray-900);
}
.batch-id {
background: #f1f5f9;
background: var(--gray-100);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
border-radius: var(--radius);
font-family: monospace;
font-size: 0.75rem;
color: #475569;
font-size: var(--font-size-xs);
color: var(--gray-600);
}
.employee-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
padding: 1.5rem;
.text-muted {
color: var(--gray-400);
font-style: italic;
}
.employee-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 1rem;
.date-info,
.time-info,
.action-info,
.platform-info,
.address-info,
.source-info {
display: flex;
align-items: center;
gap: 1rem;
transition: all 0.2s ease-in-out;
gap: 0.5rem;
font-size: var(--font-size-sm);
}
.employee-card:hover {
background: #ffffff;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.employee-card.view-all {
background: #8b5cf6;
border-color: #8b5cf6;
color: #ffffff;
justify-content: center;
.date-info i,
.time-info i,
.action-info i {
width: 14px;
text-align: center;
flex-direction: column;
gap: 0.5rem;
}
.view-all-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
.platform-info,
.address-info {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.view-all-content i {
font-size: 1.5rem;
margin-bottom: 0.25rem;
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: var(--white);
border-radius: var(--radius-xl);
box-shadow: var(--shadow);
}
.employee-card .employee-avatar {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
.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: #ffffff;
font-size: 0.875rem;
color: var(--gray-500);
font-size: 3rem;
}
.empty-state h3 {
font-size: var(--font-size-2xl);
font-weight: 600;
flex-shrink: 0;
color: var(--gray-900);
margin-bottom: 0.5rem;
}
.employee-card .employee-info {
flex: 1;
.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;
}
.employee-card .employee-name {
font-weight: 600;
color: #0f172a;
margin-bottom: 0.125rem;
}
.employee-card .employee-id {
color: #64748b;
font-size: 0.75rem;
.empty-actions {
margin-top: 1rem;
}
@media (max-width: 768px) {
.employee-grid {
grid-template-columns: 1fr;
.table-footer {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.form-grid {
grid-template-columns: 1fr;
/* 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();
}
}
</script>
{% endblock %}