From f96009df9082012bbc83c993c97227f043bce436 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 6 Mar 2026 10:54:30 -0500 Subject: [PATCH] code hygiene --- app.py | 93 ++---- logger_handler.py | 300 +++++++++--------- requirements.txt | 60 ++-- static/js/dashboard_backup.js | 573 ---------------------------------- 4 files changed, 221 insertions(+), 805 deletions(-) delete mode 100644 static/js/dashboard_backup.js diff --git a/app.py b/app.py index 0cfd9eb..a9fe568 100644 --- a/app.py +++ b/app.py @@ -37,8 +37,8 @@ app.config['TEMPLATES_AUTO_RELOAD'] = os.environ.get('TEMPLATES_AUTO_RELOAD') # Session configuration for "Remember Me" functionality 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_HTTPONLY'] = os.environ.get('SESSION_COOKIE_HTTPONLY') +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', 'true').lower() == 'true' app.config['SESSION_COOKIE_SAMESITE'] = os.environ.get('SESSION_COOKIE_SAMESITE') # Photo Verification Configuration @@ -110,8 +110,8 @@ def create_performance_indexes(): print(f"❌ Error creating performance indexes: {e}") db.session.rollback() logger_handler.log_database_error( - error_type="index_creation_error", - error_message=str(e), + 'index_creation_error', + e, query="CREATE INDEX statements" ) @@ -4596,40 +4596,6 @@ def deactivate_qr_code(qr_id): 'message': 'Error deactivating QR code. Please try again.' }), 500 -@app.route('/qr-codes//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') @login_required def attendance_report(): @@ -6039,8 +6005,8 @@ def export_configuration(): # Use your existing logger error method with correct parameters try: logger_handler.log_flask_error( - error_type="export_configuration_error", - error_message=str(e), + 'export_configuration_error', + str(e), stack_trace=traceback.format_exc() ) except Exception as log_error: @@ -6194,8 +6160,8 @@ def generate_excel_export(): # Use your existing logger error method with correct parameters try: logger_handler.log_flask_error( - error_type="excel_export_error", - error_message=str(e), + 'excel_export_error', + str(e), stack_trace=traceback.format_exc() ) except Exception as log_error: @@ -6478,8 +6444,8 @@ def create_excel_export(selected_columns, column_names, filters): # Log error try: logger_handler.log_flask_error( - error_type="excel_export_error", - error_message=str(e), + 'excel_export_error', + str(e), stack_trace=traceback.format_exc() ) except Exception as log_error: @@ -6785,8 +6751,8 @@ def create_excel_export_ordered(selected_columns, column_names, filters): # Log error try: logger_handler.log_flask_error( - error_type="excel_export_ordered_error", - error_message=str(e), + 'excel_export_ordered_error', + str(e), stack_trace=traceback.format_exc() ) except Exception as log_error: @@ -6932,8 +6898,8 @@ def payroll_dashboard(): print(f"❌ Traceback: {traceback.format_exc()}") logger_handler.log_flask_error( - error_type="payroll_dashboard_error", - error_message=str(e), + 'payroll_dashboard_error', + str(e), stack_trace=traceback.format_exc() ) @@ -7173,8 +7139,8 @@ def export_payroll_excel(): print(f"❌ Traceback: {traceback.format_exc()}") logger_handler.log_flask_error( - error_type="payroll_excel_export_error", - error_message=str(e), + 'payroll_excel_export_error', + str(e), stack_trace=traceback.format_exc() ) @@ -7231,8 +7197,8 @@ def calculate_working_hours_api(): attendance_records = query.all() - # Calculate working hours using single check-in calculator - calculator = SingleCheckInCalculator() + # Calculate working hours using WorkingHoursCalculator + calculator = WorkingHoursCalculator() hours_data = calculator.calculate_employee_hours( str(employee_id), start_date, end_date, attendance_records ) @@ -7248,8 +7214,8 @@ def calculate_working_hours_api(): except Exception as e: print(f"❌ Error in calculate_working_hours_api: {e}") logger_handler.log_flask_error( - error_type="working_hours_api_error", - error_message=str(e), + 'working_hours_api_error', + str(e), stack_trace=traceback.format_exc() ) @@ -7355,7 +7321,6 @@ def get_miss_punch_details(employee_id): converted_records.append(converted_record) # Calculate working hours using the same calculator as the dashboard - #calculator = SingleCheckInCalculator() # Calculate hours for this employee hours_data = calculator.calculate_employee_hours( @@ -7415,8 +7380,8 @@ def get_miss_punch_details(employee_id): print(f"❌ Traceback: {traceback.format_exc()}") logger_handler.log_flask_error( - error_type="miss_punch_details_api_error", - error_message=str(e), + 'miss_punch_details_api_error', + str(e), 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']}" ) - # Calculate working hours using SingleCheckInCalculator + # Calculate working hours using WorkingHoursCalculator calculator = WorkingHoursCalculator() hours_data = calculator.calculate_all_employees_hours( datetime.combine(start_date, datetime.min.time()), @@ -11217,8 +11182,8 @@ def update_existing_qr_codes(): except Exception as e: logger_handler.log_flask_error( - error_type="qr_code_update_error", - error_message=f"Failed to update QR code {qr_code.id}: {str(e)}" + 'qr_code_update_error', + f"Failed to update QR code {qr_code.id}: {str(e)}" ) continue @@ -11437,8 +11402,8 @@ def get_optimized_statistics(date_from=None, date_to=None, project_filter=None): except Exception as e: logger_handler.log_database_error( - error_type="statistics_query_error", - error_message=str(e), + 'statistics_query_error', + e, query="get_optimized_statistics" ) raise @@ -11493,8 +11458,8 @@ if __name__ == '__main__': print(f"❌ Application startup failed: {e}") if hasattr(app, 'logger_handler'): logger_handler.log_flask_error( - error_type="application_startup_error", - error_message=str(e) + 'application_startup_error', + str(e) ) raise diff --git a/logger_handler.py b/logger_handler.py index d13b4a9..1686036 100644 --- a/logger_handler.py +++ b/logger_handler.py @@ -25,7 +25,7 @@ import os import traceback from datetime import datetime, date, timedelta from functools import wraps -from flask import request, session, g +from flask import request, session, g, render_template from sqlalchemy import text from sqlalchemy.exc import SQLAlchemyError import uuid @@ -187,17 +187,7 @@ class AppLogger: if self.app.debug: return None # Let Flask handle debug errors - return ''' - - - Server Error - -

