Time Attendance

This commit is contained in:
Nguyen Ngo
2025-09-17 14:46:51 -04:00
parent 7933fcc24f
commit 89ef6bf709
8 changed files with 4697 additions and 0 deletions
+405
View File
@@ -0,0 +1,405 @@
{% extends "base_authenticated.html" %}
{% block title %}Time Attendance Dashboard - {{ COMPANY_NAME }}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/time_attendance.css') }}">
{% endblock %}
{% block page_title %}Time Attendance{% endblock %}
{% block content %}
<div class="time-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="header-content">
<h1>
<i class="fas fa-clock"></i>
Time Attendance Management
</h1>
<p class="header-description">
Import, manage, and analyze employee time attendance data from Excel files
</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-list"></i>
View Records
</a>
</div>
</div>
<!-- Statistics Grid -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon records">
<i class="fas fa-database"></i>
</div>
<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>
{% 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
</a>
</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 -->
{% if recent_imports %}
<div class="records-section">
<div class="records-header">
<h2>
<i class="fas fa-history"></i>
Recent Imports
</h2>
<div class="records-meta">
Last {{ recent_imports|length }} import batches
</div>
</div>
<div class="records-table-container">
<table class="records-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 class="datetime-cell">
{{ import_batch.import_date.strftime('%Y-%m-%d %H:%M') if import_batch.import_date else 'Unknown' }}
</td>
<td>
<div class="source-info">
<i class="fas fa-file-excel text-success"></i>
{{ import_batch.import_source or 'Excel Import' }}
</div>
</td>
<td>
<span class="record-count">
{{ "{:,}".format(import_batch.record_count) }} records
</span>
</td>
<td>
<code class="batch-id">{{ import_batch.import_batch_id[:8] }}...</code>
</td>
<td class="actions-cell">
<a href="{{ url_for('time_attendance_records', import_batch=import_batch.import_batch_id) }}"
class="action-btn view">
<i class="fas fa-eye"></i>
View
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</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">
<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 and will automatically process your data.
</p>
{% if session.role == 'admin' %}
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary">
<i class="fas fa-upload"></i>
Import Your First File
</a>
{% 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 {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 1.125rem;
font-weight: 600;
color: #0f172a;
margin-bottom: 0.5rem;
}
.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;
}
.record-count {
font-weight: 600;
color: #0f172a;
}
.batch-id {
background: #f1f5f9;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.75rem;
color: #475569;
}
.employee-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
padding: 1.5rem;
}
.employee-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 1rem;
display: flex;
align-items: center;
gap: 1rem;
transition: all 0.2s ease-in-out;
}
.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;
text-align: center;
flex-direction: column;
gap: 0.5rem;
}
.view-all-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.view-all-content i {
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.employee-card .employee-avatar {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 0.875rem;
font-weight: 600;
flex-shrink: 0;
}
.employee-card .employee-info {
flex: 1;
}
.employee-card .employee-name {
font-weight: 600;
color: #0f172a;
margin-bottom: 0.125rem;
}
.employee-card .employee-id {
color: #64748b;
font-size: 0.75rem;
}
@media (max-width: 768px) {
.employee-grid {
grid-template-columns: 1fr;
}
.form-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
+692
View File
@@ -0,0 +1,692 @@
{% extends "base_authenticated.html" %}
{% block title %}Import Time Attendance Data - {{ COMPANY_NAME }}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/time_attendance.css') }}">
{% endblock %}
{% block page_title %}Import Time Attendance{% endblock %}
{% block content %}
<div class="time-attendance-page">
<!-- Page Header -->
<div class="time-attendance-header">
<div class="header-navigation">
<a href="{{ url_for('time_attendance_dashboard') }}" class="back-button">
<i class="fas fa-arrow-left"></i>
Back to Time Attendance
</a>
</div>
<div class="header-content">
<h1>
<i class="fas fa-file-import"></i>
Import Time Attendance Data
</h1>
<p class="header-description">
Upload Excel files containing employee time attendance records
</p>
</div>
</div>
<!-- Import Instructions -->
<div class="import-section">
<div class="import-header">
<h2>
<i class="fas fa-info-circle"></i>
Import Instructions
</h2>
</div>
<div class="import-body">
<div class="instructions-grid">
<div class="instruction-item">
<div class="instruction-icon">
<i class="fas fa-file-excel"></i>
</div>
<h3>Supported Formats</h3>
<p>Excel files (.xlsx, .xls) with time attendance data</p>
</div>
<div class="instruction-item">
<div class="instruction-icon">
<i class="fas fa-columns"></i>
</div>
<h3>Required Columns</h3>
<p>ID, Name, Date, Time, Location Name, Action Description</p>
</div>
<div class="instruction-item">
<div class="instruction-icon">
<i class="fas fa-shield-alt"></i>
</div>
<h3>Data Validation</h3>
<p>Automatic validation and error reporting for invalid data</p>
</div>
</div>
<div class="sample-format">
<h4>
<i class="fas fa-table"></i>
Expected Excel Format
</h4>
<div class="format-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Platform</th>
<th>Date</th>
<th>Time</th>
<th>Location Name</th>
<th>Action Description</th>
<th>Event Description</th>
<th>Recorded Address</th>
</tr>
</thead>
<tbody>
<tr>
<td>12345</td>
<td>John Doe</td>
<td>iPhone - iOS</td>
<td>2025-09-12</td>
<td>09:00:00</td>
<td>HQ Suite 210</td>
<td>Check In</td>
<td>Main Office</td>
<td>123 Main St</td>
</tr>
<tr>
<td>67890</td>
<td>Jane Smith</td>
<td>Android</td>
<td>2025-09-12</td>
<td>17:30:00</td>
<td>HQ Suite 210</td>
<td>Check Out</td>
<td>Main Office</td>
<td>123 Main St</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- File Upload Form -->
<div class="import-section">
<div class="import-header">
<h2>
<i class="fas fa-upload"></i>
Upload Excel File
</h2>
</div>
<div class="import-body">
<form id="importForm" method="POST" enctype="multipart/form-data" action="{{ url_for('import_time_attendance') }}">
<!-- File Upload Area -->
<div class="file-upload-area" id="fileUploadArea">
<div class="upload-icon">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<div class="upload-text">
<h3>Drag & Drop Excel File</h3>
<p>Or click to browse and select an Excel file (.xlsx, .xls)</p>
<div class="file-info" id="fileInfo" style="display: none;">
<i class="fas fa-file-excel"></i>
<span class="file-name"></span>
<span class="file-size"></span>
</div>
</div>
<input type="file"
id="fileInput"
name="file"
accept=".xlsx,.xls"
class="file-input"
required>
</div>
<!-- Import Options -->
<div class="form-grid">
<div class="form-group">
<label for="import_source" class="form-label">
<i class="fas fa-tag"></i>
Import Source Description
</label>
<input type="text"
id="import_source"
name="import_source"
class="form-input"
placeholder="e.g., Monthly Attendance Report - September 2025"
value="Excel Import - {{ current_date }}">
<small class="form-help">Optional description for this import batch</small>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button type="button" class="btn btn-outline" onclick="resetForm()">
<i class="fas fa-undo"></i>
Reset Form
</button>
<button type="submit" class="btn btn-primary" id="submitBtn" disabled>
<i class="fas fa-upload"></i>
<span class="btn-text">Import Data</span>
<span class="btn-loading" style="display: none;">
<i class="fas fa-spinner fa-spin"></i>
Processing...
</span>
</button>
</div>
</form>
</div>
</div>
<!-- Validation Results (if any) -->
{% if validation_result %}
<div class="import-results {% if validation_result.valid %}success{% else %}error{% endif %}">
<div class="results-header">
<h2>
{% if validation_result.valid %}
<i class="fas fa-check-circle text-success"></i>
File Validation Successful
{% else %}
<i class="fas fa-exclamation-circle text-danger"></i>
File Validation Failed
{% endif %}
</h2>
</div>
<div class="results-body">
<div class="results-summary">
<div class="summary-item">
<div class="number">{{ validation_result.total_rows }}</div>
<div class="label">Total Rows</div>
</div>
<div class="summary-item">
<div class="number">{{ validation_result.columns|length }}</div>
<div class="label">Columns Found</div>
</div>
{% if validation_result.errors %}
<div class="summary-item error">
<div class="number">{{ validation_result.errors|length }}</div>
<div class="label">Errors</div>
</div>
{% endif %}
{% if validation_result.warnings %}
<div class="summary-item">
<div class="number">{{ validation_result.warnings|length }}</div>
<div class="label">Warnings</div>
</div>
{% endif %}
</div>
{% if validation_result.errors %}
<div class="error-list">
<h4>
<i class="fas fa-exclamation-triangle"></i>
Validation Errors
</h4>
<ul>
{% for error in validation_result.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if validation_result.warnings %}
<div class="warning-list">
<h4>
<i class="fas fa-exclamation-triangle"></i>
Warnings
</h4>
<ul>
{% for warning in validation_result.warnings %}
<li>{{ warning }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if validation_result.sample_data %}
<div class="sample-data">
<h4>
<i class="fas fa-eye"></i>
Sample Data Preview
</h4>
<div class="table-container">
<table class="preview-table">
<thead>
<tr>
{% for column in validation_result.columns %}
<th>{{ column }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in validation_result.sample_data[:3] %}
<tr>
{% for column in validation_result.columns %}
<td>{{ row.get(column, '') }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Import Results (if any) -->
{% if import_result %}
<div class="import-results {% if import_result.success %}success{% else %}error{% endif %}">
<div class="results-header">
<h2>
{% if import_result.success %}
<i class="fas fa-check-circle text-success"></i>
Import Completed
{% else %}
<i class="fas fa-exclamation-circle text-danger"></i>
Import Failed
{% endif %}
</h2>
</div>
<div class="results-body">
<div class="results-summary">
<div class="summary-item">
<div class="number">{{ import_result.total_records }}</div>
<div class="label">Total Records</div>
</div>
<div class="summary-item success">
<div class="number">{{ import_result.imported_records }}</div>
<div class="label">Successfully Imported</div>
</div>
{% if import_result.failed_records > 0 %}
<div class="summary-item error">
<div class="number">{{ import_result.failed_records }}</div>
<div class="label">Failed Records</div>
</div>
{% endif %}
</div>
{% if import_result.success %}
<div class="success-actions">
<p>
<strong>Batch ID:</strong> <code>{{ import_result.batch_id }}</code>
</p>
<div class="action-buttons">
<a href="{{ url_for('time_attendance_records', import_batch=import_result.batch_id) }}"
class="btn btn-primary">
<i class="fas fa-eye"></i>
View Imported Records
</a>
<a href="{{ url_for('time_attendance_dashboard') }}" class="btn btn-secondary">
<i class="fas fa-tachometer-alt"></i>
Back to Dashboard
</a>
</div>
</div>
{% endif %}
{% if import_result.errors %}
<div class="error-list">
<h4>
<i class="fas fa-exclamation-triangle"></i>
Import Errors
</h4>
<ul>
{% for error in import_result.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
<style>
.instructions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.instruction-item {
text-align: center;
padding: 1.5rem;
background: #f8fafc;
border-radius: 0.75rem;
border: 1px solid #e2e8f0;
}
.instruction-icon {
width: 60px;
height: 60px;
margin: 0 auto 1rem;
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 1.5rem;
}
.instruction-item h3 {
font-size: 1.125rem;
font-weight: 600;
color: #0f172a;
margin-bottom: 0.5rem;
}
.instruction-item p {
color: #64748b;
font-size: 0.875rem;
}
.sample-format {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
padding: 1.5rem;
margin-top: 2rem;
}
.sample-format h4 {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1rem;
font-weight: 600;
color: #0f172a;
margin-bottom: 1rem;
}
.format-table {
overflow-x: auto;
}
.format-table table {
width: 100%;
border-collapse: collapse;
background: #ffffff;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
.format-table th,
.format-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #e2e8f0;
font-size: 0.875rem;
}
.format-table th {
background: #f1f5f9;
font-weight: 600;
color: #374151;
}
.format-table tr:last-child td {
border-bottom: none;
}
.file-info {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 0.5rem;
margin-top: 1rem;
color: #166534;
}
.file-info i {
color: #22c55e;
}
.file-name {
font-weight: 600;
}
.file-size {
font-size: 0.875rem;
color: #15803d;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
padding-top: 1.5rem;
border-top: 1px solid #e2e8f0;
margin-top: 2rem;
}
.btn-loading {
display: none;
}
.preview-table {
width: 100%;
border-collapse: collapse;
background: #ffffff;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
.preview-table th,
.preview-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #e2e8f0;
font-size: 0.875rem;
}
.preview-table th {
background: #f8fafc;
font-weight: 600;
color: #374151;
}
.table-container {
overflow-x: auto;
margin-top: 1rem;
}
.text-success {
color: #10b981;
}
.text-danger {
color: #ef4444;
}
.warning-list {
background: #fffbeb;
border: 1px solid #fed7aa;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
}
.warning-list h4 {
color: #92400e;
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.75rem;
}
.warning-list ul {
list-style: none;
padding: 0;
margin: 0;
}
.warning-list li {
color: #d97706;
font-size: 0.875rem;
padding: 0.25rem 0;
border-bottom: 1px solid #fed7aa;
}
.warning-list li:last-child {
border-bottom: none;
}
.sample-data {
margin-top: 1.5rem;
}
.sample-data h4 {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1rem;
font-weight: 600;
color: #0f172a;
margin-bottom: 1rem;
}
.success-actions {
text-align: center;
padding: 1rem;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.success-actions p {
color: #166534;
margin-bottom: 1rem;
}
.success-actions code {
background: #dcfce7;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-family: monospace;
color: #15803d;
}
.action-buttons {
display: flex;
gap: 1rem;
justify-content: center;
}
@media (max-width: 768px) {
.instructions-grid {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.action-buttons {
flex-direction: column;
}
.format-table,
.table-container {
font-size: 0.75rem;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const fileUploadArea = document.getElementById('fileUploadArea');
const fileInput = document.getElementById('fileInput');
const fileInfo = document.getElementById('fileInfo');
const submitBtn = document.getElementById('submitBtn');
const importForm = document.getElementById('importForm');
// File upload handling
fileUploadArea.addEventListener('click', () => fileInput.click());
fileUploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
fileUploadArea.classList.add('dragover');
});
fileUploadArea.addEventListener('dragleave', () => {
fileUploadArea.classList.remove('dragover');
});
fileUploadArea.addEventListener('drop', (e) => {
e.preventDefault();
fileUploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
handleFileSelect();
}
});
fileInput.addEventListener('change', handleFileSelect);
function handleFileSelect() {
const file = fileInput.files[0];
if (file) {
const fileName = file.name;
const fileSize = formatFileSize(file.size);
fileInfo.querySelector('.file-name').textContent = fileName;
fileInfo.querySelector('.file-size').textContent = fileSize;
fileInfo.style.display = 'flex';
submitBtn.disabled = false;
// Validate file type
const validExtensions = ['.xlsx', '.xls'];
const fileExtension = fileName.toLowerCase().substring(fileName.lastIndexOf('.'));
if (!validExtensions.includes(fileExtension)) {
alert('Please select a valid Excel file (.xlsx or .xls)');
resetForm();
}
}
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Form submission handling
importForm.addEventListener('submit', function(e) {
const btnText = submitBtn.querySelector('.btn-text');
const btnLoading = submitBtn.querySelector('.btn-loading');
btnText.style.display = 'none';
btnLoading.style.display = 'inline-flex';
submitBtn.disabled = true;
});
// Reset form function
window.resetForm = function() {
fileInput.value = '';
fileInfo.style.display = 'none';
submitBtn.disabled = true;
fileUploadArea.classList.remove('dragover');
};
});
</script>
{% endblock %}
+597
View File
@@ -0,0 +1,597 @@
{% extends "base_authenticated.html" %}
{% block title %}Time Attendance Records - {{ COMPANY_NAME }}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/time_attendance.css') }}">
{% endblock %}
{% block page_title %}Time Attendance Records{% endblock %}
{% block content %}
<div class="time-attendance-page">
<!-- Page Header -->
<div class="time-attendance-header">
<div class="header-navigation">
<a href="{{ url_for('time_attendance_dashboard') }}" class="back-button">
<i class="fas fa-arrow-left"></i>
Back to Dashboard
</a>
</div>
<div class="header-content">
<h1>
<i class="fas fa-list"></i>
Time Attendance Records
</h1>
<p class="header-description">
View and manage imported time attendance data
</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 Data
</a>
{% endif %}
<button class="btn btn-secondary" onclick="exportRecords()">
<i class="fas fa-download"></i>
Export
</button>
</div>
</div>
<!-- Filters Section -->
<div class="filter-section">
<div class="filter-header">
<h3>
<i class="fas fa-filter"></i>
Filter Records
</h3>
<button class="filter-toggle" onclick="toggleFilters()">
<i class="fas fa-chevron-down" id="filterIcon"></i>
</button>
</div>
<div class="filter-body" id="filterBody">
<form method="GET" action="{{ url_for('time_attendance_records') }}" class="filter-form">
<div class="form-group">
<label for="employee_id" class="form-label">Employee</label>
<select id="employee_id" name="employee_id" class="form-select">
<option value="">All Employees</option>
{% for employee in employees %}
<option value="{{ employee.employee_id }}"
{% if current_filters.employee_id == employee.employee_id %}selected{% endif %}>
{{ employee.employee_name }} ({{ employee.employee_id }})
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="location_name" class="form-label">Location</label>
<select id="location_name" name="location_name" class="form-select">
<option value="">All Locations</option>
{% for location in locations %}
<option value="{{ location.location_name }}"
{% if current_filters.location_name == location.location_name %}selected{% endif %}>
{{ location.location_name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="start_date" class="form-label">Start Date</label>
<input type="date"
id="start_date"
name="start_date"
class="form-input"
value="{{ current_filters.start_date or '' }}">
</div>
<div class="form-group">
<label for="end_date" class="form-label">End Date</label>
<input type="date"
id="end_date"
name="end_date"
class="form-input"
value="{{ current_filters.end_date or '' }}">
</div>
<div class="form-group">
<label class="form-label">&nbsp;</label>
<div class="filter-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i>
Apply Filters
</button>
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-outline">
<i class="fas fa-times"></i>
Clear
</a>
</div>
</div>
</form>
</div>
</div>
<!-- Records Table -->
<div class="records-section">
<div class="records-header">
<h2>
<i class="fas fa-table"></i>
Attendance Records
</h2>
<div class="records-meta">
{% if records.items %}
Showing {{ records.per_page * (records.page - 1) + 1 }} -
{{ records.per_page * (records.page - 1) + records.items|length }}
of {{ records.total }} records
{% else %}
No records found
{% endif %}
</div>
</div>
{% if records.items %}
<div class="records-table-container">
<table class="records-table">
<thead>
<tr>
<th>Employee</th>
<th>Date & Time</th>
<th>Action</th>
<th>Location</th>
<th>Platform</th>
<th>Address</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for record in records.items %}
<tr>
<td>
<div class="employee-cell">
<div class="employee-avatar">
{{ record.employee_name[0].upper() if record.employee_name else record.employee_id[0].upper() }}
</div>
<div class="employee-info">
<div class="employee-name">{{ record.employee_name }}</div>
<div class="employee-id">ID: {{ record.employee_id }}</div>
</div>
</div>
</td>
<td class="datetime-cell">
<div class="datetime-info">
<div class="date">{{ record.attendance_date.strftime('%Y-%m-%d') }}</div>
<div class="time">{{ record.attendance_time.strftime('%H:%M:%S') }}</div>
</div>
</td>
<td>
<span class="action-badge {{ record.action_description.lower().replace(' ', '-') }}">
{% if record.action_description.lower() == 'check in' %}
<i class="fas fa-sign-in-alt"></i>
{% elif record.action_description.lower() == 'check out' %}
<i class="fas fa-sign-out-alt"></i>
{% else %}
<i class="fas fa-clock"></i>
{% endif %}
{{ record.action_description }}
</span>
</td>
<td class="location-cell">
<div class="location-info">
<i class="fas fa-map-marker-alt"></i>
{{ record.location_name }}
</div>
</td>
<td class="platform-cell">
{{ record.platform or 'Unknown' }}
</td>
<td class="address-cell">
{% if record.recorded_address %}
<span title="{{ record.recorded_address }}">
{{ record.recorded_address[:30] }}{% if record.recorded_address|length > 30 %}...{% endif %}
</span>
{% else %}
<span class="text-muted">No address</span>
{% endif %}
</td>
<td class="actions-cell">
<div class="action-buttons">
<a href="{{ url_for('time_attendance_record_detail', record_id=record.id) }}"
class="action-btn view">
<i class="fas fa-eye"></i>
View
</a>
{% if session.role == 'admin' %}
<button class="action-btn delete"
onclick="confirmDelete({{ record.id }}, '{{ record.employee_name }}', '{{ record.attendance_date }}')">
<i class="fas fa-trash"></i>
Delete
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if records.pages > 1 %}
<div class="pagination-section">
<div class="pagination-info">
Page {{ records.page }} of {{ records.pages }}
</div>
<div class="pagination-controls">
{% if records.has_prev %}
<a href="{{ url_for('time_attendance_records', page=records.prev_num, **current_filters) }}"
class="pagination-btn">
<i class="fas fa-chevron-left"></i>
Previous
</a>
{% else %}
<span class="pagination-btn disabled">
<i class="fas fa-chevron-left"></i>
Previous
</span>
{% endif %}
{% for page_num in records.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
{% if page_num %}
{% if page_num != records.page %}
<a href="{{ url_for('time_attendance_records', page=page_num, **current_filters) }}"
class="pagination-btn">{{ page_num }}</a>
{% else %}
<span class="pagination-btn active">{{ page_num }}</span>
{% endif %}
{% else %}
<span class="pagination-btn disabled"></span>
{% endif %}
{% endfor %}
{% if records.has_next %}
<a href="{{ url_for('time_attendance_records', page=records.next_num, **current_filters) }}"
class="pagination-btn">
Next
<i class="fas fa-chevron-right"></i>
</a>
{% else %}
<span class="pagination-btn disabled">
Next
<i class="fas fa-chevron-right"></i>
</span>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
<!-- Empty State -->
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-search"></i>
</div>
<h3>No Records Found</h3>
<p>
{% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %}
No attendance records match your current filter criteria.
Try adjusting your filters or clearing them to see all records.
{% else %}
No time attendance records have been imported yet.
Import your first Excel file to get started.
{% endif %}
</p>
{% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %}
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-primary">
<i class="fas fa-times"></i>
Clear Filters
</a>
{% elif session.role == 'admin' %}
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary">
<i class="fas fa-upload"></i>
Import Records
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="deleteModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>
<i class="fas fa-exclamation-triangle text-danger"></i>
Confirm Deletion
</h3>
<button class="modal-close" onclick="closeDeleteModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this attendance record?</p>
<div class="record-details">
<div><strong>Employee:</strong> <span id="deleteEmployeeName"></span></div>
<div><strong>Date:</strong> <span id="deleteDate"></span></div>
</div>
<p class="warning-text">
<i class="fas fa-exclamation-triangle"></i>
This action cannot be undone.
</p>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>
<form id="deleteForm" method="POST" style="display: inline;">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash"></i>
Delete Record
</button>
</form>
</div>
</div>
</div>
<style>
.datetime-info {
font-family: monospace;
}
.datetime-info .date {
font-weight: 600;
color: #0f172a;
font-size: 0.875rem;
}
.datetime-info .time {
color: #64748b;
font-size: 0.75rem;
}
.location-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.location-info i {
color: #64748b;
font-size: 0.75rem;
}
.address-cell {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.8125rem;
color: #64748b;
}
.text-muted {
color: #9ca3af;
font-style: italic;
}
.action-buttons {
display: flex;
gap: 0.5rem;
}
.filter-actions {
display: flex;
gap: 0.5rem;
}
/* Modal Styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: #ffffff;
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #e2e8f0;
}
.modal-header h3 {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: #0f172a;
}
.modal-close {
background: none;
border: none;
color: #64748b;
cursor: pointer;
font-size: 1.25rem;
padding: 0.5rem;
border-radius: 0.25rem;
transition: color 0.2s ease-in-out;
}
.modal-close:hover {
color: #0f172a;
}
.modal-body {
padding: 1.5rem;
}
.record-details {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
}
.record-details div {
margin-bottom: 0.5rem;
}
.record-details div:last-child {
margin-bottom: 0;
}
.warning-text {
color: #dc2626;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
margin-bottom: 0;
}
.modal-footer {
display: flex;
gap: 1rem;
justify-content: flex-end;
padding: 1.5rem;
border-top: 1px solid #e2e8f0;
}
@media (max-width: 768px) {
.records-table {
font-size: 0.75rem;
}
.records-table th,
.records-table td {
padding: 0.5rem 0.25rem;
}
.employee-cell {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.employee-avatar {
width: 24px;
height: 24px;
font-size: 0.625rem;
}
.action-buttons {
flex-direction: column;
gap: 0.25rem;
}
.action-btn {
padding: 0.25rem 0.5rem;
font-size: 0.625rem;
}
.filter-form {
grid-template-columns: 1fr;
}
.pagination-section {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.modal-content {
margin: 1rem;
width: calc(100% - 2rem);
}
.modal-footer {
flex-direction: column;
}
}
</style>
<script>
// Filter toggle functionality
function toggleFilters() {
const filterBody = document.getElementById('filterBody');
const filterIcon = document.getElementById('filterIcon');
if (filterBody.style.display === 'none' || filterBody.style.display === '') {
filterBody.style.display = 'block';
filterIcon.classList.remove('fa-chevron-down');
filterIcon.classList.add('fa-chevron-up');
} else {
filterBody.style.display = 'none';
filterIcon.classList.remove('fa-chevron-up');
filterIcon.classList.add('fa-chevron-down');
}
}
// Delete confirmation
function confirmDelete(recordId, employeeName, date) {
document.getElementById('deleteEmployeeName').textContent = employeeName;
document.getElementById('deleteDate').textContent = date;
document.getElementById('deleteForm').action = `{{ url_for('delete_time_attendance_record', record_id=0) }}`.replace('0', recordId);
document.getElementById('deleteModal').style.display = 'flex';
}
function closeDeleteModal() {
document.getElementById('deleteModal').style.display = 'none';
}
// Export functionality
function exportRecords() {
const params = new URLSearchParams(window.location.search);
params.set('export', 'csv');
window.location.href = `{{ url_for('time_attendance_records') }}?${params.toString()}`;
}
// Close modal when clicking outside
document.getElementById('deleteModal').addEventListener('click', function(e) {
if (e.target === this) {
closeDeleteModal();
}
});
// Escape key to close modal
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeDeleteModal();
}
});
// Initialize filters state
document.addEventListener('DOMContentLoaded', function() {
// Show filters if any are active
const hasActiveFilters = {{ 'true' if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date else 'false' }};
if (hasActiveFilters) {
toggleFilters();
}
});
</script>
{% endblock %}