Files
GOV_QR_Codes_Management/templates/time_attendance_import.html
T

787 lines
22 KiB
HTML

{% 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') }}">
<style>
.form-select:hover {
border-color: #8b5cf6;
}
.form-select:focus {
outline: none;
border-color: #8b5cf6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
}
.validation-preview {
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 1.5rem;
margin-top: 1.5rem;
}
.validation-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.validation-stat {
background: white;
padding: 1rem;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.validation-stat .number {
font-size: 1.75rem;
font-weight: 700;
color: #2d3748;
}
.validation-stat .label {
font-size: 0.875rem;
color: #718096;
margin-top: 0.25rem;
}
.validation-stat.success .number { color: #48bb78; }
.validation-stat.warning .number { color: #ed8936; }
.validation-stat.error .number { color: #f56565; }
.import-options {
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.5rem;
margin-top: 1.5rem;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 1rem;
}
.checkbox-item {
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.checkbox-item input[type="checkbox"] {
margin-top: 0.25rem;
width: 18px;
height: 18px;
}
.checkbox-label {
flex: 1;
}
.checkbox-label strong {
display: block;
color: #2d3748;
margin-bottom: 0.25rem;
}
.checkbox-label span {
color: #718096;
font-size: 0.875rem;
}
.progress-indicator {
display: none;
margin-top: 1rem;
padding: 1rem;
background: #ebf8ff;
border: 1px solid #90cdf4;
border-radius: 8px;
text-align: center;
}
.progress-indicator.active {
display: block;
}
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #e2e8f0;
border-top-color: #4299e1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Fixed styles for instructions section */
.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 {
font-size: 1.125rem;
font-weight: 600;
color: #0f172a;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.format-table {
overflow-x: auto;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.format-table table {
width: 100%;
border-collapse: collapse;
background: white;
font-size: 0.875rem;
}
.format-table thead {
background: linear-gradient(135deg, #4299e1, #3182ce);
color: white;
}
.format-table th {
padding: 0.75rem 1rem;
text-align: left;
font-weight: 600;
border-bottom: 2px solid #2b6cb0;
white-space: nowrap;
}
.format-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #e2e8f0;
color: #4a5568;
}
.format-table tbody tr:last-child td {
border-bottom: none;
}
.format-table tbody tr:hover {
background: #f7fafc;
}
</style>
{% 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 with enhanced validation
</p>
</div>
<div class="header-actions">
<a href="{{ url_for('download_import_template') }}" class="btn btn-secondary">
<i class="fas fa-download"></i>
Download Template
</a>
</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, 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>Smart Validation</h3>
<p>Automatic duplicate detection and data validation</p>
</div>
<div class="instruction-item">
<div class="instruction-icon">
<i class="fas fa-copy"></i>
</div>
<h3>Duplicate Handling</h3>
<p>Automatically skip duplicate records during import</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>
<th>Distance</th>
</tr>
</thead>
<tbody>
<tr>
<td>12345</td>
<td>John Doe</td>
<td>iPhone - iOS</td>
<td>2025-10-06</td>
<td>09:00:00</td>
<td>HQ Suite 210</td>
<td>Check In</td>
<td>Main Office</td>
<td>123 Main St</td>
<td>0.125</td>
</tr>
<tr>
<td>67890</td>
<td>Jane Smith</td>
<td>Android</td>
<td>2025-10-06</td>
<td>17:30:00</td>
<td>HQ Suite 210</td>
<td>Check Out</td>
<td>Main Office</td>
<td>123 Main St</td>
<td>0.125</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="import-options">
<h3>
<i class="fas fa-cog"></i>
Import Options
</h3>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="analyze_duplicates" name="analyze_duplicates" value="true" checked>
<label for="analyze_duplicates" class="checkbox-label">
<strong>Review Duplicates Before Import</strong>
<span>Show potential duplicate records for review and allow selective import</span>
</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="analyze_invalid" name="analyze_invalid" value="true" checked>
<label for="analyze_invalid" class="checkbox-label">
<strong>Review Invalid Rows Before Import</strong>
<span>Show all invalid rows with error details for review before importing valid records</span>
</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="skip_duplicates" name="skip_duplicates" value="true">
<label for="skip_duplicates" class="checkbox-label">
<strong>Skip Duplicate Records</strong>
<span>Automatically detect and skip records that already exist in the system</span>
</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="validate_only" name="validate_only" value="true">
<label for="validate_only" class="checkbox-label">
<strong>Validate Only (Don't Import)</strong>
<span>Check file for errors without actually importing the data</span>
</label>
</div>
</div>
</div>
<!-- Project Selection - REQUIRED -->
<div class="form-group" style="margin-bottom: 1.5rem;">
<h3 style="margin-bottom: 0.75rem;">
<i class="fas fa-project-diagram"></i>
Select Project <span style="color: #ef4444;">*</span>
</h3>
<select name="project_id" id="project_id" class="form-select" required style="
width: 100%;
padding: 0.75rem;
border: 2px solid #e2e8f0;
border-radius: 0.5rem;
font-size: 0.9375rem;
background-color: #ffffff;
transition: all 0.2s ease;
">
<option value="">-- Select Project (Required) --</option>
{% for project in projects %}
<option value="{{ project.id }}">{{ project.name }}</option>
{% endfor %}
</select>
<p style="
margin-top: 0.5rem;
font-size: 0.875rem;
color: #64748b;
">
<i class="fas fa-info-circle"></i>
<strong>Required:</strong> Select which project this import data belongs to for filtering and reporting
</p>
</div>
<!-- Import Source -->
<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 - October 2025"
value="Excel Import - {{ current_date }}">
<small class="form-help">Optional description for this import batch</small>
</div>
</div>
<!-- Progress Indicator -->
<div id="progressIndicator" class="progress-indicator">
<div class="spinner"></div>
<p style="margin-top: 0.5rem;">Processing your file, please wait...</p>
</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>
</button>
</div>
</form>
</div>
</div>
<!-- Validation Results -->
{% 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="validation-stats">
<div class="validation-stat">
<div class="number">{{ validation_result.total_rows }}</div>
<div class="label">Total Rows</div>
</div>
<div class="validation-stat success">
<div class="number">{{ validation_result.valid_rows }}</div>
<div class="label">Valid Rows</div>
</div>
{% if validation_result.invalid_rows > 0 %}
<div class="validation-stat warning">
<div class="number">{{ validation_result.invalid_rows }}</div>
<div class="label">Invalid Rows</div>
</div>
{% endif %}
<div class="validation-stat">
<div class="number">{{ validation_result.columns|length }}</div>
<div class="label">Columns Found</div>
</div>
</div>
{% if validation_result.file_info %}
<p><strong>File Size:</strong> {{ validation_result.file_info.size_mb }} MB</p>
{% endif %}
{% 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 (First 3 Rows)
</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 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="validation-stats">
<div class="validation-stat">
<div class="number">{{ import_result.total_records }}</div>
<div class="label">Total Records</div>
</div>
<div class="validation-stat success">
<div class="number">{{ import_result.imported_records }}</div>
<div class="label">Successfully Imported</div>
</div>
{% if import_result.duplicate_records > 0 %}
<div class="validation-stat warning">
<div class="number">{{ import_result.duplicate_records }}</div>
<div class="label">Duplicates Skipped</div>
</div>
{% endif %}
{% if import_result.failed_records > 0 %}
<div class="validation-stat 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 (Showing first 10)
</h4>
<ul>
{% for error in import_result.errors[:10] %}
<li>{{ error }}</li>
{% endfor %}
{% if import_result.errors|length > 10 %}
<li><em>...and {{ import_result.errors|length - 10 }} more errors</em></li>
{% endif %}
</ul>
</div>
{% endif %}
{% if import_result.warnings %}
<div class="warning-list">
<h4>
<i class="fas fa-info-circle"></i>
Import Warnings (Showing first 10)
</h4>
<ul>
{% for warning in import_result.warnings[:10] %}
<li>{{ warning }}</li>
{% endfor %}
{% if import_result.warnings|length > 10 %}
<li><em>...and {{ import_result.warnings|length - 10 }} more warnings</em></li>
{% endif %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
<script>
// File upload handling
const fileInput = document.getElementById('fileInput');
const fileUploadArea = document.getElementById('fileUploadArea');
const fileInfo = document.getElementById('fileInfo');
const submitBtn = document.getElementById('submitBtn');
const importForm = document.getElementById('importForm');
const progressIndicator = document.getElementById('progressIndicator');
// Drag and drop handlers
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();
}
});
fileUploadArea.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', handleFileSelect);
function handleFileSelect() {
const file = fileInput.files[0];
if (file) {
const fileName = file.name;
const fileSize = (file.size / 1024 / 1024).toFixed(2) + ' MB';
fileInfo.querySelector('.file-name').textContent = fileName;
fileInfo.querySelector('.file-size').textContent = fileSize;
fileInfo.style.display = 'flex';
submitBtn.disabled = false;
// Validate file extension
if (!fileName.toLowerCase().endsWith('.xlsx') && !fileName.toLowerCase().endsWith('.xls')) {
alert('Please select an Excel file (.xlsx or .xls)');
resetForm();
}
}
}
// Form submission
importForm.addEventListener('submit', (e) => {
if (!fileInput.files[0]) {
e.preventDefault();
alert('Please select a file to import');
return;
}
// Validate project selection
const projectSelect = document.getElementById('project_id');
if (!projectSelect.value || projectSelect.value === '') {
e.preventDefault();
alert('Please select a project. Project selection is required for attendance imports.');
projectSelect.focus();
projectSelect.style.borderColor = '#ef4444';
return;
}
// Show progress indicator
progressIndicator.classList.add('active');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
});
// Reset border color when project is selected
document.getElementById('project_id').addEventListener('change', function() {
if (this.value) {
this.style.borderColor = '#e2e8f0';
}
});
function resetForm() {
importForm.reset();
fileInfo.style.display = 'none';
submitBtn.disabled = true;
progressIndicator.classList.remove('active');
submitBtn.innerHTML = '<i class="fas fa-upload"></i> <span class="btn-text">Import Data</span>';
}
// Prevent form resubmission
if (window.history.replaceState) {
window.history.replaceState(null, null, window.location.href);
}
</script>
{% endblock %}