diff --git a/advanced_security_middleware.py b/advanced_security_middleware.py new file mode 100644 index 0000000..981b848 --- /dev/null +++ b/advanced_security_middleware.py @@ -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' 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'] \ No newline at end of file diff --git a/app.py b/app.py index a8abe60..b9f38f8 100644 --- a/app.py +++ b/app.py @@ -18,6 +18,8 @@ from payroll_excel_exporter import PayrollExcelExporter # Load environment variables in .env load_dotenv() from turnstile_utils import turnstile_utils +from db_performance_optimization import initialize_performance_optimizations +from app_performance_middleware import PerformanceMonitor # Initialize Flask application app = Flask(__name__) @@ -29,6 +31,13 @@ app.config['TEMPLATES_AUTO_RELOAD'] = os.environ.get('TEMPLATES_AUTO_RELOAD') # Initialize database 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(): """Create performance optimization indexes""" try: @@ -1621,6 +1630,102 @@ def dashboard(): flash('Error loading dashboard. Please try again.', 'error') 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 @app.route('/profile', methods=['GET', 'POST']) @login_required @@ -3378,7 +3483,7 @@ def delete_qr_code(qr_id): print(f"❌ Traceback: {traceback.format_exc()}") flash('Error deleting QR code. Please try again.', 'error') return redirect(url_for('dashboard')) - + @app.route('/qr/') def qr_destination(qr_url): """QR code destination page where staff check in - PRESERVING EXACT ROUTE""" @@ -3687,6 +3792,52 @@ def toggle_qr_status(qr_id): 'message': 'Error updating QR code status. Please try again.' }), 500 +@app.route('/qr-codes//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//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//activate', methods=['POST']) @login_required 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}") 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 def inject_payroll_utils(): """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" } +@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') @login_required def qr_statistics(): @@ -6048,6 +6216,7 @@ def employee_detail(employee_index): create_location_logging_routes(app, db, logger_handler) + # Jinja2 filters for better template functionality @app.template_filter('days_since') def days_since_filter(date): @@ -6440,22 +6609,16 @@ if __name__ == '__main__': # Initialize database and logging create_tables() - # Add performance optimizations - create_performance_indexes() - create_audit_triggers() + # Initialize performance optimizations + print("πŸš€ Initializing performance optimizations...") + 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 logger_handler.logger.info("QR Attendance Management System started successfully") diff --git a/app_performance_middleware.py b/app_performance_middleware.py new file mode 100644 index 0000000..73c9823 --- /dev/null +++ b/app_performance_middleware.py @@ -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 \ No newline at end of file diff --git a/db_performance_optimization.py b/db_performance_optimization.py new file mode 100644 index 0000000..7f73910 --- /dev/null +++ b/db_performance_optimization.py @@ -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 \ No newline at end of file diff --git a/static/css/dashboard.css b/static/css/dashboard.css index a5fb2db..8b736bb 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -4,31 +4,163 @@ */ /* Dashboard Header */ -.dashboard-header { +.header-stats { display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: var(--spacing-8); - padding: var(--spacing-6); - background: var(--white); - border-radius: var(--radius-xl); - box-shadow: var(--shadow); + align-items: center; +} + +.header-stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + 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; overflow: hidden; } -.dashboard-header::before { +.header-stat-card::before { content: ""; position: absolute; top: 0; left: 0; right: 0; - height: 4px; - background: linear-gradient( - 90deg, - var(--primary-color), - var(--primary-hover) - ); + height: 2px; +} + +.header-stat-card.primary::before { + 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 { @@ -892,7 +1024,7 @@ } .qr-url { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace; background: var(--gray-100); padding: var(--spacing-1) var(--spacing-2); border-radius: var(--radius-sm); @@ -927,7 +1059,8 @@ flex-wrap: wrap; } -.qr-status, .qr-project { +.qr-status, +.qr-project { display: flex; align-items: center; gap: var(--spacing-1); @@ -1088,8 +1221,12 @@ /* Animations */ @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } @keyframes slideIn { @@ -1108,28 +1245,95 @@ .detail-grid { grid-template-columns: 1fr; } - + .qr-actions { flex-direction: column; } - + .qr-actions .btn { width: 100%; justify-content: center; } - + .bulk-actions-bar { flex-direction: column; text-align: center; } - + .bulk-actions-buttons { width: 100%; justify-content: center; } - + .modal-content { margin: var(--spacing-4); 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); +} diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 2059702..cd47e2f 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -1,21 +1,32 @@ -class DashboardManager { +/** + * Unified Dashboard JavaScript for QR Code Management + * static/js/dashboard.js + */ + +class ProjectDashboardManager { constructor() { + this.expandedProjects = new Set(); + this.currentModalQR = null; this.selectedQRCodes = new Set(); this.allExpanded = false; - this.init(); + this.initialize(); } - init() { + initialize() { + const saved = localStorage.getItem("expandedProjects"); + if (saved) { + this.expandedProjects = new Set(JSON.parse(saved)); + this.restoreProjectStates(); + } + this.setupEventListeners(); this.addScrollAnimations(); } - animateOut(element, callback) { - element.classList.add("fade-out"); - setTimeout(() => { - if (callback) callback(); - element.style.display = "none"; - }, 300); + restoreProjectStates() { + this.expandedProjects.forEach((projectId) => { + this.expandProject(projectId, false); + }); } // Setup event listeners @@ -25,6 +36,7 @@ class DashboardManager { // ESC to close modals if (e.key === "Escape") { this.closeQRModal(); + this.closeImageLightbox(); } // Ctrl/Cmd + F to focus search @@ -63,135 +75,453 @@ class DashboardManager { { threshold: 0.1 } ); - const qrItems = document.querySelectorAll(".qr-item"); + const qrItems = document.querySelectorAll(".qr-item, .qr-card"); qrItems.forEach((item) => observer.observe(item)); } - // FIXED: QR Code Toggle Status - toggleQRCodeStatus(qrId) { - const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`); - if (!qrItem) return; + // Project Management Functions + toggleProject(projectId) { + const isExpanded = this.expandedProjects.has(projectId); - const currentStatus = qrItem.dataset.status; - const newStatus = currentStatus === "active" ? "inactive" : "active"; - - // Show loading state - 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; + if (isExpanded) { + this.collapseProject(projectId); + } else { + this.expandProject(projectId); } - // FIXED: Use correct endpoint with POST method for JSON response - fetch(`/qr-codes/${qrId}/toggle-status`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Requested-With": "XMLHttpRequest", - }, - }) - .then((response) => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.json(); - }) - .then((result) => { - if (result.success) { - // Update UI with new status - this.updateQRStatus(qrId, result.new_status ? "active" : "inactive"); - window.showToast(result.message, "success"); - } else { - throw new Error(result.message || "Failed to update status"); - } - }) - .catch((error) => { - console.error("Status update error:", error); - window.showToast("Failed to update QR code status", "error"); - }) - .finally(() => { - // Remove loading state - if (toggleBtn && toggleIcon) { - toggleBtn.classList.remove("status-loading"); - toggleBtn.disabled = false; - // Restore icon based on current status - const currentStatus = qrItem.dataset.status; - toggleIcon.className = `fas ${ - currentStatus === "active" ? "fa-pause" : "fa-play" - }`; - } - }); + this.saveExpandedState(); } - // Update QR status in UI - updateQRStatus(qrId, newStatus) { - const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`); - if (!qrItem) return; + expandProject(projectId, animate = true) { + const projectQR = document.getElementById(`project-qr-${projectId}`); + const toggle = document.getElementById(`toggle-${projectId}`); + const header = toggle?.closest(".project-header"); - // Update data attribute - qrItem.dataset.status = newStatus; + if (projectQR && toggle) { + projectQR.classList.add("expanded"); + toggle.classList.add("expanded"); + header?.classList.add("expanded"); + this.expandedProjects.add(projectId); - // Update status badge - const statusBadge = qrItem.querySelector(".qr-status"); - if (statusBadge) { - statusBadge.className = `qr-status ${newStatus}`; - statusBadge.innerHTML = ` - - ${newStatus === "active" ? "Active" : "Inactive"} - `; + if (animate) { + setTimeout(() => { + projectQR.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + }, 200); + } } - - // Update toggle buttons - this.updateToggleButton(qrId, newStatus); } - // Update toggle button appearance - updateToggleButton(qrId, status) { - const toggleBtn = document.getElementById(`toggle-btn-${qrId}`); - const toggleIcon = document.getElementById(`toggle-icon-${qrId}`); - const detailToggleBtn = document.getElementById( - `detail-toggle-btn-${qrId}` + 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]) ); + } - if (toggleBtn && toggleIcon) { - // 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" - }`; - } + // QR Code Status Toggle + 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 (detailToggleBtn) { - // Update detail action button - detailToggleBtn.className = `btn ${ - status === "active" ? "btn-warning" : "btn-success" - }`; - detailToggleBtn.innerHTML = ` - - ${status === "active" ? "Deactivate" : "Activate"} QR Code - `; + if (response.ok) { + const result = await response.json(); + if (result.success) { + this.showToast(result.message, "success"); + + // Reload page after short delay to show updated status + 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); + this.showToast("Failed to update QR code status", "error"); } } + // QR Modal Functions + 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 = ` + + ${destinationUrl} + `; + } + } else if (modalQRDestination) { + modalQRDestination.innerHTML = + 'No destination URL available'; + } + + 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); + }); + } + } + + copyQRDestination() { + 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"); + } + } + + // FIXED: Copy QR Code URL + async copyQRUrl(qrId) { + try { + const response = await fetch(`/qr-codes/${qrId}/copy-url`, { + method: "POST", + 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 { + 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(); + } + } + + // 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) { if (!confirm(`Delete "${qrName}"? This cannot be undone.`)) return; try { // Show loading state - const deleteBtn = document.querySelector(`[onclick*="deleteQRCode(${qrId}"]`); + const deleteBtn = document.querySelector( + `[onclick*="deleteQRCode(${qrId}"]` + ); if (deleteBtn) { deleteBtn.disabled = true; - deleteBtn.innerHTML = ' Deleting...'; + deleteBtn.innerHTML = + ' Deleting...'; } 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) { // Remove QR item from page immediately const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`); @@ -210,187 +539,95 @@ class DashboardManager { qrItem.style.opacity = "0"; setTimeout(() => { qrItem.remove(); - if (this.updateResultsCount) this.updateResultsCount(); }, 300); } - // Use simple alert instead of problematic showToast - alert(`QR code "${qrName}" deleted successfully!`); - + // Show success message + this.showToast(`QR code "${qrName}" deleted successfully!`, "success"); } else { throw new Error(`Server error: ${response.status}`); } - } catch (error) { console.error("Delete error:", error); - + // Restore button if there was an error - const deleteBtn = document.querySelector(`[onclick*="deleteQRCode(${qrId}"]`); + const deleteBtn = document.querySelector( + `[onclick*="deleteQRCode(${qrId}"]` + ); if (deleteBtn) { deleteBtn.disabled = false; deleteBtn.innerHTML = ''; } - - // Use simple alert instead of problematic showToast - alert("Failed to delete QR code. Please try again."); + + // Show error message + this.showToast("Failed to delete QR code. Please try again.", "error"); } } - - // Show custom delete confirmation dialog - showDeleteConfirmation(qrName) { - return new Promise((resolve) => { - const modal = document.createElement("div"); - modal.className = "modal"; - modal.style.display = "flex"; - modal.innerHTML = ` - - `; - - document.body.appendChild(modal); - - window.confirmDelete = () => { - document.body.removeChild(modal); - delete window.confirmDelete; - delete window.cancelDelete; - resolve(true); + // Toast notification system + showToast(message, type = "info") { + const toast = document.createElement("div"); + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${ + type === "success" + ? "#10b981" + : type === "error" + ? "#ef4444" + : type === "warning" + ? "#f59e0b" + : "#3b82f6" }; + color: white; + padding: 12px 16px; + border-radius: 8px; + z-index: 9999; + font-weight: 500; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + transition: all 0.3s ease; + opacity: 0; + transform: translateX(100%); + `; - window.cancelDelete = () => { - document.body.removeChild(modal); - delete window.confirmDelete; - delete window.cancelDelete; - resolve(false); - }; + toast.textContent = message; + document.body.appendChild(toast); - // Close on ESC key - const escHandler = (e) => { - if (e.key === "Escape") { - window.cancelDelete(); - document.removeEventListener("keydown", escHandler); + // Animate in + setTimeout(() => { + toast.style.opacity = "1"; + toast.style.transform = "translateX(0)"; + }, 100); + + // Animate out + setTimeout(() => { + toast.style.opacity = "0"; + toast.style.transform = "translateX(100%)"; + setTimeout(() => { + if (document.body.contains(toast)) { + toast.remove(); } - }; - 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 = ` - - `; - - 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 toggleExpandAll() { + this.allExpanded = !this.allExpanded; const qrItems = document.querySelectorAll(".qr-item"); const expandToggle = document.getElementById("expandAllToggle"); - this.allExpanded = !this.allExpanded; - qrItems.forEach((item) => { - if (this.allExpanded) { - item.classList.add("expanded"); - } else { - item.classList.remove("expanded"); + const details = item.querySelector(".qr-details"); + if (details) { + if (this.allExpanded) { + details.style.display = "block"; + item.classList.add("expanded"); + } else { + details.style.display = "none"; + item.classList.remove("expanded"); + } } }); @@ -400,58 +637,63 @@ class DashboardManager { : ' Expand All'; } } - - toggleQRItem(element) { - element.classList.toggle("expanded"); - } - - // Copy QR data to clipboard - copyQRData(name, location, address, event) { - 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`; - } - } } +// Global variable to hold dashboard manager instance +let dashboardManager; + // Initialize dashboard when DOM is loaded 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.dashboardManager.toggleQRCodeStatus(qrId); - window.deleteQRCode = (qrId, qrName) => - window.dashboardManager.deleteQRCode(qrId, qrName); - window.openQRModal = (qrData) => window.dashboardManager.openQRModal(qrData); - window.closeQRModal = () => window.dashboardManager.closeQRModal(); - window.toggleQRItem = (element) => - window.dashboardManager.toggleQRItem(element); + dashboardManager.toggleQRCodeStatus(qrId); + window.openQRModalFromData = (element) => + dashboardManager.openQRModalFromData(element); + window.openQRModal = (qrData) => dashboardManager.openQRModal(qrData); + 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(); + window.toggleQRItem = (element) => dashboardManager.toggleQRItem(element); window.copyQRData = (name, location, address, event) => - window.dashboardManager.copyQRData(name, location, address, event); -}); \ No newline at end of file + 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"); +}); diff --git a/static/js/qr_destination.js b/static/js/qr_destination.js index 89f45a8..0ccc6c1 100644 --- a/static/js/qr_destination.js +++ b/static/js/qr_destination.js @@ -55,8 +55,6 @@ const translations = { // DOM Content Loaded Event (PRESERVED FROM ORIGINAL) document.addEventListener("DOMContentLoaded", function () { - console.log("🎯 QR Destination page loaded"); - // CRITICAL: Initialize systems in correct order initializeLanguage(); initializeLocationServicesCheck(); @@ -75,22 +73,15 @@ document.addEventListener("DOMContentLoaded", function () { // ENHANCED STAFF ID PERSISTENCE FUNCTIONALITY function initializeStaffIdPersistence() { - console.log("πŸ‘€ Initializing staff ID persistence functionality"); - // Load last staff ID from localStorage lastStaffId = loadLastStaffId(); if (lastStaffId) { - console.log(`πŸ“± Found last staff ID: ${lastStaffId}`); - // Automatically fill the last staff ID const employeeIdInput = document.getElementById("employee_id"); if (employeeIdInput) { employeeIdInput.value = lastStaffId; 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) { return saved.trim().toUpperCase(); } - console.log("πŸ“± No valid last staff ID found"); return null; } catch (error) { - console.error("❌ Error loading last staff ID from localStorage:", error); return null; } } @@ -111,7 +100,6 @@ function loadLastStaffId() { function saveLastStaffId(staffId) { try { if (!staffId || typeof staffId !== "string" || staffId.trim().length < 2) { - console.log("⚠️ Invalid staff ID, not saving"); return false; } @@ -120,11 +108,9 @@ function saveLastStaffId(staffId) { // Save to localStorage localStorage.setItem("qr_last_staff_id", cleanId); - console.log(`πŸ’Ύ Last staff ID saved: ${cleanId}`); return true; } catch (error) { - console.error("❌ Error saving last staff ID to localStorage:", error); return false; } } @@ -155,14 +141,9 @@ function handleFormSubmit(event) { event.preventDefault(); if (isSubmitting) { - console.log( - "⏳ Check-in already in progress, ignoring duplicate submission" - ); return; } - console.log("🎯 Form submission triggered"); - const employeeId = document.getElementById("employee_id")?.value?.trim(); if (!employeeId) { @@ -187,10 +168,7 @@ function handleFormSubmit(event) { // ENHANCED CHECK-IN SUBMISSION WITH MULTIPLE CHECK-IN SUPPORT function submitCheckin() { - console.log("πŸš€ Starting check-in submission process"); - if (isSubmitting) { - console.log("⏳ Already submitting, aborting"); return; } @@ -206,9 +184,6 @@ function submitCheckin() { return; } - console.log(`πŸ‘€ Employee ID: ${employeeId}`); - console.log(`πŸ“ User location:`, userLocation); - // Prepare form data const formData = new FormData(); formData.append("employee_id", employeeId); @@ -228,8 +203,6 @@ function submitCheckin() { const currentUrl = window.location.pathname; const checkinUrl = `${currentUrl}/checkin`; - console.log("🎯 Submitting to URL:", checkinUrl); - fetch(checkinUrl, { method: "POST", body: formData, @@ -238,15 +211,12 @@ function submitCheckin() { }, }) .then((response) => { - console.log("πŸ“‘ Server response status:", response.status); return response.json(); }) .then((data) => { - console.log("πŸ“Š Server response data:", data); handleCheckinResponse(data); }) .catch((error) => { - console.error("❌ Check-in error:", error); handleCheckinError(error); }) .finally(() => { @@ -261,7 +231,6 @@ function handleCheckinResponse(data) { handleCheckinSuccess(data); } else { const errorMsg = data.message || "Submission failed"; - console.log("❌ Submission failed:", errorMsg); // NEW: Handle different types of check-in failures if (errorMsg.toLowerCase().includes("already submitted")) { @@ -280,8 +249,6 @@ function handleCheckinResponse(data) { // ENHANCED SUCCESS HANDLING WITH MULTIPLE CHECK-IN INFO function handleCheckinSuccess(data) { - console.log("βœ… Submitted successfully!"); - const responseData = data.data || data || {}; const checkinCount = responseData.checkin_count_today || 1; const checkinSequence = responseData.checkin_sequence || "Check-in"; @@ -384,8 +351,6 @@ function addCheckInAgainOption() { // NEW: Reset form for new check-in function resetForNewCheckin() { - console.log("πŸ”„ Resetting for new check-in"); - // Show form again const form = document.getElementById("checkinForm"); if (form) { @@ -510,10 +475,11 @@ function validateEmployeeId() { // All location and language functions remain unchanged from original function initializeLocation() { - console.log("πŸ“ Initializing location services"); - // 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"); AndroidLocationHandler.initializeAndroidLocation(); } else { @@ -578,8 +544,6 @@ function handleLocationSuccess(position) { address: null, }; - console.log("πŸ“ Location data:", userLocation); - // Reverse geocode to get address reverseGeocode(userLocation.latitude, userLocation.longitude); @@ -587,14 +551,11 @@ function handleLocationSuccess(position) { } function handleLocationError(error) { - console.log("❌ Location error:", error.message); userLocation.source = "manual"; locationRequestActive = false; } function reverseGeocode(lat, lng) { - console.log(`🌍 Reverse geocoding for: ${lat}, ${lng}`); - // The server will use Google Maps API first, then fall back to OpenStreetMap // This provides better accuracy and address formatting const url = "/api/reverse-geocode"; // You may want to create this endpoint @@ -613,9 +574,7 @@ function reverseGeocode(lat, lng) { .then((data) => { if (data && data.display_name) { userLocation.address = data.display_name; - console.log(`βœ… Reverse geocoded address: ${userLocation.address}`); } else { - console.log(`⚠️ No address found, using coordinates as fallback`); userLocation.address = `${lat.toFixed(10)}, ${lng.toFixed(10)}`; } }) @@ -628,13 +587,10 @@ function reverseGeocode(lat, lng) { // ENHANCED LANGUAGE FUNCTIONALITY WITH PERSISTENCE function initializeLanguage() { - console.log("🌐 Initializing language functionality with persistence"); - // Load saved language preference from localStorage const savedLanguage = loadLanguagePreference(); if (savedLanguage && savedLanguage !== currentLanguage) { currentLanguage = savedLanguage; - console.log(`πŸ“± Restored saved language preference: ${currentLanguage}`); } // Set up language toggle button event listener @@ -645,7 +601,6 @@ function initializeLanguage() { // Apply initial translations based on loaded language applyTranslations(); - console.log(`βœ… Language system initialized with: ${currentLanguage}`); } function toggleLanguage() { @@ -659,10 +614,6 @@ function toggleLanguage() { // Apply translations immediately applyTranslations(); - console.log( - `🌐 Language switched to: ${currentLanguage} (saved to localStorage)` - ); - // Optional: Show brief confirmation message showLanguageChangeConfirmation(); } @@ -674,24 +625,14 @@ function loadLanguagePreference() { // Validate saved language is supported if (savedLanguage && translations.hasOwnProperty(savedLanguage)) { - console.log(`πŸ“± Found saved language preference: ${savedLanguage}`); return savedLanguage; } else if (savedLanguage) { - console.log( - `⚠️ Invalid saved language preference: ${savedLanguage}, using default` - ); // Clean up invalid preference localStorage.removeItem("qr_staff_language"); - } else { - console.log("πŸ“± No saved language preference found, using default"); } return null; } catch (error) { - console.error( - "❌ Error loading language preference from localStorage:", - error - ); return null; } } @@ -706,13 +647,8 @@ function saveLanguagePreference(language) { // Save to localStorage localStorage.setItem("qr_staff_language", language); - console.log(`πŸ’Ύ Language preference saved: ${language}`); return true; } catch (error) { - console.error( - "❌ Error saving language preference to localStorage:", - error - ); return false; } } @@ -774,18 +710,14 @@ function startClock() { } function checkLocationServicesStatus() { - console.log("πŸ“± Checking location services status..."); - // Check if geolocation is supported if (!navigator.geolocation) { - console.log("❌ Geolocation not supported by this browser"); showLocationServicesWarning("not_supported"); return; } // Test location access with a quick check const timeoutId = setTimeout(() => { - console.log("⏰ Location permission check timed out"); showLocationServicesWarning("timeout"); }, 3000); // 3 second timeout @@ -793,13 +725,11 @@ function checkLocationServicesStatus() { (position) => { // Success - location services are working clearTimeout(timeoutId); - console.log("βœ… Location services are available and enabled"); hideLocationServicesWarning(); }, (error) => { // Error - location services may be disabled clearTimeout(timeoutId); - console.log("❌ Location services error:", error.message); switch (error.code) { case error.PERMISSION_DENIED: @@ -894,9 +824,6 @@ function showLocationServicesWarning(errorType) { if (container) { 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"); if (existingWarning) { existingWarning.remove(); - console.log("βœ… Location services warning hidden"); } } diff --git a/templates/base.html b/templates/base.html index 1d018f1..892735e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,7 +3,7 @@ - {% block title %}QR Code Management System{% endblock %} + {% block title %}{{ COMPANY_NAME }}{% endblock %} {% if turnstile_enabled %} - + {% endif %} diff --git a/templates/base_authenticated.html b/templates/base_authenticated.html index 624bb3e..525be0d 100644 --- a/templates/base_authenticated.html +++ b/templates/base_authenticated.html @@ -3,7 +3,7 @@ - {% block title %}QR Code Management System{% endblock %} + {% block title %}{{ COMPANY_NAME }}{% endblock %} {% if session.role == 'admin' %} - + Projects @@ -65,8 +67,8 @@ Users - - Employees + + Employees @@ -76,12 +78,17 @@ Payroll - + Statistics - + System Logs @@ -137,9 +144,11 @@ -

{% block page_title %}QR Code Management System{% endblock %}

+

+ {% block page_title %}{{ COMPANY_NAME }}{% endblock %} +

- +