🔧 Something went wrong

-

We're working to fix this issue. Please try again later.

- ← Back to Home - - - ''', 500 + return render_template('errors/500.html'), 500 @self.app.errorhandler(404) def handle_not_found(error): @@ -208,17 +198,7 @@ class AppLogger: severity="LOW" ) - return ''' - - - Page Not Found - -

🔍 Page Not Found

-

The page you're looking for doesn't exist.

- ← Back to Home - - - ''', 404 + return render_template('errors/404.html'), 404 def _get_request_context(self): """Get current request context information""" @@ -773,69 +753,109 @@ class AppLogger: self.log_database_error('cleanup_old_logs', e) return 0 -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""" - try: - cutoff_date = datetime.now() - timedelta(days=days) - - # Build the base query - base_sql = """ - SELECT - event_id, - event_type, - event_category, - event_description, - severity_level, - created_timestamp, - username, - ip_address, - user_id - FROM log_events - WHERE created_timestamp >= :cutoff_date - """ - - # Add filters - params = {'cutoff_date': cutoff_date} - - if category_filter: - base_sql += " AND event_category = :category_filter" - params['category_filter'] = category_filter - - if severity_filter: - base_sql += " AND severity_level = :severity_filter" - params['severity_filter'] = severity_filter - - if 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}%" - - # Add ordering and limit - base_sql += " ORDER BY created_timestamp DESC LIMIT :limit" - params['limit'] = limit - - result = self.db.session.execute(text(base_sql), params).fetchall() - - logs = [] - for row in result: - logs.append({ - 'event_id': row.event_id, - 'event_type': row.event_type, - 'event_category': row.event_category, - 'description': row.event_description, - 'severity': row.severity_level, - 'timestamp': row.created_timestamp.isoformat(), - 'username': row.username or 'System', - 'ip_address': row.ip_address or '-', - 'user_id': row.user_id - }) - - return logs - - except Exception as e: - self.log_database_error('get_recent_logs', e) - print(f"Error in get_recent_logs: {e}") - return [] - + 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""" + try: + cutoff_date = datetime.now() - timedelta(days=days) + + # Build the base query + base_sql = """ + SELECT + event_id, + event_type, + event_category, + event_description, + severity_level, + created_timestamp, + username, + ip_address, + user_id + FROM log_events + WHERE created_timestamp >= :cutoff_date + """ + + # Add filters + params = {'cutoff_date': cutoff_date} + + if category_filter: + base_sql += " AND event_category = :category_filter" + params['category_filter'] = category_filter + + if severity_filter: + base_sql += " AND severity_level = :severity_filter" + params['severity_filter'] = severity_filter + + if 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}%" + + # Add ordering and limit + base_sql += " ORDER BY created_timestamp DESC LIMIT :limit" + params['limit'] = limit + + result = self.db.session.execute(text(base_sql), params).fetchall() + + logs = [] + for row in result: + logs.append({ + 'event_id': row.event_id, + 'event_type': row.event_type, + 'event_category': row.event_category, + 'description': row.event_description, + 'severity': row.severity_level, + 'timestamp': row.created_timestamp.isoformat(), + 'username': row.username or 'System', + 'ip_address': row.ip_address or '-', + 'user_id': row.user_id + }) + + return logs + + except Exception as e: + self.log_database_error('get_recent_logs', e) + print(f"Error in get_recent_logs: {e}") + 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 def log_user_activity(activity_type): @@ -897,62 +917,62 @@ def log_database_operations(operation_name): return decorated_function return decorator -def verify_log_table_exists(self): - """Verify that the log_events table exists and has the correct structure""" - try: - # Check if table exists - check_table_sql = """ - SELECT COUNT(*) as table_exists - FROM information_schema.tables - WHERE table_schema = DATABASE() - AND table_name = 'log_events' - """ - - result = self.db.session.execute(text(check_table_sql)).fetchone() - - if result.table_exists == 0: - print("⚠️ log_events table does not exist. Creating it now...") - self._create_log_table() + def verify_log_table_exists(self): + """Verify that the log_events table exists and has the correct structure""" + try: + # Check if table exists + check_table_sql = """ + SELECT COUNT(*) as table_exists + FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = 'log_events' + """ + + result = self.db.session.execute(text(check_table_sql)).fetchone() + + if result.table_exists == 0: + print("⚠️ log_events table does not exist. Creating it now...") + 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 - - # 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 - - except Exception as e: - print(f"❌ Error verifying log table: {e}") - return False + + except Exception as e: + print(f"❌ Error verifying log table: {e}") + return False -def log_modal_interaction(self, event_type, description, additional_data=None): - """Log modal interactions for debugging""" - try: - context = self._get_request_context() - - event_data = { - 'interaction_type': event_type, - 'event_timestamp': datetime.now().isoformat(), - 'request_context': context - } - - if additional_data: - event_data['additional_data'] = additional_data - - message = f"Modal interaction: {event_type} - {description}" - - # Log to database - self._log_to_database( - event_type='modal_interaction', - event_category='ui', - description=message, - event_data=event_data, - severity='INFO' - ) - - except Exception as e: - print(f"Error logging modal interaction: {e}") + def log_modal_interaction(self, event_type, description, additional_data=None): + """Log modal interactions for debugging""" + try: + context = self._get_request_context() + + event_data = { + 'interaction_type': event_type, + 'event_timestamp': datetime.now().isoformat(), + 'request_context': context + } + + if additional_data: + event_data['additional_data'] = additional_data + + message = f"Modal interaction: {event_type} - {description}" + + # Log to database + self._log_to_database( + event_type='modal_interaction', + event_category='ui', + description=message, + event_data=event_data, + severity='INFO' + ) + + except Exception as e: + print(f"Error logging modal interaction: {e}") # INITIALIZATION FUNCTION def init_logging(app, db): diff --git a/requirements.txt b/requirements.txt index ea95fa4..9fb9bdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,64 +1,68 @@ # QR Code Management System - Python Dependencies # 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 -Flask -Flask-SQLAlchemy +Flask==3.1.0 +Flask-SQLAlchemy==3.1.1 # Database Support - MySQL -PyMySQL # Pure Python MySQL client -mysql-connector-python # Official MySQL connector (alternative) -SQLAlchemy +PyMySQL==1.1.1 # Pure Python MySQL client +mysql-connector-python==9.2.0 # Official MySQL connector (alternative) +SQLAlchemy==2.0.36 # Security and Authentication -Werkzeug # Security utilities and password hashing +Werkzeug==3.1.3 # Security utilities and password hashing # QR Code Generation -qrcode # QR code generation library -Pillow # Image processing for QR codes +qrcode==8.0 # QR code generation library +Pillow==11.1.0 # Image processing for QR codes # 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 -regex # Enhanced regex support for URL generation +regex==2024.11.6 # Enhanced regex support for URL generation # Environment and Configuration -python-dotenv # Environment variable management +python-dotenv==1.0.1 # Environment variable management # Date and Time Processing -python-dateutil # Extended date/time processing +python-dateutil==2.9.0.post0 # Extended date/time processing # Development and Testing (optional) -pytest # Testing framework -pytest-flask # Flask testing utilities -Flask-Testing # Additional Flask testing tools +pytest==8.3.4 # Testing framework +pytest-flask==1.3.0 # Flask testing utilities +Flask-Testing==0.8.1 # Additional Flask testing tools # Production Server (optional) -gunicorn # WSGI HTTP Server for production -gevent # Async worker support +gunicorn==23.0.0 # WSGI HTTP Server for production +gevent==24.11.1 # Async worker support # Utilities -click # Command line interface creation -itsdangerous # Secure data serialization -Jinja2 # Template engine -MarkupSafe # Safe string handling +click==8.1.8 # Command line interface creation +itsdangerous==2.2.0 # Secure data serialization +Jinja2==3.1.5 # Template engine +MarkupSafe==3.0.2 # Safe string handling # Data Export and Processing -openpyxl # Excel file generation for attendance reports -pandas # Data manipulation for reports (optional) +openpyxl==3.1.5 # Excel file generation for attendance reports +pandas==2.2.3 # Data manipulation for reports (optional) # 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) -Flask-Caching # Caching support for Flask +Flask-Caching==2.3.0 # Caching support for Flask # 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 # Required for MySQL SSL connections +cryptography==44.0.0 # Required for MySQL SSL connections # Employee Synchronization Dependencies -schedule # For automated scheduling \ No newline at end of file +schedule==1.2.2 # For automated scheduling \ No newline at end of file diff --git a/static/js/dashboard_backup.js b/static/js/dashboard_backup.js deleted file mode 100644 index c2b73ec..0000000 --- a/static/js/dashboard_backup.js +++ /dev/null @@ -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 = ` -
- -
-

No QR Codes Found

-

Try adjusting your search or filter criteria

- - `; - 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 = ` ${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 = ` ${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 = ` -
- - ${message} -
- `; - - 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"); -});