Add import excel file functions

This commit is contained in:
Nguyen Ngo
2025-09-17 17:22:34 -04:00
parent bf9b301940
commit b9d40460f0
5 changed files with 333 additions and 3 deletions
+5
View File
@@ -74,6 +74,11 @@
<i class="fas fa-chart-line"></i>
<span class="menu-text">Reports</span>
</a>
<a href="{{ url_for('time_attendance_dashboard') }}"
class="menu-item {% if request.endpoint and (request.endpoint.startswith('time_attendance') or request.endpoint.startswith('import_time_attendance')) %}active{% endif %}">
<i class="fas fa-clock"></i>
<span class="menu-text">Time Attendance</span>
</a>
<a href="{{ url_for('payroll_dashboard') }}" class="menu-item">
<i class="fas fa-calculator"></i>
<span class="menu-text">Payroll</span>
@@ -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,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 %}