diff --git a/app.py b/app.py
index b2436d4..aeaf9fc 100644
--- a/app.py
+++ b/app.py
@@ -70,6 +70,10 @@ class User(db.Model):
def has_staff_permissions(self):
"""Check if user has staff-level permissions (includes new roles)"""
return self.role in STAFF_LEVEL_ROLES
+
+ def has_export_permissions(user_role):
+ """Check if user role has export permissions"""
+ return user_role in ['admin', 'payroll']
def get_role_display_name(self):
"""Get user-friendly role name"""
@@ -2399,17 +2403,27 @@ def create_qr_code():
'longitude': address_longitude,
'coordinate_accuracy': coordinate_accuracy
}
- logger_handler.logger.info(f"User {session['username']} created QR code: {json.dumps(log_data)}")
- logger_handler.log_user_action(
- f'QR Code created: {name} with event type: {location_event}',
- 'create',
- additional_info={'location_event': location_event}
+ qr_data_for_logging = {
+ 'location': location,
+ 'location_address': location_address,
+ 'location_event': location_event,
+ 'has_coordinates': has_coordinates,
+ 'latitude': address_latitude,
+ 'longitude': address_longitude,
+ 'coordinate_accuracy': coordinate_accuracy
+ }
+
+ logger_handler.log_qr_code_created(
+ qr_code_id=new_qr_code.id,
+ qr_code_name=name,
+ created_by_user_id=session['user_id'],
+ qr_data=qr_data_for_logging
)
# Success message with coordinates info
project_info = f" in project '{project.name}'" if project else ""
coord_info = f" with coordinates ({new_qr_code.coordinates_display})" if has_coordinates else ""
-
+
flash(f'QR Code "{name}" created successfully{project_info}{coord_info}! URL: {qr_url}', 'success')
return redirect(url_for('dashboard'))
@@ -2991,7 +3005,7 @@ def toggle_qr_status_api(qr_id):
return redirect(url_for('dashboard'))
@app.route('/attendance')
-# @admin_required
+@login_required
def attendance_report():
"""Safe attendance report with backward compatibility for location_accuracy and fixed datetime handling"""
try:
@@ -2999,6 +3013,7 @@ def attendance_report():
# Log attendance report access
try:
+ user_role = session.get('role', 'unknown')
logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed attendance report")
except Exception as log_error:
print(f"⚠️ Logging error (non-critical): {log_error}")
@@ -3080,7 +3095,7 @@ def attendance_report():
conditions.append("ad.employee_id LIKE :employee")
params['employee'] = f"%{employee_filter}%"
- # FIXED: Apply project filter using SQL approach (not ORM)
+ # Apply project filter using SQL approach (not ORM)
if project_filter:
conditions.append("qc.project_id = :project_id")
params['project_id'] = int(project_filter)
@@ -3098,9 +3113,14 @@ def attendance_report():
# Execute query
query_result = db.session.execute(text(base_query), params)
attendance_records = query_result.fetchall()
-
print(f"✅ Found {len(attendance_records)} attendance records")
-
+ # Log first 3 records to verify QR address data
+ for i, record in enumerate(attendance_records[:3]):
+ print(f"📊 Record {i+1}: Employee={record.employee_id}")
+ print(f" QR Address: {getattr(record, 'qr_address', 'NOT_FOUND')}")
+ print(f" Check-in Address: {getattr(record, 'checked_in_address', 'NOT_FOUND')}")
+ print(f" Location Accuracy: {getattr(record, 'location_accuracy', 'NOT_FOUND')}")
+
# FIXED: Process records to add calculated fields with proper datetime handling
processed_records = []
for record in attendance_records:
@@ -3264,7 +3284,8 @@ def attendance_report():
project_filter=project_filter,
today_date=today_date,
current_date_formatted=current_date_formatted,
- has_location_accuracy_feature=has_location_accuracy)
+ has_location_accuracy_feature=has_location_accuracy,
+ user_role=user_role)
except Exception as e:
print(f"❌ Error loading attendance report: {e}")
@@ -3476,14 +3497,21 @@ def attendance_stats_api():
return jsonify({'error': 'Failed to fetch attendance statistics'}), 500
@app.route('/export-configuration')
-@admin_required
+@login_required
def export_configuration():
"""Route to display export configuration page"""
try:
print("📊 Export configuration route accessed")
+ # Check if user has export permissions
+ user_role = session.get('role')
+ if user_role not in ['admin', 'payroll']:
+ logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted to access export configuration without permissions")
+ flash('Access denied. Only administrators and payroll staff can access export configuration.', 'error')
+ return redirect(url_for('attendance_report'))
# Log export configuration access using your existing logger
try:
+ logger_handler.logger.info(f"User {session.get('username', 'unknown')} (role: {user_role}) accessed export configuration")
logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed export configuration page")
except Exception as log_error:
print(f"⚠️ Logging error (non-critical): {log_error}")
@@ -3558,10 +3586,16 @@ def export_configuration():
return redirect(url_for('attendance_report'))
@app.route('/generate-excel-export', methods=['POST'])
-@admin_required
+@login_required
def generate_excel_export():
"""Generate and download Excel file with selected columns in specified order"""
try:
+ user_role = session.get('role')
+ if user_role not in ['admin', 'payroll']:
+ logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted unauthorized Excel export")
+ flash('Access denied. Only administrators and payroll staff can export data.', 'error')
+ return redirect(url_for('attendance_report'))
+
print("📊 Excel export generation started")
# Log export action using your existing logger
diff --git a/static/css/attendance.css b/static/css/attendance.css
index eba85ac..d9d23de 100644
--- a/static/css/attendance.css
+++ b/static/css/attendance.css
@@ -5,7 +5,7 @@
/* Page Container */
.attendance-page {
- max-width: 1400px;
+ max-width: 1600px;
margin: 0 auto;
padding: var(--spacing-6);
min-height: 100vh;
@@ -993,40 +993,16 @@
opacity: 0.8;
}
-/* Location accuracy level colors - based on distance */
-.location-accuracy-badge.accuracy-excellent {
- background: var(--success-light);
- color: var(--success-color);
- border: 1px solid rgba(5, 150, 105, 0.3);
-}
-
+/* Location accuracy level colors - 2-level system based on 0.5 mile threshold */
.location-accuracy-badge.accuracy-accurate {
- background: var(--success-light);
- color: var(--success-color);
- border: 1px solid rgba(5, 150, 105, 0.3);
-}
-
-.location-accuracy-badge.accuracy-good {
- background: #ecfdf5;
+ background: #dcfce7;
color: #059669;
- border: 1px solid rgba(5, 150, 105, 0.2);
-}
-
-.location-accuracy-badge.accuracy-fair {
- background: var(--warning-light);
- color: var(--warning-color);
- border: 1px solid rgba(245, 158, 11, 0.3);
-}
-
-.location-accuracy-badge.accuracy-poor {
- background: var(--danger-light);
- color: var(--danger-color);
- border: 1px solid rgba(220, 38, 38, 0.3);
+ border: 1px solid rgba(5, 150, 105, 0.3);
}
.location-accuracy-badge.accuracy-inaccurate {
- background: var(--danger-light);
- color: var(--danger-color);
+ background: #fee2e2;
+ color: #dc2626;
border: 1px solid rgba(220, 38, 38, 0.3);
}
diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js
index 69a692a..98cd4dc 100644
--- a/static/js/attendance_report.js
+++ b/static/js/attendance_report.js
@@ -657,16 +657,14 @@ function extractLocationAccuracy(cell) {
}
function extractLocationAccuracyLevel(cell) {
- const text = cell.textContent.toLowerCase();
- if (
- text.includes("high") ||
- text.includes("excellent") ||
- text.includes("good")
- )
- return "High";
- if (text.includes("medium") || text.includes("fair")) return "Medium";
- if (text.includes("low") || text.includes("poor")) return "Low";
- return "Unknown";
+ // Get the numerical accuracy value from the cell
+ const accuracy = extractLocationAccuracy(cell);
+
+ // Return 2-level accuracy based on 0.5-mile threshold
+ if (accuracy !== null && accuracy !== undefined) {
+ return accuracy <= 0.5 ? "accurate" : "inaccurate";
+ }
+ return "unknown";
}
function createTableRow(record, displayIndex) {
@@ -690,17 +688,17 @@ function createTableRow(record, displayIndex) {
}"
title="Distance between QR location and check-in location: ${
record.location_accuracy
- } miles">
+ } miles - ${record.accuracy_level}">
${record.location_accuracy.toFixed(3)} mi
(${record.accuracy_level})
- `
+ `
: `
Unknown
- `;
+ `;
- // FIXED: Address display logic based on location accuracy
+ // Address display logic based on location accuracy
let addressDisplayHTML = "";
let addressToShow = record.checked_in_address;
let addressIcon = "fas fa-location-arrow";
@@ -812,8 +810,8 @@ function createTableRow(record, displayIndex) {
-
-
+
+
${
record.qr_address.length > 50
? record.qr_address.substring(0, 50) + "..."
@@ -1019,8 +1017,19 @@ function exportAttendanceWithAccuracy() {
}
function exportAttendance() {
+ // Check user role before proceeding
+ const userRole = window.userRole; // Read from global variable set in template
+ console.log("Template - session.role:", '{{ session.role }}');
+ console.log("Template - window.userRole set to:", window.userRole);
+
+ if (!['admin', 'payroll'].includes(userRole)) {
+ console.log("Export access denied - insufficient privileges");
+ alert("Access denied. Only administrators and payroll staff can export data.");
+ return;
+ }
+
// Log export action
- console.log("Export button clicked - redirecting to configuration page");
+ console.log(`Export button clicked by ${userRole} - redirecting to configuration page`);
// Get current filters
const currentFilters = getCurrentFilters();
@@ -1055,6 +1064,15 @@ function getCurrentFilters() {
// Add a quick CSV export function as backup (keep existing functionality)
function exportAttendanceCSV() {
+ // Check user role before proceeding
+ const userRole = window.userRole;
+
+ if (!['admin', 'payroll'].includes(userRole)) {
+ console.log("CSV export access denied - insufficient privileges");
+ alert("Access denied. Only administrators and payroll staff can export data.");
+ return;
+ }
+
// Build export URL with current filters for CSV
const params = new URLSearchParams();
const filters = getCurrentFilters();
diff --git a/static/js/export_configuration.js b/static/js/export_configuration.js
index df38f6b..8c76040 100644
--- a/static/js/export_configuration.js
+++ b/static/js/export_configuration.js
@@ -535,6 +535,13 @@ function loadSavedPreferences() {
const savedPrefs = localStorage.getItem('exportPreferences');
if (!savedPrefs) {
console.log('No saved preferences found');
+ // Check if there are already selected columns on page load and update preview
+ const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked');
+ if (alreadySelected.length > 0) {
+ console.log('Found pre-selected columns, updating preview');
+ updateSelectedColumnsList();
+ updatePreview();
+ }
return;
}
@@ -547,6 +554,13 @@ function loadSavedPreferences() {
if (savedDate < thirtyDaysAgo) {
localStorage.removeItem('exportPreferences');
console.log('Saved preferences are too old, removed');
+ // Check if there are already selected columns on page load and update preview
+ const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked');
+ if (alreadySelected.length > 0) {
+ console.log('Found pre-selected columns, updating preview');
+ updateSelectedColumnsList();
+ updatePreview();
+ }
return;
}
@@ -585,6 +599,10 @@ function loadSavedPreferences() {
console.log('Preferences loaded:', prefs);
+ // Update preview after loading preferences
+ updateSelectedColumnsList();
+ updatePreview();
+
} catch (e) {
console.warn('Could not load saved preferences:', e);
try {
@@ -592,6 +610,14 @@ function loadSavedPreferences() {
} catch (removeError) {
console.warn('Could not remove invalid preferences:', removeError);
}
+
+ // Check if there are already selected columns on page load and update preview
+ const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked');
+ if (alreadySelected.length > 0) {
+ console.log('Found pre-selected columns after preference error, updating preview');
+ updateSelectedColumnsList();
+ updatePreview();
+ }
}
}
diff --git a/static/js/qr_destination.js b/static/js/qr_destination.js
index 435fc54..358c772 100644
--- a/static/js/qr_destination.js
+++ b/static/js/qr_destination.js
@@ -59,6 +59,7 @@ document.addEventListener("DOMContentLoaded", function () {
// CRITICAL: Initialize systems in correct order
initializeLanguage();
+ initializeLocationServicesCheck();
initializeStaffIdPersistence();
initializeForm();
initializeLocation();
@@ -733,3 +734,152 @@ function startClock() {
updateClock();
setInterval(updateClock, 1000);
}
+
+function checkLocationServicesStatus() {
+ console.log("📱 Checking location services status...");
+
+ // Check if geolocation is supported
+ if (!navigator.geolocation) {
+ console.log("❌ Geolocation not supported by this browser");
+ showLocationServicesWarning("not_supported");
+ return;
+ }
+
+ // Test location access with a quick check
+ const timeoutId = setTimeout(() => {
+ console.log("⏰ Location permission check timed out");
+ showLocationServicesWarning("timeout");
+ }, 3000); // 3 second timeout
+
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ // Success - location services are working
+ clearTimeout(timeoutId);
+ console.log("✅ Location services are available and enabled");
+ hideLocationServicesWarning();
+ },
+ (error) => {
+ // Error - location services may be disabled
+ clearTimeout(timeoutId);
+ console.log("❌ Location services error:", error.message);
+
+ switch (error.code) {
+ case error.PERMISSION_DENIED:
+ showLocationServicesWarning("permission_denied");
+ break;
+ case error.POSITION_UNAVAILABLE:
+ showLocationServicesWarning("position_unavailable");
+ break;
+ case error.TIMEOUT:
+ showLocationServicesWarning("timeout");
+ break;
+ default:
+ showLocationServicesWarning("unknown_error");
+ break;
+ }
+ },
+ {
+ enableHighAccuracy: false,
+ timeout: 2500,
+ maximumAge: 60000
+ }
+ );
+}
+
+/**
+ * Show location services warning banner
+ */
+function showLocationServicesWarning(errorType) {
+ // Remove existing warning if present
+ hideLocationServicesWarning();
+
+ const warningMessages = {
+ en: {
+ not_supported: "Location services are not supported by your browser. Los servicios de ubicación no son compatibles con su navegador.",
+ permission_denied: "Location access has been denied. Please enable location services for accurate check-in. Se ha denegado el acceso a la ubicación. Habilite los servicios de ubicación para un registro preciso.",
+ position_unavailable: "Location services appear to be disabled. Please turn on location services for accurate check-in. Los servicios de ubicación parecen estar deshabilitados. Active los servicios de ubicación para un registro preciso.",
+ timeout: "Location services may be disabled. Please check your location settings for accurate check-in. Los servicios de ubicación pueden estar deshabilitados. Verifique su configuración de ubicación para un registro preciso.",
+ unknown_error: "Unable to access location services. Please check your location settings. No se puede acceder a los servicios de ubicación. Verifique su configuración de ubicación."
+ },
+ es: {
+ not_supported: "Los servicios de ubicación no son compatibles con su navegador.",
+ permission_denied: "Se ha denegado el acceso a la ubicación. Habilite los servicios de ubicación para un registro preciso.",
+ position_unavailable: "Los servicios de ubicación parecen estar deshabilitados. Active los servicios de ubicación para un registro preciso.",
+ timeout: "Los servicios de ubicación pueden estar deshabilitados. Verifique su configuración de ubicación para un registro preciso.",
+ unknown_error: "No se puede acceder a los servicios de ubicación. Verifique su configuración de ubicación."
+ }
+ };
+
+ const currentLang = currentLanguage || 'en';
+ const message = warningMessages[currentLang][errorType] || warningMessages['en'][errorType];
+
+ // Create warning banner
+ const warningBanner = document.createElement('div');
+ warningBanner.id = 'locationServicesWarning';
+ warningBanner.className = 'location-warning-banner';
+ warningBanner.innerHTML = `
+
+
+
+ ${message}
+
+
+
+
+
+
+ `;
+
+ // Insert warning at the top of the page
+ const container = document.querySelector('.destination-container');
+ if (container) {
+ container.insertBefore(warningBanner, container.firstChild);
+ }
+
+ // Log warning event
+ console.log(`⚠️ Location services warning displayed: ${errorType}`);
+}
+
+/**
+ * Hide location services warning banner
+ */
+function hideLocationServicesWarning() {
+ const existingWarning = document.getElementById('locationServicesWarning');
+ if (existingWarning) {
+ existingWarning.remove();
+ console.log("✅ Location services warning hidden");
+ }
+}
+
+/**
+ * Modified DOMContentLoaded event handler
+ * Add this to your existing initialization
+ */
+function initializeLocationServicesCheck() {
+ // Check location services status when page loads
+ setTimeout(() => {
+ checkLocationServicesStatus();
+ }, 1000); // Small delay to ensure page is fully loaded
+
+ // Also check before form submission
+ const originalHandleFormSubmit = handleFormSubmit;
+ window.handleFormSubmit = function(event) {
+ // Quick location check before submission
+ checkLocationServicesStatus();
+
+ // Continue with original form submission after brief delay
+ setTimeout(() => {
+ originalHandleFormSubmit.call(this, event);
+ }, 500);
+ };
+}
diff --git a/templates/attendance_report.html b/templates/attendance_report.html
index aa907d7..53ebb4c 100644
--- a/templates/attendance_report.html
+++ b/templates/attendance_report.html
@@ -10,7 +10,7 @@
{% endblock %}
{% block content %}
-
+
@@ -22,10 +22,12 @@
+ {% if session.role in ['admin', 'payroll'] %}
+ {% endif %}