Merge branch 'master' of https://github.com/nguyen-ngo/QR_Code_Management
This commit is contained in:
@@ -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']
|
||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
+553
-311
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
initialize() {
|
||||||
|
const saved = localStorage.getItem("expandedProjects");
|
||||||
|
if (saved) {
|
||||||
|
this.expandedProjects = new Set(JSON.parse(saved));
|
||||||
|
this.restoreProjectStates();
|
||||||
|
}
|
||||||
|
|
||||||
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`, {
|
|
||||||
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"
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update QR status in UI
|
expandProject(projectId, animate = true) {
|
||||||
updateQRStatus(qrId, newStatus) {
|
const projectQR = document.getElementById(`project-qr-${projectId}`);
|
||||||
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
|
const toggle = document.getElementById(`toggle-${projectId}`);
|
||||||
if (!qrItem) return;
|
const header = toggle?.closest(".project-header");
|
||||||
|
|
||||||
// Update data attribute
|
if (projectQR && toggle) {
|
||||||
qrItem.dataset.status = newStatus;
|
projectQR.classList.add("expanded");
|
||||||
|
toggle.classList.add("expanded");
|
||||||
|
header?.classList.add("expanded");
|
||||||
|
this.expandedProjects.add(projectId);
|
||||||
|
|
||||||
// Update status badge
|
if (animate) {
|
||||||
const statusBadge = qrItem.querySelector(".qr-status");
|
setTimeout(() => {
|
||||||
if (statusBadge) {
|
projectQR.scrollIntoView({
|
||||||
statusBadge.className = `qr-status ${newStatus}`;
|
behavior: "smooth",
|
||||||
statusBadge.innerHTML = `
|
block: "nearest",
|
||||||
<i class="fas ${
|
});
|
||||||
newStatus === "active" ? "fa-check-circle" : "fa-times-circle"
|
}, 200);
|
||||||
}"></i>
|
}
|
||||||
${newStatus === "active" ? "Active" : "Inactive"}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update toggle buttons
|
|
||||||
this.updateToggleButton(qrId, newStatus);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update toggle button appearance
|
collapseProject(projectId) {
|
||||||
updateToggleButton(qrId, status) {
|
const projectQR = document.getElementById(`project-qr-${projectId}`);
|
||||||
const toggleBtn = document.getElementById(`toggle-btn-${qrId}`);
|
const toggle = document.getElementById(`toggle-${projectId}`);
|
||||||
const toggleIcon = document.getElementById(`toggle-icon-${qrId}`);
|
const header = toggle?.closest(".project-header");
|
||||||
const detailToggleBtn = document.getElementById(
|
|
||||||
`detail-toggle-btn-${qrId}`
|
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) {
|
// QR Code Status Toggle
|
||||||
// Update quick action button
|
async toggleQRCodeStatus(qrId) {
|
||||||
toggleBtn.className = `action-btn btn-status ${
|
try {
|
||||||
status === "active" ? "btn-deactivate" : "btn-activate"
|
const response = await fetch(`/qr-codes/${qrId}/toggle-status`, {
|
||||||
}`;
|
method: "POST",
|
||||||
toggleBtn.title = `${
|
headers: {
|
||||||
status === "active" ? "Deactivate" : "Activate"
|
"Content-Type": "application/json",
|
||||||
} QR Code`;
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
toggleIcon.className = `fas ${
|
},
|
||||||
status === "active" ? "fa-pause" : "fa-play"
|
});
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (detailToggleBtn) {
|
if (response.ok) {
|
||||||
// Update detail action button
|
const result = await response.json();
|
||||||
detailToggleBtn.className = `btn ${
|
if (result.success) {
|
||||||
status === "active" ? "btn-warning" : "btn-success"
|
this.showToast(result.message, "success");
|
||||||
}`;
|
|
||||||
detailToggleBtn.innerHTML = `
|
// Reload page after short delay to show updated status
|
||||||
<i class="fas ${status === "active" ? "fa-pause" : "fa-play"}"></i>
|
setTimeout(() => {
|
||||||
${status === "active" ? "Deactivate" : "Activate"} QR Code
|
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 = `
|
||||||
|
<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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
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,187 +539,95 @@ 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">
|
|
||||||
<button class="btn btn-danger" onclick="confirmDelete()">
|
|
||||||
<i class="fas fa-trash"></i> Delete
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick="cancelDelete()">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
|
|
||||||
window.confirmDelete = () => {
|
|
||||||
document.body.removeChild(modal);
|
|
||||||
delete window.confirmDelete;
|
|
||||||
delete window.cancelDelete;
|
|
||||||
resolve(true);
|
|
||||||
};
|
};
|
||||||
|
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 = () => {
|
toast.textContent = message;
|
||||||
document.body.removeChild(modal);
|
document.body.appendChild(toast);
|
||||||
delete window.confirmDelete;
|
|
||||||
delete window.cancelDelete;
|
|
||||||
resolve(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Close on ESC key
|
// Animate in
|
||||||
const escHandler = (e) => {
|
setTimeout(() => {
|
||||||
if (e.key === "Escape") {
|
toast.style.opacity = "1";
|
||||||
window.cancelDelete();
|
toast.style.transform = "translateX(0)";
|
||||||
document.removeEventListener("keydown", escHandler);
|
}, 100);
|
||||||
|
|
||||||
|
// Animate out
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = "0";
|
||||||
|
toast.style.transform = "translateX(100%)";
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(toast)) {
|
||||||
|
toast.remove();
|
||||||
}
|
}
|
||||||
};
|
}, 300);
|
||||||
document.addEventListener("keydown", escHandler);
|
}, 3000);
|
||||||
|
|
||||||
// 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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) => {
|
||||||
if (this.allExpanded) {
|
const details = item.querySelector(".qr-details");
|
||||||
item.classList.add("expanded");
|
if (details) {
|
||||||
} else {
|
if (this.allExpanded) {
|
||||||
item.classList.remove("expanded");
|
details.style.display = "block";
|
||||||
|
item.classList.add("expanded");
|
||||||
|
} else {
|
||||||
|
details.style.display = "none";
|
||||||
|
item.classList.remove("expanded");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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
|
|
||||||
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
|
// 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");
|
||||||
});
|
});
|
||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -65,8 +67,8 @@
|
|||||||
<span class="menu-text">Users</span>
|
<span class="menu-text">Users</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('employees') }}" class="menu-item">
|
<a href="{{ url_for('employees') }}" class="menu-item">
|
||||||
<i class="fas fa-user"></i>
|
<i class="fas fa-user"></i>
|
||||||
<span class="menu-text">Employees</span>
|
<span class="menu-text">Employees</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('attendance_report') }}" class="menu-item">
|
<a href="{{ url_for('attendance_report') }}" class="menu-item">
|
||||||
<i class="fas fa-chart-line"></i>
|
<i class="fas fa-chart-line"></i>
|
||||||
@@ -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 -->
|
||||||
|
|||||||
+1109
-1348
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user