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
+54 -41
View File
@@ -6449,59 +6449,72 @@ def employee_detail(employee_index):
flash('Error loading employee details. Please try again.', 'error') flash('Error loading employee details. Please try again.', 'error')
return redirect(url_for('employees')) 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') @app.route('/time-attendance')
@login_required @login_required
@log_user_activity('time_attendance_view') @log_user_activity('time_attendance_view')
def time_attendance_dashboard(): def time_attendance_dashboard():
"""Display time attendance dashboard""" """Display time attendance dashboard with table layout"""
try: try:
# Get summary statistics # Initialize default values
total_records = TimeAttendance.query.count() total_records = 0
unique_employees = db.session.query(TimeAttendance.employee_id).distinct().count() unique_employees = 0
unique_locations = db.session.query(TimeAttendance.location_name).distinct().count() unique_locations = 0
recent_imports = []
recent_records = []
employees = []
locations = []
# Get recent imports (last 10 import batches) # Try to get data from TimeAttendance model if it exists
recent_imports = db.session.query( try:
TimeAttendance.import_batch_id, from models.time_attendance import TimeAttendance
TimeAttendance.import_date,
TimeAttendance.import_source, # Get summary statistics
db.func.count(TimeAttendance.id).label('record_count') total_records = TimeAttendance.query.count()
).filter(
TimeAttendance.import_batch_id.isnot(None) if total_records > 0:
).group_by( unique_employees = db.session.query(TimeAttendance.employee_id).distinct().count()
TimeAttendance.import_batch_id, unique_locations = db.session.query(TimeAttendance.location_name).distinct().count()
TimeAttendance.import_date,
TimeAttendance.import_source # Get recent records (last 20 records for table display)
).order_by( recent_records = TimeAttendance.query.order_by(
TimeAttendance.import_date.desc() TimeAttendance.attendance_date.desc(),
).limit(10).all() TimeAttendance.attendance_time.desc()
).limit(20).all()
# Get filter options
employees = TimeAttendance.get_unique_employees() # Get recent imports (last 10 import batches)
locations = TimeAttendance.get_unique_locations() recent_imports = db.session.query(
TimeAttendance.import_batch_id,
TimeAttendance.import_date,
TimeAttendance.import_source,
db.func.count(TimeAttendance.id).label('record_count')
).filter(
TimeAttendance.import_batch_id.isnot(None)
).group_by(
TimeAttendance.import_batch_id,
TimeAttendance.import_date,
TimeAttendance.import_source
).order_by(
TimeAttendance.import_date.desc()
).limit(10).all()
# Get filter options
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', return render_template('time_attendance_dashboard.html',
total_records=total_records, total_records=total_records,
unique_employees=unique_employees, unique_employees=unique_employees,
unique_locations=unique_locations, unique_locations=unique_locations,
recent_imports=recent_imports, recent_imports=recent_imports,
recent_records=recent_records,
employees=employees, employees=employees,
locations=locations) locations=locations)
+299 -270
View File
@@ -2,30 +2,21 @@
{% block title %}Time Attendance Dashboard - {{ COMPANY_NAME }}{% endblock %} {% block title %}Time Attendance Dashboard - {{ COMPANY_NAME }}{% endblock %}
{% block extra_head %} {% 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 %} {% endblock %}
{% block page_title %}Time Attendance{% endblock %} {% block page_title %}Time Attendance{% endblock %}
{% block content %} {% block content %}
<div class="time-attendance-page"> <div class="attendance-page">
<!-- Page Header --> <!-- Page Header -->
<div class="time-attendance-header"> <div class="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="header-content"> <div class="header-content">
<h1> <h1>
<i class="fas fa-clock"></i> <i class="fas fa-clock"></i>
Time Attendance Management Time Attendance Dashboard
</h1> </h1>
<p class="header-description"> <p>Manage and analyze employee time attendance data from Excel imports</p>
Import, manage, and analyze employee time attendance data from Excel files
</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
@@ -36,121 +27,203 @@
</a> </a>
{% endif %} {% endif %}
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-secondary"> <a href="{{ url_for('time_attendance_records') }}" class="btn btn-secondary">
<i class="fas fa-list"></i> <i class="fas fa-search"></i>
View Records View All Records
</a> </a>
</div> </div>
</div> </div>
<!-- Statistics Grid --> <!-- Statistics Section -->
<div class="stats-grid"> <div class="stats-section">
<div class="stat-card"> <div class="stats-grid">
<div class="stat-icon records"> <div class="stat-card primary">
<i class="fas fa-database"></i> <div class="stat-icon">
</div> <i class="fas fa-database"></i>
<div class="stat-info">
<h3>{{ "{:,}".format(total_records) }}</h3>
<p>Total Records</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon employees">
<i class="fas fa-users"></i>
</div>
<div class="stat-info">
<h3>{{ "{:,}".format(unique_employees) }}</h3>
<p>Unique Employees</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon locations">
<i class="fas fa-map-marker-alt"></i>
</div>
<div class="stat-info">
<h3>{{ "{:,}".format(unique_locations) }}</h3>
<p>Locations</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon imports">
<i class="fas fa-file-import"></i>
</div>
<div class="stat-info">
<h3>{{ "{:,}".format(recent_imports|length) }}</h3>
<p>Recent Imports</p>
</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">
<h3>
<i class="fas fa-search"></i>
Search 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> </div>
<div class="stat-content">
{% if session.role == 'admin' %} <h3>{{ "{:,}".format(total_records) }}</h3>
<div class="action-card"> <p>Total Records</p>
<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
</a>
</div> </div>
{% endif %} </div>
<div class="action-card"> <div class="stat-card success">
<h3> <div class="stat-icon">
<i class="fas fa-chart-bar"></i> <i class="fas fa-users"></i>
Generate Reports </div>
</h3> <div class="stat-content">
<p>Create detailed attendance reports and analytics</p> <h3>{{ "{:,}".format(unique_employees) }}</h3>
<a href="#" class="btn btn-warning"> <p>Employees</p>
<i class="fas fa-chart-line"></i> </div>
View Reports </div>
</a>
<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>
</div> </div>
</div> </div>
<!-- Recent Imports Section --> <!-- 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 %} {% if recent_imports %}
<div class="records-section"> <div class="attendance-table-section">
<div class="records-header"> <div class="table-header">
<h2> <h3>
<i class="fas fa-history"></i> <i class="fas fa-history"></i>
Recent Imports Recent Import Batches
</h2> </h3>
<div class="records-meta"> <div class="table-controls">
Last {{ recent_imports|length }} import batches <span>Last {{ recent_imports|length }} import batches</span>
</div> </div>
</div> </div>
<div class="records-table-container"> <div class="table-container">
<table class="records-table"> <table class="attendance-table">
<thead> <thead>
<tr> <tr>
<th>Import Date</th> <th>Import Date</th>
@@ -163,29 +236,31 @@
<tbody> <tbody>
{% for import_batch in recent_imports %} {% for import_batch in recent_imports %}
<tr> <tr>
<td class="datetime-cell"> <td>
{{ import_batch.import_date.strftime('%Y-%m-%d %H:%M') if import_batch.import_date else 'Unknown' }} <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>
<td> <td>
<div class="source-info"> <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' }} {{ import_batch.import_source or 'Excel Import' }}
</div> </div>
</td> </td>
<td> <td>
<span class="record-count"> <span class="record-count">{{ "{:,}".format(import_batch.record_count) }}</span>
{{ "{:,}".format(import_batch.record_count) }} records
</span>
</td> </td>
<td> <td>
<code class="batch-id">{{ import_batch.import_batch_id[:8] }}...</code> <code class="batch-id">{{ import_batch.import_batch_id[:8] }}...</code>
</td> </td>
<td class="actions-cell"> <td>
<a href="{{ url_for('time_attendance_records', import_batch=import_batch.import_batch_id) }}" <div class="record-actions">
class="action-btn view"> <a href="{{ url_for('time_attendance_records', import_batch=import_batch.import_batch_id) }}"
<i class="fas fa-eye"></i> class="action-btn btn-view" title="View Records">
View <i class="fas fa-eye"></i>
</a> </a>
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -195,52 +270,6 @@
</div> </div>
{% endif %} {% 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 --> <!-- Empty State -->
{% if total_records == 0 %} {% if total_records == 0 %}
<div class="empty-state"> <div class="empty-state">
@@ -250,156 +279,156 @@
<h3>No Time Attendance Data</h3> <h3>No Time Attendance Data</h3>
<p> <p>
Get started by importing your first Excel file with time attendance data. 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> </p>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary"> <div class="empty-actions">
<i class="fas fa-upload"></i> <a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary">
Import Your First File <i class="fas fa-upload"></i>
</a> Import Your First File
</a>
</div>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
<style> <style>
.action-card { /* Additional styles specific to this page */
background: #f8fafc; .table-footer {
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 {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: center; padding: 1rem 1.5rem;
gap: 0.5rem; background: var(--gray-50);
font-size: 1.125rem; border-top: 1px solid var(--gray-200);
font-weight: 600; font-size: var(--font-size-sm);
color: #0f172a;
margin-bottom: 0.5rem;
} }
.action-card p { .table-info {
color: #64748b; color: var(--gray-600);
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.source-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.text-success {
color: #10b981;
} }
.record-count { .record-count {
font-weight: 600; font-weight: 600;
color: #0f172a; color: var(--gray-900);
} }
.batch-id { .batch-id {
background: #f1f5f9; background: var(--gray-100);
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 0.25rem; border-radius: var(--radius);
font-family: monospace; font-family: monospace;
font-size: 0.75rem; font-size: var(--font-size-xs);
color: #475569; color: var(--gray-600);
} }
.employee-grid { .text-muted {
display: grid; color: var(--gray-400);
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); font-style: italic;
gap: 1rem;
padding: 1.5rem;
} }
.employee-card { .date-info,
background: #f8fafc; .time-info,
border: 1px solid #e2e8f0; .action-info,
border-radius: 0.5rem; .platform-info,
padding: 1rem; .address-info,
.source-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 0.5rem;
transition: all 0.2s ease-in-out; font-size: var(--font-size-sm);
} }
.employee-card:hover { .date-info i,
background: #ffffff; .time-info i,
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); .action-info i {
} width: 14px;
.employee-card.view-all {
background: #8b5cf6;
border-color: #8b5cf6;
color: #ffffff;
justify-content: center;
text-align: center; text-align: center;
flex-direction: column;
gap: 0.5rem;
} }
.view-all-content { .platform-info,
display: flex; .address-info {
flex-direction: column; max-width: 200px;
align-items: center; overflow: hidden;
gap: 0.5rem; text-overflow: ellipsis;
white-space: nowrap;
} }
.view-all-content i { .empty-state {
font-size: 1.5rem; text-align: center;
margin-bottom: 0.25rem; padding: 4rem 2rem;
background: var(--white);
border-radius: var(--radius-xl);
box-shadow: var(--shadow);
} }
.employee-card .employee-avatar { .empty-icon {
width: 40px; width: 120px;
height: 40px; height: 120px;
background: linear-gradient(135deg, #8b5cf6, #7c3aed); margin: 0 auto 1.5rem;
background: var(--gray-100);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #ffffff; color: var(--gray-500);
font-size: 0.875rem; font-size: 3rem;
}
.empty-state h3 {
font-size: var(--font-size-2xl);
font-weight: 600; font-weight: 600;
flex-shrink: 0; color: var(--gray-900);
margin-bottom: 0.5rem;
} }
.employee-card .employee-info { .empty-state p {
flex: 1; 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 { .empty-actions {
font-weight: 600; margin-top: 1rem;
color: #0f172a;
margin-bottom: 0.125rem;
}
.employee-card .employee-id {
color: #64748b;
font-size: 0.75rem;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.employee-grid { .table-footer {
grid-template-columns: 1fr; flex-direction: column;
gap: 1rem;
text-align: center;
} }
.form-grid { /* Hide some columns on mobile */
grid-template-columns: 1fr; .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> </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 %} {% endblock %}