This commit is contained in:
Nguyen Ngo
2025-09-03 12:15:21 -04:00
11 changed files with 3191 additions and 1932 deletions
+463
View File
@@ -0,0 +1,463 @@
# File: advanced_security_middleware.py
# Enhanced security middleware for QR Attendance System
from functools import wraps
from flask import request, session, jsonify, current_app, g
import hashlib
import secrets
import jwt
from datetime import datetime, timedelta
import re
from collections import defaultdict, deque
import time
import hmac
import base64
import os
# Try to import cryptography, fallback if not available
try:
from cryptography.fernet import Fernet
HAS_CRYPTOGRAPHY = True
except ImportError:
HAS_CRYPTOGRAPHY = False
class SecurityManager:
"""
Advanced security manager for QR Attendance System
"""
def __init__(self, app=None, db=None, logger_handler=None):
self.app = app
self.db = db
self.logger_handler = logger_handler
# Security tracking
self.failed_attempts = defaultdict(lambda: deque(maxlen=10))
self.suspicious_ips = defaultdict(int)
self.session_tokens = {}
# Security configuration
self.max_failed_attempts = 5
self.lockout_duration = 900 # 15 minutes
self.session_timeout = 3600 # 1 hour
if app:
self.init_app(app, db, logger_handler)
def init_app(self, app, db, logger_handler):
"""Initialize security manager with Flask app"""
self.app = app
self.db = db
self.logger_handler = logger_handler
# Generate encryption key for sensitive data
self.setup_encryption()
# Register security middleware
app.before_request(self.security_check)
# Register security routes
self.register_security_routes()
def setup_encryption(self):
"""Setup encryption for sensitive data"""
if HAS_CRYPTOGRAPHY:
encryption_key = self.app.config.get('ENCRYPTION_KEY')
if not encryption_key:
# Generate a new key (should be stored securely in production)
encryption_key = Fernet.generate_key()
if self.logger_handler:
self.logger_handler.logger.warning(
"Generated new encryption key - store this securely!"
)
self.cipher = Fernet(encryption_key)
else:
self.cipher = None
if self.logger_handler:
self.logger_handler.logger.warning(
"Cryptography not available - encryption features disabled"
)
def security_check(self):
"""Comprehensive security check before each request"""
client_ip = self.get_client_ip()
# Check for suspicious activity
if self.is_suspicious_request():
self.log_security_event('suspicious_request', {
'ip': client_ip,
'user_agent': request.headers.get('User-Agent', ''),
'endpoint': request.endpoint,
'method': request.method
})
return jsonify({'error': 'Request blocked for security reasons'}), 403
# Validate session security
if 'user_id' in session:
if not self.validate_session_security():
session.clear()
return jsonify({'error': 'Session security validation failed'}), 401
# Check for SQL injection attempts
if self.detect_sql_injection():
self.log_security_event('sql_injection_attempt', {
'ip': client_ip,
'query_params': dict(request.args),
'form_data': dict(request.form) if request.form else {}
})
return jsonify({'error': 'Malicious request detected'}), 403
# Rate limiting for authentication endpoints
if request.endpoint in ['login', 'register', 'reset_password']:
if self.is_auth_rate_limited():
return jsonify({
'error': 'Too many attempts, please try again later'
}), 429
def get_client_ip(self):
"""Get real client IP address"""
# Check for forwarded headers (in case behind proxy/CDN)
forwarded_ips = request.headers.getlist('X-Forwarded-For')
if forwarded_ips:
return forwarded_ips[0].split(',')[0].strip()
return request.headers.get('X-Real-IP') or request.remote_addr
def is_suspicious_request(self):
"""Detect suspicious request patterns"""
client_ip = self.get_client_ip()
user_agent = request.headers.get('User-Agent', '').lower()
# Check for common attack patterns
suspicious_patterns = [
r'<script', r'javascript:', r'vbscript:', r'onload=', r'onerror=',
r'union\s+select', r'drop\s+table', r'insert\s+into',
r'\.\./\.\./.*etc/passwd', r'cmd\.exe', r'/bin/bash'
]
request_data = str(request.args) + str(request.form) + request.path
for pattern in suspicious_patterns:
if re.search(pattern, request_data, re.IGNORECASE):
self.suspicious_ips[client_ip] += 1
return True
# Check for suspicious user agents
bot_patterns = ['bot', 'crawler', 'spider', 'scraper', 'scanner']
if any(pattern in user_agent for pattern in bot_patterns):
if request.endpoint not in ['static', 'favicon']:
return True
# Check request frequency (basic rate limiting)
current_time = time.time()
if not hasattr(g, 'request_history'):
g.request_history = deque(maxlen=50)
g.request_history.append(current_time)
recent_requests = [t for t in g.request_history if current_time - t < 60]
if len(recent_requests) > 30: # More than 30 requests per minute
return True
return False
def validate_session_security(self):
"""Validate session security and integrity"""
try:
user_id = session.get('user_id')
session_token = session.get('security_token')
if not user_id or not session_token:
return False
# Check if session token matches stored token
stored_token = self.session_tokens.get(user_id)
if not stored_token or not hmac.compare_digest(session_token, stored_token['token']):
return False
# Check session timeout
if time.time() - stored_token['created'] > self.session_timeout:
del self.session_tokens[user_id]
return False
# Check if session IP matches (optional security measure)
if self.app.config.get('STRICT_SESSION_IP', False):
if stored_token['ip'] != self.get_client_ip():
self.log_security_event('session_ip_mismatch', {
'user_id': user_id,
'original_ip': stored_token['ip'],
'current_ip': self.get_client_ip()
})
return False
return True
except Exception as e:
if self.logger_handler:
self.logger_handler.logger.error(f"Session validation error: {e}")
return False
def detect_sql_injection(self):
"""Detect potential SQL injection attempts"""
sql_patterns = [
r"union\s+select", r"drop\s+table", r"insert\s+into",
r"delete\s+from", r"update\s+set", r"exec\s*\(",
r"sp_executesql", r"xp_cmdshell", r";\s*--",
r"'\s*or\s*'", r'"\s*or\s*"', r"1\s*=\s*1"
]
# Check all request parameters
check_data = []
check_data.extend(request.args.values())
check_data.extend(request.form.values())
if request.json:
check_data.extend(str(v) for v in request.json.values() if isinstance(v, (str, int, float)))
for data in check_data:
data_str = str(data).lower()
for pattern in sql_patterns:
if re.search(pattern, data_str, re.IGNORECASE):
return True
return False
def is_auth_rate_limited(self):
"""Check if authentication endpoint is rate limited"""
client_ip = self.get_client_ip()
current_time = time.time()
# Clean old attempts
self.failed_attempts[client_ip] = deque([
attempt for attempt in self.failed_attempts[client_ip]
if current_time - attempt < 900 # Keep attempts from last 15 minutes
], maxlen=10)
return len(self.failed_attempts[client_ip]) >= self.max_failed_attempts
def record_failed_attempt(self, identifier):
"""Record a failed authentication attempt"""
client_ip = self.get_client_ip()
current_time = time.time()
self.failed_attempts[client_ip].append(current_time)
self.log_security_event('authentication_failure', {
'ip': client_ip,
'identifier': identifier,
'attempts': len(self.failed_attempts[client_ip])
})
def create_secure_session(self, user_id):
"""Create a secure session with additional security measures"""
# Generate secure session token
session_token = secrets.token_urlsafe(32)
# Store session information
self.session_tokens[user_id] = {
'token': session_token,
'created': time.time(),
'ip': self.get_client_ip(),
'user_agent': request.headers.get('User-Agent', '')[:200]
}
# Set session data
session['security_token'] = session_token
session['login_time'] = datetime.utcnow().isoformat()
# Clear any failed attempts for this IP
client_ip = self.get_client_ip()
if client_ip in self.failed_attempts:
del self.failed_attempts[client_ip]
self.log_security_event('secure_session_created', {
'user_id': user_id,
'ip': client_ip
})
def encrypt_sensitive_data(self, data):
"""Encrypt sensitive data before storage"""
if not self.cipher:
return data # Return as-is if encryption not available
try:
if isinstance(data, str):
data = data.encode('utf-8')
encrypted_data = self.cipher.encrypt(data)
return base64.b64encode(encrypted_data).decode('utf-8')
except Exception as e:
if self.logger_handler:
self.logger_handler.logger.error(f"Encryption error: {e}")
return data
def decrypt_sensitive_data(self, encrypted_data):
"""Decrypt sensitive data"""
if not self.cipher:
return encrypted_data # Return as-is if encryption not available
try:
encrypted_bytes = base64.b64decode(encrypted_data.encode('utf-8'))
decrypted_data = self.cipher.decrypt(encrypted_bytes)
return decrypted_data.decode('utf-8')
except Exception as e:
if self.logger_handler:
self.logger_handler.logger.error(f"Decryption error: {e}")
return encrypted_data
def log_security_event(self, event_type, details):
"""Log security events for monitoring"""
try:
security_log = {
'event_type': event_type,
'timestamp': datetime.utcnow().isoformat(),
'ip': self.get_client_ip(),
'user_agent': request.headers.get('User-Agent', ''),
'endpoint': request.endpoint,
'method': request.method,
'details': details
}
if self.logger_handler:
self.logger_handler.log_security_event(
event_type=event_type,
description=f"Security event: {event_type}",
additional_data=security_log
)
except Exception as e:
if self.logger_handler:
self.logger_handler.logger.error(f"Security logging error: {e}")
def register_security_routes(self):
"""Register security monitoring API endpoints"""
@self.app.route('/api/security/status')
def security_status():
"""Get current security status"""
try:
# Admin only endpoint
if not session.get('user_id') or session.get('role') != 'admin':
return jsonify({'error': 'Access denied'}), 403
current_time = time.time()
# Count active suspicious IPs
suspicious_count = len([
ip for ip, count in self.suspicious_ips.items()
if count > 3
])
# Count recent failed attempts
recent_failures = sum(
len([
attempt for attempt in attempts
if current_time - attempt < 300 # Last 5 minutes
])
for attempts in self.failed_attempts.values()
)
# Count active sessions
active_sessions = len([
token for token in self.session_tokens.values()
if current_time - token['created'] < self.session_timeout
])
return jsonify({
'suspicious_ips': suspicious_count,
'recent_failed_attempts': recent_failures,
'active_sessions': active_sessions,
'rate_limited_ips': len(self.failed_attempts),
'security_status': 'normal' if suspicious_count < 5 else 'elevated'
})
except Exception as e:
if self.logger_handler:
self.logger_handler.logger.error(f"Security status error: {e}")
return jsonify({'error': 'Failed to get security status'}), 500
@self.app.route('/api/security/clear-blocks', methods=['POST'])
def clear_security_blocks():
"""Clear security blocks (admin only)"""
try:
if not session.get('user_id') or session.get('role') != 'admin':
return jsonify({'error': 'Access denied'}), 403
# Clear failed attempts
cleared_ips = len(self.failed_attempts)
self.failed_attempts.clear()
# Clear suspicious IPs
cleared_suspicious = len(self.suspicious_ips)
self.suspicious_ips.clear()
self.log_security_event('security_blocks_cleared', {
'admin_user': session.get('username'),
'cleared_failed_attempts': cleared_ips,
'cleared_suspicious_ips': cleared_suspicious
})
return jsonify({
'message': 'Security blocks cleared successfully',
'cleared_failed_attempts': cleared_ips,
'cleared_suspicious_ips': cleared_suspicious
})
except Exception as e:
if self.logger_handler:
self.logger_handler.logger.error(f"Clear blocks error: {e}")
return jsonify({'error': 'Failed to clear security blocks'}), 500
def enhanced_login_required(f):
"""
Enhanced login required decorator with security checks
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return jsonify({'error': 'Authentication required'}), 401
# Additional security validation
if not session.get('security_token'):
session.clear()
return jsonify({'error': 'Session security validation failed'}), 401
# Check session timeout
login_time_str = session.get('login_time')
if login_time_str:
try:
login_time = datetime.fromisoformat(login_time_str)
if datetime.utcnow() - login_time > timedelta(hours=8):
session.clear()
return jsonify({'error': 'Session expired'}), 401
except ValueError:
session.clear()
return jsonify({'error': 'Invalid session data'}), 401
return f(*args, **kwargs)
return decorated_function
def csrf_protect(f):
"""
CSRF protection decorator
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if request.method == 'POST':
token = request.form.get('csrf_token') or request.headers.get('X-CSRF-Token')
expected_token = session.get('csrf_token')
if not token or not expected_token or not hmac.compare_digest(token, expected_token):
return jsonify({'error': 'CSRF token validation failed'}), 403
return f(*args, **kwargs)
return decorated_function
def generate_csrf_token():
"""Generate CSRF token for forms"""
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_urlsafe(32)
return session['csrf_token']
+178 -15
View File
@@ -18,6 +18,8 @@ from payroll_excel_exporter import PayrollExcelExporter
# Load environment variables in .env # Load environment variables in .env
load_dotenv() load_dotenv()
from turnstile_utils import turnstile_utils from turnstile_utils import turnstile_utils
from db_performance_optimization import initialize_performance_optimizations
from app_performance_middleware import PerformanceMonitor
# Initialize Flask application # Initialize Flask application
app = Flask(__name__) app = Flask(__name__)
@@ -29,6 +31,13 @@ app.config['TEMPLATES_AUTO_RELOAD'] = os.environ.get('TEMPLATES_AUTO_RELOAD')
# Initialize database # Initialize database
db = SQLAlchemy(app) db = SQLAlchemy(app)
@app.context_processor
def inject_company_name():
"""Make COMPANY_NAME available to all templates"""
return {
'COMPANY_NAME': os.environ.get('COMPANY_NAME', 'QR Code Management System')
}
def create_performance_indexes(): def create_performance_indexes():
"""Create performance optimization indexes""" """Create performance optimization indexes"""
try: try:
@@ -1621,6 +1630,102 @@ def dashboard():
flash('Error loading dashboard. Please try again.', 'error') flash('Error loading dashboard. Please try again.', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
@app.route('/api/dashboard/stats')
@login_required
def dashboard_stats_api():
"""API endpoint for dashboard statistics"""
try:
# Get current stats
total_qr_codes = QRCode.query.filter_by(active_status=True).count()
# Today's check-ins
today = datetime.utcnow().date()
today_checkins = AttendanceData.query.filter(
AttendanceData.check_in_date == today
).count()
# Active projects
active_projects = Project.query.filter_by(active_status=True).count()
# Unique locations
unique_locations = db.session.query(
AttendanceData.location_name
).distinct().count()
# Calculate trends (compared to last month)
last_month = datetime.utcnow() - timedelta(days=30)
# QR codes trend
old_qr_count = QRCode.query.filter(
QRCode.created_date <= last_month,
QRCode.active_status == True
).count()
qr_change = ((total_qr_codes - old_qr_count) / max(old_qr_count, 1)) * 100
# Check-ins trend (yesterday)
yesterday = today - timedelta(days=1)
yesterday_checkins = AttendanceData.query.filter(
AttendanceData.check_in_date == yesterday
).count()
checkin_change = ((today_checkins - yesterday_checkins) / max(yesterday_checkins, 1)) * 100
return jsonify({
'success': True,
'total_qr_codes': total_qr_codes,
'today_checkins': today_checkins,
'active_projects': active_projects,
'unique_locations': unique_locations,
'qr_change': round(qr_change, 1),
'checkin_change': round(checkin_change, 1),
'project_change': 0, # You can calculate this based on your needs
'location_change': 0 # You can calculate this based on your needs
})
except Exception as e:
logger_handler.log_database_error('dashboard_stats_api', e)
return jsonify({
'success': False,
'error': 'Failed to fetch dashboard statistics'
}), 500
@app.route('/api/dashboard/realtime')
@login_required
def dashboard_realtime_api():
"""API endpoint for real-time dashboard data"""
try:
# Get recent activity (last 10 check-ins)
recent_activity = db.session.query(
AttendanceData.employee_id,
AttendanceData.location_name,
AttendanceData.check_in_time,
AttendanceData.check_in_date
).order_by(
AttendanceData.check_in_date.desc(),
AttendanceData.check_in_time.desc()
).limit(10).all()
activity_data = [
{
'employee_id': activity.employee_id,
'location': activity.location_name,
'time': activity.check_in_time.strftime('%H:%M'),
'date': activity.check_in_date.strftime('%Y-%m-%d')
}
for activity in recent_activity
]
return jsonify({
'success': True,
'recent_activity': activity_data
})
except Exception as e:
logger_handler.log_database_error('dashboard_realtime_api', e)
return jsonify({
'success': False,
'error': 'Failed to fetch real-time data'
}), 500
# USER MANAGEMENT ROUTES # USER MANAGEMENT ROUTES
@app.route('/profile', methods=['GET', 'POST']) @app.route('/profile', methods=['GET', 'POST'])
@login_required @login_required
@@ -3687,6 +3792,52 @@ def toggle_qr_status(qr_id):
'message': 'Error updating QR code status. Please try again.' 'message': 'Error updating QR code status. Please try again.'
}), 500 }), 500
@app.route('/qr-codes/<int:qr_id>/copy-url', methods=['POST'])
@login_required
def copy_qr_url(qr_id):
"""Log QR code URL copy action"""
try:
qr_code = QRCode.query.get_or_404(qr_id)
# Log URL copy action
logger_handler.logger.info(f"User {session.get('username', 'unknown')} copied URL for QR code {qr_code.name} (ID: {qr_id})")
return jsonify({
'success': True,
'message': f'QR code URL copied to clipboard!',
'url': f"{request.url_root}qr/{qr_code.qr_url}"
})
except Exception as e:
logger_handler.logger.error(f"Error copying QR URL for ID {qr_id}: {e}")
return jsonify({
'success': False,
'message': 'Error copying QR code URL.'
}), 500
@app.route('/qr-codes/<int:qr_id>/open-link', methods=['POST'])
@login_required
def open_qr_link(qr_id):
"""Log QR code link open action"""
try:
qr_code = QRCode.query.get_or_404(qr_id)
# Log link open action
logger_handler.logger.info(f"User {session.get('username', 'unknown')} opened link for QR code {qr_code.name} (ID: {qr_id})")
return jsonify({
'success': True,
'message': f'Opening QR code link...',
'url': f"{request.url_root}qr/{qr_code.qr_url}"
})
except Exception as e:
logger_handler.logger.error(f"Error opening QR link for ID {qr_id}: {e}")
return jsonify({
'success': False,
'message': 'Error opening QR code link.'
}), 500
@app.route('/qr-codes/<int:qr_id>/activate', methods=['POST']) @app.route('/qr-codes/<int:qr_id>/activate', methods=['POST'])
@login_required @login_required
def activate_qr_code(qr_id): def activate_qr_code(qr_id):
@@ -5407,6 +5558,16 @@ def get_employee_name(employee_id):
print(f"⚠️ Error getting employee name for ID {employee_id}: {e}") print(f"⚠️ Error getting employee name for ID {employee_id}: {e}")
return f"Employee {employee_id}" return f"Employee {employee_id}"
def get_qr_code_checkin_count(qr_code_id):
"""Helper function to get total check-ins count for a QR code"""
try:
count = AttendanceData.query.filter_by(qr_code_id=qr_code_id).count()
logger_handler.logger.info(f"QR Code {qr_code_id} total check-ins: {count}")
return count
except Exception as e:
logger_handler.logger.error(f"Error getting check-ins count for QR {qr_code_id}: {e}")
return 0
@app.context_processor @app.context_processor
def inject_payroll_utils(): def inject_payroll_utils():
"""Inject payroll utility functions into templates""" """Inject payroll utility functions into templates"""
@@ -5415,6 +5576,13 @@ def inject_payroll_utils():
'format_hours': lambda hours: f"{hours:.2f}" if hours else "0.00" 'format_hours': lambda hours: f"{hours:.2f}" if hours else "0.00"
} }
@app.context_processor
def inject_dashboard_utils():
"""Inject dashboard utility functions into templates"""
return {
'get_qr_code_checkin_count': get_qr_code_checkin_count
}
@app.route('/statistics') @app.route('/statistics')
@login_required @login_required
def qr_statistics(): def qr_statistics():
@@ -6048,6 +6216,7 @@ def employee_detail(employee_index):
create_location_logging_routes(app, db, logger_handler) create_location_logging_routes(app, db, logger_handler)
# Jinja2 filters for better template functionality # Jinja2 filters for better template functionality
@app.template_filter('days_since') @app.template_filter('days_since')
def days_since_filter(date): def days_since_filter(date):
@@ -6440,22 +6609,16 @@ if __name__ == '__main__':
# Initialize database and logging # Initialize database and logging
create_tables() create_tables()
# Add performance optimizations # Initialize performance optimizations
create_performance_indexes() print("🚀 Initializing performance optimizations...")
create_audit_triggers() cached_query = initialize_performance_optimizations(app, db, logger_handler)
performance_monitor = PerformanceMonitor(app, db, logger_handler)
if cached_query:
print("✅ Performance optimizations completed successfully")
else:
print("⚠️ Performance optimizations completed with warnings")
# Log optimization completion
logger_handler.log_system_event(
event_type="database_optimization_complete",
description="Database performance optimization completed successfully",
severity="INFO",
additional_data={
"indexes_created": True,
"triggers_created": True,
"optimization_timestamp": datetime.utcnow().isoformat()
}
)
print("🚀 Database optimization completed successfully")
# Log application startup # Log application startup
logger_handler.logger.info("QR Attendance Management System started successfully") logger_handler.logger.info("QR Attendance Management System started successfully")
+347
View File
@@ -0,0 +1,347 @@
# File: app_performance_middleware.py
# Advanced performance middleware for QR Attendance System
from functools import wraps
from flask import request, g, jsonify, current_app
import time
import threading
import queue
from datetime import datetime, timedelta
from collections import defaultdict, deque
import gc
import psutil
import os
class PerformanceMonitor:
"""
Advanced performance monitoring and optimization middleware
"""
def __init__(self, app=None, db=None, logger_handler=None):
self.app = app
self.db = db
self.logger_handler = logger_handler
# Performance metrics storage
self.request_times = deque(maxlen=1000) # Keep last 1000 requests
self.slow_queries = deque(maxlen=100)
self.error_rates = defaultdict(int)
self.endpoint_stats = defaultdict(lambda: {'count': 0, 'total_time': 0, 'errors': 0})
# Rate limiting storage
self.rate_limit_storage = defaultdict(lambda: {'requests': deque(), 'blocked_until': None})
# Background task queue
self.task_queue = queue.Queue()
self.background_worker = None
if app:
self.init_app(app, db, logger_handler)
def init_app(self, app, db, logger_handler):
"""Initialize performance monitoring with Flask app"""
self.app = app
self.db = db
self.logger_handler = logger_handler
# Register before/after request handlers
app.before_request(self.before_request)
app.after_request(self.after_request)
# Start background worker
self.start_background_worker()
# Register performance monitoring routes
self.register_performance_routes()
def before_request(self):
"""Performance monitoring before each request"""
g.start_time = time.time()
g.request_id = f"{int(time.time())}-{threading.get_ident()}"
# Rate limiting check
if self.is_rate_limited():
return jsonify({
'error': 'Rate limit exceeded',
'retry_after': 60
}), 429
# Memory usage monitoring
self.monitor_memory_usage()
def after_request(self, response):
"""Performance monitoring after each request"""
if hasattr(g, 'start_time'):
request_time = time.time() - g.start_time
# Record request metrics
self.record_request_metrics(request_time, response.status_code)
# Log slow requests
if request_time > 2.0: # Requests taking more than 2 seconds
self.log_slow_request(request_time)
# Add performance headers
response.headers['X-Response-Time'] = f"{request_time:.3f}s"
response.headers['X-Request-ID'] = getattr(g, 'request_id', 'unknown')
return response
def record_request_metrics(self, request_time, status_code):
"""Record request performance metrics"""
endpoint = request.endpoint or 'unknown'
# Store request time
self.request_times.append({
'endpoint': endpoint,
'time': request_time,
'status': status_code,
'timestamp': datetime.utcnow()
})
# Update endpoint statistics
self.endpoint_stats[endpoint]['count'] += 1
self.endpoint_stats[endpoint]['total_time'] += request_time
if status_code >= 400:
self.endpoint_stats[endpoint]['errors'] += 1
self.error_rates[status_code] += 1
def is_rate_limited(self):
"""Check if current request should be rate limited"""
client_ip = request.environ.get('REMOTE_ADDR', 'unknown')
current_time = time.time()
# Clean up old requests
client_data = self.rate_limit_storage[client_ip]
client_data['requests'] = deque([
req_time for req_time in client_data['requests']
if current_time - req_time < 60 # 1 minute window
], maxlen=100)
# Check if currently blocked
if client_data['blocked_until'] and current_time < client_data['blocked_until']:
return True
# Add current request
client_data['requests'].append(current_time)
# Check rate limit (100 requests per minute)
if len(client_data['requests']) > 100:
client_data['blocked_until'] = current_time + 300 # Block for 5 minutes
self.logger_handler.logger.warning(f"Rate limit exceeded for IP: {client_ip}")
return True
return False
def monitor_memory_usage(self):
"""Monitor application memory usage"""
# Get memory usage every 10 requests (approximately)
import random
if random.randint(1, 10) == 1:
process = psutil.Process(os.getpid())
memory_info = process.memory_info()
memory_mb = memory_info.rss / 1024 / 1024
if memory_mb > 1000: # More than 1GB
self.logger_handler.logger.warning(f"High memory usage: {memory_mb:.1f}MB")
# Force garbage collection
gc.collect()
# Queue background cleanup task
self.task_queue.put({
'type': 'memory_cleanup',
'timestamp': datetime.utcnow()
})
def log_slow_request(self, request_time):
"""Log slow requests for optimization"""
slow_request_data = {
'endpoint': request.endpoint,
'method': request.method,
'time': request_time,
'args': dict(request.args),
'timestamp': datetime.utcnow()
}
self.slow_queries.append(slow_request_data)
self.logger_handler.logger.warning(
f"Slow request: {request.method} {request.endpoint} - {request_time:.3f}s"
)
def start_background_worker(self):
"""Start background worker for performance tasks"""
def worker():
while True:
try:
task = self.task_queue.get(timeout=30)
self.process_background_task(task)
self.task_queue.task_done()
except queue.Empty:
continue
except Exception as e:
if self.logger_handler:
self.logger_handler.logger.error(f"Background worker error: {e}")
self.background_worker = threading.Thread(target=worker, daemon=True)
self.background_worker.start()
def process_background_task(self, task):
"""Process background performance tasks"""
task_type = task.get('type')
if task_type == 'memory_cleanup':
self.perform_memory_cleanup()
elif task_type == 'performance_analysis':
self.perform_performance_analysis()
elif task_type == 'database_optimization':
self.optimize_database_connections()
def perform_memory_cleanup(self):
"""Perform memory cleanup operations"""
try:
# Clear old metrics
cutoff_time = datetime.utcnow() - timedelta(hours=1)
# Clean request times
self.request_times = deque([
req for req in self.request_times
if req['timestamp'] > cutoff_time
], maxlen=1000)
# Clean slow queries
self.slow_queries = deque([
query for query in self.slow_queries
if query['timestamp'] > cutoff_time
], maxlen=100)
# Clean rate limit storage
current_time = time.time()
for ip, data in list(self.rate_limit_storage.items()):
if not data['requests'] and (
not data['blocked_until'] or current_time > data['blocked_until']
):
del self.rate_limit_storage[ip]
# Force garbage collection
gc.collect()
self.logger_handler.logger.info("Memory cleanup completed")
except Exception as e:
self.logger_handler.logger.error(f"Memory cleanup failed: {e}")
def register_performance_routes(self):
"""Register performance monitoring API endpoints"""
@self.app.route('/api/performance/stats')
def performance_stats():
"""Get current performance statistics"""
try:
# Calculate average response times
recent_requests = [
req for req in self.request_times
if req['timestamp'] > datetime.utcnow() - timedelta(minutes=5)
]
avg_response_time = (
sum(req['time'] for req in recent_requests) / len(recent_requests)
if recent_requests else 0
)
# Get endpoint statistics
endpoint_performance = {}
for endpoint, stats in self.endpoint_stats.items():
endpoint_performance[endpoint] = {
'avg_response_time': stats['total_time'] / stats['count'] if stats['count'] > 0 else 0,
'total_requests': stats['count'],
'error_rate': stats['errors'] / stats['count'] if stats['count'] > 0 else 0
}
# Get memory info
process = psutil.Process(os.getpid())
memory_info = process.memory_info()
return jsonify({
'avg_response_time': round(avg_response_time, 3),
'total_requests': len(self.request_times),
'slow_requests': len(self.slow_queries),
'memory_usage_mb': round(memory_info.rss / 1024 / 1024, 1),
'endpoint_performance': endpoint_performance,
'error_rates': dict(self.error_rates)
})
except Exception as e:
self.logger_handler.logger.error(f"Performance stats error: {e}")
return jsonify({'error': 'Failed to get performance stats'}), 500
@self.app.route('/api/performance/slow-requests')
def slow_requests():
"""Get recent slow requests for analysis"""
try:
slow_request_list = [
{
'endpoint': req['endpoint'],
'method': req.get('method', 'GET'),
'time': round(req['time'], 3),
'timestamp': req['timestamp'].isoformat()
}
for req in list(self.slow_queries)[-20:] # Last 20 slow requests
]
return jsonify({
'slow_requests': slow_request_list,
'total_slow_requests': len(self.slow_queries)
})
except Exception as e:
self.logger_handler.logger.error(f"Slow requests API error: {e}")
return jsonify({'error': 'Failed to get slow requests'}), 500
def performance_optimization_decorator(threshold=1.0):
"""
Decorator to monitor and optimize specific function performance
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = func(*args, **kwargs)
execution_time = time.time() - start_time
if execution_time > threshold:
print(f"⚠️ Slow function: {func.__name__} took {execution_time:.3f}s")
return result
except Exception as e:
execution_time = time.time() - start_time
print(f"❌ Function error: {func.__name__} failed after {execution_time:.3f}s - {e}")
raise
return wrapper
return decorator
def optimize_database_queries():
"""
Database query optimization decorator
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Enable query logging for this function
query_start = time.time()
result = func(*args, **kwargs)
query_time = time.time() - query_start
if query_time > 0.5: # Queries taking more than 500ms
print(f"🐌 Slow query in {func.__name__}: {query_time:.3f}s")
return result
return wrapper
return decorator
+217
View File
@@ -0,0 +1,217 @@
# File: db_performance_optimization_fixed.py
# Fixed version compatible with your existing AppLogger
from sqlalchemy import text
from datetime import datetime, timedelta
import logging
def create_advanced_performance_indexes(db, logger_handler):
"""
Create advanced performance indexes for optimal query performance
Compatible with existing AppLogger
"""
try:
# Critical indexes for attendance data
performance_indexes = [
# Composite index for attendance queries by date range and employee
"CREATE INDEX IF NOT EXISTS idx_attendance_employee_date ON attendance_data(employee_id, check_in_date)",
# Index for location-based queries
"CREATE INDEX IF NOT EXISTS idx_attendance_location_date ON attendance_data(location_name, check_in_date)",
# Index for time-based analytics
"CREATE INDEX IF NOT EXISTS idx_attendance_datetime ON attendance_data(check_in_date, check_in_time)",
# QR Code performance indexes
"CREATE INDEX IF NOT EXISTS idx_qrcode_project_active ON qr_codes(project_id, active_status)",
# User authentication indexes
"CREATE INDEX IF NOT EXISTS idx_users_username_active ON users(username, active_status)",
"CREATE INDEX IF NOT EXISTS idx_users_role_active ON users(role, active_status)",
# Project management indexes
"CREATE INDEX IF NOT EXISTS idx_projects_active_name ON projects(active_status, name)",
# Employee search optimization
"CREATE INDEX IF NOT EXISTS idx_employee_search ON employee(firstName, lastName, id)",
]
indexes_created = 0
for index_sql in performance_indexes:
try:
db.session.execute(text(index_sql))
logger_handler.logger.info(f"Created index: {index_sql[:50]}...")
indexes_created += 1
except Exception as e:
logger_handler.logger.warning(f"Index creation skipped: {str(e)[:100]}")
db.session.commit()
# Log success using compatible method
logger_handler.logger.info(f"Performance optimization complete: {indexes_created} indexes created")
return True
except Exception as e:
db.session.rollback()
# Use compatible logging method
logger_handler.log_database_error('performance_optimization', e)
return False
def optimize_database_configuration(db, logger_handler):
"""
Optimize database configuration for better performance
"""
try:
optimization_queries = [
# Query cache optimization (MySQL specific)
"SET SESSION query_cache_type = ON",
# Connection optimization
"SET SESSION wait_timeout = 28800",
"SET SESSION interactive_timeout = 28800",
]
optimizations_applied = 0
for query in optimization_queries:
try:
db.session.execute(text(query))
optimizations_applied += 1
except Exception as e:
# Some settings may require specific privileges
logger_handler.logger.debug(f"Configuration skip: {str(e)[:50]}")
logger_handler.logger.info(f"Database configuration optimization completed: {optimizations_applied} optimizations applied")
except Exception as e:
logger_handler.log_database_error('database_configuration', e)
def create_database_maintenance_routine(app, db, logger_handler):
"""
Create automated database maintenance routine
"""
@app.cli.command()
def db_maintenance():
"""Run database maintenance tasks"""
try:
with app.app_context():
logger_handler.logger.info("Starting database maintenance routine")
# Optimize all tables
maintenance_queries = [
"OPTIMIZE TABLE attendance_data",
"OPTIMIZE TABLE qr_codes",
"OPTIMIZE TABLE projects",
"OPTIMIZE TABLE users",
"OPTIMIZE TABLE employee",
]
successful_optimizations = 0
for query in maintenance_queries:
try:
db.session.execute(text(query))
logger_handler.logger.info(f"Executed: {query}")
successful_optimizations += 1
except Exception as e:
logger_handler.logger.warning(f"Maintenance query failed: {query} - {str(e)}")
db.session.commit()
logger_handler.logger.info(f"Database maintenance completed: {successful_optimizations} tables optimized")
print("✅ Database maintenance completed successfully")
except Exception as e:
logger_handler.log_database_error('database_maintenance', e)
print(f"❌ Database maintenance failed: {e}")
def implement_caching_strategy(app, db, logger_handler):
"""
Implement intelligent caching strategy for improved performance
"""
from functools import wraps
import hashlib
# Simple in-memory cache
cache_storage = {}
cache_ttl = {}
def cached_query(ttl=300): # 5 minutes default TTL
"""Decorator for caching database queries"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Create cache key
cache_key = f"{func.__name__}_{hashlib.md5(str(args + tuple(kwargs.items())).encode()).hexdigest()}"
current_time = datetime.utcnow().timestamp()
# Check if cached result exists and is still valid
if cache_key in cache_storage:
if current_time - cache_ttl.get(cache_key, 0) < ttl:
logger_handler.logger.debug(f"Cache hit for {func.__name__}")
return cache_storage[cache_key]
# Execute function and cache result
result = func(*args, **kwargs)
cache_storage[cache_key] = result
cache_ttl[cache_key] = current_time
logger_handler.logger.debug(f"Cache miss for {func.__name__} - result cached")
return result
return wrapper
return decorator
# Clean up expired cache entries periodically
def cleanup_cache():
current_time = datetime.utcnow().timestamp()
expired_keys = [
key for key, timestamp in cache_ttl.items()
if current_time - timestamp > 300 # 5 minutes
]
for key in expired_keys:
cache_storage.pop(key, None)
cache_ttl.pop(key, None)
if expired_keys:
logger_handler.logger.debug(f"Cleaned up {len(expired_keys)} expired cache entries")
# Schedule cache cleanup
@app.before_request
def before_request_cache_cleanup():
# Cleanup cache every 100 requests (approximately)
import random
if random.randint(1, 100) == 1:
cleanup_cache()
logger_handler.logger.info("Caching strategy implemented successfully")
return cached_query
def initialize_performance_optimizations(app, db, logger_handler):
"""
Initialize all performance optimizations with compatibility
"""
try:
logger_handler.logger.info("Starting performance optimization...")
# Create advanced indexes
index_success = create_advanced_performance_indexes(db, logger_handler)
# Optimize database configuration
optimize_database_configuration(db, logger_handler)
# Create maintenance routines
create_database_maintenance_routine(app, db, logger_handler)
# Implement caching
cached_query = implement_caching_strategy(app, db, logger_handler)
if index_success:
logger_handler.logger.info("✅ Performance optimization completed successfully")
else:
logger_handler.logger.warning("⚠️ Performance optimization completed with some issues")
return cached_query
except Exception as e:
logger_handler.log_database_error('performance_initialization', e)
return None
+223 -19
View File
@@ -4,31 +4,163 @@
*/ */
/* Dashboard Header */ /* Dashboard Header */
.dashboard-header { .header-stats {
display: flex; display: flex;
justify-content: space-between; align-items: center;
align-items: flex-start; }
margin-bottom: var(--spacing-8);
padding: var(--spacing-6); .header-stats-grid {
background: var(--white); display: grid;
border-radius: var(--radius-xl); grid-template-columns: repeat(2, 1fr);
box-shadow: var(--shadow); gap: 1rem;
min-width: 400px;
}
@media (min-width: 1200px) {
.header-stats-grid {
grid-template-columns: repeat(4, 1fr);
min-width: 500px;
}
}
.header-stat-card {
background: rgba(255, 255, 255, 0.95);
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 0.75rem;
transition: all 0.2s ease-in-out;
backdrop-filter: blur(10px);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.dashboard-header::before { .header-stat-card::before {
content: ""; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 4px; height: 2px;
background: linear-gradient( }
90deg,
var(--primary-color), .header-stat-card.primary::before {
var(--primary-hover) background: linear-gradient(90deg, #2563eb, #1d4ed8);
); }
.header-stat-card.success::before {
background: linear-gradient(90deg, #10b981, #047857);
}
.header-stat-card.danger::before {
background: linear-gradient(90deg, #ef4444, #dc2626);
}
.header-stat-card.info::before {
background: linear-gradient(90deg, #8b5cf6, #7c3aed);
}
.header-stat-card:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.15);
}
.header-stat-icon {
width: 36px;
height: 36px;
border-radius: 0.375rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
color: #ffffff;
flex-shrink: 0;
}
.header-stat-card.primary .header-stat-icon {
background: linear-gradient(135deg, #2563eb, #1d4ed8);
}
.header-stat-card.success .header-stat-icon {
background: linear-gradient(135deg, #10b981, #047857);
}
.header-stat-card.danger .header-stat-icon {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.header-stat-card.info .header-stat-icon {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
}
.header-stat-content h3 {
font-size: 1.25rem;
font-weight: 700;
color: #0f172a;
margin-bottom: 0.125rem;
line-height: 1;
}
.header-stat-content p {
color: #64748b;
font-size: 0.75rem;
margin: 0;
line-height: 1.2;
}
/* Update existing dashboard header to accommodate new layout */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2rem;
background: linear-gradient(135deg, #a0aff1 0%, #ebdef8 100%);
border-radius: 0.75rem;
color: white;
margin-bottom: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
flex-wrap: wrap;
gap: 2rem;
}
/* Responsive Design for Header */
@media (max-width: 1024px) {
.dashboard-header {
flex-direction: column;
text-align: center;
gap: 1.5rem;
}
.header-stats-grid {
grid-template-columns: repeat(2, 1fr);
min-width: 300px;
}
}
@media (max-width: 768px) {
.dashboard-header {
padding: 1.5rem;
}
.header-stats-grid {
grid-template-columns: 1fr;
min-width: 250px;
}
.header-stat-card {
padding: 0.75rem;
}
.header-stat-content h3 {
font-size: 1.125rem;
}
.header-stat-content p {
font-size: 0.6875rem;
}
} }
.welcome-section h1 { .welcome-section h1 {
@@ -892,7 +1024,7 @@
} }
.qr-url { .qr-url {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
background: var(--gray-100); background: var(--gray-100);
padding: var(--spacing-1) var(--spacing-2); padding: var(--spacing-1) var(--spacing-2);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
@@ -927,7 +1059,8 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.qr-status, .qr-project { .qr-status,
.qr-project {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-1); gap: var(--spacing-1);
@@ -1088,8 +1221,12 @@
/* Animations */ /* Animations */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
@keyframes slideIn { @keyframes slideIn {
@@ -1133,3 +1270,70 @@
max-width: calc(100vw - 2rem); max-width: calc(100vw - 2rem);
} }
} }
/* Enhanced QR Action Buttons */
.qr-action-btn.copy {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
}
.qr-action-btn.copy:hover {
background: linear-gradient(135deg, #059669, #047857);
transform: translateY(-1px);
}
.qr-action-btn.link {
background: linear-gradient(135deg, #3b82f6, #2563eb);
color: white;
}
.qr-action-btn.link:hover {
background: linear-gradient(135deg, #2563eb, #1d4ed8);
transform: translateY(-1px);
}
/* Check-ins Counter Styling */
.qr-checkins-count {
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: var(--radius-md);
padding: 0.25rem 0.5rem;
font-weight: 500;
color: #1e40af;
}
.qr-checkins-count i {
color: #3b82f6;
}
/* Responsive Action Buttons */
@media (max-width: 768px) {
.qr-actions-compact {
flex-wrap: wrap;
gap: 0.25rem;
}
.qr-action-btn {
min-width: 32px;
height: 32px;
font-size: 0.75rem;
}
}
/* Enhanced QR Actions Layout */
.qr-actions-compact {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
.qr-card-actions {
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
padding: var(--spacing-3);
border-top: 1px solid var(--gray-200);
background: var(--gray-50);
}
+540 -298
View File
@@ -1,21 +1,32 @@
class DashboardManager { /**
* Unified Dashboard JavaScript for QR Code Management
* static/js/dashboard.js
*/
class ProjectDashboardManager {
constructor() { constructor() {
this.expandedProjects = new Set();
this.currentModalQR = null;
this.selectedQRCodes = new Set(); this.selectedQRCodes = new Set();
this.allExpanded = false; this.allExpanded = false;
this.init(); this.initialize();
}
initialize() {
const saved = localStorage.getItem("expandedProjects");
if (saved) {
this.expandedProjects = new Set(JSON.parse(saved));
this.restoreProjectStates();
} }
init() {
this.setupEventListeners(); this.setupEventListeners();
this.addScrollAnimations(); this.addScrollAnimations();
} }
animateOut(element, callback) { restoreProjectStates() {
element.classList.add("fade-out"); this.expandedProjects.forEach((projectId) => {
setTimeout(() => { this.expandProject(projectId, false);
if (callback) callback(); });
element.style.display = "none";
}, 300);
} }
// Setup event listeners // Setup event listeners
@@ -25,6 +36,7 @@ class DashboardManager {
// ESC to close modals // ESC to close modals
if (e.key === "Escape") { if (e.key === "Escape") {
this.closeQRModal(); this.closeQRModal();
this.closeImageLightbox();
} }
// Ctrl/Cmd + F to focus search // Ctrl/Cmd + F to focus search
@@ -63,135 +75,453 @@ class DashboardManager {
{ threshold: 0.1 } { threshold: 0.1 }
); );
const qrItems = document.querySelectorAll(".qr-item"); const qrItems = document.querySelectorAll(".qr-item, .qr-card");
qrItems.forEach((item) => observer.observe(item)); qrItems.forEach((item) => observer.observe(item));
} }
// FIXED: QR Code Toggle Status // Project Management Functions
toggleQRCodeStatus(qrId) { toggleProject(projectId) {
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`); const isExpanded = this.expandedProjects.has(projectId);
if (!qrItem) return;
const currentStatus = qrItem.dataset.status; if (isExpanded) {
const newStatus = currentStatus === "active" ? "inactive" : "active"; this.collapseProject(projectId);
} else {
// Show loading state this.expandProject(projectId);
const toggleBtn = document.getElementById(`toggle-btn-${qrId}`);
const toggleIcon = document.getElementById(`toggle-icon-${qrId}`);
if (toggleBtn && toggleIcon) {
toggleBtn.classList.add("status-loading");
toggleIcon.className = "fas fa-spinner fa-spin";
toggleBtn.disabled = true;
} }
// FIXED: Use correct endpoint with POST method for JSON response this.saveExpandedState();
fetch(`/qr-codes/${qrId}/toggle-status`, { }
expandProject(projectId, animate = true) {
const projectQR = document.getElementById(`project-qr-${projectId}`);
const toggle = document.getElementById(`toggle-${projectId}`);
const header = toggle?.closest(".project-header");
if (projectQR && toggle) {
projectQR.classList.add("expanded");
toggle.classList.add("expanded");
header?.classList.add("expanded");
this.expandedProjects.add(projectId);
if (animate) {
setTimeout(() => {
projectQR.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}, 200);
}
}
}
collapseProject(projectId) {
const projectQR = document.getElementById(`project-qr-${projectId}`);
const toggle = document.getElementById(`toggle-${projectId}`);
const header = toggle?.closest(".project-header");
if (projectQR && toggle) {
projectQR.classList.remove("expanded");
toggle.classList.remove("expanded");
header?.classList.remove("expanded");
this.expandedProjects.delete(projectId);
}
}
saveExpandedState() {
localStorage.setItem(
"expandedProjects",
JSON.stringify([...this.expandedProjects])
);
}
// QR Code Status Toggle
async toggleQRCodeStatus(qrId) {
try {
const response = await fetch(`/qr-codes/${qrId}/toggle-status`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
}, },
}) });
.then((response) => {
if (!response.ok) { if (response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); const result = await response.json();
}
return response.json();
})
.then((result) => {
if (result.success) { if (result.success) {
// Update UI with new status this.showToast(result.message, "success");
this.updateQRStatus(qrId, result.new_status ? "active" : "inactive");
window.showToast(result.message, "success"); // Reload page after short delay to show updated status
setTimeout(() => {
window.location.reload();
}, 1000);
} else { } else {
throw new Error(result.message || "Failed to update status"); throw new Error(result.message || "Failed to toggle QR code status");
} }
}) } else {
.catch((error) => { throw new Error("Failed to toggle QR code status");
console.error("Status update error:", error); }
window.showToast("Failed to update QR code status", "error"); } catch (error) {
}) console.error("Toggle status failed:", error);
.finally(() => { this.showToast("Failed to update QR code status", "error");
// Remove loading state }
if (toggleBtn && toggleIcon) { }
toggleBtn.classList.remove("status-loading");
toggleBtn.disabled = false; // QR Modal Functions
// Restore icon based on current status openQRModalFromData(element) {
const currentStatus = qrItem.dataset.status; const qrData = {
toggleIcon.className = `fas ${ name: element.dataset.qrName,
currentStatus === "active" ? "fa-pause" : "fa-play" image: element.querySelector("img").src,
location: element.dataset.qrLocation,
address: element.dataset.qrAddress,
event: element.dataset.qrEvent,
qr_url: element.dataset.qrUrl,
};
this.openQRModal(qrData);
}
openQRModal(qrData) {
const modal = document.getElementById("qrModal");
const modalImage = document.getElementById("modalQRImage");
const modalTitle = document.getElementById("modalTitle");
const modalQRName = document.getElementById("modalQRName");
const modalQRLocation = document.getElementById("modalQRLocation");
const modalQRAddress = document.getElementById("modalQRAddress");
const modalQREvent = document.getElementById("modalQREvent");
const modalQRDestination = document.getElementById("modalQRDestination");
if (modal && modalImage && modalTitle) {
modalTitle.textContent = `QR Code: ${qrData.name}`;
modalImage.src = qrData.image;
modalImage.alt = `QR Code for ${qrData.name}`;
if (modalQRName) modalQRName.textContent = qrData.name || "-";
if (modalQRLocation) modalQRLocation.textContent = qrData.location || "-";
if (modalQRAddress) modalQRAddress.textContent = qrData.address || "-";
if (modalQREvent)
modalQREvent.textContent = qrData.event || "No event specified";
if (modalQRDestination && qrData.qr_url) {
const destinationUrl = `${window.location.origin}/qr/${qrData.qr_url}`;
const linkElement = modalQRDestination.querySelector("a");
if (linkElement) {
linkElement.href = destinationUrl;
linkElement.innerHTML = `
<i class="fas fa-external-link-alt"></i>
${destinationUrl}
`;
}
} else if (modalQRDestination) {
modalQRDestination.innerHTML =
'<span style="color: var(--gray-500); font-style: italic;">No destination URL available</span>';
}
this.currentModalQR = {
name: qrData.name,
image: qrData.image,
location: qrData.location,
address: qrData.address,
event: qrData.event,
qr_url: qrData.qr_url,
destination_url: qrData.qr_url
? `${window.location.origin}/qr/${qrData.qr_url}`
: null,
};
modal.style.display = "flex";
document.addEventListener("keydown", this.handleModalKeydown.bind(this));
}
}
closeQRModal() {
const modal = document.getElementById("qrModal");
if (modal) {
modal.style.display = "none";
this.currentModalQR = null;
document.removeEventListener(
"keydown",
this.handleModalKeydown.bind(this)
);
}
}
handleModalKeydown(event) {
if (event.key === "Escape") {
this.closeQRModal();
}
}
// Download Functions
downloadModalQR() {
if (this.currentModalQR) {
const base64Data = this.currentModalQR.image.includes("base64,")
? this.currentModalQR.image.split("base64,")[1]
: this.currentModalQR.image;
this.downloadQR(base64Data, this.currentModalQR.name);
}
}
downloadQRFromCard(button) {
const qrCard = button.closest(".qr-card") || button.closest(".qr-item");
const img = qrCard.querySelector("img");
const qrName =
qrCard.dataset.qrName ||
qrCard.querySelector(".qr-name")?.textContent ||
"qr_code";
if (img && img.src) {
const base64Data = img.src.includes("base64,")
? img.src.split("base64,")[1]
: img.src;
this.downloadQR(base64Data, qrName);
}
}
downloadQR(base64Image, filename) {
try {
const base64Data = base64Image.includes("base64,")
? base64Image.split("base64,")[1]
: base64Image;
const link = document.createElement("a");
link.href = `data:image/png;base64,${base64Data}`;
link.download = `${filename
.replace(/[^a-z0-9]/gi, "_")
.toLowerCase()}_qr_code.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.showToast("QR code downloaded successfully!", "success");
} catch (error) {
console.error("Download error:", error);
this.showToast("Failed to download QR code", "error");
}
}
// Copy Functions
copyModalQRData() {
if (this.currentModalQR) {
const data = `QR Code: ${this.currentModalQR.name}\nLocation: ${
this.currentModalQR.location
}\nAddress: ${this.currentModalQR.address}\nEvent: ${
this.currentModalQR.event
}${
this.currentModalQR.destination_url
? `\nQR Link: ${this.currentModalQR.destination_url}`
: ""
}`; }`;
}
navigator.clipboard
.writeText(data)
.then(() => {
this.showToast("QR code information copied to clipboard!", "success");
})
.catch(() => {
this.fallbackCopyText(data);
}); });
} }
// Update QR status in UI
updateQRStatus(qrId, newStatus) {
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
if (!qrItem) return;
// Update data attribute
qrItem.dataset.status = newStatus;
// Update status badge
const statusBadge = qrItem.querySelector(".qr-status");
if (statusBadge) {
statusBadge.className = `qr-status ${newStatus}`;
statusBadge.innerHTML = `
<i class="fas ${
newStatus === "active" ? "fa-check-circle" : "fa-times-circle"
}"></i>
${newStatus === "active" ? "Active" : "Inactive"}
`;
} }
// Update toggle buttons copyQRDestination() {
this.updateToggleButton(qrId, newStatus); if (this.currentModalQR && this.currentModalQR.destination_url) {
navigator.clipboard
.writeText(this.currentModalQR.destination_url)
.then(() => {
this.showToast("QR destination link copied to clipboard!", "success");
})
.catch(() => {
this.showToast("Failed to copy QR link", "error");
});
} else {
this.showToast("No QR destination link available", "warning");
}
} }
// Update toggle button appearance // FIXED: Copy QR Code URL
updateToggleButton(qrId, status) { async copyQRUrl(qrId) {
const toggleBtn = document.getElementById(`toggle-btn-${qrId}`); try {
const toggleIcon = document.getElementById(`toggle-icon-${qrId}`); const response = await fetch(`/qr-codes/${qrId}/copy-url`, {
const detailToggleBtn = document.getElementById( method: "POST",
`detail-toggle-btn-${qrId}` headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data.success && data.url) {
// Copy to clipboard
await navigator.clipboard.writeText(data.url);
this.showToast("QR code URL copied to clipboard!", "success");
// Log the action
console.log(`QR URL copied for ID: ${qrId}`);
} else {
this.showToast("Failed to copy URL", "error");
}
} catch (error) {
console.error("Copy URL error:", error);
this.showToast("Failed to copy URL", "error");
}
}
// FIXED: Open QR Code Link
async openQRLink(qrId) {
try {
const response = await fetch(`/qr-codes/${qrId}/open-link`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data.success && data.url) {
// Open in new tab
window.open(data.url, "_blank");
this.showToast("QR code link opened!", "success");
// Log the action
console.log(`QR link opened for ID: ${qrId}`);
} else {
this.showToast("Failed to open link", "error");
}
} catch (error) {
console.error("Open link error:", error);
this.showToast("Failed to open link", "error");
}
}
fallbackCopyText(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand("copy");
this.showToast("QR code information copied to clipboard!", "success");
} catch (err) {
this.showToast("Failed to copy to clipboard", "error");
}
document.body.removeChild(textArea);
}
// Image Lightbox Functions
openImageLightbox(previewElement, qrName) {
console.log("Opening lightbox for:", qrName); // Debug log
const img = previewElement.querySelector("img");
if (img && img.src) {
const lightbox = document.getElementById("imageLightbox");
const lightboxImage = document.getElementById("lightboxImage");
const lightboxInfo = document.getElementById("lightboxInfo");
console.log(
"Lightbox elements found:",
!!lightbox,
!!lightboxImage,
!!lightboxInfo
); // Debug log
if (lightbox && lightboxImage && lightboxInfo) {
lightboxImage.src = img.src;
lightboxImage.alt = img.alt;
lightboxInfo.textContent = `QR Code: ${qrName}`;
lightbox.style.display = "flex";
console.log("Lightbox should be visible now"); // Debug log
// Add keyboard listener for ESC key
document.addEventListener(
"keydown",
this.handleLightboxKeydown.bind(this)
); );
} else {
if (toggleBtn && toggleIcon) { console.error("Lightbox elements not found");
// Update quick action button
toggleBtn.className = `action-btn btn-status ${
status === "active" ? "btn-deactivate" : "btn-activate"
}`;
toggleBtn.title = `${
status === "active" ? "Deactivate" : "Activate"
} QR Code`;
toggleIcon.className = `fas ${
status === "active" ? "fa-pause" : "fa-play"
}`;
} }
} else {
if (detailToggleBtn) { console.error("Image element not found or no src");
// Update detail action button
detailToggleBtn.className = `btn ${
status === "active" ? "btn-warning" : "btn-success"
}`;
detailToggleBtn.innerHTML = `
<i class="fas ${status === "active" ? "fa-pause" : "fa-play"}"></i>
${status === "active" ? "Deactivate" : "Activate"} QR Code
`;
} }
} }
closeImageLightbox() {
console.log("Closing lightbox"); // Debug log
const lightbox = document.getElementById("imageLightbox");
if (lightbox) {
lightbox.style.display = "none";
// Remove keyboard listener
document.removeEventListener(
"keydown",
this.handleLightboxKeydown.bind(this)
);
}
}
handleLightboxKeydown(event) {
if (event.key === "Escape") {
this.closeImageLightbox();
}
}
// QR Item Toggle functionality (for selection)
toggleQRItem(element) {
const qrId = element.dataset.qrId;
if (this.selectedQRCodes.has(qrId)) {
this.selectedQRCodes.delete(qrId);
element.classList.remove("selected");
} else {
this.selectedQRCodes.add(qrId);
element.classList.add("selected");
}
// Update bulk action buttons if they exist
this.updateBulkActionButtons();
}
updateBulkActionButtons() {
const bulkActions = document.querySelector(".bulk-actions");
if (bulkActions) {
bulkActions.style.display =
this.selectedQRCodes.size > 0 ? "flex" : "none";
}
}
// Copy QR data functionality
copyQRData(name, location, address, event) {
const data = `QR Code: ${name}\nLocation: ${location}\nAddress: ${address}\nEvent: ${event}`;
if (navigator.clipboard) {
navigator.clipboard
.writeText(data)
.then(() =>
this.showToast("QR code information copied to clipboard!", "success")
)
.catch(() => this.fallbackCopyText(data));
} else {
this.fallbackCopyText(data);
}
}
// Delete QR Code
async deleteQRCode(qrId, qrName) { async deleteQRCode(qrId, qrName) {
if (!confirm(`Delete "${qrName}"? This cannot be undone.`)) return; if (!confirm(`Delete "${qrName}"? This cannot be undone.`)) return;
try { try {
// Show loading state // Show loading state
const deleteBtn = document.querySelector(`[onclick*="deleteQRCode(${qrId}"]`); const deleteBtn = document.querySelector(
`[onclick*="deleteQRCode(${qrId}"]`
);
if (deleteBtn) { if (deleteBtn) {
deleteBtn.disabled = true; deleteBtn.disabled = true;
deleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...'; deleteBtn.innerHTML =
'<i class="fas fa-spinner fa-spin"></i> Deleting...';
} }
const response = await fetch(`/qr-codes/${qrId}/delete`, { const response = await fetch(`/qr-codes/${qrId}/delete`, {
@@ -201,7 +531,6 @@ class DashboardManager {
}, },
}); });
// Don't try to parse as JSON - just check if request was successful
if (response.ok) { if (response.ok) {
// Remove QR item from page immediately // Remove QR item from page immediately
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`); const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
@@ -210,188 +539,96 @@ class DashboardManager {
qrItem.style.opacity = "0"; qrItem.style.opacity = "0";
setTimeout(() => { setTimeout(() => {
qrItem.remove(); qrItem.remove();
if (this.updateResultsCount) this.updateResultsCount();
}, 300); }, 300);
} }
// Use simple alert instead of problematic showToast // Show success message
alert(`QR code "${qrName}" deleted successfully!`); this.showToast(`QR code "${qrName}" deleted successfully!`, "success");
} else { } else {
throw new Error(`Server error: ${response.status}`); throw new Error(`Server error: ${response.status}`);
} }
} catch (error) { } catch (error) {
console.error("Delete error:", error); console.error("Delete error:", error);
// Restore button if there was an error // Restore button if there was an error
const deleteBtn = document.querySelector(`[onclick*="deleteQRCode(${qrId}"]`); const deleteBtn = document.querySelector(
`[onclick*="deleteQRCode(${qrId}"]`
);
if (deleteBtn) { if (deleteBtn) {
deleteBtn.disabled = false; deleteBtn.disabled = false;
deleteBtn.innerHTML = '<i class="fas fa-trash"></i>'; deleteBtn.innerHTML = '<i class="fas fa-trash"></i>';
} }
// Use simple alert instead of problematic showToast // Show error message
alert("Failed to delete QR code. Please try again."); this.showToast("Failed to delete QR code. Please try again.", "error");
} }
} }
// Toast notification system
// Show custom delete confirmation dialog showToast(message, type = "info") {
showDeleteConfirmation(qrName) { const toast = document.createElement("div");
return new Promise((resolve) => { toast.style.cssText = `
const modal = document.createElement("div"); position: fixed;
modal.className = "modal"; top: 20px;
modal.style.display = "flex"; right: 20px;
modal.innerHTML = ` background: ${
<div class="modal-content confirmation-modal"> type === "success"
<div class="modal-header"> ? "#10b981"
<h3><i class="fas fa-exclamation-triangle text-warning"></i> Confirm Deletion</h3> : type === "error"
</div> ? "#ef4444"
<div class="modal-body"> : type === "warning"
<p><strong>Are you sure you want to permanently delete "${qrName}"?</strong></p> ? "#f59e0b"
<p class="text-muted">This action cannot be undone.</p> : "#3b82f6"
</div> };
<div class="modal-footer"> color: white;
<button class="btn btn-danger" onclick="confirmDelete()"> padding: 12px 16px;
<i class="fas fa-trash"></i> Delete border-radius: 8px;
</button> z-index: 9999;
<button class="btn btn-secondary" onclick="cancelDelete()">Cancel</button> font-weight: 500;
</div> box-shadow: 0 4px 12px rgba(0,0,0,0.1);
</div> transition: all 0.3s ease;
opacity: 0;
transform: translateX(100%);
`; `;
document.body.appendChild(modal); toast.textContent = message;
document.body.appendChild(toast);
window.confirmDelete = () => { // Animate in
document.body.removeChild(modal); setTimeout(() => {
delete window.confirmDelete; toast.style.opacity = "1";
delete window.cancelDelete; toast.style.transform = "translateX(0)";
resolve(true); }, 100);
};
window.cancelDelete = () => { // Animate out
document.body.removeChild(modal); setTimeout(() => {
delete window.confirmDelete; toast.style.opacity = "0";
delete window.cancelDelete; toast.style.transform = "translateX(100%)";
resolve(false); setTimeout(() => {
}; if (document.body.contains(toast)) {
toast.remove();
// Close on ESC key
const escHandler = (e) => {
if (e.key === "Escape") {
window.cancelDelete();
document.removeEventListener("keydown", escHandler);
}
};
document.addEventListener("keydown", escHandler);
// Close on backdrop click
modal.addEventListener("click", (e) => {
if (e.target === modal) {
window.cancelDelete();
}
});
});
}
// Bulk delete functionality
async bulkDeleteQRCodes() {
if (this.selectedQRCodes.size === 0) return;
const confirmed = await this.showBulkDeleteConfirmation(this.selectedQRCodes.size);
if (!confirmed) return;
const deletePromises = Array.from(this.selectedQRCodes).map(qrId => {
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
const qrName = qrItem ? qrItem.querySelector('.qr-name')?.textContent || 'Unknown' : 'Unknown';
return this.deleteQRCode(qrId, qrName);
});
try {
await Promise.all(deletePromises);
this.selectedQRCodes.clear();
window.showToast(`Successfully deleted ${deletePromises.length} QR codes`, "success");
} catch (error) {
console.error("Bulk delete error:", error);
window.showToast("Some QR codes could not be deleted", "error");
}
}
showBulkDeleteConfirmation(count) {
return new Promise((resolve) => {
const modal = document.createElement("div");
modal.className = "modal";
modal.style.display = "flex";
modal.innerHTML = `
<div class="modal-content confirmation-modal">
<div class="modal-header">
<h3><i class="fas fa-exclamation-triangle text-warning"></i> Confirm Bulk Deletion</h3>
</div>
<div class="modal-body">
<p><strong>Are you sure you want to permanently delete ${count} QR codes?</strong></p>
<p class="text-muted">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button class="btn btn-danger" onclick="confirmBulkDelete()">
<i class="fas fa-trash"></i> Delete All
</button>
<button class="btn btn-secondary" onclick="cancelBulkDelete()">Cancel</button>
</div>
</div>
`;
document.body.appendChild(modal);
window.confirmBulkDelete = () => {
document.body.removeChild(modal);
delete window.confirmBulkDelete;
delete window.cancelBulkDelete;
resolve(true);
};
window.cancelBulkDelete = () => {
document.body.removeChild(modal);
delete window.confirmBulkDelete;
delete window.cancelBulkDelete;
resolve(false);
};
});
}
// QR Modal functions
openQRModal(qrData) {
const modal = document.getElementById("qrModal");
const modalImage = document.getElementById("modalQRImage");
const modalTitle = document.getElementById("modalTitle");
if (modal && modalImage && modalTitle) {
modalTitle.textContent = `QR Code: ${qrData.name}`;
modalImage.src = qrData.image;
modal.style.display = "flex";
}
}
closeQRModal() {
const modal = document.getElementById("qrModal");
if (modal) {
modal.style.display = "none";
} }
}, 300);
}, 3000);
} }
// Expand/collapse functionality // Expand/collapse functionality
toggleExpandAll() { toggleExpandAll() {
this.allExpanded = !this.allExpanded;
const qrItems = document.querySelectorAll(".qr-item"); const qrItems = document.querySelectorAll(".qr-item");
const expandToggle = document.getElementById("expandAllToggle"); const expandToggle = document.getElementById("expandAllToggle");
this.allExpanded = !this.allExpanded;
qrItems.forEach((item) => { qrItems.forEach((item) => {
const details = item.querySelector(".qr-details");
if (details) {
if (this.allExpanded) { if (this.allExpanded) {
details.style.display = "block";
item.classList.add("expanded"); item.classList.add("expanded");
} else { } else {
details.style.display = "none";
item.classList.remove("expanded"); item.classList.remove("expanded");
} }
}
}); });
if (expandToggle) { if (expandToggle) {
@@ -400,58 +637,63 @@ class DashboardManager {
: '<i class="fas fa-expand-alt"></i> Expand All'; : '<i class="fas fa-expand-alt"></i> Expand All';
} }
} }
toggleQRItem(element) {
element.classList.toggle("expanded");
} }
// Copy QR data to clipboard // Global variable to hold dashboard manager instance
copyQRData(name, location, address, event) { let dashboardManager;
const data = `QR Code: ${name}\nLocation: ${location}\nAddress: ${address}\nEvent: ${event}`;
navigator.clipboard
.writeText(data)
.then(() => {
window.showToast("QR code information copied to clipboard!", "success");
})
.catch(() => {
window.showToast("Failed to copy to clipboard", "error");
});
}
// Update results display
updateResultsDisplay(count) {
const resultsDisplay = document.getElementById("resultsDisplay");
if (resultsDisplay) {
resultsDisplay.textContent = `${count} QR codes found`;
}
}
updateResultsCount() {
const qrItems = document.querySelectorAll(
'.qr-item[style*="block"], .qr-item:not([style*="none"])'
);
const counter = document.querySelector(".results-counter");
if (counter) {
counter.textContent = `${qrItems.length} results`;
}
}
}
// Initialize dashboard when DOM is loaded // Initialize dashboard when DOM is loaded
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
window.dashboardManager = new DashboardManager(); dashboardManager = new ProjectDashboardManager();
// Global functions for inline event handlers // Register all global functions for template compatibility
window.toggleProject = (projectId) =>
dashboardManager.toggleProject(projectId);
window.toggleQRCodeStatus = (qrId) => window.toggleQRCodeStatus = (qrId) =>
window.dashboardManager.toggleQRCodeStatus(qrId); dashboardManager.toggleQRCodeStatus(qrId);
window.deleteQRCode = (qrId, qrName) => window.openQRModalFromData = (element) =>
window.dashboardManager.deleteQRCode(qrId, qrName); dashboardManager.openQRModalFromData(element);
window.openQRModal = (qrData) => window.dashboardManager.openQRModal(qrData); window.openQRModal = (qrData) => dashboardManager.openQRModal(qrData);
window.closeQRModal = () => window.dashboardManager.closeQRModal(); window.closeQRModal = () => dashboardManager.closeQRModal();
window.toggleQRItem = (element) => window.downloadModalQR = () => dashboardManager.downloadModalQR();
window.dashboardManager.toggleQRItem(element); window.copyModalQRData = () => dashboardManager.copyModalQRData();
window.copyQRDestination = () => dashboardManager.copyQRDestination();
window.downloadQRFromCard = (button) =>
dashboardManager.downloadQRFromCard(button);
window.openImageLightbox = (element, qrName) =>
dashboardManager.openImageLightbox(element, qrName);
window.closeImageLightbox = () => dashboardManager.closeImageLightbox();
window.toggleQRItem = (element) => dashboardManager.toggleQRItem(element);
window.copyQRData = (name, location, address, event) => window.copyQRData = (name, location, address, event) =>
window.dashboardManager.copyQRData(name, location, address, event); dashboardManager.copyQRData(name, location, address, event);
window.deleteQRCode = (qrId, qrName) =>
dashboardManager.deleteQRCode(qrId, qrName);
// FIXED: Global functions for copy/open link functionality
window.copyQRUrl = function (qrId) {
if (dashboardManager && dashboardManager.copyQRUrl) {
dashboardManager.copyQRUrl(qrId);
} else {
console.error(
"ProjectDashboardManager not initialized or copyQRUrl method missing"
);
}
};
window.openQRLink = function (qrId) {
if (dashboardManager && dashboardManager.openQRLink) {
dashboardManager.openQRLink(qrId);
} else {
console.error(
"ProjectDashboardManager not initialized or openQRLink method missing"
);
}
};
console.log(
"Project Dashboard initialized successfully with copy/open link functionality"
);
console.log("Dashboard keyboard shortcuts:");
console.log("Ctrl/Cmd + F: Focus search");
console.log("Escape: Close modal");
}); });
+4 -78
View File
@@ -55,8 +55,6 @@ const translations = {
// DOM Content Loaded Event (PRESERVED FROM ORIGINAL) // DOM Content Loaded Event (PRESERVED FROM ORIGINAL)
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
console.log("🎯 QR Destination page loaded");
// CRITICAL: Initialize systems in correct order // CRITICAL: Initialize systems in correct order
initializeLanguage(); initializeLanguage();
initializeLocationServicesCheck(); initializeLocationServicesCheck();
@@ -75,22 +73,15 @@ document.addEventListener("DOMContentLoaded", function () {
// ENHANCED STAFF ID PERSISTENCE FUNCTIONALITY // ENHANCED STAFF ID PERSISTENCE FUNCTIONALITY
function initializeStaffIdPersistence() { function initializeStaffIdPersistence() {
console.log("👤 Initializing staff ID persistence functionality");
// Load last staff ID from localStorage // Load last staff ID from localStorage
lastStaffId = loadLastStaffId(); lastStaffId = loadLastStaffId();
if (lastStaffId) { if (lastStaffId) {
console.log(`📱 Found last staff ID: ${lastStaffId}`);
// Automatically fill the last staff ID // Automatically fill the last staff ID
const employeeIdInput = document.getElementById("employee_id"); const employeeIdInput = document.getElementById("employee_id");
if (employeeIdInput) { if (employeeIdInput) {
employeeIdInput.value = lastStaffId; employeeIdInput.value = lastStaffId;
validateEmployeeId(); validateEmployeeId();
console.log(`✅ Auto-filled staff ID: ${lastStaffId}`);
} }
} else {
console.log("📱 No previous staff ID found");
} }
} }
@@ -100,10 +91,8 @@ function loadLastStaffId() {
if (saved && saved.trim().length >= 2) { if (saved && saved.trim().length >= 2) {
return saved.trim().toUpperCase(); return saved.trim().toUpperCase();
} }
console.log("📱 No valid last staff ID found");
return null; return null;
} catch (error) { } catch (error) {
console.error("❌ Error loading last staff ID from localStorage:", error);
return null; return null;
} }
} }
@@ -111,7 +100,6 @@ function loadLastStaffId() {
function saveLastStaffId(staffId) { function saveLastStaffId(staffId) {
try { try {
if (!staffId || typeof staffId !== "string" || staffId.trim().length < 2) { if (!staffId || typeof staffId !== "string" || staffId.trim().length < 2) {
console.log("⚠️ Invalid staff ID, not saving");
return false; return false;
} }
@@ -120,11 +108,9 @@ function saveLastStaffId(staffId) {
// Save to localStorage // Save to localStorage
localStorage.setItem("qr_last_staff_id", cleanId); localStorage.setItem("qr_last_staff_id", cleanId);
console.log(`💾 Last staff ID saved: ${cleanId}`);
return true; return true;
} catch (error) { } catch (error) {
console.error("❌ Error saving last staff ID to localStorage:", error);
return false; return false;
} }
} }
@@ -155,14 +141,9 @@ function handleFormSubmit(event) {
event.preventDefault(); event.preventDefault();
if (isSubmitting) { if (isSubmitting) {
console.log(
"⏳ Check-in already in progress, ignoring duplicate submission"
);
return; return;
} }
console.log("🎯 Form submission triggered");
const employeeId = document.getElementById("employee_id")?.value?.trim(); const employeeId = document.getElementById("employee_id")?.value?.trim();
if (!employeeId) { if (!employeeId) {
@@ -187,10 +168,7 @@ function handleFormSubmit(event) {
// ENHANCED CHECK-IN SUBMISSION WITH MULTIPLE CHECK-IN SUPPORT // ENHANCED CHECK-IN SUBMISSION WITH MULTIPLE CHECK-IN SUPPORT
function submitCheckin() { function submitCheckin() {
console.log("🚀 Starting check-in submission process");
if (isSubmitting) { if (isSubmitting) {
console.log("⏳ Already submitting, aborting");
return; return;
} }
@@ -206,9 +184,6 @@ function submitCheckin() {
return; return;
} }
console.log(`👤 Employee ID: ${employeeId}`);
console.log(`📍 User location:`, userLocation);
// Prepare form data // Prepare form data
const formData = new FormData(); const formData = new FormData();
formData.append("employee_id", employeeId); formData.append("employee_id", employeeId);
@@ -228,8 +203,6 @@ function submitCheckin() {
const currentUrl = window.location.pathname; const currentUrl = window.location.pathname;
const checkinUrl = `${currentUrl}/checkin`; const checkinUrl = `${currentUrl}/checkin`;
console.log("🎯 Submitting to URL:", checkinUrl);
fetch(checkinUrl, { fetch(checkinUrl, {
method: "POST", method: "POST",
body: formData, body: formData,
@@ -238,15 +211,12 @@ function submitCheckin() {
}, },
}) })
.then((response) => { .then((response) => {
console.log("📡 Server response status:", response.status);
return response.json(); return response.json();
}) })
.then((data) => { .then((data) => {
console.log("📊 Server response data:", data);
handleCheckinResponse(data); handleCheckinResponse(data);
}) })
.catch((error) => { .catch((error) => {
console.error("❌ Check-in error:", error);
handleCheckinError(error); handleCheckinError(error);
}) })
.finally(() => { .finally(() => {
@@ -261,7 +231,6 @@ function handleCheckinResponse(data) {
handleCheckinSuccess(data); handleCheckinSuccess(data);
} else { } else {
const errorMsg = data.message || "Submission failed"; const errorMsg = data.message || "Submission failed";
console.log("❌ Submission failed:", errorMsg);
// NEW: Handle different types of check-in failures // NEW: Handle different types of check-in failures
if (errorMsg.toLowerCase().includes("already submitted")) { if (errorMsg.toLowerCase().includes("already submitted")) {
@@ -280,8 +249,6 @@ function handleCheckinResponse(data) {
// ENHANCED SUCCESS HANDLING WITH MULTIPLE CHECK-IN INFO // ENHANCED SUCCESS HANDLING WITH MULTIPLE CHECK-IN INFO
function handleCheckinSuccess(data) { function handleCheckinSuccess(data) {
console.log("✅ Submitted successfully!");
const responseData = data.data || data || {}; const responseData = data.data || data || {};
const checkinCount = responseData.checkin_count_today || 1; const checkinCount = responseData.checkin_count_today || 1;
const checkinSequence = responseData.checkin_sequence || "Check-in"; const checkinSequence = responseData.checkin_sequence || "Check-in";
@@ -384,8 +351,6 @@ function addCheckInAgainOption() {
// NEW: Reset form for new check-in // NEW: Reset form for new check-in
function resetForNewCheckin() { function resetForNewCheckin() {
console.log("🔄 Resetting for new check-in");
// Show form again // Show form again
const form = document.getElementById("checkinForm"); const form = document.getElementById("checkinForm");
if (form) { if (form) {
@@ -510,10 +475,11 @@ function validateEmployeeId() {
// All location and language functions remain unchanged from original // All location and language functions remain unchanged from original
function initializeLocation() { function initializeLocation() {
console.log("📍 Initializing location services");
// Check if Android enhanced location handler is available // Check if Android enhanced location handler is available
if (typeof AndroidLocationHandler !== 'undefined' && AndroidLocationHandler.isAndroidDevice()) { if (
typeof AndroidLocationHandler !== "undefined" &&
AndroidLocationHandler.isAndroidDevice()
) {
console.log("📱 Using Android-enhanced location initialization"); console.log("📱 Using Android-enhanced location initialization");
AndroidLocationHandler.initializeAndroidLocation(); AndroidLocationHandler.initializeAndroidLocation();
} else { } else {
@@ -578,8 +544,6 @@ function handleLocationSuccess(position) {
address: null, address: null,
}; };
console.log("📍 Location data:", userLocation);
// Reverse geocode to get address // Reverse geocode to get address
reverseGeocode(userLocation.latitude, userLocation.longitude); reverseGeocode(userLocation.latitude, userLocation.longitude);
@@ -587,14 +551,11 @@ function handleLocationSuccess(position) {
} }
function handleLocationError(error) { function handleLocationError(error) {
console.log("❌ Location error:", error.message);
userLocation.source = "manual"; userLocation.source = "manual";
locationRequestActive = false; locationRequestActive = false;
} }
function reverseGeocode(lat, lng) { function reverseGeocode(lat, lng) {
console.log(`🌍 Reverse geocoding for: ${lat}, ${lng}`);
// The server will use Google Maps API first, then fall back to OpenStreetMap // The server will use Google Maps API first, then fall back to OpenStreetMap
// This provides better accuracy and address formatting // This provides better accuracy and address formatting
const url = "/api/reverse-geocode"; // You may want to create this endpoint const url = "/api/reverse-geocode"; // You may want to create this endpoint
@@ -613,9 +574,7 @@ function reverseGeocode(lat, lng) {
.then((data) => { .then((data) => {
if (data && data.display_name) { if (data && data.display_name) {
userLocation.address = data.display_name; userLocation.address = data.display_name;
console.log(`✅ Reverse geocoded address: ${userLocation.address}`);
} else { } else {
console.log(`⚠️ No address found, using coordinates as fallback`);
userLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(10)}`; userLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(10)}`;
} }
}) })
@@ -628,13 +587,10 @@ function reverseGeocode(lat, lng) {
// ENHANCED LANGUAGE FUNCTIONALITY WITH PERSISTENCE // ENHANCED LANGUAGE FUNCTIONALITY WITH PERSISTENCE
function initializeLanguage() { function initializeLanguage() {
console.log("🌐 Initializing language functionality with persistence");
// Load saved language preference from localStorage // Load saved language preference from localStorage
const savedLanguage = loadLanguagePreference(); const savedLanguage = loadLanguagePreference();
if (savedLanguage && savedLanguage !== currentLanguage) { if (savedLanguage && savedLanguage !== currentLanguage) {
currentLanguage = savedLanguage; currentLanguage = savedLanguage;
console.log(`📱 Restored saved language preference: ${currentLanguage}`);
} }
// Set up language toggle button event listener // Set up language toggle button event listener
@@ -645,7 +601,6 @@ function initializeLanguage() {
// Apply initial translations based on loaded language // Apply initial translations based on loaded language
applyTranslations(); applyTranslations();
console.log(`✅ Language system initialized with: ${currentLanguage}`);
} }
function toggleLanguage() { function toggleLanguage() {
@@ -659,10 +614,6 @@ function toggleLanguage() {
// Apply translations immediately // Apply translations immediately
applyTranslations(); applyTranslations();
console.log(
`🌐 Language switched to: ${currentLanguage} (saved to localStorage)`
);
// Optional: Show brief confirmation message // Optional: Show brief confirmation message
showLanguageChangeConfirmation(); showLanguageChangeConfirmation();
} }
@@ -674,24 +625,14 @@ function loadLanguagePreference() {
// Validate saved language is supported // Validate saved language is supported
if (savedLanguage && translations.hasOwnProperty(savedLanguage)) { if (savedLanguage && translations.hasOwnProperty(savedLanguage)) {
console.log(`📱 Found saved language preference: ${savedLanguage}`);
return savedLanguage; return savedLanguage;
} else if (savedLanguage) { } else if (savedLanguage) {
console.log(
`⚠️ Invalid saved language preference: ${savedLanguage}, using default`
);
// Clean up invalid preference // Clean up invalid preference
localStorage.removeItem("qr_staff_language"); localStorage.removeItem("qr_staff_language");
} else {
console.log("📱 No saved language preference found, using default");
} }
return null; return null;
} catch (error) { } catch (error) {
console.error(
"❌ Error loading language preference from localStorage:",
error
);
return null; return null;
} }
} }
@@ -706,13 +647,8 @@ function saveLanguagePreference(language) {
// Save to localStorage // Save to localStorage
localStorage.setItem("qr_staff_language", language); localStorage.setItem("qr_staff_language", language);
console.log(`💾 Language preference saved: ${language}`);
return true; return true;
} catch (error) { } catch (error) {
console.error(
"❌ Error saving language preference to localStorage:",
error
);
return false; return false;
} }
} }
@@ -774,18 +710,14 @@ function startClock() {
} }
function checkLocationServicesStatus() { function checkLocationServicesStatus() {
console.log("📱 Checking location services status...");
// Check if geolocation is supported // Check if geolocation is supported
if (!navigator.geolocation) { if (!navigator.geolocation) {
console.log("❌ Geolocation not supported by this browser");
showLocationServicesWarning("not_supported"); showLocationServicesWarning("not_supported");
return; return;
} }
// Test location access with a quick check // Test location access with a quick check
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
console.log("⏰ Location permission check timed out");
showLocationServicesWarning("timeout"); showLocationServicesWarning("timeout");
}, 3000); // 3 second timeout }, 3000); // 3 second timeout
@@ -793,13 +725,11 @@ function checkLocationServicesStatus() {
(position) => { (position) => {
// Success - location services are working // Success - location services are working
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.log("✅ Location services are available and enabled");
hideLocationServicesWarning(); hideLocationServicesWarning();
}, },
(error) => { (error) => {
// Error - location services may be disabled // Error - location services may be disabled
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.log("❌ Location services error:", error.message);
switch (error.code) { switch (error.code) {
case error.PERMISSION_DENIED: case error.PERMISSION_DENIED:
@@ -894,9 +824,6 @@ function showLocationServicesWarning(errorType) {
if (container) { if (container) {
container.insertBefore(warningBanner, container.firstChild); container.insertBefore(warningBanner, container.firstChild);
} }
// Log warning event
console.log(`⚠️ Location services warning displayed: ${errorType}`);
} }
/** /**
@@ -906,7 +833,6 @@ function hideLocationServicesWarning() {
const existingWarning = document.getElementById("locationServicesWarning"); const existingWarning = document.getElementById("locationServicesWarning");
if (existingWarning) { if (existingWarning) {
existingWarning.remove(); existingWarning.remove();
console.log("✅ Location services warning hidden");
} }
} }
+6 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}QR Code Management System{% endblock %}</title> <title>{% block title %}{{ COMPANY_NAME }}{% endblock %}</title>
<!-- Main CSS --> <!-- Main CSS -->
<link <link
@@ -87,7 +87,11 @@
</script> </script>
{% if turnstile_enabled %} {% if turnstile_enabled %}
<!-- Cloudflare Turnstile --> <!-- Cloudflare Turnstile -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer
></script>
{% endif %} {% endif %}
</body> </body>
</html> </html>
+17 -10
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}QR Code Management System{% endblock %}</title> <title>{% block title %}{{ COMPANY_NAME }}{% endblock %}</title>
<!-- Main CSS --> <!-- Main CSS -->
<link <link
@@ -55,8 +55,10 @@
</a> </a>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<a href="{{ url_for('projects') }}" <a
class="menu-item {% if request.endpoint in ['projects', 'create_project', 'edit_project'] %}active{% endif %}"> href="{{ url_for('projects') }}"
class="menu-item {% if request.endpoint in ['projects', 'create_project', 'edit_project'] %}active{% endif %}"
>
<i class="fas fa-folder"></i> <i class="fas fa-folder"></i>
<span class="menu-text">Projects</span> <span class="menu-text">Projects</span>
</a> </a>
@@ -76,12 +78,17 @@
<i class="fas fa-calculator"></i> <i class="fas fa-calculator"></i>
<span class="menu-text">Payroll</span> <span class="menu-text">Payroll</span>
</a> </a>
<a href="{{ url_for('qr_statistics') }}" class="menu-item {% if request.endpoint == 'qr_statistics' %}active{% endif %}"> <a
href="{{ url_for('qr_statistics') }}"
class="menu-item {% if request.endpoint == 'qr_statistics' %}active{% endif %}"
>
<i class="fas fa-chart-pie"></i> <i class="fas fa-chart-pie"></i>
<span class="menu-text">Statistics</span> <span class="menu-text">Statistics</span>
</a> </a>
<a href="{{ url_for('admin_logs') }}" <a
class="menu-item {% if request.endpoint == 'admin_logs' %}active{% endif %}"> href="{{ url_for('admin_logs') }}"
class="menu-item {% if request.endpoint == 'admin_logs' %}active{% endif %}"
>
<i class="fas fa-clipboard-list"></i> <i class="fas fa-clipboard-list"></i>
<span class="menu-text">System Logs</span> <span class="menu-text">System Logs</span>
</a> </a>
@@ -137,7 +144,9 @@
<span class="hamburger-line"></span> <span class="hamburger-line"></span>
<span class="hamburger-line"></span> <span class="hamburger-line"></span>
</button> </button>
<h1 class="page-title">{% block page_title %}QR Code Management System{% endblock %}</h1> <h1 class="page-title">
{% block page_title %}{{ COMPANY_NAME }}{% endblock %}
</h1>
</div> </div>
<div class="header-right"> <div class="header-right">
@@ -172,9 +181,7 @@
{% endif %} {% endwith %} {% endif %} {% endwith %}
<!-- Page Content --> <!-- Page Content -->
<div class="container"> <div class="container">{% block content %}{% endblock %}</div>
{% block content %}{% endblock %}
</div>
</main> </main>
<!-- Footer --> <!-- Footer -->
+196 -435
View File
@@ -1,8 +1,9 @@
{% extends "base_authenticated.html" %} {% extends "base_authenticated.html" %} {% block title %}Dashboard - QR Code
{% block title %}Dashboard - QR Code Management{% endblock %} Management{% endblock %} {% block extra_head %}
<link
{% block extra_head %} rel="stylesheet"
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"> href="{{ url_for('static', filename='css/dashboard.css') }}"
/>
<style> <style>
/* Project-Centric Dashboard Styles */ /* Project-Centric Dashboard Styles */
.projects-dashboard { .projects-dashboard {
@@ -770,7 +771,7 @@
.qr-preview-large::after, .qr-preview-large::after,
.qr-preview-compact::after { .qr-preview-compact::after {
content: '🔍'; content: "🔍";
position: absolute; position: absolute;
top: 2px; top: 2px;
right: 2px; right: 2px;
@@ -794,8 +795,12 @@
/* Animations */ /* Animations */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
@keyframes slideIn { @keyframes slideIn {
@@ -957,9 +962,7 @@
} }
} }
</style> </style>
{% endblock %} {% endblock %} {% block content %}
{% block content %}
<div class="dashboard-container"> <div class="dashboard-container">
<!-- Dashboard Header --> <!-- Dashboard Header -->
<div class="dashboard-header"> <div class="dashboard-header">
@@ -968,87 +971,87 @@
<p>Manage your QR codes organized by projects</p> <p>Manage your QR codes organized by projects</p>
<div class="user-info-badge"> <div class="user-info-badge">
<div class="role-indicator {{ session.role }}"> <div class="role-indicator {{ session.role }}">
<i class="fas {{ 'fa-crown' if session.role == 'admin' else 'fa-user' }}"></i> <i
class="fas {{ 'fa-crown' if session.role == 'admin' else 'fa-user' }}"
></i>
{{ session.role.title() }} {{ session.role.title() }}
</div> </div>
{% if session.last_login_date %} {% if session.last_login_date %}
<div class="last-login"> <div class="last-login">
<i class="fas fa-clock"></i> <i class="fas fa-clock"></i>
Last login: {{ session.last_login_date.strftime('%b %d, %Y at %H:%M') }} Last login: {{ session.last_login_date.strftime('%b %d, %Y at %H:%M')
}}
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="quick-actions">
<a href="{{ url_for('create_qr_code') }}" class="btn btn-primary btn-lg">
<i class="fas fa-plus"></i>
Create QR Code
</a>
{% if session.role == 'admin' %}
<a href="{{ url_for('users') }}" class="btn btn-secondary btn-lg">
<i class="fas fa-users"></i>
Manage Users
</a>
{% endif %}
</div>
</div>
<!-- Statistics Section --> <!-- QR Statistics in Header -->
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<div class="stats-section"> <div class="header-stats">
<div class="stats-grid"> <div class="header-stats-grid">
<div class="stat-card primary"> <div class="header-stat-card primary">
<div class="stat-icon"> <div class="header-stat-icon">
<i class="fas fa-qrcode"></i> <i class="fas fa-qrcode"></i>
</div> </div>
<div class="stat-content"> <div class="header-stat-content">
<h3>{{ qr_codes|length }}</h3> <h3>{{ qr_codes|length }}</h3>
<p>Total QR Codes</p> <p>Total QR Codes</p>
</div> </div>
</div> </div>
<div class="stat-card success"> <div class="header-stat-card success">
<div class="stat-icon"> <div class="header-stat-icon">
<i class="fas fa-check-circle"></i> <i class="fas fa-check-circle"></i>
</div> </div>
<div class="stat-content"> <div class="header-stat-content">
<h3>{{ qr_codes|selectattr('active_status', 'equalto', True)|list|length }}</h3> <h3>
{{ qr_codes|selectattr('active_status', 'equalto',
True)|list|length }}
</h3>
<p>Active QR Codes</p> <p>Active QR Codes</p>
</div> </div>
</div> </div>
<div class="stat-card danger"> <div class="header-stat-card danger">
<div class="stat-icon"> <div class="header-stat-icon">
<i class="fas fa-pause-circle"></i> <i class="fas fa-pause-circle"></i>
</div> </div>
<div class="stat-content"> <div class="header-stat-content">
<h3>{{ qr_codes|selectattr('active_status', 'equalto', False)|list|length }}</h3> <h3>
{{ qr_codes|selectattr('active_status', 'equalto',
False)|list|length }}
</h3>
<p>Inactive QR Codes</p> <p>Inactive QR Codes</p>
</div> </div>
</div> </div>
<div class="stat-card warning"> <div class="header-stat-card info">
<div class="stat-icon"> <div class="header-stat-icon">
<i class="fas fa-folder"></i> <i class="fas fa-folder"></i>
</div> </div>
<div class="stat-content"> <div class="header-stat-content">
<h3>{{ projects|length }}</h3> <h3>{{ projects|length if projects else 0 }}</h3>
<p>Total Projects</p> <p>Total Projects</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div>
<!-- Projects Dashboard --> <!-- Projects Dashboard -->
{% if projects %} {% if projects %}
<div class="projects-dashboard"> <div class="projects-dashboard">
{% for project in projects %} {% for project in projects %} {% set project_qr_codes =
{% set project_qr_codes = qr_codes|selectattr('project_id', 'equalto', project.id)|list %} qr_codes|selectattr('project_id', 'equalto', project.id)|list %}
<div class="project-list-item"> <div class="project-list-item">
<!-- Project Header --> <!-- Project Header -->
<div class="project-header" onclick="toggleProject({{ project.id }})"> <div class="project-header" onclick="toggleProject({{ project.id }})">
<div class="project-info"> <div class="project-info">
<div class="project-icon {{ 'inactive' if not project.active_status }}"> <div
<i class="fas fa-folder{{ '-open' if project.active_status else '' }}"></i> class="project-icon {{ 'inactive' if not project.active_status }}"
>
<i
class="fas fa-folder{{ '-open' if project.active_status else '' }}"
></i>
</div> </div>
<div class="project-details"> <div class="project-details">
<h3 class="project-name">{{ project.name }}</h3> <h3 class="project-name">{{ project.name }}</h3>
@@ -1062,9 +1065,14 @@
<div class="project-stats"> <div class="project-stats">
<div class="project-qr-count"> <div class="project-qr-count">
<i class="fas fa-qrcode"></i> <i class="fas fa-qrcode"></i>
<span>{{ project_qr_codes|length }} QR Code{{ 's' if project_qr_codes|length != 1 else '' }}</span> <span
>{{ project_qr_codes|length }} QR Code{{ 's' if
project_qr_codes|length != 1 else '' }}</span
>
</div> </div>
<span class="project-status {{ 'active' if project.active_status else 'inactive' }}"> <span
class="project-status {{ 'active' if project.active_status else 'inactive' }}"
>
{{ 'Active' if project.active_status else 'Inactive' }} {{ 'Active' if project.active_status else 'Inactive' }}
</span> </span>
</div> </div>
@@ -1079,18 +1087,26 @@
{% if project_qr_codes %} {% if project_qr_codes %}
<div class="project-qr-grid"> <div class="project-qr-grid">
{% for qr in project_qr_codes %} {% for qr in project_qr_codes %}
<div class="qr-card {{ 'inactive' if not qr.active_status }}" <div
class="qr-card {{ 'inactive' if not qr.active_status }}"
data-qr-id="{{ qr.id }}" data-qr-id="{{ qr.id }}"
data-qr-name="{{ qr.name }}" data-qr-name="{{ qr.name }}"
data-qr-location="{{ qr.location }}" data-qr-location="{{ qr.location }}"
data-qr-address="{{ qr.location_address }}" data-qr-address="{{ qr.location_address }}"
data-qr-event="{{ qr.location_event }}" data-qr-event="{{ qr.location_event }}"
data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}" data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}"
onclick="openQRModalFromData(this)"> onclick="openQRModalFromData(this)"
>
<div class="qr-card-layout"> <div class="qr-card-layout">
<div class="qr-preview-large" onclick="event.stopPropagation(); openImageLightbox(this, '{{ qr.name }}')"> <div
<img src="data:image/png;base64,{{ qr.qr_code_image }}" alt="QR Code for {{ qr.name }}" loading="lazy"> class="qr-preview-large"
onclick="event.stopPropagation(); openImageLightbox(this, '{{ qr.name }}')"
>
<img
src="data:image/png;base64,{{ qr.qr_code_image }}"
alt="QR Code for {{ qr.name }}"
loading="lazy"
/>
</div> </div>
<div class="qr-details"> <div class="qr-details">
@@ -1107,32 +1123,65 @@
</div> </div>
{% endif %} {% endif %}
{% if qr.qr_url %} <div class="qr-detail-item qr-checkins-count">
<div class="qr-detail-item"> <i class="fas fa-users"></i>
<i class="fas fa-link"></i> <span>{{ get_qr_code_checkin_count(qr.id) }} check-ins</span>
<span>{{ qr.qr_url }}</span>
</div> </div>
{% endif %}
<span class="qr-status-badge {{ 'active' if qr.active_status else 'inactive' }}"> <span
<i class="fas {{ 'fa-check-circle' if qr.active_status else 'fa-pause-circle' }}"></i> class="qr-status-badge {{ 'active' if qr.active_status else 'inactive' }}"
>
<i
class="fas {{ 'fa-check-circle' if qr.active_status else 'fa-pause-circle' }}"
></i>
{{ 'Active' if qr.active_status else 'Inactive' }} {{ 'Active' if qr.active_status else 'Inactive' }}
</span> </span>
</div> </div>
</div> </div>
<div class="qr-card-actions"> <div class="qr-card-actions">
<button class="qr-action-btn download" onclick="event.stopPropagation(); downloadQRFromCard(this)" title="Download QR Code"> <button
class="qr-action-btn copy"
onclick="event.stopPropagation(); copyQRUrl({{ qr.id }})"
title="Copy QR Code URL"
>
<i class="fas fa-copy"></i>
</button>
<button
class="qr-action-btn link"
onclick="event.stopPropagation(); openQRLink({{ qr.id }})"
title="Open QR Code Link"
>
<i class="fas fa-external-link-alt"></i>
</button>
<button
class="qr-action-btn download"
onclick="event.stopPropagation(); downloadQRFromCard(this)"
title="Download QR Code"
>
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
</button> </button>
<a href="{{ url_for('edit_qr_code', qr_id=qr.id) }}" class="qr-action-btn edit" onclick="event.stopPropagation()" title="Edit QR Code"> <a
href="{{ url_for('edit_qr_code', qr_id=qr.id) }}"
class="qr-action-btn edit"
onclick="event.stopPropagation()"
title="Edit QR Code"
>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<button class="qr-action-btn toggle" onclick="event.stopPropagation(); toggleQRCodeStatus({{ qr.id }})" title="{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code"> <button
<i class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"></i> class="qr-action-btn toggle"
onclick="event.stopPropagation(); toggleQRCodeStatus({{ qr.id }})"
title="{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code"
>
<i
class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"
></i>
</button> </button>
{% endif %} {% endif %}
</div> </div>
@@ -1159,31 +1208,42 @@
{% endif %} {% endif %}
<!-- Unassigned QR Codes Section --> <!-- Unassigned QR Codes Section -->
{% set unassigned_qr_codes = qr_codes|selectattr('project_id', 'equalto', None)|list %} {% set unassigned_qr_codes = qr_codes|selectattr('project_id', 'equalto',
{% if unassigned_qr_codes %} None)|list %} {% if unassigned_qr_codes %}
<div class="unassigned-qr-section"> <div class="unassigned-qr-section">
<div class="unassigned-header"> <div class="unassigned-header">
<h3 class="unassigned-title"> <h3 class="unassigned-title">
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
Unassigned QR Codes Unassigned QR Codes
</h3> </h3>
<span class="unassigned-count">{{ unassigned_qr_codes|length }} QR Code{{ 's' if unassigned_qr_codes|length != 1 else '' }}</span> <span class="unassigned-count"
>{{ unassigned_qr_codes|length }} QR Code{{ 's' if
unassigned_qr_codes|length != 1 else '' }}</span
>
</div> </div>
<div class="project-qr-grid"> <div class="project-qr-grid">
{% for qr in unassigned_qr_codes %} {% for qr in unassigned_qr_codes %}
<div class="qr-card {{ 'inactive' if not qr.active_status }}" <div
class="qr-card {{ 'inactive' if not qr.active_status }}"
data-qr-id="{{ qr.id }}" data-qr-id="{{ qr.id }}"
data-qr-name="{{ qr.name }}" data-qr-name="{{ qr.name }}"
data-qr-location="{{ qr.location }}" data-qr-location="{{ qr.location }}"
data-qr-address="{{ qr.location_address }}" data-qr-address="{{ qr.location_address }}"
data-qr-event="{{ qr.location_event }}" data-qr-event="{{ qr.location_event }}"
data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}" data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}"
onclick="openQRModalFromData(this)"> onclick="openQRModalFromData(this)"
>
<div class="qr-card-compact"> <div class="qr-card-compact">
<div class="qr-preview-compact" onclick="event.stopPropagation(); openImageLightbox(this, '{{ qr.name }}')"> <div
<img src="data:image/png;base64,{{ qr.qr_code_image }}" alt="QR Code for {{ qr.name }}" loading="lazy"> class="qr-preview-compact"
onclick="event.stopPropagation(); openImageLightbox(this, '{{ qr.name }}')"
>
<img
src="data:image/png;base64,{{ qr.qr_code_image }}"
alt="QR Code for {{ qr.name }}"
loading="lazy"
/>
</div> </div>
<div class="qr-info-compact"> <div class="qr-info-compact">
@@ -1201,34 +1261,77 @@
</div> </div>
{% endif %} {% endif %}
<div class="qr-detail-item qr-checkins-count">
<i class="fas fa-users"></i>
<span>{{ get_qr_code_checkin_count(qr.id) }} check-ins</span>
</div>
{% if qr.qr_url %} {% if qr.qr_url %}
<div class="qr-detail-item"> <div class="qr-detail-item">
<i class="fas fa-link"></i> <i class="fas fa-link"></i>
<span class="qr-url-short">{{ qr.qr_url[:50] }}{% if qr.qr_url|length > 50 %}...{% endif %}</span> <span class="qr-url-short"
>{{ qr.qr_url[:50] }}{% if qr.qr_url|length > 50 %}...{% endif
%}</span
>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="qr-status-compact"> <div class="qr-status-compact">
<span class="qr-status-badge {{ 'active' if qr.active_status else 'inactive' }}"> <span
<i class="fas {{ 'fa-check-circle' if qr.active_status else 'fa-pause-circle' }}"></i> class="qr-status-badge {{ 'active' if qr.active_status else 'inactive' }}"
>
<i
class="fas {{ 'fa-check-circle' if qr.active_status else 'fa-pause-circle' }}"
></i>
{{ 'Active' if qr.active_status else 'Inactive' }} {{ 'Active' if qr.active_status else 'Inactive' }}
</span> </span>
</div> </div>
<div class="qr-actions-compact"> <div class="qr-actions-compact">
<button class="qr-action-btn download" onclick="event.stopPropagation(); downloadQRFromCard(this)" title="Download QR Code"> <button
class="qr-action-btn copy"
onclick="event.stopPropagation(); copyQRUrl({{ qr.id }})"
title="Copy QR Code URL"
>
<i class="fas fa-copy"></i>
</button>
<button
class="qr-action-btn link"
onclick="event.stopPropagation(); openQRLink({{ qr.id }})"
title="Open QR Code Link"
>
<i class="fas fa-external-link-alt"></i>
</button>
<button
class="qr-action-btn download"
onclick="event.stopPropagation(); downloadQRFromCard(this)"
title="Download QR Code"
>
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
</button> </button>
<a href="{{ url_for('edit_qr_code', qr_id=qr.id) }}" class="qr-action-btn edit" onclick="event.stopPropagation()" title="Edit QR Code"> <a
href="{{ url_for('edit_qr_code', qr_id=qr.id) }}"
class="qr-action-btn edit"
onclick="event.stopPropagation()"
title="Edit QR Code"
>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<button class="qr-action-btn toggle" onclick="event.stopPropagation(); toggleQRCodeStatus({{ qr.id }})" title="{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code"> <button
<i class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"></i> class="qr-action-btn toggle"
onclick="event.stopPropagation(); toggleQRCodeStatus({{ qr.id }})"
title="{{ 'Deactivate' if qr.active_status else 'Activate' }} QR Code"
>
<i
class="fas {{ 'fa-pause' if qr.active_status else 'fa-play' }}"
></i>
</button> </button>
{% endif %} {% endif %}
</div> </div>
@@ -1246,8 +1349,13 @@
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
</div> </div>
<h3>Welcome to Your QR Code Dashboard</h3> <h3>Welcome to Your QR Code Dashboard</h3>
<p>Get started by creating your first project and QR codes to organize your digital assets effectively.</p> <p>
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;"> Get started by creating your first project and QR codes to organize your
digital assets effectively.
</p>
<div
style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap"
>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<a href="{{ url_for('create_project') }}" class="btn btn-primary"> <a href="{{ url_for('create_project') }}" class="btn btn-primary">
<i class="fas fa-folder-plus"></i> <i class="fas fa-folder-plus"></i>
@@ -1269,7 +1377,7 @@
<button class="lightbox-close" onclick="closeImageLightbox()"> <button class="lightbox-close" onclick="closeImageLightbox()">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
<img id="lightboxImage" src="" alt="" class="lightbox-image"> <img id="lightboxImage" src="" alt="" class="lightbox-image" />
<div class="lightbox-info" id="lightboxInfo"></div> <div class="lightbox-info" id="lightboxInfo"></div>
<div class="lightbox-hint">Click anywhere outside the image to close</div> <div class="lightbox-hint">Click anywhere outside the image to close</div>
</div> </div>
@@ -1287,7 +1395,7 @@
<div class="modal-body qr-modal-body"> <div class="modal-body qr-modal-body">
<div class="qr-modal-layout"> <div class="qr-modal-layout">
<div class="qr-modal-image"> <div class="qr-modal-image">
<img id="modalQRImage" src="" alt="QR Code"> <img id="modalQRImage" src="" alt="QR Code" />
</div> </div>
<div class="qr-modal-info"> <div class="qr-modal-info">
@@ -1348,353 +1456,6 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% block extra_scripts %}
<script> <script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
class ProjectDashboardManager {
constructor() {
this.expandedProjects = new Set();
this.currentModalQR = null;
this.initialize();
}
initialize() {
const saved = localStorage.getItem('expandedProjects');
if (saved) {
this.expandedProjects = new Set(JSON.parse(saved));
this.restoreProjectStates();
}
}
restoreProjectStates() {
this.expandedProjects.forEach(projectId => {
this.expandProject(projectId, false);
});
}
toggleProject(projectId) {
const isExpanded = this.expandedProjects.has(projectId);
if (isExpanded) {
this.collapseProject(projectId);
} else {
this.expandProject(projectId);
}
this.saveExpandedState();
}
expandProject(projectId, animate = true) {
const projectQR = document.getElementById(`project-qr-${projectId}`);
const toggle = document.getElementById(`toggle-${projectId}`);
const header = toggle?.closest('.project-header');
if (projectQR && toggle) {
projectQR.classList.add('expanded');
toggle.classList.add('expanded');
header?.classList.add('expanded');
this.expandedProjects.add(projectId);
if (animate) {
setTimeout(() => {
projectQR.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}, 200);
}
}
}
collapseProject(projectId) {
const projectQR = document.getElementById(`project-qr-${projectId}`);
const toggle = document.getElementById(`toggle-${projectId}`);
const header = toggle?.closest('.project-header');
if (projectQR && toggle) {
projectQR.classList.remove('expanded');
toggle.classList.remove('expanded');
header?.classList.remove('expanded');
this.expandedProjects.delete(projectId);
}
}
saveExpandedState() {
localStorage.setItem('expandedProjects', JSON.stringify([...this.expandedProjects]));
}
async toggleQRCodeStatus(qrId) {
try {
const response = await fetch(`/qr-codes/${qrId}/toggle-status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const result = await response.json();
if (result.success) {
if (window.showToast) {
window.showToast(result.message, 'success');
}
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
throw new Error(result.message || 'Failed to toggle QR code status');
}
} else {
throw new Error('Failed to toggle QR code status');
}
} catch (error) {
console.error('Toggle status failed:', error);
if (window.showToast) {
window.showToast('Failed to update QR code status', 'error');
}
}
}
openQRModalFromData(element) {
const qrData = {
name: element.dataset.qrName,
image: element.querySelector('img').src,
location: element.dataset.qrLocation,
address: element.dataset.qrAddress,
event: element.dataset.qrEvent,
qr_url: element.dataset.qrUrl
};
this.openQRModal(qrData);
}
openQRModal(qrData) {
const modal = document.getElementById('qrModal');
const modalImage = document.getElementById('modalQRImage');
const modalTitle = document.getElementById('modalTitle');
const modalQRName = document.getElementById('modalQRName');
const modalQRLocation = document.getElementById('modalQRLocation');
const modalQRAddress = document.getElementById('modalQRAddress');
const modalQREvent = document.getElementById('modalQREvent');
const modalQRDestination = document.getElementById('modalQRDestination');
if (modal && modalImage && modalTitle) {
modalTitle.textContent = `QR Code: ${qrData.name}`;
modalImage.src = qrData.image;
modalImage.alt = `QR Code for ${qrData.name}`;
if (modalQRName) modalQRName.textContent = qrData.name || '-';
if (modalQRLocation) modalQRLocation.textContent = qrData.location || '-';
if (modalQRAddress) modalQRAddress.textContent = qrData.address || '-';
if (modalQREvent) modalQREvent.textContent = qrData.event || 'No event specified';
if (modalQRDestination && qrData.qr_url) {
const destinationUrl = `${window.location.origin}/qr/${qrData.qr_url}`;
const linkElement = modalQRDestination.querySelector('a');
if (linkElement) {
linkElement.href = destinationUrl;
linkElement.innerHTML = `
<i class="fas fa-external-link-alt"></i>
${destinationUrl}
`;
}
} else if (modalQRDestination) {
modalQRDestination.innerHTML = '<span style="color: var(--gray-500); font-style: italic;">No destination URL available</span>';
}
this.currentModalQR = {
name: qrData.name,
image: qrData.image,
location: qrData.location,
address: qrData.address,
event: qrData.event,
qr_url: qrData.qr_url,
destination_url: qrData.qr_url ? `${window.location.origin}/qr/${qrData.qr_url}` : null
};
modal.style.display = 'flex';
document.addEventListener('keydown', this.handleModalKeydown.bind(this));
}
}
closeQRModal() {
const modal = document.getElementById('qrModal');
if (modal) {
modal.style.display = 'none';
this.currentModalQR = null;
document.removeEventListener('keydown', this.handleModalKeydown.bind(this));
}
}
handleModalKeydown(event) {
if (event.key === 'Escape') {
this.closeQRModal();
}
}
downloadQRFromCard(button) {
const qrCard = button.closest('.qr-card');
const img = qrCard.querySelector('img');
const qrName = qrCard.dataset.qrName;
if (img && img.src) {
const base64Data = img.src.split('base64,')[1];
this.downloadQR(base64Data, qrName);
}
}
downloadQR(base64Image, filename) {
try {
const base64Data = base64Image.includes('base64,')
? base64Image.split('base64,')[1]
: base64Image;
const link = document.createElement('a');
link.href = `data:image/png;base64,${base64Data}`;
link.download = `${filename.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_qr_code.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
if (window.showToast) {
window.showToast('QR code downloaded successfully!', 'success');
}
} catch (error) {
console.error('Download error:', error);
if (window.showToast) {
window.showToast('Failed to download QR code', 'error');
}
}
}
downloadModalQR() {
if (this.currentModalQR) {
const base64Data = this.currentModalQR.image.split('base64,')[1];
this.downloadQR(base64Data, this.currentModalQR.name);
}
}
copyModalQRData() {
if (this.currentModalQR) {
const data = `QR Code: ${this.currentModalQR.name}\nLocation: ${this.currentModalQR.location}\nAddress: ${this.currentModalQR.address}\nEvent: ${this.currentModalQR.event}${this.currentModalQR.destination_url ? `\nQR Link: ${this.currentModalQR.destination_url}` : ''}`;
navigator.clipboard.writeText(data)
.then(() => {
if (window.showToast) {
window.showToast('QR code information copied to clipboard!', 'success');
}
})
.catch(() => {
const textArea = document.createElement('textarea');
textArea.value = data;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
if (window.showToast) {
window.showToast('QR code information copied to clipboard!', 'success');
}
} catch (err) {
if (window.showToast) {
window.showToast('Failed to copy to clipboard', 'error');
}
}
document.body.removeChild(textArea);
});
}
}
copyQRDestination() {
if (this.currentModalQR && this.currentModalQR.destination_url) {
navigator.clipboard.writeText(this.currentModalQR.destination_url)
.then(() => {
if (window.showToast) {
window.showToast('QR destination link copied to clipboard!', 'success');
}
})
.catch(() => {
if (window.showToast) {
window.showToast('Failed to copy QR link', 'error');
}
});
} else {
if (window.showToast) {
window.showToast('No QR destination link available', 'warning');
}
}
}
// Image Lightbox Functions
openImageLightbox(previewElement, qrName) {
console.log('Opening lightbox for:', qrName); // Debug log
const img = previewElement.querySelector('img');
if (img && img.src) {
const lightbox = document.getElementById('imageLightbox');
const lightboxImage = document.getElementById('lightboxImage');
const lightboxInfo = document.getElementById('lightboxInfo');
console.log('Lightbox elements found:', !!lightbox, !!lightboxImage, !!lightboxInfo); // Debug log
if (lightbox && lightboxImage && lightboxInfo) {
lightboxImage.src = img.src;
lightboxImage.alt = img.alt;
lightboxInfo.textContent = `QR Code: ${qrName}`;
lightbox.style.display = 'flex';
console.log('Lightbox should be visible now'); // Debug log
// Add keyboard listener for ESC key
document.addEventListener('keydown', this.handleLightboxKeydown.bind(this));
} else {
console.error('Lightbox elements not found');
}
} else {
console.error('Image element not found or no src');
}
}
closeImageLightbox() {
console.log('Closing lightbox'); // Debug log
const lightbox = document.getElementById('imageLightbox');
if (lightbox) {
lightbox.style.display = 'none';
// Remove keyboard listener
document.removeEventListener('keydown', this.handleLightboxKeydown.bind(this));
}
}
handleLightboxKeydown(event) {
if (event.key === 'Escape') {
this.closeImageLightbox();
}
}
}
let dashboardManager;
document.addEventListener('DOMContentLoaded', function() {
dashboardManager = new ProjectDashboardManager();
window.toggleProject = (projectId) => dashboardManager.toggleProject(projectId);
window.toggleQRCodeStatus = (qrId) => dashboardManager.toggleQRCodeStatus(qrId);
window.openQRModalFromData = (element) => dashboardManager.openQRModalFromData(element);
window.closeQRModal = () => dashboardManager.closeQRModal();
window.downloadModalQR = () => dashboardManager.downloadModalQR();
window.copyModalQRData = () => dashboardManager.copyModalQRData();
window.copyQRDestination = () => dashboardManager.copyQRDestination();
window.downloadQRFromCard = (button) => dashboardManager.downloadQRFromCard(button);
window.openImageLightbox = (element, qrName) => dashboardManager.openImageLightbox(element, qrName);
window.closeImageLightbox = () => dashboardManager.closeImageLightbox();
console.log('Project Dashboard initialized successfully');
});
</script>
{% endblock %} {% endblock %}
+4 -79
View File
@@ -874,9 +874,6 @@
// Initialize the page when DOM is loaded // Initialize the page when DOM is loaded
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
console.log("🚀 QR Destination page loaded successfully");
console.log("📱 Enhanced with color-coded bilingual support");
// Initialize Employee ID auto-fill FIRST (before other functionality) // Initialize Employee ID auto-fill FIRST (before other functionality)
initializeEmployeeIdAutoFill(); initializeEmployeeIdAutoFill();
@@ -894,32 +891,23 @@
// FIXED Employee ID auto-fill functionality // FIXED Employee ID auto-fill functionality
function initializeEmployeeIdAutoFill() { function initializeEmployeeIdAutoFill() {
console.log("👤 Initializing Employee ID auto-fill functionality");
const employeeIdInput = document.getElementById("employee_id"); const employeeIdInput = document.getElementById("employee_id");
if (!employeeIdInput) { if (!employeeIdInput) {
console.log("❌ Employee ID input not found");
return; return;
} }
// Load and auto-fill last used Employee ID immediately // Load and auto-fill last used Employee ID immediately
try { try {
const lastEmployeeId = localStorage.getItem("qr_last_employee_id"); const lastEmployeeId = localStorage.getItem("qr_last_employee_id");
console.log(
`📱 Checking localStorage for last employee ID: ${lastEmployeeId}`
);
if (lastEmployeeId && lastEmployeeId.trim() !== "") { if (lastEmployeeId && lastEmployeeId.trim() !== "") {
employeeIdInput.value = lastEmployeeId.trim(); employeeIdInput.value = lastEmployeeId.trim();
console.log(`✅ Auto-filled employee ID: ${lastEmployeeId}`);
// Visual feedback that it's auto-filled // Visual feedback that it's auto-filled
employeeIdInput.style.backgroundColor = "#f0f9ff"; employeeIdInput.style.backgroundColor = "#f0f9ff";
setTimeout(() => { setTimeout(() => {
employeeIdInput.style.backgroundColor = ""; employeeIdInput.style.backgroundColor = "";
}, 2000); }, 2000);
} else {
console.log(`📱 No previous employee ID found`);
} }
} catch (error) { } catch (error) {
console.log(`❌ Error loading employee ID: ${error.message}`); console.log(`❌ Error loading employee ID: ${error.message}`);
@@ -932,43 +920,38 @@
// Save if at least 2 characters // Save if at least 2 characters
try { try {
localStorage.setItem("qr_last_employee_id", currentId); localStorage.setItem("qr_last_employee_id", currentId);
console.log(`💾 Saved employee ID: ${currentId}`);
} catch (error) { } catch (error) {
console.log(`❌ Error saving employee ID: ${error.message}`); console.log(`❌ Error saving employee ID: ${error.message}`);
} }
} }
}); });
console.log("✅ Employee ID auto-fill initialized successfully");
} }
// Initialize location services (renamed to avoid conflicts) // Initialize location services (renamed to avoid conflicts)
function initializeLocationCapture() { function initializeLocationCapture() {
console.log("📍 Initializing enhanced location capture");
requestUserLocationData(); requestUserLocationData();
} }
// Request location data from device (renamed to avoid conflicts) // Request location data from device (renamed to avoid conflicts)
function requestUserLocationData() { function requestUserLocationData() {
if (typeof AndroidLocationHandler !== 'undefined' && AndroidLocationHandler.isAndroidDevice()) { if (
console.log("📱 Using Android-enhanced location request"); typeof AndroidLocationHandler !== "undefined" &&
AndroidLocationHandler.isAndroidDevice()
) {
AndroidLocationHandler.initializeAndroidLocation(); AndroidLocationHandler.initializeAndroidLocation();
return; return;
} }
if (locationCaptureActive) { if (locationCaptureActive) {
console.log("📍 Location request already active, skipping");
return; return;
} }
if (!navigator.geolocation) { if (!navigator.geolocation) {
console.log("❌ Geolocation not supported");
currentUserLocation.source = "manual"; currentUserLocation.source = "manual";
return; return;
} }
locationCaptureActive = true; locationCaptureActive = true;
console.log("📍 Requesting enhanced location data...");
const options = { const options = {
enableHighAccuracy: true, enableHighAccuracy: true,
@@ -979,8 +962,6 @@
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
handleEnhancedLocationSuccess, handleEnhancedLocationSuccess,
(error) => { (error) => {
console.log("❌ High accuracy failed, trying low accuracy...");
// Simple fallback with low accuracy // Simple fallback with low accuracy
const lowAccuracyOptions = { const lowAccuracyOptions = {
enableHighAccuracy: false, enableHighAccuracy: false,
@@ -1000,8 +981,6 @@
// Handle successful location capture (renamed to avoid conflicts) // Handle successful location capture (renamed to avoid conflicts)
function handleEnhancedLocationSuccess(position) { function handleEnhancedLocationSuccess(position) {
console.log("✅ Enhanced location obtained successfully");
currentUserLocation = { currentUserLocation = {
latitude: position.coords.latitude, latitude: position.coords.latitude,
longitude: position.coords.longitude, longitude: position.coords.longitude,
@@ -1012,8 +991,6 @@
address: null, address: null,
}; };
console.log("📍 Enhanced location data:", currentUserLocation);
// Reverse geocode to get address // Reverse geocode to get address
reverseGeocodeEnhanced( reverseGeocodeEnhanced(
currentUserLocation.latitude, currentUserLocation.latitude,
@@ -1025,17 +1002,12 @@
// Handle location capture errors (renamed to avoid conflicts) // Handle location capture errors (renamed to avoid conflicts)
function handleEnhancedLocationError(error) { function handleEnhancedLocationError(error) {
console.log("❌ Enhanced location error:", error.message);
currentUserLocation.source = "manual"; currentUserLocation.source = "manual";
locationCaptureActive = false; locationCaptureActive = false;
} }
// Reverse geocode coordinates to address (renamed to avoid conflicts) // Reverse geocode coordinates to address (renamed to avoid conflicts)
function reverseGeocodeEnhanced(lat, lng) { function reverseGeocodeEnhanced(lat, lng) {
console.log(
`🌍 Starting enhanced reverse geocoding for: ${lat}, ${lng}`
);
// Using Nominatim (OpenStreetMap) reverse geocoding service // Using Nominatim (OpenStreetMap) reverse geocoding service
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&addressdetails=1&zoom=18`; const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&addressdetails=1&zoom=18`;
@@ -1049,11 +1021,7 @@
.then((data) => { .then((data) => {
if (data && data.display_name) { if (data && data.display_name) {
currentUserLocation.address = data.display_name; currentUserLocation.address = data.display_name;
console.log(
`✅ Enhanced reverse geocoded address: ${currentUserLocation.address}`
);
} else { } else {
console.log(`⚠️ No address found, using coordinates as fallback`);
currentUserLocation.address = `${lat.toFixed(10)}, ${lng.toFixed( currentUserLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(
10 10
)}`; )}`;
@@ -1073,10 +1041,6 @@
const form = document.getElementById("checkinForm"); const form = document.getElementById("checkinForm");
const submitButton = document.getElementById("submitButton"); const submitButton = document.getElementById("submitButton");
console.log("🔧 Initializing enhanced form handling...");
console.log("Form element:", form);
console.log("Submit button:", submitButton);
if (form && submitButton) { if (form && submitButton) {
// Remove any existing event listeners // Remove any existing event listeners
form.removeEventListener("submit", handleEnhancedFormSubmission); form.removeEventListener("submit", handleEnhancedFormSubmission);
@@ -1087,36 +1051,24 @@
// Add form submit event listener // Add form submit event listener
form.addEventListener("submit", function (e) { form.addEventListener("submit", function (e) {
console.log("📝 Enhanced form submit event triggered");
e.preventDefault(); e.preventDefault();
handleEnhancedFormSubmission(); handleEnhancedFormSubmission();
}); });
// Also add click event listener to button as backup // Also add click event listener to button as backup
submitButton.addEventListener("click", function (e) { submitButton.addEventListener("click", function (e) {
console.log("🖱️ Enhanced button click event triggered");
e.preventDefault(); e.preventDefault();
handleEnhancedFormSubmission(); handleEnhancedFormSubmission();
}); });
console.log("✅ Enhanced form handling initialized successfully");
} else {
console.error("❌ Form or submit button not found!");
} }
} }
// Handle form submission with location data (renamed to avoid conflicts) // Handle form submission with location data (renamed to avoid conflicts)
function handleEnhancedFormSubmission() { function handleEnhancedFormSubmission() {
console.log("🚀 handleEnhancedFormSubmission called");
const employeeId = document.getElementById("employee_id").value.trim(); const employeeId = document.getElementById("employee_id").value.trim();
const submitButton = document.getElementById("submitButton"); const submitButton = document.getElementById("submitButton");
console.log("Employee ID:", employeeId);
console.log("Submit button:", submitButton);
if (!employeeId) { if (!employeeId) {
console.log("❌ No employee ID provided");
showStatusMessage( showStatusMessage(
"Please enter your Employee ID / Por favor ingrese su ID de empleado", "Please enter your Employee ID / Por favor ingrese su ID de empleado",
"error" "error"
@@ -1124,14 +1076,9 @@
return; return;
} }
console.log(
"✅ Employee ID valid, proceeding with enhanced submission"
);
// Save Employee ID to localStorage for next time // Save Employee ID to localStorage for next time
try { try {
localStorage.setItem("qr_last_employee_id", employeeId); localStorage.setItem("qr_last_employee_id", employeeId);
console.log(`💾 Saved employee ID for next time: ${employeeId}`);
} catch (error) { } catch (error) {
console.log(`❌ Error saving employee ID: ${error.message}`); console.log(`❌ Error saving employee ID: ${error.message}`);
} }
@@ -1156,9 +1103,6 @@
const pathParts = window.location.pathname.split("/"); const pathParts = window.location.pathname.split("/");
const qrUrl = pathParts[pathParts.length - 1]; const qrUrl = pathParts[pathParts.length - 1];
console.log("QR URL:", qrUrl);
console.log("Current enhanced location data:", currentUserLocation);
// Prepare form data with location information // Prepare form data with location information
const formData = new FormData(); const formData = new FormData();
formData.append("employee_id", employeeId); formData.append("employee_id", employeeId);
@@ -1184,27 +1128,15 @@
} }
formData.append("location_source", currentUserLocation.source); formData.append("location_source", currentUserLocation.source);
console.log("📤 Submitting enhanced form with location data:", {
employee_id: employeeId,
latitude: currentUserLocation.latitude,
longitude: currentUserLocation.longitude,
accuracy: currentUserLocation.accuracy,
altitude: currentUserLocation.altitude,
address: currentUserLocation.address,
location_source: currentUserLocation.source,
});
// Submit to backend // Submit to backend
fetch(`/qr/${qrUrl}/checkin`, { fetch(`/qr/${qrUrl}/checkin`, {
method: "POST", method: "POST",
body: formData, body: formData,
}) })
.then((response) => { .then((response) => {
console.log("📨 Received enhanced response:", response);
return response.json(); return response.json();
}) })
.then((data) => { .then((data) => {
console.log("📨 Enhanced response data:", data);
if (data.success) { if (data.success) {
showStatusMessage( showStatusMessage(
"Submission successful! / Envío exitoso", "Submission successful! / Envío exitoso",
@@ -1348,13 +1280,6 @@
// Show success card // Show success card
successCard.classList.add("show"); successCard.classList.add("show");
console.log("✅ Enhanced success card displayed with:", {
employeeId: employeeId,
locationEvent: locationEvent,
qrLocation: qrLocationName,
timestamp: data.timestamp || new Date().toLocaleString(),
});
} }
</script> </script>
</body> </body>