diff --git a/templates/time_attendance_import.html b/templates/time_attendance_import.html index fa57dde..b6bce9d 100644 --- a/templates/time_attendance_import.html +++ b/templates/time_attendance_import.html @@ -333,6 +333,140 @@ + +
+
+

+ + Upload Excel File +

+
+
+
+ + +
+

+ + Select Project * +

+ +

+ + Required: Select which project this import data belongs to for filtering and reporting +

+
+ + +
+
+ +
+
+

Drag & Drop Excel Files

+

Or click to browse and select Excel files (.xlsx, .xls). Multiple files supported.

+ +
+ +
+ + + + + +
+
+ + + Optional description for this import batch +
+
+ + +
+
+
+ Uploading & preparing import... +
+
+
+ 0% +
+
+
+ 0 of 0 records processed + 0% +
+
Starting...
+
+
+ + +
+ + +
+
+
+
+
@@ -429,162 +563,6 @@
- -
-
-

- - Upload Excel File -

-
-
-
- -
-
- -
-
-

Drag & Drop Excel Files

-

Or click to browse and select Excel files (.xlsx, .xls). Multiple files supported.

- -
- -
- - -
-

- - Import Options -

-
-
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
- - -
-

- - Select Project * -

- -

- - Required: Select which project this import data belongs to for filtering and reporting -

-
- - -
-
- - - Optional description for this import batch -
-
- - -
-
-
- Uploading & preparing import... -
-
-
- 0% -
-
-
- 0 of 0 records processed - 0% -
-
Starting...
-
-
- - -
- - -
-
-
-
- {% if validation_result %}
@@ -858,11 +836,19 @@ function setProgress(current, total, message) { if (message) progressMessage.textContent = message; } -function showDone(success, message) { +function showDone(success, message, errors) { document.getElementById('progressSpinner').style.display = 'none'; progressDoneMsg.className = 'progress-done-message ' + (success ? 'success' : 'error'); - progressDoneMsg.textContent = message; progressTitle.textContent = success ? 'Import complete!' : 'Import failed'; + let html = '' + message + ''; + if (errors && errors.length > 0) { + html += ''; + } + progressDoneMsg.innerHTML = html; } // ─── Form submit → SSE import flow ──────────────────────────────────────── @@ -947,7 +933,10 @@ importForm.addEventListener('submit', async (e) => { if (result.failed_records > 0) 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 = ' Done'; // Redirect to records view after a short delay diff --git a/time_attendance_import_service.py b/time_attendance_import_service.py index 60c1a08..9533a90 100644 --- a/time_attendance_import_service.py +++ b/time_attendance_import_service.py @@ -647,11 +647,68 @@ class TimeAttendanceImportService: import_results['errors'].append(error_msg) 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 duplicate_hashes = set() if skip_duplicates: duplicate_hashes = self._get_existing_record_hashes() - + # Process each row with enhanced validation for index, row in df.iterrows(): try: