code hygiene

This commit is contained in:
2026-03-06 10:54:30 -05:00
parent b15b0478ac
commit f96009df90
4 changed files with 221 additions and 805 deletions
+29 -64
View File
@@ -37,8 +37,8 @@ app.config['TEMPLATES_AUTO_RELOAD'] = os.environ.get('TEMPLATES_AUTO_RELOAD')
# Session configuration for "Remember Me" functionality # Session configuration for "Remember Me" functionality
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30) app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)
app.config['SESSION_COOKIE_SECURE'] = os.environ.get('SESSION_COOKIE_SECURE') # Set to True if using HTTPS app.config['SESSION_COOKIE_SECURE'] = os.environ.get('SESSION_COOKIE_SECURE', 'false').lower() == 'true' # Set to True if using HTTPS
app.config['SESSION_COOKIE_HTTPONLY'] = os.environ.get('SESSION_COOKIE_HTTPONLY') app.config['SESSION_COOKIE_HTTPONLY'] = os.environ.get('SESSION_COOKIE_HTTPONLY', 'true').lower() == 'true'
app.config['SESSION_COOKIE_SAMESITE'] = os.environ.get('SESSION_COOKIE_SAMESITE') app.config['SESSION_COOKIE_SAMESITE'] = os.environ.get('SESSION_COOKIE_SAMESITE')
# Photo Verification Configuration # Photo Verification Configuration
@@ -110,8 +110,8 @@ def create_performance_indexes():
print(f"❌ Error creating performance indexes: {e}") print(f"❌ Error creating performance indexes: {e}")
db.session.rollback() db.session.rollback()
logger_handler.log_database_error( logger_handler.log_database_error(
error_type="index_creation_error", 'index_creation_error',
error_message=str(e), e,
query="CREATE INDEX statements" query="CREATE INDEX statements"
) )
@@ -4596,40 +4596,6 @@ def deactivate_qr_code(qr_id):
'message': 'Error deactivating QR code. Please try again.' 'message': 'Error deactivating QR code. Please try again.'
}), 500 }), 500
@app.route('/qr-codes/<int:qr_id>/toggle-status', methods=['POST'])
@admin_required
def toggle_qr_status_api(qr_id):
"""Toggle QR code active/inactive status - Enhanced JSON API"""
try:
qr_code = QRCode.query.get_or_404(qr_id)
qr_code.active_status = not qr_code.active_status
db.session.commit()
status_text = "activated" if qr_code.active_status else "deactivated"
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'success': True,
'new_status': qr_code.active_status,
'status_text': 'Active' if qr_code.active_status else 'Inactive',
'message': f'QR code "{qr_code.name}" has been {status_text} successfully!'
})
else:
flash(f'QR code "{qr_code.name}" has been {status_text} successfully!', 'success')
return redirect(url_for('dashboard'))
except Exception as e:
db.session.rollback()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'success': False,
'message': 'Error updating QR code status. Please try again.'
}), 500
else:
flash('Error updating QR code status. Please try again.', 'error')
return redirect(url_for('dashboard'))
@app.route('/attendance') @app.route('/attendance')
@login_required @login_required
def attendance_report(): def attendance_report():
@@ -6039,8 +6005,8 @@ def export_configuration():
# Use your existing logger error method with correct parameters # Use your existing logger error method with correct parameters
try: try:
logger_handler.log_flask_error( logger_handler.log_flask_error(
error_type="export_configuration_error", 'export_configuration_error',
error_message=str(e), str(e),
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
except Exception as log_error: except Exception as log_error:
@@ -6194,8 +6160,8 @@ def generate_excel_export():
# Use your existing logger error method with correct parameters # Use your existing logger error method with correct parameters
try: try:
logger_handler.log_flask_error( logger_handler.log_flask_error(
error_type="excel_export_error", 'excel_export_error',
error_message=str(e), str(e),
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
except Exception as log_error: except Exception as log_error:
@@ -6478,8 +6444,8 @@ def create_excel_export(selected_columns, column_names, filters):
# Log error # Log error
try: try:
logger_handler.log_flask_error( logger_handler.log_flask_error(
error_type="excel_export_error", 'excel_export_error',
error_message=str(e), str(e),
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
except Exception as log_error: except Exception as log_error:
@@ -6785,8 +6751,8 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
# Log error # Log error
try: try:
logger_handler.log_flask_error( logger_handler.log_flask_error(
error_type="excel_export_ordered_error", 'excel_export_ordered_error',
error_message=str(e), str(e),
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
except Exception as log_error: except Exception as log_error:
@@ -6932,8 +6898,8 @@ def payroll_dashboard():
print(f"❌ Traceback: {traceback.format_exc()}") print(f"❌ Traceback: {traceback.format_exc()}")
logger_handler.log_flask_error( logger_handler.log_flask_error(
error_type="payroll_dashboard_error", 'payroll_dashboard_error',
error_message=str(e), str(e),
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
@@ -7173,8 +7139,8 @@ def export_payroll_excel():
print(f"❌ Traceback: {traceback.format_exc()}") print(f"❌ Traceback: {traceback.format_exc()}")
logger_handler.log_flask_error( logger_handler.log_flask_error(
error_type="payroll_excel_export_error", 'payroll_excel_export_error',
error_message=str(e), str(e),
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
@@ -7231,8 +7197,8 @@ def calculate_working_hours_api():
attendance_records = query.all() attendance_records = query.all()
# Calculate working hours using single check-in calculator # Calculate working hours using WorkingHoursCalculator
calculator = SingleCheckInCalculator() calculator = WorkingHoursCalculator()
hours_data = calculator.calculate_employee_hours( hours_data = calculator.calculate_employee_hours(
str(employee_id), start_date, end_date, attendance_records str(employee_id), start_date, end_date, attendance_records
) )
@@ -7248,8 +7214,8 @@ def calculate_working_hours_api():
except Exception as e: except Exception as e:
print(f"❌ Error in calculate_working_hours_api: {e}") print(f"❌ Error in calculate_working_hours_api: {e}")
logger_handler.log_flask_error( logger_handler.log_flask_error(
error_type="working_hours_api_error", 'working_hours_api_error',
error_message=str(e), str(e),
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
@@ -7355,7 +7321,6 @@ def get_miss_punch_details(employee_id):
converted_records.append(converted_record) converted_records.append(converted_record)
# Calculate working hours using the same calculator as the dashboard # Calculate working hours using the same calculator as the dashboard
#calculator = SingleCheckInCalculator()
# Calculate hours for this employee # Calculate hours for this employee
hours_data = calculator.calculate_employee_hours( hours_data = calculator.calculate_employee_hours(
@@ -7415,8 +7380,8 @@ def get_miss_punch_details(employee_id):
print(f"❌ Traceback: {traceback.format_exc()}") print(f"❌ Traceback: {traceback.format_exc()}")
logger_handler.log_flask_error( logger_handler.log_flask_error(
error_type="miss_punch_details_api_error", 'miss_punch_details_api_error',
error_message=str(e), str(e),
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
@@ -9451,7 +9416,7 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
f"SP: {work_type_counts['SP']}, PW: {work_type_counts['PW']}" f"SP: {work_type_counts['SP']}, PW: {work_type_counts['PW']}"
) )
# Calculate working hours using SingleCheckInCalculator # Calculate working hours using WorkingHoursCalculator
calculator = WorkingHoursCalculator() calculator = WorkingHoursCalculator()
hours_data = calculator.calculate_all_employees_hours( hours_data = calculator.calculate_all_employees_hours(
datetime.combine(start_date, datetime.min.time()), datetime.combine(start_date, datetime.min.time()),
@@ -11217,8 +11182,8 @@ def update_existing_qr_codes():
except Exception as e: except Exception as e:
logger_handler.log_flask_error( logger_handler.log_flask_error(
error_type="qr_code_update_error", 'qr_code_update_error',
error_message=f"Failed to update QR code {qr_code.id}: {str(e)}" f"Failed to update QR code {qr_code.id}: {str(e)}"
) )
continue continue
@@ -11437,8 +11402,8 @@ def get_optimized_statistics(date_from=None, date_to=None, project_filter=None):
except Exception as e: except Exception as e:
logger_handler.log_database_error( logger_handler.log_database_error(
error_type="statistics_query_error", 'statistics_query_error',
error_message=str(e), e,
query="get_optimized_statistics" query="get_optimized_statistics"
) )
raise raise
@@ -11493,8 +11458,8 @@ if __name__ == '__main__':
print(f"❌ Application startup failed: {e}") print(f"❌ Application startup failed: {e}")
if hasattr(app, 'logger_handler'): if hasattr(app, 'logger_handler'):
logger_handler.log_flask_error( logger_handler.log_flask_error(
error_type="application_startup_error", 'application_startup_error',
error_message=str(e) str(e)
) )
raise raise
+160 -140
View File
@@ -25,7 +25,7 @@ import os
import traceback import traceback
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from functools import wraps from functools import wraps
from flask import request, session, g from flask import request, session, g, render_template
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
import uuid import uuid
@@ -187,17 +187,7 @@ class AppLogger:
if self.app.debug: if self.app.debug:
return None # Let Flask handle debug errors return None # Let Flask handle debug errors
return ''' return render_template('errors/500.html'), 500
<!DOCTYPE html>
<html>
<head><title>Server Error</title></head>
<body style="font-family: Arial; text-align: center; margin-top: 100px;">
<h1>🔧 Something went wrong</h1>
<p>We're working to fix this issue. Please try again later.</p>
<a href="/" style="color: #2563eb;">← Back to Home</a>
</body>
</html>
''', 500
@self.app.errorhandler(404) @self.app.errorhandler(404)
def handle_not_found(error): def handle_not_found(error):
@@ -208,17 +198,7 @@ class AppLogger:
severity="LOW" severity="LOW"
) )
return ''' return render_template('errors/404.html'), 404
<!DOCTYPE html>
<html>
<head><title>Page Not Found</title></head>
<body style="font-family: Arial; text-align: center; margin-top: 100px;">
<h1>🔍 Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<a href="/" style="color: #2563eb;">← Back to Home</a>
</body>
</html>
''', 404
def _get_request_context(self): def _get_request_context(self):
"""Get current request context information""" """Get current request context information"""
@@ -773,69 +753,109 @@ class AppLogger:
self.log_database_error('cleanup_old_logs', e) self.log_database_error('cleanup_old_logs', e)
return 0 return 0
def get_recent_logs(self, days=7, limit=100, category_filter=None, severity_filter=None, search_term=None): def get_recent_logs(self, days=7, limit=100, category_filter=None, severity_filter=None, search_term=None):
"""Enhanced method to get recent logs with filtering options""" """Enhanced method to get recent logs with filtering options"""
try: try:
cutoff_date = datetime.now() - timedelta(days=days) cutoff_date = datetime.now() - timedelta(days=days)
# Build the base query # Build the base query
base_sql = """ base_sql = """
SELECT SELECT
event_id, event_id,
event_type, event_type,
event_category, event_category,
event_description, event_description,
severity_level, severity_level,
created_timestamp, created_timestamp,
username, username,
ip_address, ip_address,
user_id user_id
FROM log_events FROM log_events
WHERE created_timestamp >= :cutoff_date WHERE created_timestamp >= :cutoff_date
""" """
# Add filters # Add filters
params = {'cutoff_date': cutoff_date} params = {'cutoff_date': cutoff_date}
if category_filter: if category_filter:
base_sql += " AND event_category = :category_filter" base_sql += " AND event_category = :category_filter"
params['category_filter'] = category_filter params['category_filter'] = category_filter
if severity_filter: if severity_filter:
base_sql += " AND severity_level = :severity_filter" base_sql += " AND severity_level = :severity_filter"
params['severity_filter'] = severity_filter params['severity_filter'] = severity_filter
if search_term: if search_term:
base_sql += " AND (event_description LIKE :search_term OR event_type LIKE :search_term OR username LIKE :search_term)" base_sql += " AND (event_description LIKE :search_term OR event_type LIKE :search_term OR username LIKE :search_term)"
params['search_term'] = f"%{search_term}%" params['search_term'] = f"%{search_term}%"
# Add ordering and limit # Add ordering and limit
base_sql += " ORDER BY created_timestamp DESC LIMIT :limit" base_sql += " ORDER BY created_timestamp DESC LIMIT :limit"
params['limit'] = limit params['limit'] = limit
result = self.db.session.execute(text(base_sql), params).fetchall() result = self.db.session.execute(text(base_sql), params).fetchall()
logs = [] logs = []
for row in result: for row in result:
logs.append({ logs.append({
'event_id': row.event_id, 'event_id': row.event_id,
'event_type': row.event_type, 'event_type': row.event_type,
'event_category': row.event_category, 'event_category': row.event_category,
'description': row.event_description, 'description': row.event_description,
'severity': row.severity_level, 'severity': row.severity_level,
'timestamp': row.created_timestamp.isoformat(), 'timestamp': row.created_timestamp.isoformat(),
'username': row.username or 'System', 'username': row.username or 'System',
'ip_address': row.ip_address or '-', 'ip_address': row.ip_address or '-',
'user_id': row.user_id 'user_id': row.user_id
}) })
return logs return logs
except Exception as e: except Exception as e:
self.log_database_error('get_recent_logs', e) self.log_database_error('get_recent_logs', e)
print(f"Error in get_recent_logs: {e}") print(f"Error in get_recent_logs: {e}")
return [] return []
def log_system_event(self, event_type, description, severity='INFO', additional_data=None):
"""Log system-level events such as startup, optimization, and slow queries"""
event_data = {
'system_event_type': event_type,
'severity': severity,
'event_timestamp': datetime.now().isoformat()
}
if additional_data:
event_data['additional_data'] = additional_data
message = f"System event: {event_type} - {description}"
self.logger.info(json.dumps({'event': 'system_event', 'data': event_data}))
self._log_to_database(
event_type=event_type,
event_category='system',
description=message,
event_data=event_data,
severity=severity
)
def log_user_activity(self, activity_type, description='', additional_data=None):
"""Log user activity events called directly from route handlers"""
context = self._get_request_context()
event_data = {
'activity_type': activity_type,
'event_timestamp': datetime.now().isoformat(),
'user_id': context.get('user_id'),
'username': context.get('username')
}
if additional_data:
event_data['additional_data'] = additional_data
message = f"User activity: {activity_type} - {description}" if description else f"User activity: {activity_type}"
self.logger.info(json.dumps({'event': 'user_activity', 'data': event_data}))
self._log_to_database(
event_type=f'user_activity_{activity_type}',
event_category='activity',
description=message,
event_data=event_data,
severity='INFO'
)
# DECORATOR FUNCTIONS FOR AUTOMATIC LOGGING # DECORATOR FUNCTIONS FOR AUTOMATIC LOGGING
def log_user_activity(activity_type): def log_user_activity(activity_type):
@@ -897,62 +917,62 @@ def log_database_operations(operation_name):
return decorated_function return decorated_function
return decorator return decorator
def verify_log_table_exists(self): def verify_log_table_exists(self):
"""Verify that the log_events table exists and has the correct structure""" """Verify that the log_events table exists and has the correct structure"""
try: try:
# Check if table exists # Check if table exists
check_table_sql = """ check_table_sql = """
SELECT COUNT(*) as table_exists SELECT COUNT(*) as table_exists
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = DATABASE() WHERE table_schema = DATABASE()
AND table_name = 'log_events' AND table_name = 'log_events'
""" """
result = self.db.session.execute(text(check_table_sql)).fetchone() result = self.db.session.execute(text(check_table_sql)).fetchone()
if result.table_exists == 0: if result.table_exists == 0:
print("⚠️ log_events table does not exist. Creating it now...") print("⚠️ log_events table does not exist. Creating it now...")
self._create_log_table() self._create_log_table()
return True
# Check if table has records
count_sql = "SELECT COUNT(*) as record_count FROM log_events"
count_result = self.db.session.execute(text(count_sql)).fetchone()
print(f"✅ log_events table exists with {count_result.record_count} records")
return True return True
# Check if table has records except Exception as e:
count_sql = "SELECT COUNT(*) as record_count FROM log_events" print(f"❌ Error verifying log table: {e}")
count_result = self.db.session.execute(text(count_sql)).fetchone() return False
print(f"✅ log_events table exists with {count_result.record_count} records")
return True
except Exception as e:
print(f"❌ Error verifying log table: {e}")
return False
def log_modal_interaction(self, event_type, description, additional_data=None): def log_modal_interaction(self, event_type, description, additional_data=None):
"""Log modal interactions for debugging""" """Log modal interactions for debugging"""
try: try:
context = self._get_request_context() context = self._get_request_context()
event_data = { event_data = {
'interaction_type': event_type, 'interaction_type': event_type,
'event_timestamp': datetime.now().isoformat(), 'event_timestamp': datetime.now().isoformat(),
'request_context': context 'request_context': context
} }
if additional_data: if additional_data:
event_data['additional_data'] = additional_data event_data['additional_data'] = additional_data
message = f"Modal interaction: {event_type} - {description}" message = f"Modal interaction: {event_type} - {description}"
# Log to database # Log to database
self._log_to_database( self._log_to_database(
event_type='modal_interaction', event_type='modal_interaction',
event_category='ui', event_category='ui',
description=message, description=message,
event_data=event_data, event_data=event_data,
severity='INFO' severity='INFO'
) )
except Exception as e: except Exception as e:
print(f"Error logging modal interaction: {e}") print(f"Error logging modal interaction: {e}")
# INITIALIZATION FUNCTION # INITIALIZATION FUNCTION
def init_logging(app, db): def init_logging(app, db):
+32 -28
View File
@@ -1,64 +1,68 @@
# QR Code Management System - Python Dependencies # QR Code Management System - Python Dependencies
# Production-ready Flask application with MySQL support # Production-ready Flask application with MySQL support
# Versions pinned as of March 2026 — run `pip install -r requirements.txt` on fresh deploy
# Core Flask Framework # Core Flask Framework
Flask Flask==3.1.0
Flask-SQLAlchemy Flask-SQLAlchemy==3.1.1
# Database Support - MySQL # Database Support - MySQL
PyMySQL # Pure Python MySQL client PyMySQL==1.1.1 # Pure Python MySQL client
mysql-connector-python # Official MySQL connector (alternative) mysql-connector-python==9.2.0 # Official MySQL connector (alternative)
SQLAlchemy SQLAlchemy==2.0.36
# Security and Authentication # Security and Authentication
Werkzeug # Security utilities and password hashing Werkzeug==3.1.3 # Security utilities and password hashing
# QR Code Generation # QR Code Generation
qrcode # QR code generation library qrcode==8.0 # QR code generation library
Pillow # Image processing for QR codes Pillow==11.1.0 # Image processing for QR codes
# User Agent Detection # User Agent Detection
user-agents # Device and browser detection from user agent strings user-agents==2.2.0 # Device and browser detection from user agent strings
# URL and Regex Processing # URL and Regex Processing
regex # Enhanced regex support for URL generation regex==2024.11.6 # Enhanced regex support for URL generation
# Environment and Configuration # Environment and Configuration
python-dotenv # Environment variable management python-dotenv==1.0.1 # Environment variable management
# Date and Time Processing # Date and Time Processing
python-dateutil # Extended date/time processing python-dateutil==2.9.0.post0 # Extended date/time processing
# Development and Testing (optional) # Development and Testing (optional)
pytest # Testing framework pytest==8.3.4 # Testing framework
pytest-flask # Flask testing utilities pytest-flask==1.3.0 # Flask testing utilities
Flask-Testing # Additional Flask testing tools Flask-Testing==0.8.1 # Additional Flask testing tools
# Production Server (optional) # Production Server (optional)
gunicorn # WSGI HTTP Server for production gunicorn==23.0.0 # WSGI HTTP Server for production
gevent # Async worker support gevent==24.11.1 # Async worker support
# Utilities # Utilities
click # Command line interface creation click==8.1.8 # Command line interface creation
itsdangerous # Secure data serialization itsdangerous==2.2.0 # Secure data serialization
Jinja2 # Template engine Jinja2==3.1.5 # Template engine
MarkupSafe # Safe string handling MarkupSafe==3.0.2 # Safe string handling
# Data Export and Processing # Data Export and Processing
openpyxl # Excel file generation for attendance reports openpyxl==3.1.5 # Excel file generation for attendance reports
pandas # Data manipulation for reports (optional) pandas==2.2.3 # Data manipulation for reports (optional)
# HTTP Requests (for potential integrations) # HTTP Requests (for potential integrations)
requests # HTTP library for external API calls requests==2.32.3 # HTTP library for external API calls
# Google Maps Integration
googlemaps==4.10.0 # Google Maps API client
# Caching (optional for performance) # Caching (optional for performance)
Flask-Caching # Caching support for Flask Flask-Caching==2.3.0 # Caching support for Flask
# Logging and Monitoring (optional) # Logging and Monitoring (optional)
python-json-logger # Structured logging support python-json-logger==3.2.1 # Structured logging support
# Cryptography dependencies (required for some MySQL features) # Cryptography dependencies (required for some MySQL features)
cryptography # Required for MySQL SSL connections cryptography==44.0.0 # Required for MySQL SSL connections
# Employee Synchronization Dependencies # Employee Synchronization Dependencies
schedule # For automated scheduling schedule==1.2.2 # For automated scheduling
-573
View File
@@ -1,573 +0,0 @@
/**
* Dashboard-specific JavaScript functionality with QR Toggle
* static/js/dashboard.js
*/
// Dashboard QR Management Class
class DashboardManager {
constructor() {
this.allExpanded = false;
this.currentModalQR = null;
this.init();
}
init() {
this.initializeSearch();
this.initializeFilters();
this.setupEventListeners();
this.addScrollAnimations();
this.updateResultsCount();
}
// Initialize search functionality with debouncing
initializeSearch() {
const searchInput = document.getElementById("qrSearch");
if (!searchInput) return;
let searchTimeout;
searchInput.addEventListener("input", () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.filterQRCodes();
}, 300);
});
}
// Initialize filter functionality
initializeFilters() {
const statusFilter = document.getElementById("statusFilter");
if (!statusFilter) return;
statusFilter.addEventListener("change", () => {
this.filterQRCodes();
});
}
// Enhanced QR code filtering with animation
filterQRCodes() {
const searchTerm = document.getElementById("qrSearch").value.toLowerCase();
const statusFilter = document.getElementById("statusFilter").value;
const qrItems = document.querySelectorAll(".qr-item");
let visibleCount = 0;
qrItems.forEach((item) => {
const name = item.dataset.name || "";
const location = item.dataset.location || "";
const status = item.dataset.status || "";
const matchesSearch =
!searchTerm ||
name.includes(searchTerm) ||
location.includes(searchTerm);
const matchesStatus = !statusFilter || status === statusFilter;
if (matchesSearch && matchesStatus) {
this.showQRItem(item);
visibleCount++;
} else {
this.hideQRItem(item);
}
});
this.updateResultsDisplay(visibleCount);
this.updateResultsCount();
}
// Show QR item with animation
showQRItem(item) {
item.style.display = "block";
setTimeout(() => {
item.classList.add("fade-in");
item.classList.remove("fade-out");
}, 10);
}
// Hide QR item with animation
hideQRItem(item) {
item.classList.add("fade-out");
item.classList.remove("fade-in");
setTimeout(() => {
item.style.display = "none";
}, 300);
}
// Update results display and empty state
updateResultsDisplay(count) {
const qrList = document.getElementById("qrList");
let existingEmpty = document.querySelector(".search-empty-state");
if (
count === 0 &&
(document.getElementById("qrSearch").value ||
document.getElementById("statusFilter").value)
) {
if (!existingEmpty) {
const emptyState = document.createElement("div");
emptyState.className = "search-empty-state";
emptyState.innerHTML = `
<div class="empty-icon">
<i class="fas fa-search"></i>
</div>
<h3>No QR Codes Found</h3>
<p>Try adjusting your search or filter criteria</p>
<button onclick="dashboardManager.clearFilters()" class="btn btn-outline">
<i class="fas fa-refresh"></i>
Clear Filters
</button>
`;
qrList.parentNode.appendChild(emptyState);
}
} else {
if (existingEmpty) {
existingEmpty.remove();
}
}
}
// Clear all filters
clearFilters() {
document.getElementById("qrSearch").value = "";
document.getElementById("statusFilter").value = "";
this.filterQRCodes();
}
// Update results counter
updateResultsCount() {
const qrItems = document.querySelectorAll(
'.qr-item[style*="block"], .qr-item:not([style*="none"])'
);
const totalItems = document.querySelectorAll(".qr-item").length;
const visibleCount = qrItems.length;
let counter = document.querySelector(".results-counter");
if (!counter) {
counter = document.createElement("div");
counter.className = "results-counter";
const searchContainer = document.querySelector(".search-container");
if (searchContainer) {
searchContainer.appendChild(counter);
}
}
if (visibleCount !== totalItems) {
counter.textContent = `Showing ${visibleCount} of ${totalItems} QR codes`;
counter.style.display = "block";
} else {
counter.style.display = "none";
}
}
// Setup additional event listeners
setupEventListeners() {
// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case "f":
e.preventDefault();
document.getElementById("qrSearch")?.focus();
break;
}
}
});
// Enhanced modal functionality
this.setupModalHandling();
}
// Enhanced modal handling
setupModalHandling() {
const modal = document.getElementById("qrModal");
if (!modal) return;
// Close modal with better animation
const closeButtons = modal.querySelectorAll("[onclick*='closeQRModal']");
closeButtons.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
this.closeQRModal();
});
});
// Click outside to close
modal.addEventListener("click", (e) => {
if (e.target === modal) {
this.closeQRModal();
}
});
// Escape key to close
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.style.display === "flex") {
this.closeQRModal();
}
});
}
// Close QR modal with improved animation
closeQRModal() {
const modal = document.getElementById("qrModal");
if (modal) {
modal.classList.remove("show");
setTimeout(() => {
modal.style.display = "none";
}, 200);
}
this.currentModalQR = null;
}
// Add scroll animations for better UX
addScrollAnimations() {
const observerOptions = {
threshold: 0.1,
rootMargin: "0px 0px -50px 0px",
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.style.opacity = "1";
entry.target.style.transform = "translateY(0)";
}
});
}, observerOptions);
// Observe QR items
document.querySelectorAll(".qr-item").forEach((item) => {
item.style.opacity = "0";
item.style.transform = "translateY(20px)";
item.style.transition = "opacity 0.6s ease, transform 0.6s ease";
observer.observe(item);
});
}
// Toggle QR details with smooth animation
toggleQRDetails(qrId) {
const details = document.getElementById(`qr-details-${qrId}`);
const chevron = document.querySelector(
`[onclick="toggleQRDetails(${qrId})"] .chevron`
);
if (!details) return;
if (details.classList.contains("expanded")) {
details.classList.remove("expanded");
if (chevron) chevron.style.transform = "rotate(0deg)";
} else {
// Close other expanded items first
document
.querySelectorAll(".qr-item-details.expanded")
.forEach((detail) => {
if (detail !== details) {
detail.classList.remove("expanded");
}
});
details.classList.add("expanded");
if (chevron) chevron.style.transform = "rotate(180deg)";
}
}
// Toggle all QR codes expand/collapse
toggleAllQRs() {
const details = document.querySelectorAll(".qr-item-details");
const expandIcon = document.getElementById("expandIcon");
const expandText = document.getElementById("expandText");
this.allExpanded = !this.allExpanded;
details.forEach((detail) => {
if (this.allExpanded) {
detail.classList.add("expanded");
} else {
detail.classList.remove("expanded");
}
});
// Update button text and icon
if (expandIcon && expandText) {
if (this.allExpanded) {
expandIcon.className = "fas fa-compress-alt";
expandText.textContent = "Collapse All";
} else {
expandIcon.className = "fas fa-expand-alt";
expandText.textContent = "Expand All";
}
}
// Update chevron icons
document.querySelectorAll(".chevron").forEach((chevron) => {
chevron.style.transform = this.allExpanded
? "rotate(180deg)"
: "rotate(0deg)";
});
}
// Enhanced QR preview functionality
previewQR(qrData, qrName) {
const modal = document.getElementById("qrModal");
const modalTitle = document.getElementById("modalTitle");
const modalImage = document.getElementById("modalQRImage");
if (modal && modalTitle && modalImage) {
modalTitle.textContent = `${qrName} - QR Code`;
modalImage.src = `data:image/png;base64,${qrData}`;
modalImage.alt = `QR Code for ${qrName}`;
this.currentModalQR = {
name: qrName,
image: `data:image/png;base64,${qrData}`,
};
modal.style.display = "flex";
setTimeout(() => modal.classList.add("show"), 10);
}
}
// Enhanced download functionality
downloadQR(base64Image, filename) {
try {
const link = document.createElement("a");
link.href = `data:image/png;base64,${base64Image}`;
link.download = `${filename
.replace(/[^a-z0-9]/gi, "_")
.toLowerCase()}_qr_code.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Show success toast
this.showToast("QR code downloaded successfully!", "success");
} catch (error) {
this.showToast("Failed to download QR code", "error");
console.error("Download error:", error);
}
}
// Download from modal
downloadModalQR() {
if (this.currentModalQR) {
const base64Data = this.currentModalQR.image.split("base64,")[1];
this.downloadQR(base64Data, this.currentModalQR.name);
}
}
// NEW: Toggle QR Code Status (Activate/Deactivate)
async toggleQRCodeStatus(qrId) {
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
const statusElement = document.getElementById(`status-${qrId}`);
const statusIcon = document.getElementById(`status-icon-${qrId}`);
const statusText = document.getElementById(`status-text-${qrId}`);
const toggleBtn = document.getElementById(`toggle-btn-${qrId}`);
const toggleIcon = document.getElementById(`toggle-icon-${qrId}`);
const detailToggleBtn = document.getElementById(
`detail-toggle-btn-${qrId}`
);
if (!statusElement || !qrItem) return;
// Add loading state
statusElement.classList.add("status-loading");
if (toggleBtn) toggleBtn.disabled = true;
if (detailToggleBtn) detailToggleBtn.disabled = true;
try {
const response = await fetch(`/qr-codes/${qrId}/toggle-status`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data.success) {
// Update UI elements
const newStatus = data.new_status;
const newStatusClass = newStatus ? "active" : "inactive";
const newIconClass = newStatus ? "fa-check-circle" : "fa-times-circle";
const newToggleIcon = newStatus ? "fa-pause" : "fa-play";
const newToggleBtnClass = newStatus ? "btn-deactivate" : "btn-activate";
const newDetailBtnClass = newStatus ? "btn-warning" : "btn-success";
const newDetailBtnText = newStatus ? "Deactivate" : "Activate";
// Update status badge
statusElement.className = `qr-status ${newStatusClass}`;
if (statusIcon) statusIcon.className = `fas ${newIconClass}`;
if (statusText) statusText.textContent = data.status_text;
// Update QR item data attribute and styling
qrItem.setAttribute("data-status", newStatusClass);
// Update toggle button (collapsed view)
if (toggleBtn) {
toggleBtn.className = `action-btn btn-status ${newToggleBtnClass}`;
toggleBtn.title = `${newDetailBtnText} QR Code`;
}
if (toggleIcon) {
toggleIcon.className = `fas ${newToggleIcon}`;
}
// Update detail toggle button (expanded view)
if (detailToggleBtn) {
detailToggleBtn.className = `btn ${newDetailBtnClass}`;
detailToggleBtn.innerHTML = `<i class="fas ${newToggleIcon}"></i> ${newDetailBtnText} QR Code`;
}
// Update status badges in expanded view
const detailStatusBadges = qrItem.querySelectorAll(".status-badge");
detailStatusBadges.forEach((badge) => {
badge.className = `status-badge ${newStatusClass}`;
const icon = badge.querySelector("i");
if (icon) icon.className = `fas ${newIconClass}`;
const text = badge.textContent.trim();
if (text === "Active" || text === "Inactive") {
badge.innerHTML = `<i class="fas ${newIconClass}"></i> ${data.status_text}`;
}
});
// Show success message
this.showToast(data.message, "success");
// Update statistics if needed
this.updateStatistics();
} else {
this.showToast(
data.message || "Failed to update QR code status",
"error"
);
}
} catch (error) {
console.error("Error toggling QR status:", error);
this.showToast("Network error. Please try again.", "error");
} finally {
// Remove loading state
statusElement.classList.remove("status-loading");
if (toggleBtn) toggleBtn.disabled = false;
if (detailToggleBtn) detailToggleBtn.disabled = false;
}
}
// Update statistics after status change
updateStatistics() {
const activeCount = document.querySelectorAll(
'[data-status="active"]'
).length;
const inactiveCount = document.querySelectorAll(
'[data-status="inactive"]'
).length;
const totalCount = activeCount + inactiveCount;
// Update active count
const activeStatElement = document.querySelector(".stat-card.success h3");
if (activeStatElement) {
activeStatElement.textContent = activeCount;
}
// Update active percentage
const activePercentElement = document.querySelector(
".stat-card.success .stat-trend"
);
if (activePercentElement && totalCount > 0) {
const percentage = ((activeCount / totalCount) * 100).toFixed(1);
activePercentElement.textContent = `${percentage}% active`;
}
// Update inactive count if there's a specific stat card for it
const inactiveStatElement = document.querySelector(".stat-card.warning h3");
if (inactiveStatElement) {
inactiveStatElement.textContent = inactiveCount;
}
// Update inactive percentage
const inactivePercentElement = document.querySelector(
".stat-card.warning .stat-trend"
);
if (inactivePercentElement && totalCount > 0) {
const percentage = ((inactiveCount / totalCount) * 100).toFixed(1);
inactivePercentElement.textContent = `${percentage}% inactive`;
}
}
// Toast notification system
showToast(message, type = "info") {
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<div class="toast-content">
<i class="fas ${this.getToastIcon(type)}"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(toast);
// Animate in
setTimeout(() => toast.classList.add("show"), 100);
// Auto remove
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 300);
}, 3000);
}
// Get toast icon based on type
getToastIcon(type) {
const icons = {
success: "fa-check-circle",
error: "fa-exclamation-circle",
warning: "fa-exclamation-triangle",
info: "fa-info-circle",
};
return icons[type] || icons.info;
}
}
// Global functions for compatibility with existing onclick handlers
let dashboardManager;
function toggleQRDetails(qrId) {
dashboardManager?.toggleQRDetails(qrId);
}
function toggleAllQRs() {
dashboardManager?.toggleAllQRs();
}
function previewQR(qrData, qrName) {
dashboardManager?.previewQR(qrData, qrName);
}
function downloadQR(base64Image, filename) {
dashboardManager?.downloadQR(base64Image, filename);
}
function closeQRModal() {
dashboardManager?.closeQRModal();
}
function downloadModalQR() {
dashboardManager?.downloadModalQR();
}
// NEW: Global function for QR status toggle
function toggleQRCodeStatus(qrId) {
dashboardManager?.toggleQRCodeStatus(qrId);
}
// Initialize dashboard when DOM is ready
document.addEventListener("DOMContentLoaded", function () {
dashboardManager = new DashboardManager();
// Add helpful keyboard shortcuts tooltip
console.log("Dashboard keyboard shortcuts:");
console.log("Ctrl/Cmd + F: Focus search");
console.log("Escape: Close modal");
});