Feb25 2026: updated import interface

This commit is contained in:
2026-02-25 13:33:28 -05:00
parent b96a305033
commit 50206499f3
2 changed files with 206 additions and 160 deletions
+148 -159
View File
@@ -333,6 +333,140 @@
</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') }}">
<!-- Project Selection - REQUIRED (moved to top) -->
<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>
<!-- 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 &amp; Drop Excel Files</h3>
<p>Or click to browse and select Excel files (.xlsx, .xls). Multiple files supported.</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>
<span class="file-count"></span>
</div>
</div>
<input type="file"
id="fileInput"
name="files"
accept=".xlsx,.xls"
class="file-input"
multiple
required>
</div>
<!-- Import Options (hidden — defaults preserved in DOM for form submission) -->
<div class="import-options" style="display: none;" aria-hidden="true">
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="analyze_duplicates" name="analyze_duplicates" value="true" checked>
</div>
<div class="checkbox-item">
<input type="checkbox" id="analyze_invalid" name="analyze_invalid" value="true" checked>
</div>
<div class="checkbox-item">
<input type="checkbox" id="skip_duplicates" name="skip_duplicates" value="true">
</div>
<div class="checkbox-item">
<input type="checkbox" id="validate_only" name="validate_only" value="true">
</div>
</div>
</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="progress-header">
<div class="spinner" id="progressSpinner"></div>
<span id="progressTitle">Uploading &amp; preparing import...</span>
</div>
<div class="progress-bar-track">
<div class="progress-bar-fill" id="progressBarFill">
<span class="pct-label" id="progressPctLabel">0%</span>
</div>
</div>
<div class="progress-counters">
<span id="progressCounter">0 of 0 records processed</span>
<span id="progressPct">0%</span>
</div>
<div class="progress-message" id="progressMessage">Starting...</div>
<div class="progress-done-message" id="progressDoneMsg"></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>
</button>
</div>
</form>
</div>
</div>
<!-- Import Instructions --> <!-- Import Instructions -->
<div class="import-section"> <div class="import-section">
<div class="import-header"> <div class="import-header">
@@ -429,162 +563,6 @@
</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 Files</h3>
<p>Or click to browse and select Excel files (.xlsx, .xls). Multiple files supported.</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>
<span class="file-count"></span>
</div>
</div>
<input type="file"
id="fileInput"
name="files"
accept=".xlsx,.xls"
class="file-input"
multiple
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="progress-header">
<div class="spinner" id="progressSpinner"></div>
<span id="progressTitle">Uploading &amp; preparing import...</span>
</div>
<div class="progress-bar-track">
<div class="progress-bar-fill" id="progressBarFill">
<span class="pct-label" id="progressPctLabel">0%</span>
</div>
</div>
<div class="progress-counters">
<span id="progressCounter">0 of 0 records processed</span>
<span id="progressPct">0%</span>
</div>
<div class="progress-message" id="progressMessage">Starting...</div>
<div class="progress-done-message" id="progressDoneMsg"></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>
</button>
</div>
</form>
</div>
</div>
<!-- Validation Results --> <!-- Validation Results -->
{% if validation_result %} {% if validation_result %}
<div class="import-results {% if validation_result.valid %}success{% else %}error{% endif %}"> <div class="import-results {% if validation_result.valid %}success{% else %}error{% endif %}">
@@ -858,11 +836,19 @@ function setProgress(current, total, message) {
if (message) progressMessage.textContent = message; if (message) progressMessage.textContent = message;
} }
function showDone(success, message) { function showDone(success, message, errors) {
document.getElementById('progressSpinner').style.display = 'none'; document.getElementById('progressSpinner').style.display = 'none';
progressDoneMsg.className = 'progress-done-message ' + (success ? 'success' : 'error'); progressDoneMsg.className = 'progress-done-message ' + (success ? 'success' : 'error');
progressDoneMsg.textContent = message;
progressTitle.textContent = success ? 'Import complete!' : 'Import failed'; progressTitle.textContent = success ? 'Import complete!' : 'Import failed';
let html = '<strong>' + message + '</strong>';
if (errors && errors.length > 0) {
html += '<ul style="margin: 0.5rem 0 0 1.25rem; padding: 0; text-align: left;">';
errors.forEach(function(err) {
html += '<li style="margin-bottom: 0.25rem;">' + err + '</li>';
});
html += '</ul>';
}
progressDoneMsg.innerHTML = html;
} }
// ─── Form submit → SSE import flow ──────────────────────────────────────── // ─── Form submit → SSE import flow ────────────────────────────────────────
@@ -947,7 +933,10 @@ importForm.addEventListener('submit', async (e) => {
if (result.failed_records > 0) if (result.failed_records > 0)
summary += ' ⚠️ ' + result.failed_records + ' records failed.'; summary += ' ⚠️ ' + result.failed_records + ' records failed.';
showDone(result.success, summary); const errors = (!result.success && result.errors && result.errors.length > 0)
? result.errors
: [];
showDone(result.success, summary, errors);
submitBtn.innerHTML = '<i class="fas fa-check"></i> Done'; submitBtn.innerHTML = '<i class="fas fa-check"></i> Done';
// Redirect to records view after a short delay // Redirect to records view after a short delay
+58 -1
View File
@@ -647,11 +647,68 @@ class TimeAttendanceImportService:
import_results['errors'].append(error_msg) import_results['errors'].append(error_msg)
return import_results return import_results
# ── Project-location validation ──────────────────────────────────────
# If a project_id is provided, verify that every unique Location Name
# in the file belongs to that project. Return immediately on the first
# mismatch so the user can correct the file or project selection.
if project_id:
try:
from models.qrcode import QRCode
from models.project import Project
# Collect all unique, non-empty location names from the file
file_locations = set(
str(loc).strip()
for loc in df['Location Name'].dropna().unique()
if str(loc).strip()
)
# Fetch all location names that belong to the selected project
project_locations = set(
qr.location
for qr in QRCode.query.filter_by(project_id=project_id)
.with_entities(QRCode.location).all()
)
# Find the first location in the file that is not in the project
unmatched = next(
(loc for loc in sorted(file_locations) if loc not in project_locations),
None
)
if unmatched:
project_obj = Project.query.get(project_id)
project_name = project_obj.name if project_obj else f'ID {project_id}'
error_msg = (
f"Location '{unmatched}' in the file does not belong to "
f"project '{project_name}'. "
f"Please verify the selected project or correct the file."
)
import_results['errors'].append(error_msg)
if self.logger:
self.logger.logger.warning(
f"Project-location mismatch: {error_msg}"
)
return import_results
if self.logger:
self.logger.logger.info(
f"Project-location validation passed: all {len(file_locations)} "
f"location(s) belong to project ID {project_id}."
)
except Exception as e:
if self.logger:
self.logger.logger.warning(
f"Could not perform project-location validation: {e}"
)
# ── End project-location validation ──────────────────────────────────
# Track duplicates using hash # Track duplicates using hash
duplicate_hashes = set() duplicate_hashes = set()
if skip_duplicates: if skip_duplicates:
duplicate_hashes = self._get_existing_record_hashes() duplicate_hashes = self._get_existing_record_hashes()
# Process each row with enhanced validation # Process each row with enhanced validation
for index, row in df.iterrows(): for index, row in df.iterrows():
try: try: