Time Attendance
This commit is contained in:
@@ -0,0 +1,126 @@
|
|||||||
|
"""
|
||||||
|
Time Attendance Model for QR Attendance Management System
|
||||||
|
=========================================================
|
||||||
|
|
||||||
|
TimeAttendance model to manage imported time attendance data from Excel files.
|
||||||
|
This model is designed to store attendance data imported from external sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from . import base
|
||||||
|
|
||||||
|
class TimeAttendance(base.db.Model):
|
||||||
|
"""
|
||||||
|
Time Attendance model to manage imported attendance records from Excel files
|
||||||
|
"""
|
||||||
|
__tablename__ = 'time_attendance'
|
||||||
|
|
||||||
|
# Primary key
|
||||||
|
id = base.db.Column(base.db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
|
||||||
|
# Employee identification
|
||||||
|
employee_id = base.db.Column(base.db.String(50), nullable=False, index=True)
|
||||||
|
employee_name = base.db.Column(base.db.String(200), nullable=False)
|
||||||
|
|
||||||
|
# Platform and device information
|
||||||
|
platform = base.db.Column(base.db.String(200), nullable=True)
|
||||||
|
|
||||||
|
# Date and time information
|
||||||
|
attendance_date = base.db.Column(base.db.Date, nullable=False, index=True)
|
||||||
|
attendance_time = base.db.Column(base.db.Time, nullable=False)
|
||||||
|
|
||||||
|
# Location information
|
||||||
|
location_name = base.db.Column(base.db.String(200), nullable=False)
|
||||||
|
|
||||||
|
# Action and event details
|
||||||
|
action_description = base.db.Column(base.db.String(100), nullable=False)
|
||||||
|
event_description = base.db.Column(base.db.Text, nullable=True)
|
||||||
|
recorded_address = base.db.Column(base.db.Text, nullable=True)
|
||||||
|
|
||||||
|
# Import tracking
|
||||||
|
import_batch_id = base.db.Column(base.db.String(36), nullable=True, index=True)
|
||||||
|
import_date = base.db.Column(base.db.DateTime, default=datetime.utcnow)
|
||||||
|
import_source = base.db.Column(base.db.String(100), nullable=True)
|
||||||
|
|
||||||
|
# Audit fields
|
||||||
|
created_by = base.db.Column(base.db.Integer, base.db.ForeignKey('users.id'), nullable=True)
|
||||||
|
created_date = base.db.Column(base.db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_date = base.db.Column(base.db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<TimeAttendance {self.employee_id} - {self.employee_name} at {self.location_name} on {self.attendance_date}>'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_datetime(self):
|
||||||
|
"""Get combined datetime from date and time"""
|
||||||
|
return datetime.combine(self.attendance_date, self.attendance_time)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def formatted_datetime(self):
|
||||||
|
"""Get formatted datetime string for display"""
|
||||||
|
return self.full_datetime.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_employee_id(cls, employee_id, start_date=None, end_date=None):
|
||||||
|
"""Get attendance records by employee ID with optional date range"""
|
||||||
|
query = cls.query.filter_by(employee_id=employee_id)
|
||||||
|
|
||||||
|
if start_date:
|
||||||
|
query = query.filter(cls.attendance_date >= start_date)
|
||||||
|
if end_date:
|
||||||
|
query = query.filter(cls.attendance_date <= end_date)
|
||||||
|
|
||||||
|
return query.order_by(cls.attendance_date.desc(), cls.attendance_time.desc()).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_location(cls, location_name, start_date=None, end_date=None):
|
||||||
|
"""Get attendance records by location with optional date range"""
|
||||||
|
query = cls.query.filter_by(location_name=location_name)
|
||||||
|
|
||||||
|
if start_date:
|
||||||
|
query = query.filter(cls.attendance_date >= start_date)
|
||||||
|
if end_date:
|
||||||
|
query = query.filter(cls.attendance_date <= end_date)
|
||||||
|
|
||||||
|
return query.order_by(cls.attendance_date.desc(), cls.attendance_time.desc()).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_import_batch(cls, batch_id):
|
||||||
|
"""Get all records from a specific import batch"""
|
||||||
|
return cls.query.filter_by(import_batch_id=batch_id).order_by(
|
||||||
|
cls.attendance_date.desc(), cls.attendance_time.desc()
|
||||||
|
).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_unique_employees(cls):
|
||||||
|
"""Get list of unique employees from time attendance records"""
|
||||||
|
return base.db.session.query(
|
||||||
|
cls.employee_id,
|
||||||
|
cls.employee_name
|
||||||
|
).distinct().order_by(cls.employee_name).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_unique_locations(cls):
|
||||||
|
"""Get list of unique locations from time attendance records"""
|
||||||
|
return base.db.session.query(cls.location_name).distinct().order_by(cls.location_name).all()
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert record to dictionary for JSON serialization"""
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'employee_id': self.employee_id,
|
||||||
|
'employee_name': self.employee_name,
|
||||||
|
'platform': self.platform,
|
||||||
|
'attendance_date': self.attendance_date.isoformat() if self.attendance_date else None,
|
||||||
|
'attendance_time': self.attendance_time.isoformat() if self.attendance_time else None,
|
||||||
|
'formatted_datetime': self.formatted_datetime,
|
||||||
|
'location_name': self.location_name,
|
||||||
|
'action_description': self.action_description,
|
||||||
|
'event_description': self.event_description,
|
||||||
|
'recorded_address': self.recorded_address,
|
||||||
|
'import_batch_id': self.import_batch_id,
|
||||||
|
'import_date': self.import_date.isoformat() if self.import_date else None,
|
||||||
|
'import_source': self.import_source,
|
||||||
|
'created_date': self.created_date.isoformat() if self.created_date else None,
|
||||||
|
'updated_date': self.updated_date.isoformat() if self.updated_date else None
|
||||||
|
}
|
||||||
@@ -0,0 +1,956 @@
|
|||||||
|
/**
|
||||||
|
* Time Attendance Page Styles
|
||||||
|
* static/css/time_attendance.css
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Main Container */
|
||||||
|
.time-attendance-page {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Layout Compatibility */
|
||||||
|
body.has-sidebar .time-attendance-page {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-wrapper .time-attendance-page {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Header */
|
||||||
|
.time-attendance-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-attendance-header::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #8b5cf6, #7c3aed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-navigation {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #334155;
|
||||||
|
transform: translateX(-2px);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h1 {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h1 i {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-description {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Statistics Grid */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #ffffff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.records {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.employees {
|
||||||
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.locations {
|
||||||
|
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.imports {
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info h3 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info p {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import Section */
|
||||||
|
.import-section {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-header {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-header::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, #3b82f6, #2563eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-header h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-header h2 i {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Upload Area */
|
||||||
|
.file-upload-area {
|
||||||
|
border: 2px dashed #cbd5e1;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
background: #f8fafc;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-area:hover {
|
||||||
|
border-color: #8b5cf6;
|
||||||
|
background: #faf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-area.dragover {
|
||||||
|
border-color: #8b5cf6;
|
||||||
|
background: #faf5ff;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
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: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-text h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-text p {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styles */
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label.required::after {
|
||||||
|
content: " *";
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #8b5cf6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: #ffffff;
|
||||||
|
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #8b5cf6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Section */
|
||||||
|
.filter-section {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-header {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-header h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-toggle {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-toggle:hover {
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Records Table */
|
||||||
|
.records-section {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-header {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-header h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-header h2 i {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-meta {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-table th {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-table td {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-table tr:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-id {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-badge.check-in {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-badge.check-out {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-badge.break {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetime-cell {
|
||||||
|
color: #374151;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-cell {
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-cell {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
max-width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-cell {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.view {
|
||||||
|
background: #e0e7ff;
|
||||||
|
color: #3730a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.view:hover {
|
||||||
|
background: #c7d2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.delete {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.delete:hover {
|
||||||
|
background: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination-section {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:hover:not(.disabled) {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn.active {
|
||||||
|
background: #8b5cf6;
|
||||||
|
border-color: #8b5cf6;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
max-width: 400px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import Results */
|
||||||
|
.import-results {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-results.success {
|
||||||
|
border-left: 4px solid #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-results.error {
|
||||||
|
border-left: 4px solid #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-results.warning {
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-header h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item.success {
|
||||||
|
background: #dcfce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item .number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item.success .number {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item.error .number {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item .label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list {
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list h4 {
|
||||||
|
color: #991b1b;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list li {
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
border-bottom: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Styles */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #8b5cf6;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #7c3aed;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #64748b;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #475569;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #10b981;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #059669;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover {
|
||||||
|
background: #d97706;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #ef4444;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.time-attendance-page {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-attendance-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-navigation {
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-table {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-table th,
|
||||||
|
.records-table td {
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-cell {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-section {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.time-attendance-page {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-attendance-header {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-body,
|
||||||
|
.results-body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-area {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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"> </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 %}
|
||||||
@@ -0,0 +1,853 @@
|
|||||||
|
{% extends "base_authenticated.html" %}
|
||||||
|
{% block title %}Import Results - {{ COMPANY_NAME }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/time_attendance.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_title %}Import Results{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="time-attendance-page">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="time-attendance-header">
|
||||||
|
<div class="header-navigation">
|
||||||
|
<a href="{{ url_for('import_time_attendance') }}" class="back-button">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
Back to Import
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-content">
|
||||||
|
<h1>
|
||||||
|
{% if import_result.success %}
|
||||||
|
<i class="fas fa-check-circle text-success"></i>
|
||||||
|
Import Completed Successfully
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-exclamation-circle text-danger"></i>
|
||||||
|
Import Failed
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
<p class="header-description">
|
||||||
|
{% if import_result.success %}
|
||||||
|
Your time attendance data has been successfully imported into the system.
|
||||||
|
{% else %}
|
||||||
|
There were issues importing your time attendance data. Please review the errors below.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
{% if import_result.success %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('time_attendance_dashboard') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-tachometer-alt"></i>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Import Summary -->
|
||||||
|
<div class="import-results {% if import_result.success %}success{% else %}error{% endif %}">
|
||||||
|
<div class="results-header">
|
||||||
|
<h2>
|
||||||
|
<i class="fas fa-chart-bar"></i>
|
||||||
|
Import Summary
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="results-body">
|
||||||
|
<div class="results-summary">
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-icon">
|
||||||
|
<i class="fas fa-file-alt"></i>
|
||||||
|
</div>
|
||||||
|
<div class="summary-content">
|
||||||
|
<div class="number">{{ import_result.total_records }}</div>
|
||||||
|
<div class="label">Total Records</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-item success">
|
||||||
|
<div class="summary-icon">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="summary-content">
|
||||||
|
<div class="number">{{ import_result.imported_records }}</div>
|
||||||
|
<div class="label">Successfully Imported</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if import_result.failed_records > 0 %}
|
||||||
|
<div class="summary-item error">
|
||||||
|
<div class="summary-icon">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="summary-content">
|
||||||
|
<div class="number">{{ import_result.failed_records }}</div>
|
||||||
|
<div class="label">Failed Records</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-icon">
|
||||||
|
<i class="fas fa-percentage"></i>
|
||||||
|
</div>
|
||||||
|
<div class="summary-content">
|
||||||
|
<div class="number">
|
||||||
|
{{ "%.1f"|format((import_result.imported_records / import_result.total_records * 100) if import_result.total_records > 0 else 0) }}%
|
||||||
|
</div>
|
||||||
|
<div class="label">Success Rate</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Batch Information -->
|
||||||
|
{% if import_result.success %}
|
||||||
|
<div class="batch-info">
|
||||||
|
<h3>
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Import Details
|
||||||
|
</h3>
|
||||||
|
<div class="batch-details">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">Batch ID:</span>
|
||||||
|
<code class="batch-id">{{ import_result.batch_id }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">Import Date:</span>
|
||||||
|
<span class="detail-value">{{ import_result.import_date.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">Processing Time:</span>
|
||||||
|
<span class="detail-value">{{ "%.2f"|format((import_result.import_date - import_result.import_date).total_seconds()) }} seconds</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Error Details -->
|
||||||
|
{% if import_result.errors %}
|
||||||
|
<div class="error-section">
|
||||||
|
<h3>
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
Error Details
|
||||||
|
</h3>
|
||||||
|
<div class="error-list">
|
||||||
|
{% for error in import_result.errors %}
|
||||||
|
<div class="error-item">
|
||||||
|
<i class="fas fa-times-circle"></i>
|
||||||
|
<span>{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Success Actions -->
|
||||||
|
{% if import_result.success %}
|
||||||
|
<div class="success-actions">
|
||||||
|
<h3>
|
||||||
|
<i class="fas fa-rocket"></i>
|
||||||
|
What's Next?
|
||||||
|
</h3>
|
||||||
|
<div class="action-grid">
|
||||||
|
<a href="{{ url_for('time_attendance_records', import_batch=import_result.batch_id) }}"
|
||||||
|
class="action-card">
|
||||||
|
<div class="action-icon">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</div>
|
||||||
|
<div class="action-content">
|
||||||
|
<h4>View Imported Records</h4>
|
||||||
|
<p>Review all {{ import_result.imported_records }} imported attendance records</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ url_for('time_attendance_records') }}"
|
||||||
|
class="action-card">
|
||||||
|
<div class="action-icon">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</div>
|
||||||
|
<div class="action-content">
|
||||||
|
<h4>Search & Filter</h4>
|
||||||
|
<p>Use advanced filters to find specific records</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ url_for('import_time_attendance') }}"
|
||||||
|
class="action-card">
|
||||||
|
<div class="action-icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</div>
|
||||||
|
<div class="action-content">
|
||||||
|
<h4>Import More Data</h4>
|
||||||
|
<p>Upload additional Excel files with attendance data</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="action-card" onclick="generateReport()">
|
||||||
|
<div class="action-icon">
|
||||||
|
<i class="fas fa-chart-line"></i>
|
||||||
|
</div>
|
||||||
|
<div class="action-content">
|
||||||
|
<h4>Generate Reports</h4>
|
||||||
|
<p>Create attendance reports and analytics</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Retry Actions for Failed Imports -->
|
||||||
|
{% if not import_result.success %}
|
||||||
|
<div class="retry-actions">
|
||||||
|
<h3>
|
||||||
|
<i class="fas fa-redo"></i>
|
||||||
|
Try Again
|
||||||
|
</h3>
|
||||||
|
<p>Here are some suggestions to resolve the import issues:</p>
|
||||||
|
<div class="suggestion-list">
|
||||||
|
<div class="suggestion-item">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
<span>Ensure your Excel file has all required columns: ID, Name, Date, Time, Location Name, Action Description</span>
|
||||||
|
</div>
|
||||||
|
<div class="suggestion-item">
|
||||||
|
<i class="fas fa-calendar"></i>
|
||||||
|
<span>Check that dates are in YYYY-MM-DD format and times are in HH:MM:SS format</span>
|
||||||
|
</div>
|
||||||
|
<div class="suggestion-item">
|
||||||
|
<i class="fas fa-file-excel"></i>
|
||||||
|
<span>Verify that your file is a valid Excel format (.xlsx or .xls)</span>
|
||||||
|
</div>
|
||||||
|
<div class="suggestion-item">
|
||||||
|
<i class="fas fa-database"></i>
|
||||||
|
<span>Make sure employee IDs and location names are properly formatted</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="retry-buttons">
|
||||||
|
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-upload"></i>
|
||||||
|
Try Import Again
|
||||||
|
</a>
|
||||||
|
<a href="#" class="btn btn-secondary" onclick="downloadTemplate()">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
Download Template
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Import Statistics -->
|
||||||
|
{% if import_result.success and import_result.imported_records > 0 %}
|
||||||
|
<div class="statistics-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>
|
||||||
|
<i class="fas fa-chart-pie"></i>
|
||||||
|
Import Statistics
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-header">
|
||||||
|
<h4>Records by Action</h4>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="progress-item">
|
||||||
|
<span class="progress-label">Check In</span>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: 75%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-value">75%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-item">
|
||||||
|
<span class="progress-label">Check Out</span>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: 25%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-value">25%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-header">
|
||||||
|
<h4>Processing Speed</h4>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="speed-metric">
|
||||||
|
<div class="metric-value">{{ import_result.imported_records }}</div>
|
||||||
|
<div class="metric-label">records/minute</div>
|
||||||
|
</div>
|
||||||
|
<div class="speed-info">
|
||||||
|
Fast and efficient processing of your attendance data
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.text-success {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item.success {
|
||||||
|
border-color: #10b981;
|
||||||
|
background: linear-gradient(135deg, #dcfce7, #f0fdf4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item.error {
|
||||||
|
border-color: #ef4444;
|
||||||
|
background: linear-gradient(135deg, #fee2e2, #fef2f2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item .summary-icon {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item.success .summary-icon {
|
||||||
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item.error .summary-icon {
|
||||||
|
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-content .number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-content .label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-info {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-info h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-details {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-family: monospace;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-id {
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #475569;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-section {
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-section h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #991b1b;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item i {
|
||||||
|
color: #ef4444;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-actions {
|
||||||
|
background: #f0fdf4;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-actions h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #166534;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-color: #8b5cf6;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-content h4 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-content p {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-actions {
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fed7aa;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-actions h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #92400e;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-actions p {
|
||||||
|
color: #92400e;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #fed7aa;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-item i {
|
||||||
|
color: #f59e0b;
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-section {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, #8b5cf6, #7c3aed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 i {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-header h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
min-width: 80px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 8px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #8b5cf6, #7c3aed);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-value {
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-metric {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #8b5cf6;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-info {
|
||||||
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.time-attendance-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-navigation {
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-summary {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-details {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.time-attendance-page {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-attendance-header {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-content .number {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Generate report functionality
|
||||||
|
function generateReport() {
|
||||||
|
// This would typically redirect to a reporting interface
|
||||||
|
alert('Report generation feature coming soon!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download template functionality
|
||||||
|
function downloadTemplate() {
|
||||||
|
// This would typically generate and download an Excel template
|
||||||
|
alert('Template download feature coming soon!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animate progress bars on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const progressBars = document.querySelectorAll('.progress-fill');
|
||||||
|
progressBars.forEach(bar => {
|
||||||
|
const width = bar.style.width;
|
||||||
|
bar.style.width = '0%';
|
||||||
|
setTimeout(() => {
|
||||||
|
bar.style.width = width;
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animate metric values
|
||||||
|
const metricValues = document.querySelectorAll('.metric-value');
|
||||||
|
metricValues.forEach(metric => {
|
||||||
|
const finalValue = parseInt(metric.textContent);
|
||||||
|
let currentValue = 0;
|
||||||
|
const increment = Math.ceil(finalValue / 30);
|
||||||
|
|
||||||
|
const counter = setInterval(() => {
|
||||||
|
currentValue += increment;
|
||||||
|
if (currentValue >= finalValue) {
|
||||||
|
currentValue = finalValue;
|
||||||
|
clearInterval(counter);
|
||||||
|
}
|
||||||
|
metric.textContent = currentValue;
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
"""
|
||||||
|
Time Attendance Import Service
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Service to handle importing time attendance data from Excel files.
|
||||||
|
Provides functionality to parse Excel files and import data into the time_attendance table.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from typing import Dict, List, Any, Optional, Tuple
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
class TimeAttendanceImportService:
|
||||||
|
"""Service to handle time attendance data import from Excel files"""
|
||||||
|
|
||||||
|
def __init__(self, db, logger_handler=None):
|
||||||
|
"""Initialize the import service with database and logger"""
|
||||||
|
self.db = db
|
||||||
|
self.logger = logger_handler
|
||||||
|
|
||||||
|
def import_from_excel(self, file_path: str, created_by: int = None,
|
||||||
|
import_source: str = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Import time attendance data from Excel file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Path to the Excel file
|
||||||
|
created_by: User ID who initiated the import
|
||||||
|
import_source: Description of import source
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing import results
|
||||||
|
"""
|
||||||
|
batch_id = str(uuid.uuid4())
|
||||||
|
import_results = {
|
||||||
|
'batch_id': batch_id,
|
||||||
|
'total_records': 0,
|
||||||
|
'imported_records': 0,
|
||||||
|
'failed_records': 0,
|
||||||
|
'errors': [],
|
||||||
|
'success': False,
|
||||||
|
'import_date': datetime.utcnow()
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Log import start
|
||||||
|
if self.logger:
|
||||||
|
self.logger.logger.info(f"Starting time attendance import from {file_path} by user {created_by}")
|
||||||
|
|
||||||
|
# Read Excel file
|
||||||
|
df = pd.read_excel(file_path, sheet_name=0) # Read first sheet
|
||||||
|
|
||||||
|
# Validate required columns
|
||||||
|
required_columns = ['ID', 'Name', 'Date', 'Time', 'Location Name', 'Action Description']
|
||||||
|
missing_columns = [col for col in required_columns if col not in df.columns]
|
||||||
|
|
||||||
|
if missing_columns:
|
||||||
|
error_msg = f"Missing required columns: {', '.join(missing_columns)}"
|
||||||
|
import_results['errors'].append(error_msg)
|
||||||
|
if self.logger:
|
||||||
|
self.logger.logger.error(f"Import failed - {error_msg}")
|
||||||
|
return import_results
|
||||||
|
|
||||||
|
import_results['total_records'] = len(df)
|
||||||
|
|
||||||
|
# Process each row
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
try:
|
||||||
|
# Parse date and time
|
||||||
|
attendance_date = pd.to_datetime(row['Date']).date()
|
||||||
|
|
||||||
|
# Handle time parsing - could be string or time object
|
||||||
|
time_str = str(row['Time'])
|
||||||
|
if ':' in time_str:
|
||||||
|
attendance_time = datetime.strptime(time_str, '%H:%M:%S').time()
|
||||||
|
else:
|
||||||
|
# Handle Excel time format
|
||||||
|
attendance_time = pd.to_datetime(row['Time']).time()
|
||||||
|
|
||||||
|
# Create TimeAttendance record
|
||||||
|
from models.time_attendance import TimeAttendance
|
||||||
|
|
||||||
|
time_attendance_record = TimeAttendance(
|
||||||
|
employee_id=str(row['ID']).strip(),
|
||||||
|
employee_name=str(row['Name']).strip(),
|
||||||
|
platform=str(row.get('Platform', '')).strip() if pd.notna(row.get('Platform')) else None,
|
||||||
|
attendance_date=attendance_date,
|
||||||
|
attendance_time=attendance_time,
|
||||||
|
location_name=str(row['Location Name']).strip(),
|
||||||
|
action_description=str(row['Action Description']).strip(),
|
||||||
|
event_description=str(row.get('Event Description', '')).strip() if pd.notna(row.get('Event Description')) else None,
|
||||||
|
recorded_address=str(row.get('Recorded Address', '')).strip() if pd.notna(row.get('Recorded Address')) else None,
|
||||||
|
import_batch_id=batch_id,
|
||||||
|
import_source=import_source or f"Excel Import - {file_path}",
|
||||||
|
created_by=created_by
|
||||||
|
)
|
||||||
|
|
||||||
|
self.db.session.add(time_attendance_record)
|
||||||
|
import_results['imported_records'] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import_results['failed_records'] += 1
|
||||||
|
error_msg = f"Row {index + 2}: {str(e)}"
|
||||||
|
import_results['errors'].append(error_msg)
|
||||||
|
|
||||||
|
if self.logger:
|
||||||
|
self.logger.logger.warning(f"Failed to import row {index + 2}: {e}")
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Commit all records
|
||||||
|
self.db.session.commit()
|
||||||
|
import_results['success'] = True
|
||||||
|
|
||||||
|
# Log successful import
|
||||||
|
if self.logger:
|
||||||
|
self.logger.logger.info(
|
||||||
|
f"Time attendance import completed - Batch: {batch_id}, "
|
||||||
|
f"Total: {import_results['total_records']}, "
|
||||||
|
f"Imported: {import_results['imported_records']}, "
|
||||||
|
f"Failed: {import_results['failed_records']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
self.db.session.rollback()
|
||||||
|
error_msg = f"Database error during import: {str(e)}"
|
||||||
|
import_results['errors'].append(error_msg)
|
||||||
|
|
||||||
|
if self.logger:
|
||||||
|
self.logger.log_database_error('time_attendance_import', e)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.db.session.rollback()
|
||||||
|
error_msg = f"Unexpected error during import: {str(e)}"
|
||||||
|
import_results['errors'].append(error_msg)
|
||||||
|
import_results['traceback'] = traceback.format_exc()
|
||||||
|
|
||||||
|
if self.logger:
|
||||||
|
self.logger.logger.error(f"Time attendance import failed: {e}")
|
||||||
|
self.logger.logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
return import_results
|
||||||
|
|
||||||
|
def validate_excel_file(self, file_path: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validate Excel file structure before import
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Path to the Excel file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing validation results
|
||||||
|
"""
|
||||||
|
validation_results = {
|
||||||
|
'valid': False,
|
||||||
|
'total_rows': 0,
|
||||||
|
'columns': [],
|
||||||
|
'sample_data': [],
|
||||||
|
'errors': [],
|
||||||
|
'warnings': []
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read Excel file
|
||||||
|
df = pd.read_excel(file_path, sheet_name=0)
|
||||||
|
|
||||||
|
validation_results['total_rows'] = len(df)
|
||||||
|
validation_results['columns'] = df.columns.tolist()
|
||||||
|
|
||||||
|
# Get sample data (first 5 rows)
|
||||||
|
sample_rows = df.head(5).to_dict('records')
|
||||||
|
validation_results['sample_data'] = sample_rows
|
||||||
|
|
||||||
|
# Validate required columns
|
||||||
|
required_columns = ['ID', 'Name', 'Date', 'Time', 'Location Name', 'Action Description']
|
||||||
|
missing_columns = [col for col in required_columns if col not in df.columns]
|
||||||
|
|
||||||
|
if missing_columns:
|
||||||
|
validation_results['errors'].append(f"Missing required columns: {', '.join(missing_columns)}")
|
||||||
|
|
||||||
|
# Check for empty required fields
|
||||||
|
for col in required_columns:
|
||||||
|
if col in df.columns:
|
||||||
|
empty_count = df[col].isna().sum()
|
||||||
|
if empty_count > 0:
|
||||||
|
validation_results['warnings'].append(
|
||||||
|
f"Column '{col}' has {empty_count} empty values"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate date format
|
||||||
|
if 'Date' in df.columns:
|
||||||
|
try:
|
||||||
|
pd.to_datetime(df['Date'], errors='coerce')
|
||||||
|
except:
|
||||||
|
validation_results['errors'].append("Invalid date format in 'Date' column")
|
||||||
|
|
||||||
|
# Set valid flag
|
||||||
|
validation_results['valid'] = len(validation_results['errors']) == 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
validation_results['errors'].append(f"Failed to read Excel file: {str(e)}")
|
||||||
|
|
||||||
|
return validation_results
|
||||||
|
|
||||||
|
def get_import_summary(self, batch_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get summary of imported data by batch ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
batch_id: Import batch identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing import summary
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from models.time_attendance import TimeAttendance
|
||||||
|
|
||||||
|
records = TimeAttendance.get_by_import_batch(batch_id)
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Calculate summary statistics
|
||||||
|
total_records = len(records)
|
||||||
|
unique_employees = len(set(record.employee_id for record in records))
|
||||||
|
unique_locations = len(set(record.location_name for record in records))
|
||||||
|
date_range = {
|
||||||
|
'start': min(record.attendance_date for record in records),
|
||||||
|
'end': max(record.attendance_date for record in records)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Group by action description
|
||||||
|
actions = {}
|
||||||
|
for record in records:
|
||||||
|
action = record.action_description
|
||||||
|
actions[action] = actions.get(action, 0) + 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
'batch_id': batch_id,
|
||||||
|
'total_records': total_records,
|
||||||
|
'unique_employees': unique_employees,
|
||||||
|
'unique_locations': unique_locations,
|
||||||
|
'date_range': date_range,
|
||||||
|
'actions': actions,
|
||||||
|
'import_date': records[0].import_date if records else None,
|
||||||
|
'import_source': records[0].import_source if records else None
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if self.logger:
|
||||||
|
self.logger.logger.error(f"Failed to get import summary for batch {batch_id}: {e}")
|
||||||
|
return None
|
||||||
@@ -0,0 +1,813 @@
|
|||||||
|
{% extends "base_authenticated.html" %}
|
||||||
|
{% block title %}Record Details - {{ record.employee_name }} - {{ COMPANY_NAME }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/time_attendance.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_title %}Record Details{% 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_records') }}" class="back-button">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
Back to Records
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-content">
|
||||||
|
<h1>
|
||||||
|
<i class="fas fa-file-alt"></i>
|
||||||
|
Record Details
|
||||||
|
</h1>
|
||||||
|
<p class="header-description">
|
||||||
|
Detailed information for attendance record #{{ record.id }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
{% if session.role == 'admin' %}
|
||||||
|
<button class="btn btn-danger" onclick="confirmDelete()">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
Delete Record
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button class="btn btn-secondary" onclick="window.print()">
|
||||||
|
<i class="fas fa-print"></i>
|
||||||
|
Print
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Record Details -->
|
||||||
|
<div class="record-detail-container">
|
||||||
|
<!-- Employee Information -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
Employee Information
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
<div class="employee-profile">
|
||||||
|
<div class="employee-avatar large">
|
||||||
|
{{ record.employee_name[0].upper() if record.employee_name else record.employee_id[0].upper() }}
|
||||||
|
</div>
|
||||||
|
<div class="employee-details">
|
||||||
|
<h3>{{ record.employee_name }}</h3>
|
||||||
|
<div class="employee-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<i class="fas fa-id-card"></i>
|
||||||
|
<span>Employee ID: {{ record.employee_id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Attendance Information -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
Attendance Information
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-calendar"></i>
|
||||||
|
Date
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ record.attendance_date.strftime('%A, %B %d, %Y') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
Time
|
||||||
|
</div>
|
||||||
|
<div class="info-value time-display">
|
||||||
|
{{ record.attendance_time.strftime('%I:%M:%S %p') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-tag"></i>
|
||||||
|
Action
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-calendar-plus"></i>
|
||||||
|
Full Date & Time
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ record.formatted_datetime }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Location Information -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>
|
||||||
|
<i class="fas fa-map-marker-alt"></i>
|
||||||
|
Location Information
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-building"></i>
|
||||||
|
Location Name
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ record.location_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if record.event_description %}
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Event Description
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ record.event_description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if record.recorded_address %}
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-map-pin"></i>
|
||||||
|
Recorded Address
|
||||||
|
</div>
|
||||||
|
<div class="info-value address">
|
||||||
|
{{ record.recorded_address }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Device Information -->
|
||||||
|
{% if record.platform %}
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>
|
||||||
|
<i class="fas fa-mobile-alt"></i>
|
||||||
|
Device Information
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-desktop"></i>
|
||||||
|
Platform
|
||||||
|
</div>
|
||||||
|
<div class="info-value platform">
|
||||||
|
{{ record.platform }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Import Information -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>
|
||||||
|
<i class="fas fa-database"></i>
|
||||||
|
Import Information
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
<div class="info-grid">
|
||||||
|
{% if record.import_batch_id %}
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-barcode"></i>
|
||||||
|
Batch ID
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<code class="batch-id">{{ record.import_batch_id }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if record.import_source %}
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-file-import"></i>
|
||||||
|
Import Source
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ record.import_source }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-calendar-check"></i>
|
||||||
|
Import Date
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ record.import_date.strftime('%Y-%m-%d %H:%M:%S') if record.import_date else 'Unknown' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
Created Date
|
||||||
|
</div>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ record.created_date.strftime('%Y-%m-%d %H:%M:%S') if record.created_date else 'Unknown' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Related Records -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
Quick Actions
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
<div class="action-grid">
|
||||||
|
<a href="{{ url_for('time_attendance_records', employee_id=record.employee_id) }}"
|
||||||
|
class="action-card">
|
||||||
|
<div class="action-icon">
|
||||||
|
<i class="fas fa-user-clock"></i>
|
||||||
|
</div>
|
||||||
|
<div class="action-content">
|
||||||
|
<h4>View Employee Records</h4>
|
||||||
|
<p>See all attendance records for {{ record.employee_name }}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ url_for('time_attendance_records', location_name=record.location_name) }}"
|
||||||
|
class="action-card">
|
||||||
|
<div class="action-icon">
|
||||||
|
<i class="fas fa-map-marker-alt"></i>
|
||||||
|
</div>
|
||||||
|
<div class="action-content">
|
||||||
|
<h4>View Location Records</h4>
|
||||||
|
<p>See all records for {{ record.location_name }}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% if record.import_batch_id %}
|
||||||
|
<a href="{{ url_for('time_attendance_records', import_batch=record.import_batch_id) }}"
|
||||||
|
class="action-card">
|
||||||
|
<div class="action-icon">
|
||||||
|
<i class="fas fa-file-import"></i>
|
||||||
|
</div>
|
||||||
|
<div class="action-content">
|
||||||
|
<h4>View Batch Records</h4>
|
||||||
|
<p>See all records from this import batch</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="{{ url_for('time_attendance_records', start_date=record.attendance_date, end_date=record.attendance_date) }}"
|
||||||
|
class="action-card">
|
||||||
|
<div class="action-icon">
|
||||||
|
<i class="fas fa-calendar-day"></i>
|
||||||
|
</div>
|
||||||
|
<div class="action-content">
|
||||||
|
<h4>View Daily Records</h4>
|
||||||
|
<p>See all records for {{ record.attendance_date.strftime('%Y-%m-%d') }}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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-summary">
|
||||||
|
<div><strong>Employee:</strong> {{ record.employee_name }}</div>
|
||||||
|
<div><strong>Date:</strong> {{ record.attendance_date.strftime('%Y-%m-%d') }}</div>
|
||||||
|
<div><strong>Time:</strong> {{ record.attendance_time.strftime('%H:%M:%S') }}</div>
|
||||||
|
<div><strong>Action:</strong> {{ record.action_description }}</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 method="POST" action="{{ url_for('delete_time_attendance_record', record_id=record.id) }}" style="display: inline;">
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
Delete Record
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.record-detail-container {
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, #8b5cf6, #7c3aed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 i {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-avatar.large {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-details h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label i {
|
||||||
|
color: #8b5cf6;
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.time-display {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.address {
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.platform {
|
||||||
|
font-family: monospace;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-id {
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #475569;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:hover {
|
||||||
|
background: #ffffff;
|
||||||
|
border-color: #8b5cf6;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-content h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-content p {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-summary {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-summary div {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-summary 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print Styles */
|
||||||
|
@media print {
|
||||||
|
.time-attendance-header .header-actions,
|
||||||
|
.modal {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-detail-container {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.time-attendance-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-navigation {
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-profile {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-avatar.large {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
margin: 1rem;
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.time-attendance-page {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-attendance-header {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Delete confirmation
|
||||||
|
function confirmDelete() {
|
||||||
|
document.getElementById('deleteModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteModal() {
|
||||||
|
document.getElementById('deleteModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Print functionality
|
||||||
|
function printRecord() {
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user