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') 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:
# 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 # Get summary statistics
total_records = TimeAttendance.query.count() total_records = TimeAttendance.query.count()
if total_records > 0:
unique_employees = db.session.query(TimeAttendance.employee_id).distinct().count() unique_employees = db.session.query(TimeAttendance.employee_id).distinct().count()
unique_locations = db.session.query(TimeAttendance.location_name).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) # Get recent imports (last 10 import batches)
recent_imports = db.session.query( recent_imports = db.session.query(
TimeAttendance.import_batch_id, TimeAttendance.import_batch_id,
@@ -6497,11 +6501,20 @@ def time_attendance_dashboard():
employees = TimeAttendance.get_unique_employees() employees = TimeAttendance.get_unique_employees()
locations = TimeAttendance.get_unique_locations() 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)
+261 -232
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-section">
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card primary">
<div class="stat-icon records"> <div class="stat-icon">
<i class="fas fa-database"></i> <i class="fas fa-database"></i>
</div> </div>
<div class="stat-info"> <div class="stat-content">
<h3>{{ "{:,}".format(total_records) }}</h3> <h3>{{ "{:,}".format(total_records) }}</h3>
<p>Total Records</p> <p>Total Records</p>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card success">
<div class="stat-icon employees"> <div class="stat-icon">
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
</div> </div>
<div class="stat-info"> <div class="stat-content">
<h3>{{ "{:,}".format(unique_employees) }}</h3> <h3>{{ "{:,}".format(unique_employees) }}</h3>
<p>Unique Employees</p> <p>Employees</p>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card info">
<div class="stat-icon locations"> <div class="stat-icon">
<i class="fas fa-map-marker-alt"></i> <i class="fas fa-map-marker-alt"></i>
</div> </div>
<div class="stat-info"> <div class="stat-content">
<h3>{{ "{:,}".format(unique_locations) }}</h3> <h3>{{ "{:,}".format(unique_locations) }}</h3>
<p>Locations</p> <p>Locations</p>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card warning">
<div class="stat-icon imports"> <div class="stat-icon">
<i class="fas fa-file-import"></i> <i class="fas fa-file-import"></i>
</div> </div>
<div class="stat-info"> <div class="stat-content">
<h3>{{ "{:,}".format(recent_imports|length) }}</h3> <h3>{{ "{:,}".format(recent_imports|length) }}</h3>
<p>Recent Imports</p> <p>Import Batches</p>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Quick Actions Section --> <!-- Recent Records Table -->
<div class="import-section"> {% if recent_records %}
<div class="import-header"> <div class="attendance-table-section">
<h2> <div class="table-header">
<i class="fas fa-bolt"></i>
Quick Actions
</h2>
</div>
<div class="import-body">
<div class="form-grid">
<div class="action-card">
<h3> <h3>
<i class="fas fa-search"></i> <i class="fas fa-table"></i>
Search Records Recent Time Attendance Records
</h3> </h3>
<p>Find specific attendance records by employee, date, or location</p> <div class="table-controls">
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-primary"> <div class="entries-per-page">
<i class="fas fa-search"></i> <span>Showing latest {{ recent_records|length }} records</span>
Search Now </div>
</a> </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' %} {% if session.role == 'admin' %}
<div class="action-card"> <button class="action-btn btn-delete" onclick="deleteRecord({{ record.id }}, '{{ record.employee_name }}', '{{ record.attendance_date }}')" title="Delete">
<h3> <i class="fas fa-trash"></i>
<i class="fas fa-upload"></i> </button>
Import Excel File {% endif %}
</h3> </div>
<p>Upload and import time attendance data from Excel spreadsheets</p> </td>
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-success"> </tr>
<i class="fas fa-file-excel"></i> {% endfor %}
Import Data </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> </a>
</div> </div>
</div>
</div>
{% endif %} {% endif %}
<div class="action-card"> <!-- Recent Imports Table -->
<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 -->
{% 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>
<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' }} {{ 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>
<div class="record-actions">
<a href="{{ url_for('time_attendance_records', import_batch=import_batch.import_batch_id) }}" <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> <i class="fas fa-eye"></i>
View
</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' %}
<div class="empty-actions">
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary"> <a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary">
<i class="fas fa-upload"></i> <i class="fas fa-upload"></i>
Import Your First File Import Your First File
</a> </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 %}