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 @@
+
+
@@ -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 += '
';
+ errors.forEach(function(err) {
+ html += '- ' + err + '
';
+ });
+ 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: