From bfc2f58fe205d11c2e66042b1fa0bc4cb2931e05 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Tue, 28 Apr 2026 16:27:39 -0400 Subject: [PATCH] 04/28 Fixed some security issues --- advanced_security_middleware.py | 53 +- app.py | 38 + requirements.txt | 4 +- routes/auth.py | 23 +- static/js/attendance_report.js | 2379 +++++++++-------- static/js/dashboard.js | 1402 +++++----- static/js/script.js | 5 +- static/js/users.js | 1486 +++++----- templates/add_manual_attendance.html | 1 + templates/admin_logs.html | 4 + templates/attendance_report.html | 3 +- templates/bulk_qr_import.html | 2 + templates/confirm_delete_qr.html | 1 + templates/create_employee.html | 1 + templates/create_project.html | 1 + templates/create_qr_code.html | 2 + templates/create_user.html | 2 + templates/edit_attendance.html | 1 + templates/edit_employee.html | 1 + templates/edit_project.html | 1 + templates/edit_qr_code.html | 539 ++-- templates/edit_user.html | 2 + templates/employees.html | 1 + templates/export_configuration.html | 1 + templates/login.html | 1 + templates/payroll_dashboard.html | 5 + templates/profile.html | 2366 ++++++++-------- templates/projects.html | 1 + templates/register.html | 1 + .../time_attendance_duplicate_review.html | 1 + templates/time_attendance_import.html | 2 + .../time_attendance_import_progress.html | 1 + templates/time_attendance_invalid_review.html | 1 + templates/time_attendance_record_detail.html | 2 +- templates/time_attendance_records.html | 2 +- templates/users.html | 2 + templates/verification_review.html | 1 + templates/verification_review_detail.html | 1 + 38 files changed, 4273 insertions(+), 4067 deletions(-) diff --git a/advanced_security_middleware.py b/advanced_security_middleware.py index c3f8145..6f15ee5 100644 --- a/advanced_security_middleware.py +++ b/advanced_security_middleware.py @@ -60,24 +60,24 @@ class SecurityManager: self.register_security_routes() def setup_encryption(self): - """Setup encryption for sensitive data""" + """Setup encryption for sensitive data. + + Derives a stable Fernet key from the app's SECRET_KEY so that all + gunicorn workers share the same key without needing a separate + ENCRYPTION_KEY env var. A random key is only generated as a last + resort (dev mode without SECRET_KEY set). + """ 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!" - ) - + # Derive a deterministic 32-byte key from SECRET_KEY so every + # worker produces the same value — no per-worker randomness. + secret = self.app.config.get('SECRET_KEY', '') + derived = hashlib.sha256(secret.encode()).digest() + encryption_key = base64.urlsafe_b64encode(derived) 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""" @@ -93,12 +93,12 @@ class SecurityManager: }) 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 - + # NOTE: per-worker in-memory session token validation removed. + # Flask's cryptographically signed session cookie provides session + # integrity; CSRF tokens handle cross-site forgery. Keeping the + # validate_session_security() call here would log users out on every + # gunicorn worker boundary because session_tokens is not shared. + # Check for SQL injection attempts if self.detect_sql_injection(): self.log_security_event('sql_injection_attempt', { @@ -212,9 +212,20 @@ class SecurityManager: 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))) + + # Only attempt JSON parsing when the client declared application/json. + # Calling request.json without this guard raises a 415 Unsupported Media Type + # on every non-JSON request (GET pages, form POSTs, favicon, etc.). + if request.content_type and 'application/json' in request.content_type: + try: + json_body = request.get_json(silent=True, force=False) + if json_body and isinstance(json_body, dict): + check_data.extend( + str(v) for v in json_body.values() + if isinstance(v, (str, int, float)) + ) + except Exception: + pass for data in check_data: data_str = str(data).lower() diff --git a/app.py b/app.py index 4c08359..4237e90 100644 --- a/app.py +++ b/app.py @@ -99,7 +99,45 @@ def create_app() -> Flask: from extensions import logger_handler as _lh create_location_logging_routes(app, db, _lh) + # ------------------------------------------------------------------ + # Security: CSRF protection + rate-limiting via SecurityManager + # ------------------------------------------------------------------ + from advanced_security_middleware import SecurityManager, generate_csrf_token + from extensions import logger_handler as _lh2 + security_manager = SecurityManager() + security_manager.init_app(app, db, _lh2) + # Endpoints exempt from CSRF validation: + # - login / register (no session token exists yet) + # - qr_checkin (public, unauthenticated QR scan endpoint) + _CSRF_EXEMPT = {'auth.login', 'auth.register', 'qr_codes.qr_checkin', 'static'} + + @app.before_request + def csrf_protect(): + """Validate CSRF token on every state-mutating request.""" + if request.method not in ('POST', 'PUT', 'PATCH', 'DELETE'): + return + if request.endpoint in _CSRF_EXEMPT: + return + token = (request.form.get('csrf_token') + or request.headers.get('X-CSRF-Token')) + expected = session.get('csrf_token') + import hmac as _hmac + if not token or not expected or not _hmac.compare_digest(token, expected): + _lh2.logger.warning( + f"CSRF validation failed | endpoint={request.endpoint} " + f"| ip={request.remote_addr} | user={session.get('username','anon')}" + ) + from flask import abort + abort(403) + + # Make generate_csrf_token() available in every template as csrf_token() + @app.context_processor + def inject_csrf_token(): + return {'csrf_token': generate_csrf_token} + + # Expose security_manager to routes that need it (login rate-limiting) + app.security_manager = security_manager # ------------------------------------------------------------------ # Template filters (global — must be on app, not blueprints) diff --git a/requirements.txt b/requirements.txt index 9fb9bdd..2075565 100644 --- a/requirements.txt +++ b/requirements.txt @@ -65,4 +65,6 @@ python-json-logger==3.2.1 # Structured logging support cryptography==44.0.0 # Required for MySQL SSL connections # Employee Synchronization Dependencies -schedule==1.2.2 # For automated scheduling \ No newline at end of file +schedule==1.2.2 # For automated scheduling + +jwt \ No newline at end of file diff --git a/routes/auth.py b/routes/auth.py index 9d1666e..85064ae 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -28,6 +28,8 @@ def index(): return redirect(url_for('auth.login')) @bp.route('/register', methods=['GET', 'POST'], endpoint='register') +@login_required +@admin_required @log_user_activity('user_registration') def register(): """User registration endpoint""" @@ -84,6 +86,18 @@ def login(): flash('Please enter both username and password.', 'error') return render_template('login.html') + # Rate-limit check — blocks IPs with 5+ failed attempts in 15 minutes + from flask import current_app + sec_mgr = getattr(current_app, 'security_manager', None) + if sec_mgr and sec_mgr.is_auth_rate_limited(): + logger_handler.log_security_event( + event_type="login_rate_limited", + description=f"Login blocked by rate limiter for username: {username}", + severity="HIGH" + ) + flash('Too many failed attempts. Please wait 15 minutes before trying again.', 'error') + return render_template('login.html') + # Verify Turnstile if enabled if turnstile_utils.is_enabled(): if not turnstile_utils.verify_turnstile(turnstile_response): @@ -126,6 +140,10 @@ def login(): session['full_name'] = user.full_name session['login_time'] = datetime.now().isoformat() + # Create a secure session token (also clears failed attempts for this IP) + if sec_mgr: + sec_mgr.create_secure_session(user.id) + # Update last login date user.last_login_date = datetime.utcnow() db.session.commit() @@ -162,7 +180,10 @@ def login(): return redirect(url_for('attendance.attendance_report')) else: - # Invalid credentials - log failed attempt + # Invalid credentials — record failed attempt for rate limiting + if sec_mgr: + sec_mgr.record_failed_attempt(username) + user_id = user.id if user else None logger_handler.log_user_login( user_id=user_id, diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index d2ced6e..57ca777 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -1,1190 +1,1191 @@ -/** - * Enhanced Attendance Report JavaScript - * Handles filtering, sorting, pagination, and new location/accuracy features - */ - -// Global variables -let currentPage = 1; -let entriesPerPage = 50; -let sortColumn = -1; -let sortDirection = "asc"; -let attendanceData = []; -let filteredData = []; - -// Charts -let dailyChart = null; -let locationChart = null; - -// Initialize page when DOM is loaded -document.addEventListener("DOMContentLoaded", function () { - console.log("Enhanced Attendance Report page initialized"); - - initializeReport(); - loadAttendanceData(); - initializeCharts(); - setupEventListeners(); - initializeDateRangeFilters(); -}); - -function initializeReport() { - // Load data from table - loadTableData(); - - // Initialize pagination - updatePagination(); - - // Apply initial filters if any - applyFilters(); -} - - -function extractAccuracyValue(cell) { - const text = cell.textContent; - const match = text.match(/(\d+\.?\d*)m/); - return match ? parseFloat(match[1]) : null; -} - -function extractAccuracyLevel(cell) { - const text = cell.textContent; - if (text.includes("high")) return "high"; - if (text.includes("medium")) return "medium"; - if (text.includes("low")) return "low"; - return "unknown"; -} - -function extractCoordinates(cell) { - // This would need to be enhanced based on actual data structure - // For now, return placeholder - return "Coordinates available"; -} - -function initializeDateRangeFilters() { - const dateFromInput = document.getElementById("date_from"); - const dateToInput = document.getElementById("date_to"); - - if (dateFromInput && dateToInput) { - // Set max date to today - const today = new Date().toISOString().split("T")[0]; - dateFromInput.max = today; - dateToInput.max = today; - - // Add validation to ensure 'from' date is not after 'to' date - dateFromInput.addEventListener("change", function () { - if (dateToInput.value && this.value > dateToInput.value) { - dateToInput.value = this.value; - } - }); - - dateToInput.addEventListener("change", function () { - if (dateFromInput.value && this.value < dateFromInput.value) { - dateFromInput.value = this.value; - } - }); - } -} - -function setupEventListeners() { - // Enhanced search and filter listeners - const searchInput = document.getElementById("searchInput"); - const locationFilter = document.getElementById("location"); - const employeeFilter = document.getElementById("employee"); - - if (searchInput) { - searchInput.addEventListener("input", debounce(applyFilters, 300)); - } - - if (locationFilter) { - locationFilter.addEventListener("change", applyFilters); - } - - if (employeeFilter) { - employeeFilter.addEventListener("input", debounce(applyFilters, 300)); - } - - // Entries per page listener - const entriesSelect = document.getElementById("entriesPerPage"); - if (entriesSelect) { - entriesSelect.addEventListener("change", changeEntriesPerPage); - } - - // Modal close listeners - window.addEventListener("click", function (event) { - const recordModal = document.getElementById("recordModal"); - const mapModal = document.getElementById("mapModal"); - - if (event.target === recordModal) { - closeModal(); - } - if (event.target === mapModal) { - closeMapModal(); - } - }); - - // Keyboard shortcuts - document.addEventListener("keydown", function (event) { - if (event.key === "Escape") { - closeModal(); - closeMapModal(); - } - }); -} - -function applyFilters() { - const searchTerm = - document.getElementById("searchInput")?.value.toLowerCase() || ""; - const locationFilter = document.getElementById("location")?.value || ""; - // Support comma-separated multi-employee filter - const employeeFilterRaw = - document.getElementById("employee")?.value || ""; - const employeeFilterIds = employeeFilterRaw - ? employeeFilterRaw.split(",").map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean) - : []; - - filteredData = attendanceData.filter((record) => { - const matchesSearch = - !searchTerm || - record.employeeId.toLowerCase().includes(searchTerm) || - record.location.toLowerCase().includes(searchTerm) || - record.event.toLowerCase().includes(searchTerm); - - const matchesLocation = - !locationFilter || (record.location && record.location.trim() === locationFilter.trim()); - const matchesEmployee = - employeeFilterIds.length === 0 || - employeeFilterIds.some(function(id) { - return record.employeeId.toLowerCase() === id; - }); - - return matchesSearch && matchesLocation && matchesEmployee; - }); - - currentPage = 1; - updateTable(); - updatePagination(); - updateFilterStats(); -} - -function sortTable(columnIndex) { - if (sortColumn === columnIndex) { - sortDirection = sortDirection === "asc" ? "desc" : "asc"; - } else { - sortColumn = columnIndex; - sortDirection = "asc"; - } - - const sortKey = getSortKey(columnIndex); - - filteredData.sort((a, b) => { - let aVal = a[sortKey]; - let bVal = b[sortKey]; - - // Handle numeric values for accuracy - if (columnIndex === 8 && aVal !== null && bVal !== null) { - aVal = parseFloat(aVal); - bVal = parseFloat(bVal); - } - - // Handle null values - if (aVal === null || aVal === undefined) aVal = ""; - if (bVal === null || bVal === undefined) bVal = ""; - - if (typeof aVal === "string") { - aVal = aVal.toLowerCase(); - bVal = bVal.toLowerCase(); - } - - let result; - if (aVal < bVal) result = -1; - else if (aVal > bVal) result = 1; - else result = 0; - - return sortDirection === "asc" ? result : -result; - }); - - updateTable(); - updateSortIndicators(columnIndex); -} - -function getSortKey(columnIndex) { - const sortKeys = [ - "index", - "employeeId", - "location", - "event", - "date", - "time", - "qr_address", - "checked_in_address", - "accuracy", - "device", - ]; - return sortKeys[columnIndex] || "index"; -} - -function updateSortIndicators(activeColumn) { - // Update sort indicators in table headers - const headers = document.querySelectorAll(".attendance-table th"); - headers.forEach((header, index) => { - const icon = header.querySelector("i"); - if (icon) { - icon.className = "fas fa-sort"; - if (index === activeColumn) { - icon.className = - sortDirection === "asc" ? "fas fa-sort-up" : "fas fa-sort-down"; - } - } - }); -} - -function updateTable() { - const table = document.getElementById("attendanceTable"); - if (!table) return; - - const tbody = table.querySelector("tbody"); - const startIndex = (currentPage - 1) * entriesPerPage; - const endIndex = - entriesPerPage === "all" - ? filteredData.length - : startIndex + entriesPerPage; - const pageData = filteredData.slice(startIndex, endIndex); - - tbody.innerHTML = ""; - - pageData.forEach((record, index) => { - const row = createTableRow(record, startIndex + index + 1); - tbody.appendChild(row); - }); - - // Update any dynamic elements - updateFilterStats(); -} - -function changeEntriesPerPage() { - const select = document.getElementById("entriesPerPage"); - entriesPerPage = select.value === "all" ? "all" : parseInt(select.value); - currentPage = 1; - updateTable(); - updatePagination(); -} - -function updatePagination() { - const container = document.getElementById("paginationContainer"); - if (!container || entriesPerPage === "all") { - if (container) container.innerHTML = ""; - return; - } - - const totalPages = Math.ceil(filteredData.length / entriesPerPage); - - if (totalPages <= 1) { - container.innerHTML = ""; - return; - } - - let paginationHTML = '"; - - // Add pagination info - const startRecord = (currentPage - 1) * entriesPerPage + 1; - const endRecord = Math.min(currentPage * entriesPerPage, filteredData.length); - - paginationHTML += ` -
- Showing ${startRecord} to ${endRecord} of ${ - filteredData.length - } entries - ${ - filteredData.length !== attendanceData.length - ? `(filtered from ${attendanceData.length} total entries)` - : "" - } -
- `; - - container.innerHTML = paginationHTML; -} - -function goToPage(page) { - const totalPages = Math.ceil(filteredData.length / entriesPerPage); - - if (page < 1 || page > totalPages) return; - - currentPage = page; - updateTable(); - updatePagination(); - - // Scroll to top of table - const table = document.getElementById("attendanceTable"); - if (table) { - table.scrollIntoView({ behavior: "smooth", block: "start" }); - } -} - -function updateFilterStats() { - // Update stats display if needed - const totalRecords = filteredData.length; - console.log(`Filtered records: ${totalRecords}`); -} - -// Enhanced record actions -function editRecord(recordId) { - // Check permissions before allowing edit - if (!hasEditPermission) { - alert( - "Access denied. Only administrators can edit attendance records." - ); - return; - } - - console.log(`Edit record: ${recordId}`); - // Log the action - console.log(`[LOG] User attempting to edit attendance record: ${recordId}`); - - // Redirect to edit page - window.location.href = `/attendance/${recordId}/edit`; -} - -function deleteRecord(recordId, employeeId) { - // Check permissions before allowing delete - if (!hasEditPermission) { - alert( - "Access denied. Only administrators can delete attendance records." - ); - return; - } - - console.log(`Delete record: ${recordId}`); - - // Confirmation dialog - const confirmMessage = `Are you sure you want to delete the attendance record for employee "${employeeId}"?\n\nThis action cannot be undone.`; - - if (confirm(confirmMessage)) { - console.log( - `[LOG] User confirmed deletion of attendance record: ${recordId}` - ); - - // Send delete request - fetch(`/attendance/${recordId}/delete`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Requested-With": "XMLHttpRequest", - }, - }) - .then((response) => response.json()) - .then((data) => { - if (data.success) { - console.log( - `[LOG] Successfully deleted attendance record: ${recordId}` - ); - alert("Attendance record deleted successfully!"); - window.location.reload(); - } else { - console.error( - `[LOG] Failed to delete attendance record: ${recordId} - ${data.message}` - ); - alert(data.message || "Error deleting record. Please try again."); - } - }) - .catch((error) => { - console.error( - `[LOG] Error during attendance record deletion: ${recordId}`, - error - ); - alert("Error deleting record. Please try again."); - }); - } -} - -function closeModal() { - const modal = document.getElementById("recordModal"); - if (modal) { - modal.style.display = "none"; - } -} - -// Chart initialization (placeholder) -function initializeCharts() { - console.log("Initializing charts..."); - // Chart implementation would go here -} - -function loadAttendanceData() { - console.log("Loading attendance data for charts..."); - // Additional data loading for charts would go here -} - -// Utility function -function debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -} - -// Enhanced JavaScript functions for location accuracy features - -function loadTableData() { - const table = document.getElementById("attendanceTable"); - if (table) { - const rows = table.querySelectorAll("tbody tr"); - attendanceData = Array.from(rows).map((row, index) => { - const cells = row.querySelectorAll("td"); - - // Extract verification data from the accuracy badge - const verificationData = extractVerificationData(cells[9]); - - return { - id: row.dataset.recordId, - index: index + 1, - employeeId: cells[1] ? cells[1].textContent.trim() : "", - employeeName: cells[2] ? cells[2].textContent.trim() : "", // NEW: Employee Name column - location: cells[3] ? cells[3].textContent.trim() : "", // Updated from cells[2] - event: cells[4] ? cells[4].textContent.trim() : "", // Updated from cells[3] - date: cells[5] ? cells[5].textContent.trim() : "", // Updated from cells[4] - time: cells[6] ? cells[6].textContent.trim() : "", // Updated from cells[5] - qr_address: cells[7] // Updated from cells[6] - ? cells[7].getAttribute("title") || cells[7].textContent.trim() - : "", - checked_in_address: cells[8] // Updated from cells[7] - ? cells[8].getAttribute("title") || cells[8].textContent.trim() - : "", - // FIXED: Extract location accuracy for address display logic - location_accuracy: cells[9] ? extractLocationAccuracy(cells[9]) : null, // Updated from cells[8] - accuracy_level: cells[9] // Updated from cells[8] - ? extractLocationAccuracyLevel(cells[9]) - : "unknown", - device: cells[10] // Updated from cells[9] - ? cells[10].textContent.trim() - : "", - isModified: row.classList.contains('modified-record'), - isDynamic: row.dataset.isDynamic === '1', - verification_required: verificationData.required, - verification_status: verificationData.status - }; - }); - - filteredData = [...attendanceData]; - console.log(`Loaded ${attendanceData.length} attendance records`); - - // Debug log for location accuracy data - const recordsWithAccuracy = attendanceData.filter( - (r) => r.location_accuracy !== null - ); - console.log( - `Records with location accuracy: ${recordsWithAccuracy.length}` - ); - if (recordsWithAccuracy.length > 0) { - console.log( - `Sample location accuracy values:`, - recordsWithAccuracy.slice(0, 3).map((r) => r.location_accuracy) - ); - } - } -} - -function extractLocationAccuracy(cell) { - const text = cell.textContent; - console.log(`Extracting accuracy from: "${text}"`); - - // Look for miles pattern (e.g., "0.003 mi", "1.234 mi") - const milesMatch = text.match(/(\d+\.?\d*)\s*mi/); - if (milesMatch) { - const value = parseFloat(milesMatch[1]); - console.log(`Found miles: ${value}`); - return value; - } - - // Look for specific accuracy patterns in the HTML - const accuracyMatch = text.match(/accuracy[:\s]*(\d+\.?\d*)/i); - if (accuracyMatch) { - const value = parseFloat(accuracyMatch[1]); - console.log(`Found accuracy: ${value}`); - return value; - } - - // Check for data attributes - const dataAccuracy = cell.getAttribute("data-accuracy"); - if (dataAccuracy) { - const value = parseFloat(dataAccuracy); - console.log(`Found data-accuracy: ${value}`); - return value; - } - - // Fallback: look for GPS accuracy in meters and convert to miles (approximate) - const metersMatch = text.match(/(\d+\.?\d*)\s*m/); - if (metersMatch) { - const meters = parseFloat(metersMatch[1]); - const miles = meters * 0.000621371; // Convert meters to miles (approximate) - console.log(`Found meters: ${meters}, converted to miles: ${miles}`); - return miles; - } - - console.log(`No accuracy found in: "${text}"`); - return null; -} - -function extractLocationAccuracyLevel(cell) { - // Get the numerical accuracy value from the cell - const accuracy = extractLocationAccuracy(cell); - - // Return 2-level accuracy based on 0.5-mile threshold - if (accuracy !== null && accuracy !== undefined) { - return accuracy < 0.3 ? "accurate" : "inaccurate"; - } - return "unknown"; -} - -function extractVerificationData(cell) { - // Extract verification status from badge classes in the HTML - if (!cell) { - console.log('extractVerificationData: No cell provided'); - return { required: false, status: null }; - } - - const badge = cell.querySelector('.location-accuracy-badge'); - if (!badge) { - console.log('extractVerificationData: No badge found in cell'); - return { required: false, status: null }; - } - - console.log('extractVerificationData: Badge classes:', badge.className); - - // Check badge classes for verification status - if (badge.classList.contains('badge-review-needed')) { - console.log('extractVerificationData: Found pending verification'); - return { required: true, status: 'pending' }; - } else if (badge.classList.contains('badge-verified')) { - console.log('extractVerificationData: Found approved verification'); - return { required: true, status: 'approved' }; - } else if (badge.classList.contains('badge-rejected')) { - console.log('extractVerificationData: Found rejected verification'); - return { required: true, status: 'rejected' }; - } - - console.log('extractVerificationData: No verification status found, standard badge'); - return { required: false, status: null }; -} - -function createTableRow(record, displayIndex) { - const row = document.createElement("tr"); - row.dataset.recordId = record.id; - - // Apply highlighting if record was modified - if (record.isModified) { - row.classList.add('modified-record'); - } - // Apply blue-border highlight for Dynamic QR records - if (record.isDynamic) { - row.classList.add('dynamic-qr-record'); - } - - // Debug logging for first few records - if (displayIndex <= 3) { - console.log(`=== CREATING ROW ${displayIndex} ===`); - console.log(`Employee: ${record.employeeId}`); - console.log(`Location accuracy: ${record.location_accuracy}`); - console.log(`Verification required: ${record.verification_required}`); - console.log(`Verification status: ${record.verification_status}`); - console.log(`QR address: ${record.qr_address}`); - console.log(`Check-in address: ${record.checked_in_address}`); - } - - // Create location accuracy badge HTML - check verification status first - let locationAccuracyBadge; - - if (record.verification_required && record.verification_status === 'pending') { - // Show Review Needed badge for pending verification - LINK to review page - locationAccuracyBadge = ` - - Review Needed - (${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi) - `; - } else if (record.verification_status === 'approved') { - // Show Verified badge for approved verification - locationAccuracyBadge = ` - - Verified - (${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi) - `; - } else if (record.verification_status === 'rejected') { - // Show Rejected badge for rejected verification - locationAccuracyBadge = ` - - Rejected - (${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi) - `; - } else if (record.location_accuracy !== null) { - // Show standard location accuracy badge - locationAccuracyBadge = ` - - ${record.location_accuracy.toFixed(3)} mi - (${record.accuracy_level}) - `; - } else { - // No accuracy data - locationAccuracyBadge = ` - - Unknown - `; - } - - // Address display logic based on location accuracy - let addressDisplayHTML = ""; - let addressToShow = record.checked_in_address; - let addressIcon = "fas fa-location-arrow"; - let addressClass = "address-normal-accuracy"; - let addressTitle = `Check-in Address: ${record.checked_in_address}`; - - // Apply 0.5-mile threshold logic - if ( - record.location_accuracy !== null && - record.location_accuracy !== undefined - ) { - const accuracy = parseFloat(record.location_accuracy); - - if (displayIndex <= 3) { - console.log(`Applying address logic for ${record.employeeId}:`); - console.log(` Accuracy value: ${accuracy}`); - console.log(` Is <= 0.3? ${accuracy <= 0.3}`); - } - - if (!isNaN(accuracy) && accuracy <= 0.3) { - // High accuracy - use QR address - addressToShow = record.qr_address; - addressIcon = "fas fa-check-circle"; - addressClass = "address-high-accuracy"; - addressTitle = `QR Address (High Accuracy ≤ 0.5 mi): ${record.qr_address}`; - - if (displayIndex <= 3) { - console.log(` → Using QR address: ${addressToShow}`); - } - - addressDisplayHTML = ` - - - ${ - addressToShow.length > 45 - ? addressToShow.substring(0, 45) + "..." - : addressToShow - } - - `; - } else { - // Lower accuracy - use check-in address - if (displayIndex <= 3) { - console.log(` → Using check-in address: ${addressToShow}`); - } - - addressDisplayHTML = ` - - - ${ - addressToShow.length > 45 - ? addressToShow.substring(0, 45) + "..." - : addressToShow - } - - `; - } - } else { - // No accuracy data - use check-in address - if (displayIndex <= 3) { - console.log( - ` → No accuracy data, using check-in address: ${addressToShow}` - ); - } - - addressDisplayHTML = ` - - - ${ - addressToShow.length > 45 - ? addressToShow.substring(0, 45) + "..." - : addressToShow - } - - `; - } - - row.innerHTML = ` - ${displayIndex} - -
- ${record.employeeId} -
- - -
- - ${record.employeeName || 'Unknown'} -
- - -
- - ${record.location} -
- - -
- ${record.event} -
- - -
- ${record.date} -
- - -
- ${record.time} -
- - -
- - - ${ - record.qr_address.length > 50 - ? record.qr_address.substring(0, 50) + "..." - : record.qr_address - } - -
- - -
- ${addressDisplayHTML} -
- - -
- ${locationAccuracyBadge} -
- - -
- - - ${ - record.device.length > 20 - ? record.device.substring(0, 20) + "..." - : record.device - } - -
- - -
- ${ - record.verification_required && record.verification_status === 'pending' - ? ` - - ` - : '' - } - ${ - hasEditPermission - ? ` - - - ` - : ` - - - - ` - } -
- - `; - - return row; -} - -function getSortKey(columnIndex) { - const sortKeys = [ - "index", - "employeeId", - "location", - "event", - "date", - "time", - "qr_address", - "checked_in_address", - "location_accuracy", - "device", - ]; - return sortKeys[columnIndex] || "index"; -} - -// Enhanced sorting for location accuracy (numeric sorting) -function sortTable(columnIndex) { - if (sortColumn === columnIndex) { - sortDirection = sortDirection === "asc" ? "desc" : "asc"; - } else { - sortColumn = columnIndex; - sortDirection = "asc"; - } - - const sortKey = getSortKey(columnIndex); - - filteredData.sort((a, b) => { - let aVal = a[sortKey]; - let bVal = b[sortKey]; - - // Handle numeric values for location accuracy - if (columnIndex === 8 && aVal !== null && bVal !== null) { - aVal = parseFloat(aVal); - bVal = parseFloat(bVal); - } - - // Handle null values - put them at the end - if (aVal === null || aVal === undefined) { - return sortDirection === "asc" ? 1 : -1; - } - if (bVal === null || bVal === undefined) { - return sortDirection === "asc" ? -1 : 1; - } - - if (typeof aVal === "string") { - aVal = aVal.toLowerCase(); - bVal = bVal.toLowerCase(); - } - - let result; - if (aVal < bVal) result = -1; - else if (aVal > bVal) result = 1; - else result = 0; - - return sortDirection === "asc" ? result : -result; - }); - - updateTable(); - updateSortIndicators(columnIndex); -} - -// Enhanced statistics display for location accuracy -function updateFilterStats() { - const totalRecords = filteredData.length; - const recordsWithAccuracy = filteredData.filter( - (r) => r.location_accuracy !== null - ).length; - const avgAccuracy = - recordsWithAccuracy > 0 - ? filteredData - .filter((r) => r.location_accuracy !== null) - .reduce((sum, r) => sum + r.location_accuracy, 0) / - recordsWithAccuracy - : 0; - - console.log(`Filtered records: ${totalRecords}`); - console.log(`Records with location accuracy: ${recordsWithAccuracy}`); - console.log(`Average location accuracy: ${avgAccuracy.toFixed(3)} miles`); -} - -// Function to get accuracy level color for charts or displays -function getAccuracyLevelColor(level) { - const colors = { - accurate: "#059669", // green - inaccurate: "#dc2626", // red - unknown: "#6b7280", // gray - }; - return colors[level] || colors["unknown"]; -} - -// Enhanced export function to include location accuracy -function exportAttendanceWithAccuracy() { - // Build CSV header with location accuracy - const headers = [ - "#", - "Employee ID", - "Employee Name", - "Location", - "Event", - "Date", - "Time", - "QR Address", - "Check-in Address", - "Location Accuracy (miles)", - "Accuracy Level", - "Device", - ]; - - // Build CSV rows - const rows = filteredData.map((record, index) => [ - index + 1, - record.employeeId, - record.employeeName || "Unknown", - record.location, - record.event, - record.date, - record.time, - record.qr_address, - record.checked_in_address, - record.location_accuracy ? record.location_accuracy.toFixed(3) : "Unknown", - record.accuracy_level, - record.device, - ]); - - // Create CSV content - const csvContent = [headers, ...rows] - .map((row) => row.map((field) => `"${field}"`).join(",")) - .join("\n"); - - // Download CSV - const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); - const link = document.createElement("a"); - const url = URL.createObjectURL(blob); - link.setAttribute("href", url); - link.setAttribute( - "download", - `attendance_report_with_accuracy_${ - new Date().toISOString().split("T")[0] - }.csv` - ); - link.style.visibility = "hidden"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -} - -function exportAttendance() { - // Check user role before proceeding - const userRole = window.userRole; // Read from global variable set in template - console.log("Template - session.role:", '{{ session.role }}'); - console.log("Template - window.userRole set to:", window.userRole); - - if (!['admin', 'payroll', 'accounting'].includes(userRole)) { - console.log("Export access denied - insufficient privileges"); - alert("Access denied. Only administrators and payroll staff can export data."); - return; - } - - // Log export action - console.log(`Export button clicked by ${userRole} - redirecting to configuration page`); - - // Get current filters - const currentFilters = getCurrentFilters(); - - // Build URL with current filters - const params = new URLSearchParams(); - if (currentFilters.date_from) - params.append("date_from", currentFilters.date_from); - if (currentFilters.date_to) params.append("date_to", currentFilters.date_to); - if (currentFilters.location) - params.append("location", currentFilters.location); - if (currentFilters.employee) - params.append("employee", currentFilters.employee); - if (currentFilters.project) - params.append("project", currentFilters.project); - - // Navigate to export configuration page - const configUrl = - "/export-configuration" + - (params.toString() ? "?" + params.toString() : ""); - window.location.href = configUrl; -} - -function getCurrentFilters() { - // Extract current filter values from the page - return { - date_from: document.getElementById("date_from")?.value || "", - date_to: document.getElementById("date_to")?.value || "", - location: document.getElementById("location")?.value || "", - employee: document.getElementById("employee")?.value || "", - project: document.getElementById("project")?.value || "", - }; -} - -// Add a quick CSV export function as backup (keep existing functionality) -function exportAttendanceCSV() { - // Check user role before proceeding - const userRole = window.userRole; - - if (!['admin', 'payroll', 'accounting'].includes(userRole)) { - console.log("CSV export access denied - insufficient privileges"); - alert("Access denied. Only administrators and payroll staff can export data."); - return; - } - - // Build export URL with current filters for CSV - const params = new URLSearchParams(); - const filters = getCurrentFilters(); - - if (filters.date_from) params.append("date_from", filters.date_from); - if (filters.date_to) params.append("date_to", filters.date_to); - if (filters.location) params.append("location", filters.location); - if (filters.employee) params.append("employee", filters.employee); - if (filters.project) params.append("project", filters.project); - params.append("export", "csv"); - - // Create a temporary link and click it to download - const downloadUrl = window.location.pathname + "?" + params.toString(); - window.open(downloadUrl, "_blank"); -} - -// Enhanced export menu (if you want to add dropdown with multiple export options) -function showExportMenu() { - // Create export options menu - const existingMenu = document.getElementById("exportMenu"); - if (existingMenu) { - existingMenu.remove(); - return; - } - - const exportBtn = document.querySelector( - 'button[onclick="exportAttendance()"]' - ); - if (!exportBtn) return; - - const menu = document.createElement("div"); - menu.id = "exportMenu"; - menu.style.cssText = ` - position: absolute; - top: 100%; - right: 0; - background: white; - border: 1px solid #e2e8f0; - border-radius: 8px; - box-shadow: 0 4px 16px rgba(0,0,0,0.1); - z-index: 1000; - min-width: 200px; - margin-top: 5px; - `; - - menu.innerHTML = ` -
- - -
- `; - - exportBtn.parentElement.style.position = "relative"; - exportBtn.parentElement.appendChild(menu); - - // Close menu when clicking outside - setTimeout(() => { - document.addEventListener("click", function closeOnClickOutside(e) { - if (!menu.contains(e.target) && e.target !== exportBtn) { - closeExportMenu(); - document.removeEventListener("click", closeOnClickOutside); - } - }); - }, 100); -} - -function closeExportMenu() { - const menu = document.getElementById("exportMenu"); - if (menu) { - menu.remove(); - } -} - -// Initialize export functionality when page loads -document.addEventListener("DOMContentLoaded", function () { - // Update export button to use enhanced functionality - const exportBtn = document.querySelector( - 'button[onclick="exportAttendance()"]' - ); - if (exportBtn) { - // You can modify the button to show a dropdown instead - // exportBtn.onclick = showExportMenu; - // exportBtn.innerHTML = ' Export Data '; - } - - console.log("Enhanced export functionality initialized"); +/** + * Enhanced Attendance Report JavaScript + * Handles filtering, sorting, pagination, and new location/accuracy features + */ + +// Global variables +let currentPage = 1; +let entriesPerPage = 50; +let sortColumn = -1; +let sortDirection = "asc"; +let attendanceData = []; +let filteredData = []; + +// Charts +let dailyChart = null; +let locationChart = null; + +// Initialize page when DOM is loaded +document.addEventListener("DOMContentLoaded", function () { + console.log("Enhanced Attendance Report page initialized"); + + initializeReport(); + loadAttendanceData(); + initializeCharts(); + setupEventListeners(); + initializeDateRangeFilters(); +}); + +function initializeReport() { + // Load data from table + loadTableData(); + + // Initialize pagination + updatePagination(); + + // Apply initial filters if any + applyFilters(); +} + + +function extractAccuracyValue(cell) { + const text = cell.textContent; + const match = text.match(/(\d+\.?\d*)m/); + return match ? parseFloat(match[1]) : null; +} + +function extractAccuracyLevel(cell) { + const text = cell.textContent; + if (text.includes("high")) return "high"; + if (text.includes("medium")) return "medium"; + if (text.includes("low")) return "low"; + return "unknown"; +} + +function extractCoordinates(cell) { + // This would need to be enhanced based on actual data structure + // For now, return placeholder + return "Coordinates available"; +} + +function initializeDateRangeFilters() { + const dateFromInput = document.getElementById("date_from"); + const dateToInput = document.getElementById("date_to"); + + if (dateFromInput && dateToInput) { + // Set max date to today + const today = new Date().toISOString().split("T")[0]; + dateFromInput.max = today; + dateToInput.max = today; + + // Add validation to ensure 'from' date is not after 'to' date + dateFromInput.addEventListener("change", function () { + if (dateToInput.value && this.value > dateToInput.value) { + dateToInput.value = this.value; + } + }); + + dateToInput.addEventListener("change", function () { + if (dateFromInput.value && this.value < dateFromInput.value) { + dateFromInput.value = this.value; + } + }); + } +} + +function setupEventListeners() { + // Enhanced search and filter listeners + const searchInput = document.getElementById("searchInput"); + const locationFilter = document.getElementById("location"); + const employeeFilter = document.getElementById("employee"); + + if (searchInput) { + searchInput.addEventListener("input", debounce(applyFilters, 300)); + } + + if (locationFilter) { + locationFilter.addEventListener("change", applyFilters); + } + + if (employeeFilter) { + employeeFilter.addEventListener("input", debounce(applyFilters, 300)); + } + + // Entries per page listener + const entriesSelect = document.getElementById("entriesPerPage"); + if (entriesSelect) { + entriesSelect.addEventListener("change", changeEntriesPerPage); + } + + // Modal close listeners + window.addEventListener("click", function (event) { + const recordModal = document.getElementById("recordModal"); + const mapModal = document.getElementById("mapModal"); + + if (event.target === recordModal) { + closeModal(); + } + if (event.target === mapModal) { + closeMapModal(); + } + }); + + // Keyboard shortcuts + document.addEventListener("keydown", function (event) { + if (event.key === "Escape") { + closeModal(); + closeMapModal(); + } + }); +} + +function applyFilters() { + const searchTerm = + document.getElementById("searchInput")?.value.toLowerCase() || ""; + const locationFilter = document.getElementById("location")?.value || ""; + // Support comma-separated multi-employee filter + const employeeFilterRaw = + document.getElementById("employee")?.value || ""; + const employeeFilterIds = employeeFilterRaw + ? employeeFilterRaw.split(",").map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean) + : []; + + filteredData = attendanceData.filter((record) => { + const matchesSearch = + !searchTerm || + record.employeeId.toLowerCase().includes(searchTerm) || + record.location.toLowerCase().includes(searchTerm) || + record.event.toLowerCase().includes(searchTerm); + + const matchesLocation = + !locationFilter || (record.location && record.location.trim() === locationFilter.trim()); + const matchesEmployee = + employeeFilterIds.length === 0 || + employeeFilterIds.some(function(id) { + return record.employeeId.toLowerCase() === id; + }); + + return matchesSearch && matchesLocation && matchesEmployee; + }); + + currentPage = 1; + updateTable(); + updatePagination(); + updateFilterStats(); +} + +function sortTable(columnIndex) { + if (sortColumn === columnIndex) { + sortDirection = sortDirection === "asc" ? "desc" : "asc"; + } else { + sortColumn = columnIndex; + sortDirection = "asc"; + } + + const sortKey = getSortKey(columnIndex); + + filteredData.sort((a, b) => { + let aVal = a[sortKey]; + let bVal = b[sortKey]; + + // Handle numeric values for accuracy + if (columnIndex === 8 && aVal !== null && bVal !== null) { + aVal = parseFloat(aVal); + bVal = parseFloat(bVal); + } + + // Handle null values + if (aVal === null || aVal === undefined) aVal = ""; + if (bVal === null || bVal === undefined) bVal = ""; + + if (typeof aVal === "string") { + aVal = aVal.toLowerCase(); + bVal = bVal.toLowerCase(); + } + + let result; + if (aVal < bVal) result = -1; + else if (aVal > bVal) result = 1; + else result = 0; + + return sortDirection === "asc" ? result : -result; + }); + + updateTable(); + updateSortIndicators(columnIndex); +} + +function getSortKey(columnIndex) { + const sortKeys = [ + "index", + "employeeId", + "location", + "event", + "date", + "time", + "qr_address", + "checked_in_address", + "accuracy", + "device", + ]; + return sortKeys[columnIndex] || "index"; +} + +function updateSortIndicators(activeColumn) { + // Update sort indicators in table headers + const headers = document.querySelectorAll(".attendance-table th"); + headers.forEach((header, index) => { + const icon = header.querySelector("i"); + if (icon) { + icon.className = "fas fa-sort"; + if (index === activeColumn) { + icon.className = + sortDirection === "asc" ? "fas fa-sort-up" : "fas fa-sort-down"; + } + } + }); +} + +function updateTable() { + const table = document.getElementById("attendanceTable"); + if (!table) return; + + const tbody = table.querySelector("tbody"); + const startIndex = (currentPage - 1) * entriesPerPage; + const endIndex = + entriesPerPage === "all" + ? filteredData.length + : startIndex + entriesPerPage; + const pageData = filteredData.slice(startIndex, endIndex); + + tbody.innerHTML = ""; + + pageData.forEach((record, index) => { + const row = createTableRow(record, startIndex + index + 1); + tbody.appendChild(row); + }); + + // Update any dynamic elements + updateFilterStats(); +} + +function changeEntriesPerPage() { + const select = document.getElementById("entriesPerPage"); + entriesPerPage = select.value === "all" ? "all" : parseInt(select.value); + currentPage = 1; + updateTable(); + updatePagination(); +} + +function updatePagination() { + const container = document.getElementById("paginationContainer"); + if (!container || entriesPerPage === "all") { + if (container) container.innerHTML = ""; + return; + } + + const totalPages = Math.ceil(filteredData.length / entriesPerPage); + + if (totalPages <= 1) { + container.innerHTML = ""; + return; + } + + let paginationHTML = '"; + + // Add pagination info + const startRecord = (currentPage - 1) * entriesPerPage + 1; + const endRecord = Math.min(currentPage * entriesPerPage, filteredData.length); + + paginationHTML += ` +
+ Showing ${startRecord} to ${endRecord} of ${ + filteredData.length + } entries + ${ + filteredData.length !== attendanceData.length + ? `(filtered from ${attendanceData.length} total entries)` + : "" + } +
+ `; + + container.innerHTML = paginationHTML; +} + +function goToPage(page) { + const totalPages = Math.ceil(filteredData.length / entriesPerPage); + + if (page < 1 || page > totalPages) return; + + currentPage = page; + updateTable(); + updatePagination(); + + // Scroll to top of table + const table = document.getElementById("attendanceTable"); + if (table) { + table.scrollIntoView({ behavior: "smooth", block: "start" }); + } +} + +function updateFilterStats() { + // Update stats display if needed + const totalRecords = filteredData.length; + console.log(`Filtered records: ${totalRecords}`); +} + +// Enhanced record actions +function editRecord(recordId) { + // Check permissions before allowing edit + if (!hasEditPermission) { + alert( + "Access denied. Only administrators can edit attendance records." + ); + return; + } + + console.log(`Edit record: ${recordId}`); + // Log the action + console.log(`[LOG] User attempting to edit attendance record: ${recordId}`); + + // Redirect to edit page + window.location.href = `/attendance/${recordId}/edit`; +} + +function deleteRecord(recordId, employeeId) { + // Check permissions before allowing delete + if (!hasEditPermission) { + alert( + "Access denied. Only administrators can delete attendance records." + ); + return; + } + + console.log(`Delete record: ${recordId}`); + + // Confirmation dialog + const confirmMessage = `Are you sure you want to delete the attendance record for employee "${employeeId}"?\n\nThis action cannot be undone.`; + + if (confirm(confirmMessage)) { + console.log( + `[LOG] User confirmed deletion of attendance record: ${recordId}` + ); + + // Send delete request + fetch(`/attendance/${recordId}/delete`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', + }, + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + console.log( + `[LOG] Successfully deleted attendance record: ${recordId}` + ); + alert("Attendance record deleted successfully!"); + window.location.reload(); + } else { + console.error( + `[LOG] Failed to delete attendance record: ${recordId} - ${data.message}` + ); + alert(data.message || "Error deleting record. Please try again."); + } + }) + .catch((error) => { + console.error( + `[LOG] Error during attendance record deletion: ${recordId}`, + error + ); + alert("Error deleting record. Please try again."); + }); + } +} + +function closeModal() { + const modal = document.getElementById("recordModal"); + if (modal) { + modal.style.display = "none"; + } +} + +// Chart initialization (placeholder) +function initializeCharts() { + console.log("Initializing charts..."); + // Chart implementation would go here +} + +function loadAttendanceData() { + console.log("Loading attendance data for charts..."); + // Additional data loading for charts would go here +} + +// Utility function +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +// Enhanced JavaScript functions for location accuracy features + +function loadTableData() { + const table = document.getElementById("attendanceTable"); + if (table) { + const rows = table.querySelectorAll("tbody tr"); + attendanceData = Array.from(rows).map((row, index) => { + const cells = row.querySelectorAll("td"); + + // Extract verification data from the accuracy badge + const verificationData = extractVerificationData(cells[9]); + + return { + id: row.dataset.recordId, + index: index + 1, + employeeId: cells[1] ? cells[1].textContent.trim() : "", + employeeName: cells[2] ? cells[2].textContent.trim() : "", // NEW: Employee Name column + location: cells[3] ? cells[3].textContent.trim() : "", // Updated from cells[2] + event: cells[4] ? cells[4].textContent.trim() : "", // Updated from cells[3] + date: cells[5] ? cells[5].textContent.trim() : "", // Updated from cells[4] + time: cells[6] ? cells[6].textContent.trim() : "", // Updated from cells[5] + qr_address: cells[7] // Updated from cells[6] + ? cells[7].getAttribute("title") || cells[7].textContent.trim() + : "", + checked_in_address: cells[8] // Updated from cells[7] + ? cells[8].getAttribute("title") || cells[8].textContent.trim() + : "", + // FIXED: Extract location accuracy for address display logic + location_accuracy: cells[9] ? extractLocationAccuracy(cells[9]) : null, // Updated from cells[8] + accuracy_level: cells[9] // Updated from cells[8] + ? extractLocationAccuracyLevel(cells[9]) + : "unknown", + device: cells[10] // Updated from cells[9] + ? cells[10].textContent.trim() + : "", + isModified: row.classList.contains('modified-record'), + isDynamic: row.dataset.isDynamic === '1', + verification_required: verificationData.required, + verification_status: verificationData.status + }; + }); + + filteredData = [...attendanceData]; + console.log(`Loaded ${attendanceData.length} attendance records`); + + // Debug log for location accuracy data + const recordsWithAccuracy = attendanceData.filter( + (r) => r.location_accuracy !== null + ); + console.log( + `Records with location accuracy: ${recordsWithAccuracy.length}` + ); + if (recordsWithAccuracy.length > 0) { + console.log( + `Sample location accuracy values:`, + recordsWithAccuracy.slice(0, 3).map((r) => r.location_accuracy) + ); + } + } +} + +function extractLocationAccuracy(cell) { + const text = cell.textContent; + console.log(`Extracting accuracy from: "${text}"`); + + // Look for miles pattern (e.g., "0.003 mi", "1.234 mi") + const milesMatch = text.match(/(\d+\.?\d*)\s*mi/); + if (milesMatch) { + const value = parseFloat(milesMatch[1]); + console.log(`Found miles: ${value}`); + return value; + } + + // Look for specific accuracy patterns in the HTML + const accuracyMatch = text.match(/accuracy[:\s]*(\d+\.?\d*)/i); + if (accuracyMatch) { + const value = parseFloat(accuracyMatch[1]); + console.log(`Found accuracy: ${value}`); + return value; + } + + // Check for data attributes + const dataAccuracy = cell.getAttribute("data-accuracy"); + if (dataAccuracy) { + const value = parseFloat(dataAccuracy); + console.log(`Found data-accuracy: ${value}`); + return value; + } + + // Fallback: look for GPS accuracy in meters and convert to miles (approximate) + const metersMatch = text.match(/(\d+\.?\d*)\s*m/); + if (metersMatch) { + const meters = parseFloat(metersMatch[1]); + const miles = meters * 0.000621371; // Convert meters to miles (approximate) + console.log(`Found meters: ${meters}, converted to miles: ${miles}`); + return miles; + } + + console.log(`No accuracy found in: "${text}"`); + return null; +} + +function extractLocationAccuracyLevel(cell) { + // Get the numerical accuracy value from the cell + const accuracy = extractLocationAccuracy(cell); + + // Return 2-level accuracy based on 0.5-mile threshold + if (accuracy !== null && accuracy !== undefined) { + return accuracy < 0.3 ? "accurate" : "inaccurate"; + } + return "unknown"; +} + +function extractVerificationData(cell) { + // Extract verification status from badge classes in the HTML + if (!cell) { + console.log('extractVerificationData: No cell provided'); + return { required: false, status: null }; + } + + const badge = cell.querySelector('.location-accuracy-badge'); + if (!badge) { + console.log('extractVerificationData: No badge found in cell'); + return { required: false, status: null }; + } + + console.log('extractVerificationData: Badge classes:', badge.className); + + // Check badge classes for verification status + if (badge.classList.contains('badge-review-needed')) { + console.log('extractVerificationData: Found pending verification'); + return { required: true, status: 'pending' }; + } else if (badge.classList.contains('badge-verified')) { + console.log('extractVerificationData: Found approved verification'); + return { required: true, status: 'approved' }; + } else if (badge.classList.contains('badge-rejected')) { + console.log('extractVerificationData: Found rejected verification'); + return { required: true, status: 'rejected' }; + } + + console.log('extractVerificationData: No verification status found, standard badge'); + return { required: false, status: null }; +} + +function createTableRow(record, displayIndex) { + const row = document.createElement("tr"); + row.dataset.recordId = record.id; + + // Apply highlighting if record was modified + if (record.isModified) { + row.classList.add('modified-record'); + } + // Apply blue-border highlight for Dynamic QR records + if (record.isDynamic) { + row.classList.add('dynamic-qr-record'); + } + + // Debug logging for first few records + if (displayIndex <= 3) { + console.log(`=== CREATING ROW ${displayIndex} ===`); + console.log(`Employee: ${record.employeeId}`); + console.log(`Location accuracy: ${record.location_accuracy}`); + console.log(`Verification required: ${record.verification_required}`); + console.log(`Verification status: ${record.verification_status}`); + console.log(`QR address: ${record.qr_address}`); + console.log(`Check-in address: ${record.checked_in_address}`); + } + + // Create location accuracy badge HTML - check verification status first + let locationAccuracyBadge; + + if (record.verification_required && record.verification_status === 'pending') { + // Show Review Needed badge for pending verification - LINK to review page + locationAccuracyBadge = ` + + Review Needed + (${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi) + `; + } else if (record.verification_status === 'approved') { + // Show Verified badge for approved verification + locationAccuracyBadge = ` + + Verified + (${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi) + `; + } else if (record.verification_status === 'rejected') { + // Show Rejected badge for rejected verification + locationAccuracyBadge = ` + + Rejected + (${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi) + `; + } else if (record.location_accuracy !== null) { + // Show standard location accuracy badge + locationAccuracyBadge = ` + + ${record.location_accuracy.toFixed(3)} mi + (${record.accuracy_level}) + `; + } else { + // No accuracy data + locationAccuracyBadge = ` + + Unknown + `; + } + + // Address display logic based on location accuracy + let addressDisplayHTML = ""; + let addressToShow = record.checked_in_address; + let addressIcon = "fas fa-location-arrow"; + let addressClass = "address-normal-accuracy"; + let addressTitle = `Check-in Address: ${record.checked_in_address}`; + + // Apply 0.5-mile threshold logic + if ( + record.location_accuracy !== null && + record.location_accuracy !== undefined + ) { + const accuracy = parseFloat(record.location_accuracy); + + if (displayIndex <= 3) { + console.log(`Applying address logic for ${record.employeeId}:`); + console.log(` Accuracy value: ${accuracy}`); + console.log(` Is <= 0.3? ${accuracy <= 0.3}`); + } + + if (!isNaN(accuracy) && accuracy <= 0.3) { + // High accuracy - use QR address + addressToShow = record.qr_address; + addressIcon = "fas fa-check-circle"; + addressClass = "address-high-accuracy"; + addressTitle = `QR Address (High Accuracy ≤ 0.5 mi): ${record.qr_address}`; + + if (displayIndex <= 3) { + console.log(` → Using QR address: ${addressToShow}`); + } + + addressDisplayHTML = ` + + + ${ + addressToShow.length > 45 + ? addressToShow.substring(0, 45) + "..." + : addressToShow + } + + `; + } else { + // Lower accuracy - use check-in address + if (displayIndex <= 3) { + console.log(` → Using check-in address: ${addressToShow}`); + } + + addressDisplayHTML = ` + + + ${ + addressToShow.length > 45 + ? addressToShow.substring(0, 45) + "..." + : addressToShow + } + + `; + } + } else { + // No accuracy data - use check-in address + if (displayIndex <= 3) { + console.log( + ` → No accuracy data, using check-in address: ${addressToShow}` + ); + } + + addressDisplayHTML = ` + + + ${ + addressToShow.length > 45 + ? addressToShow.substring(0, 45) + "..." + : addressToShow + } + + `; + } + + row.innerHTML = ` + ${displayIndex} + +
+ ${record.employeeId} +
+ + +
+ + ${record.employeeName || 'Unknown'} +
+ + +
+ + ${record.location} +
+ + +
+ ${record.event} +
+ + +
+ ${record.date} +
+ + +
+ ${record.time} +
+ + +
+ + + ${ + record.qr_address.length > 50 + ? record.qr_address.substring(0, 50) + "..." + : record.qr_address + } + +
+ + +
+ ${addressDisplayHTML} +
+ + +
+ ${locationAccuracyBadge} +
+ + +
+ + + ${ + record.device.length > 20 + ? record.device.substring(0, 20) + "..." + : record.device + } + +
+ + +
+ ${ + record.verification_required && record.verification_status === 'pending' + ? ` + + ` + : '' + } + ${ + hasEditPermission + ? ` + + + ` + : ` + + + + ` + } +
+ + `; + + return row; +} + +function getSortKey(columnIndex) { + const sortKeys = [ + "index", + "employeeId", + "location", + "event", + "date", + "time", + "qr_address", + "checked_in_address", + "location_accuracy", + "device", + ]; + return sortKeys[columnIndex] || "index"; +} + +// Enhanced sorting for location accuracy (numeric sorting) +function sortTable(columnIndex) { + if (sortColumn === columnIndex) { + sortDirection = sortDirection === "asc" ? "desc" : "asc"; + } else { + sortColumn = columnIndex; + sortDirection = "asc"; + } + + const sortKey = getSortKey(columnIndex); + + filteredData.sort((a, b) => { + let aVal = a[sortKey]; + let bVal = b[sortKey]; + + // Handle numeric values for location accuracy + if (columnIndex === 8 && aVal !== null && bVal !== null) { + aVal = parseFloat(aVal); + bVal = parseFloat(bVal); + } + + // Handle null values - put them at the end + if (aVal === null || aVal === undefined) { + return sortDirection === "asc" ? 1 : -1; + } + if (bVal === null || bVal === undefined) { + return sortDirection === "asc" ? -1 : 1; + } + + if (typeof aVal === "string") { + aVal = aVal.toLowerCase(); + bVal = bVal.toLowerCase(); + } + + let result; + if (aVal < bVal) result = -1; + else if (aVal > bVal) result = 1; + else result = 0; + + return sortDirection === "asc" ? result : -result; + }); + + updateTable(); + updateSortIndicators(columnIndex); +} + +// Enhanced statistics display for location accuracy +function updateFilterStats() { + const totalRecords = filteredData.length; + const recordsWithAccuracy = filteredData.filter( + (r) => r.location_accuracy !== null + ).length; + const avgAccuracy = + recordsWithAccuracy > 0 + ? filteredData + .filter((r) => r.location_accuracy !== null) + .reduce((sum, r) => sum + r.location_accuracy, 0) / + recordsWithAccuracy + : 0; + + console.log(`Filtered records: ${totalRecords}`); + console.log(`Records with location accuracy: ${recordsWithAccuracy}`); + console.log(`Average location accuracy: ${avgAccuracy.toFixed(3)} miles`); +} + +// Function to get accuracy level color for charts or displays +function getAccuracyLevelColor(level) { + const colors = { + accurate: "#059669", // green + inaccurate: "#dc2626", // red + unknown: "#6b7280", // gray + }; + return colors[level] || colors["unknown"]; +} + +// Enhanced export function to include location accuracy +function exportAttendanceWithAccuracy() { + // Build CSV header with location accuracy + const headers = [ + "#", + "Employee ID", + "Employee Name", + "Location", + "Event", + "Date", + "Time", + "QR Address", + "Check-in Address", + "Location Accuracy (miles)", + "Accuracy Level", + "Device", + ]; + + // Build CSV rows + const rows = filteredData.map((record, index) => [ + index + 1, + record.employeeId, + record.employeeName || "Unknown", + record.location, + record.event, + record.date, + record.time, + record.qr_address, + record.checked_in_address, + record.location_accuracy ? record.location_accuracy.toFixed(3) : "Unknown", + record.accuracy_level, + record.device, + ]); + + // Create CSV content + const csvContent = [headers, ...rows] + .map((row) => row.map((field) => `"${field}"`).join(",")) + .join("\n"); + + // Download CSV + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute( + "download", + `attendance_report_with_accuracy_${ + new Date().toISOString().split("T")[0] + }.csv` + ); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +function exportAttendance() { + // Check user role before proceeding + const userRole = window.userRole; // Read from global variable set in template + console.log("Template - session.role:", '{{ session.role }}'); + console.log("Template - window.userRole set to:", window.userRole); + + if (!['admin', 'payroll', 'accounting'].includes(userRole)) { + console.log("Export access denied - insufficient privileges"); + alert("Access denied. Only administrators and payroll staff can export data."); + return; + } + + // Log export action + console.log(`Export button clicked by ${userRole} - redirecting to configuration page`); + + // Get current filters + const currentFilters = getCurrentFilters(); + + // Build URL with current filters + const params = new URLSearchParams(); + if (currentFilters.date_from) + params.append("date_from", currentFilters.date_from); + if (currentFilters.date_to) params.append("date_to", currentFilters.date_to); + if (currentFilters.location) + params.append("location", currentFilters.location); + if (currentFilters.employee) + params.append("employee", currentFilters.employee); + if (currentFilters.project) + params.append("project", currentFilters.project); + + // Navigate to export configuration page + const configUrl = + "/export-configuration" + + (params.toString() ? "?" + params.toString() : ""); + window.location.href = configUrl; +} + +function getCurrentFilters() { + // Extract current filter values from the page + return { + date_from: document.getElementById("date_from")?.value || "", + date_to: document.getElementById("date_to")?.value || "", + location: document.getElementById("location")?.value || "", + employee: document.getElementById("employee")?.value || "", + project: document.getElementById("project")?.value || "", + }; +} + +// Add a quick CSV export function as backup (keep existing functionality) +function exportAttendanceCSV() { + // Check user role before proceeding + const userRole = window.userRole; + + if (!['admin', 'payroll', 'accounting'].includes(userRole)) { + console.log("CSV export access denied - insufficient privileges"); + alert("Access denied. Only administrators and payroll staff can export data."); + return; + } + + // Build export URL with current filters for CSV + const params = new URLSearchParams(); + const filters = getCurrentFilters(); + + if (filters.date_from) params.append("date_from", filters.date_from); + if (filters.date_to) params.append("date_to", filters.date_to); + if (filters.location) params.append("location", filters.location); + if (filters.employee) params.append("employee", filters.employee); + if (filters.project) params.append("project", filters.project); + params.append("export", "csv"); + + // Create a temporary link and click it to download + const downloadUrl = window.location.pathname + "?" + params.toString(); + window.open(downloadUrl, "_blank"); +} + +// Enhanced export menu (if you want to add dropdown with multiple export options) +function showExportMenu() { + // Create export options menu + const existingMenu = document.getElementById("exportMenu"); + if (existingMenu) { + existingMenu.remove(); + return; + } + + const exportBtn = document.querySelector( + 'button[onclick="exportAttendance()"]' + ); + if (!exportBtn) return; + + const menu = document.createElement("div"); + menu.id = "exportMenu"; + menu.style.cssText = ` + position: absolute; + top: 100%; + right: 0; + background: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0,0,0,0.1); + z-index: 1000; + min-width: 200px; + margin-top: 5px; + `; + + menu.innerHTML = ` +
+ + +
+ `; + + exportBtn.parentElement.style.position = "relative"; + exportBtn.parentElement.appendChild(menu); + + // Close menu when clicking outside + setTimeout(() => { + document.addEventListener("click", function closeOnClickOutside(e) { + if (!menu.contains(e.target) && e.target !== exportBtn) { + closeExportMenu(); + document.removeEventListener("click", closeOnClickOutside); + } + }); + }, 100); +} + +function closeExportMenu() { + const menu = document.getElementById("exportMenu"); + if (menu) { + menu.remove(); + } +} + +// Initialize export functionality when page loads +document.addEventListener("DOMContentLoaded", function () { + // Update export button to use enhanced functionality + const exportBtn = document.querySelector( + 'button[onclick="exportAttendance()"]' + ); + if (exportBtn) { + // You can modify the button to show a dropdown instead + // exportBtn.onclick = showExportMenu; + // exportBtn.innerHTML = ' Export Data '; + } + + console.log("Enhanced export functionality initialized"); }); \ No newline at end of file diff --git a/static/js/dashboard.js b/static/js/dashboard.js index cd47e2f..d3af0e9 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -1,699 +1,703 @@ -/** - * Unified Dashboard JavaScript for QR Code Management - * static/js/dashboard.js - */ - -class ProjectDashboardManager { - constructor() { - this.expandedProjects = new Set(); - this.currentModalQR = null; - this.selectedQRCodes = new Set(); - this.allExpanded = false; - this.initialize(); - } - - initialize() { - const saved = localStorage.getItem("expandedProjects"); - if (saved) { - this.expandedProjects = new Set(JSON.parse(saved)); - this.restoreProjectStates(); - } - - this.setupEventListeners(); - this.addScrollAnimations(); - } - - restoreProjectStates() { - this.expandedProjects.forEach((projectId) => { - this.expandProject(projectId, false); - }); - } - - // Setup event listeners - setupEventListeners() { - // Keyboard shortcuts - document.addEventListener("keydown", (e) => { - // ESC to close modals - if (e.key === "Escape") { - this.closeQRModal(); - this.closeImageLightbox(); - } - - // Ctrl/Cmd + F to focus search - if ((e.ctrlKey || e.metaKey) && e.key === "f") { - e.preventDefault(); - const searchInput = document.getElementById("qrSearch"); - if (searchInput) searchInput.focus(); - } - - // Delete key for bulk delete (when items selected) - if (e.key === "Delete" && this.selectedQRCodes.size > 0) { - e.preventDefault(); - this.bulkDeleteQRCodes(); - } - }); - - // Expand/collapse all toggle - const expandToggle = document.getElementById("expandAllToggle"); - if (expandToggle) { - expandToggle.addEventListener("click", () => { - this.toggleExpandAll(); - }); - } - } - - // Add scroll animations for QR items - addScrollAnimations() { - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - entry.target.classList.add("animate-in"); - } - }); - }, - { threshold: 0.1 } - ); - - const qrItems = document.querySelectorAll(".qr-item, .qr-card"); - qrItems.forEach((item) => observer.observe(item)); - } - - // Project Management Functions - toggleProject(projectId) { - const isExpanded = this.expandedProjects.has(projectId); - - if (isExpanded) { - this.collapseProject(projectId); - } else { - this.expandProject(projectId); - } - - this.saveExpandedState(); - } - - expandProject(projectId, animate = true) { - const projectQR = document.getElementById(`project-qr-${projectId}`); - const toggle = document.getElementById(`toggle-${projectId}`); - const header = toggle?.closest(".project-header"); - - if (projectQR && toggle) { - projectQR.classList.add("expanded"); - toggle.classList.add("expanded"); - header?.classList.add("expanded"); - this.expandedProjects.add(projectId); - - if (animate) { - setTimeout(() => { - projectQR.scrollIntoView({ - behavior: "smooth", - block: "nearest", - }); - }, 200); - } - } - } - - collapseProject(projectId) { - const projectQR = document.getElementById(`project-qr-${projectId}`); - const toggle = document.getElementById(`toggle-${projectId}`); - const header = toggle?.closest(".project-header"); - - if (projectQR && toggle) { - projectQR.classList.remove("expanded"); - toggle.classList.remove("expanded"); - header?.classList.remove("expanded"); - this.expandedProjects.delete(projectId); - } - } - - saveExpandedState() { - localStorage.setItem( - "expandedProjects", - JSON.stringify([...this.expandedProjects]) - ); - } - - // QR Code Status Toggle - async toggleQRCodeStatus(qrId) { - try { - const response = await fetch(`/qr-codes/${qrId}/toggle-status`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Requested-With": "XMLHttpRequest", - }, - }); - - if (response.ok) { - const result = await response.json(); - if (result.success) { - this.showToast(result.message, "success"); - - // Reload page after short delay to show updated status - setTimeout(() => { - window.location.reload(); - }, 1000); - } else { - throw new Error(result.message || "Failed to toggle QR code status"); - } - } else { - throw new Error("Failed to toggle QR code status"); - } - } catch (error) { - console.error("Toggle status failed:", error); - this.showToast("Failed to update QR code status", "error"); - } - } - - // QR Modal Functions - openQRModalFromData(element) { - const qrData = { - name: element.dataset.qrName, - image: element.querySelector("img").src, - location: element.dataset.qrLocation, - address: element.dataset.qrAddress, - event: element.dataset.qrEvent, - qr_url: element.dataset.qrUrl, - }; - - this.openQRModal(qrData); - } - - openQRModal(qrData) { - const modal = document.getElementById("qrModal"); - const modalImage = document.getElementById("modalQRImage"); - const modalTitle = document.getElementById("modalTitle"); - const modalQRName = document.getElementById("modalQRName"); - const modalQRLocation = document.getElementById("modalQRLocation"); - const modalQRAddress = document.getElementById("modalQRAddress"); - const modalQREvent = document.getElementById("modalQREvent"); - const modalQRDestination = document.getElementById("modalQRDestination"); - - if (modal && modalImage && modalTitle) { - modalTitle.textContent = `QR Code: ${qrData.name}`; - modalImage.src = qrData.image; - modalImage.alt = `QR Code for ${qrData.name}`; - - if (modalQRName) modalQRName.textContent = qrData.name || "-"; - if (modalQRLocation) modalQRLocation.textContent = qrData.location || "-"; - if (modalQRAddress) modalQRAddress.textContent = qrData.address || "-"; - if (modalQREvent) - modalQREvent.textContent = qrData.event || "No event specified"; - - if (modalQRDestination && qrData.qr_url) { - const destinationUrl = `${window.location.origin}/qr/${qrData.qr_url}`; - const linkElement = modalQRDestination.querySelector("a"); - if (linkElement) { - linkElement.href = destinationUrl; - linkElement.innerHTML = ` - - ${destinationUrl} - `; - } - } else if (modalQRDestination) { - modalQRDestination.innerHTML = - 'No destination URL available'; - } - - this.currentModalQR = { - name: qrData.name, - image: qrData.image, - location: qrData.location, - address: qrData.address, - event: qrData.event, - qr_url: qrData.qr_url, - destination_url: qrData.qr_url - ? `${window.location.origin}/qr/${qrData.qr_url}` - : null, - }; - - modal.style.display = "flex"; - - document.addEventListener("keydown", this.handleModalKeydown.bind(this)); - } - } - - closeQRModal() { - const modal = document.getElementById("qrModal"); - if (modal) { - modal.style.display = "none"; - this.currentModalQR = null; - - document.removeEventListener( - "keydown", - this.handleModalKeydown.bind(this) - ); - } - } - - handleModalKeydown(event) { - if (event.key === "Escape") { - this.closeQRModal(); - } - } - - // Download Functions - downloadModalQR() { - if (this.currentModalQR) { - const base64Data = this.currentModalQR.image.includes("base64,") - ? this.currentModalQR.image.split("base64,")[1] - : this.currentModalQR.image; - this.downloadQR(base64Data, this.currentModalQR.name); - } - } - - downloadQRFromCard(button) { - const qrCard = button.closest(".qr-card") || button.closest(".qr-item"); - const img = qrCard.querySelector("img"); - const qrName = - qrCard.dataset.qrName || - qrCard.querySelector(".qr-name")?.textContent || - "qr_code"; - - if (img && img.src) { - const base64Data = img.src.includes("base64,") - ? img.src.split("base64,")[1] - : img.src; - this.downloadQR(base64Data, qrName); - } - } - - downloadQR(base64Image, filename) { - try { - const base64Data = base64Image.includes("base64,") - ? base64Image.split("base64,")[1] - : base64Image; - - const link = document.createElement("a"); - link.href = `data:image/png;base64,${base64Data}`; - link.download = `${filename - .replace(/[^a-z0-9]/gi, "_") - .toLowerCase()}_qr_code.png`; - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - this.showToast("QR code downloaded successfully!", "success"); - } catch (error) { - console.error("Download error:", error); - this.showToast("Failed to download QR code", "error"); - } - } - - // Copy Functions - copyModalQRData() { - if (this.currentModalQR) { - const data = `QR Code: ${this.currentModalQR.name}\nLocation: ${ - this.currentModalQR.location - }\nAddress: ${this.currentModalQR.address}\nEvent: ${ - this.currentModalQR.event - }${ - this.currentModalQR.destination_url - ? `\nQR Link: ${this.currentModalQR.destination_url}` - : "" - }`; - - navigator.clipboard - .writeText(data) - .then(() => { - this.showToast("QR code information copied to clipboard!", "success"); - }) - .catch(() => { - this.fallbackCopyText(data); - }); - } - } - - copyQRDestination() { - if (this.currentModalQR && this.currentModalQR.destination_url) { - navigator.clipboard - .writeText(this.currentModalQR.destination_url) - .then(() => { - this.showToast("QR destination link copied to clipboard!", "success"); - }) - .catch(() => { - this.showToast("Failed to copy QR link", "error"); - }); - } else { - this.showToast("No QR destination link available", "warning"); - } - } - - // FIXED: Copy QR Code URL - async copyQRUrl(qrId) { - try { - const response = await fetch(`/qr-codes/${qrId}/copy-url`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); - - const data = await response.json(); - - if (data.success && data.url) { - // Copy to clipboard - await navigator.clipboard.writeText(data.url); - this.showToast("QR code URL copied to clipboard!", "success"); - - // Log the action - console.log(`QR URL copied for ID: ${qrId}`); - } else { - this.showToast("Failed to copy URL", "error"); - } - } catch (error) { - console.error("Copy URL error:", error); - this.showToast("Failed to copy URL", "error"); - } - } - - // FIXED: Open QR Code Link - async openQRLink(qrId) { - try { - const response = await fetch(`/qr-codes/${qrId}/open-link`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); - - const data = await response.json(); - - if (data.success && data.url) { - // Open in new tab - window.open(data.url, "_blank"); - this.showToast("QR code link opened!", "success"); - - // Log the action - console.log(`QR link opened for ID: ${qrId}`); - } else { - this.showToast("Failed to open link", "error"); - } - } catch (error) { - console.error("Open link error:", error); - this.showToast("Failed to open link", "error"); - } - } - - fallbackCopyText(text) { - const textArea = document.createElement("textarea"); - textArea.value = text; - document.body.appendChild(textArea); - textArea.select(); - try { - document.execCommand("copy"); - this.showToast("QR code information copied to clipboard!", "success"); - } catch (err) { - this.showToast("Failed to copy to clipboard", "error"); - } - document.body.removeChild(textArea); - } - - // Image Lightbox Functions - openImageLightbox(previewElement, qrName) { - console.log("Opening lightbox for:", qrName); // Debug log - - const img = previewElement.querySelector("img"); - - if (img && img.src) { - const lightbox = document.getElementById("imageLightbox"); - const lightboxImage = document.getElementById("lightboxImage"); - const lightboxInfo = document.getElementById("lightboxInfo"); - - console.log( - "Lightbox elements found:", - !!lightbox, - !!lightboxImage, - !!lightboxInfo - ); // Debug log - - if (lightbox && lightboxImage && lightboxInfo) { - lightboxImage.src = img.src; - lightboxImage.alt = img.alt; - lightboxInfo.textContent = `QR Code: ${qrName}`; - - lightbox.style.display = "flex"; - console.log("Lightbox should be visible now"); // Debug log - - // Add keyboard listener for ESC key - document.addEventListener( - "keydown", - this.handleLightboxKeydown.bind(this) - ); - } else { - console.error("Lightbox elements not found"); - } - } else { - console.error("Image element not found or no src"); - } - } - - closeImageLightbox() { - console.log("Closing lightbox"); // Debug log - const lightbox = document.getElementById("imageLightbox"); - if (lightbox) { - lightbox.style.display = "none"; - - // Remove keyboard listener - document.removeEventListener( - "keydown", - this.handleLightboxKeydown.bind(this) - ); - } - } - - handleLightboxKeydown(event) { - if (event.key === "Escape") { - this.closeImageLightbox(); - } - } - - // QR Item Toggle functionality (for selection) - toggleQRItem(element) { - const qrId = element.dataset.qrId; - if (this.selectedQRCodes.has(qrId)) { - this.selectedQRCodes.delete(qrId); - element.classList.remove("selected"); - } else { - this.selectedQRCodes.add(qrId); - element.classList.add("selected"); - } - - // Update bulk action buttons if they exist - this.updateBulkActionButtons(); - } - - updateBulkActionButtons() { - const bulkActions = document.querySelector(".bulk-actions"); - if (bulkActions) { - bulkActions.style.display = - this.selectedQRCodes.size > 0 ? "flex" : "none"; - } - } - - // Copy QR data functionality - copyQRData(name, location, address, event) { - const data = `QR Code: ${name}\nLocation: ${location}\nAddress: ${address}\nEvent: ${event}`; - - if (navigator.clipboard) { - navigator.clipboard - .writeText(data) - .then(() => - this.showToast("QR code information copied to clipboard!", "success") - ) - .catch(() => this.fallbackCopyText(data)); - } else { - this.fallbackCopyText(data); - } - } - - // Delete QR Code - async deleteQRCode(qrId, qrName) { - if (!confirm(`Delete "${qrName}"? This cannot be undone.`)) return; - - try { - // Show loading state - const deleteBtn = document.querySelector( - `[onclick*="deleteQRCode(${qrId}"]` - ); - if (deleteBtn) { - deleteBtn.disabled = true; - deleteBtn.innerHTML = - ' Deleting...'; - } - - const response = await fetch(`/qr-codes/${qrId}/delete`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); - - if (response.ok) { - // Remove QR item from page immediately - const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`); - if (qrItem) { - qrItem.style.transition = "opacity 0.3s"; - qrItem.style.opacity = "0"; - setTimeout(() => { - qrItem.remove(); - }, 300); - } - - // Show success message - this.showToast(`QR code "${qrName}" deleted successfully!`, "success"); - } else { - throw new Error(`Server error: ${response.status}`); - } - } catch (error) { - console.error("Delete error:", error); - - // Restore button if there was an error - const deleteBtn = document.querySelector( - `[onclick*="deleteQRCode(${qrId}"]` - ); - if (deleteBtn) { - deleteBtn.disabled = false; - deleteBtn.innerHTML = ''; - } - - // Show error message - this.showToast("Failed to delete QR code. Please try again.", "error"); - } - } - - // Toast notification system - showToast(message, type = "info") { - const toast = document.createElement("div"); - toast.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - background: ${ - type === "success" - ? "#10b981" - : type === "error" - ? "#ef4444" - : type === "warning" - ? "#f59e0b" - : "#3b82f6" - }; - color: white; - padding: 12px 16px; - border-radius: 8px; - z-index: 9999; - font-weight: 500; - box-shadow: 0 4px 12px rgba(0,0,0,0.1); - transition: all 0.3s ease; - opacity: 0; - transform: translateX(100%); - `; - - toast.textContent = message; - document.body.appendChild(toast); - - // Animate in - setTimeout(() => { - toast.style.opacity = "1"; - toast.style.transform = "translateX(0)"; - }, 100); - - // Animate out - setTimeout(() => { - toast.style.opacity = "0"; - toast.style.transform = "translateX(100%)"; - setTimeout(() => { - if (document.body.contains(toast)) { - toast.remove(); - } - }, 300); - }, 3000); - } - - // Expand/collapse functionality - toggleExpandAll() { - this.allExpanded = !this.allExpanded; - const qrItems = document.querySelectorAll(".qr-item"); - const expandToggle = document.getElementById("expandAllToggle"); - - qrItems.forEach((item) => { - const details = item.querySelector(".qr-details"); - if (details) { - if (this.allExpanded) { - details.style.display = "block"; - item.classList.add("expanded"); - } else { - details.style.display = "none"; - item.classList.remove("expanded"); - } - } - }); - - if (expandToggle) { - expandToggle.innerHTML = this.allExpanded - ? ' Collapse All' - : ' Expand All'; - } - } -} - -// Global variable to hold dashboard manager instance -let dashboardManager; - -// Initialize dashboard when DOM is loaded -document.addEventListener("DOMContentLoaded", function () { - dashboardManager = new ProjectDashboardManager(); - - // Register all global functions for template compatibility - window.toggleProject = (projectId) => - dashboardManager.toggleProject(projectId); - window.toggleQRCodeStatus = (qrId) => - dashboardManager.toggleQRCodeStatus(qrId); - window.openQRModalFromData = (element) => - dashboardManager.openQRModalFromData(element); - window.openQRModal = (qrData) => dashboardManager.openQRModal(qrData); - window.closeQRModal = () => dashboardManager.closeQRModal(); - window.downloadModalQR = () => dashboardManager.downloadModalQR(); - window.copyModalQRData = () => dashboardManager.copyModalQRData(); - window.copyQRDestination = () => dashboardManager.copyQRDestination(); - window.downloadQRFromCard = (button) => - dashboardManager.downloadQRFromCard(button); - window.openImageLightbox = (element, qrName) => - dashboardManager.openImageLightbox(element, qrName); - window.closeImageLightbox = () => dashboardManager.closeImageLightbox(); - window.toggleQRItem = (element) => dashboardManager.toggleQRItem(element); - window.copyQRData = (name, location, address, event) => - 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"); -}); +/** + * Unified Dashboard JavaScript for QR Code Management + * static/js/dashboard.js + */ + +class ProjectDashboardManager { + constructor() { + this.expandedProjects = new Set(); + this.currentModalQR = null; + this.selectedQRCodes = new Set(); + this.allExpanded = false; + this.initialize(); + } + + initialize() { + const saved = localStorage.getItem("expandedProjects"); + if (saved) { + this.expandedProjects = new Set(JSON.parse(saved)); + this.restoreProjectStates(); + } + + this.setupEventListeners(); + this.addScrollAnimations(); + } + + restoreProjectStates() { + this.expandedProjects.forEach((projectId) => { + this.expandProject(projectId, false); + }); + } + + // Setup event listeners + setupEventListeners() { + // Keyboard shortcuts + document.addEventListener("keydown", (e) => { + // ESC to close modals + if (e.key === "Escape") { + this.closeQRModal(); + this.closeImageLightbox(); + } + + // Ctrl/Cmd + F to focus search + if ((e.ctrlKey || e.metaKey) && e.key === "f") { + e.preventDefault(); + const searchInput = document.getElementById("qrSearch"); + if (searchInput) searchInput.focus(); + } + + // Delete key for bulk delete (when items selected) + if (e.key === "Delete" && this.selectedQRCodes.size > 0) { + e.preventDefault(); + this.bulkDeleteQRCodes(); + } + }); + + // Expand/collapse all toggle + const expandToggle = document.getElementById("expandAllToggle"); + if (expandToggle) { + expandToggle.addEventListener("click", () => { + this.toggleExpandAll(); + }); + } + } + + // Add scroll animations for QR items + addScrollAnimations() { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add("animate-in"); + } + }); + }, + { threshold: 0.1 } + ); + + const qrItems = document.querySelectorAll(".qr-item, .qr-card"); + qrItems.forEach((item) => observer.observe(item)); + } + + // Project Management Functions + toggleProject(projectId) { + const isExpanded = this.expandedProjects.has(projectId); + + if (isExpanded) { + this.collapseProject(projectId); + } else { + this.expandProject(projectId); + } + + this.saveExpandedState(); + } + + expandProject(projectId, animate = true) { + const projectQR = document.getElementById(`project-qr-${projectId}`); + const toggle = document.getElementById(`toggle-${projectId}`); + const header = toggle?.closest(".project-header"); + + if (projectQR && toggle) { + projectQR.classList.add("expanded"); + toggle.classList.add("expanded"); + header?.classList.add("expanded"); + this.expandedProjects.add(projectId); + + if (animate) { + setTimeout(() => { + projectQR.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + }, 200); + } + } + } + + collapseProject(projectId) { + const projectQR = document.getElementById(`project-qr-${projectId}`); + const toggle = document.getElementById(`toggle-${projectId}`); + const header = toggle?.closest(".project-header"); + + if (projectQR && toggle) { + projectQR.classList.remove("expanded"); + toggle.classList.remove("expanded"); + header?.classList.remove("expanded"); + this.expandedProjects.delete(projectId); + } + } + + saveExpandedState() { + localStorage.setItem( + "expandedProjects", + JSON.stringify([...this.expandedProjects]) + ); + } + + // QR Code Status Toggle + async toggleQRCodeStatus(qrId) { + try { + const response = await fetch(`/qr-codes/${qrId}/toggle-status`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', + }, + }); + + if (response.ok) { + const result = await response.json(); + if (result.success) { + this.showToast(result.message, "success"); + + // Reload page after short delay to show updated status + setTimeout(() => { + window.location.reload(); + }, 1000); + } else { + throw new Error(result.message || "Failed to toggle QR code status"); + } + } else { + throw new Error("Failed to toggle QR code status"); + } + } catch (error) { + console.error("Toggle status failed:", error); + this.showToast("Failed to update QR code status", "error"); + } + } + + // QR Modal Functions + openQRModalFromData(element) { + const qrData = { + name: element.dataset.qrName, + image: element.querySelector("img").src, + location: element.dataset.qrLocation, + address: element.dataset.qrAddress, + event: element.dataset.qrEvent, + qr_url: element.dataset.qrUrl, + }; + + this.openQRModal(qrData); + } + + openQRModal(qrData) { + const modal = document.getElementById("qrModal"); + const modalImage = document.getElementById("modalQRImage"); + const modalTitle = document.getElementById("modalTitle"); + const modalQRName = document.getElementById("modalQRName"); + const modalQRLocation = document.getElementById("modalQRLocation"); + const modalQRAddress = document.getElementById("modalQRAddress"); + const modalQREvent = document.getElementById("modalQREvent"); + const modalQRDestination = document.getElementById("modalQRDestination"); + + if (modal && modalImage && modalTitle) { + modalTitle.textContent = `QR Code: ${qrData.name}`; + modalImage.src = qrData.image; + modalImage.alt = `QR Code for ${qrData.name}`; + + if (modalQRName) modalQRName.textContent = qrData.name || "-"; + if (modalQRLocation) modalQRLocation.textContent = qrData.location || "-"; + if (modalQRAddress) modalQRAddress.textContent = qrData.address || "-"; + if (modalQREvent) + modalQREvent.textContent = qrData.event || "No event specified"; + + if (modalQRDestination && qrData.qr_url) { + const destinationUrl = `${window.location.origin}/qr/${qrData.qr_url}`; + const linkElement = modalQRDestination.querySelector("a"); + if (linkElement) { + linkElement.href = destinationUrl; + linkElement.innerHTML = ` + + ${destinationUrl} + `; + } + } else if (modalQRDestination) { + modalQRDestination.innerHTML = + 'No destination URL available'; + } + + this.currentModalQR = { + name: qrData.name, + image: qrData.image, + location: qrData.location, + address: qrData.address, + event: qrData.event, + qr_url: qrData.qr_url, + destination_url: qrData.qr_url + ? `${window.location.origin}/qr/${qrData.qr_url}` + : null, + }; + + modal.style.display = "flex"; + + document.addEventListener("keydown", this.handleModalKeydown.bind(this)); + } + } + + closeQRModal() { + const modal = document.getElementById("qrModal"); + if (modal) { + modal.style.display = "none"; + this.currentModalQR = null; + + document.removeEventListener( + "keydown", + this.handleModalKeydown.bind(this) + ); + } + } + + handleModalKeydown(event) { + if (event.key === "Escape") { + this.closeQRModal(); + } + } + + // Download Functions + downloadModalQR() { + if (this.currentModalQR) { + const base64Data = this.currentModalQR.image.includes("base64,") + ? this.currentModalQR.image.split("base64,")[1] + : this.currentModalQR.image; + this.downloadQR(base64Data, this.currentModalQR.name); + } + } + + downloadQRFromCard(button) { + const qrCard = button.closest(".qr-card") || button.closest(".qr-item"); + const img = qrCard.querySelector("img"); + const qrName = + qrCard.dataset.qrName || + qrCard.querySelector(".qr-name")?.textContent || + "qr_code"; + + if (img && img.src) { + const base64Data = img.src.includes("base64,") + ? img.src.split("base64,")[1] + : img.src; + this.downloadQR(base64Data, qrName); + } + } + + downloadQR(base64Image, filename) { + try { + const base64Data = base64Image.includes("base64,") + ? base64Image.split("base64,")[1] + : base64Image; + + const link = document.createElement("a"); + link.href = `data:image/png;base64,${base64Data}`; + link.download = `${filename + .replace(/[^a-z0-9]/gi, "_") + .toLowerCase()}_qr_code.png`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + this.showToast("QR code downloaded successfully!", "success"); + } catch (error) { + console.error("Download error:", error); + this.showToast("Failed to download QR code", "error"); + } + } + + // Copy Functions + copyModalQRData() { + if (this.currentModalQR) { + const data = `QR Code: ${this.currentModalQR.name}\nLocation: ${ + this.currentModalQR.location + }\nAddress: ${this.currentModalQR.address}\nEvent: ${ + this.currentModalQR.event + }${ + this.currentModalQR.destination_url + ? `\nQR Link: ${this.currentModalQR.destination_url}` + : "" + }`; + + navigator.clipboard + .writeText(data) + .then(() => { + this.showToast("QR code information copied to clipboard!", "success"); + }) + .catch(() => { + this.fallbackCopyText(data); + }); + } + } + + copyQRDestination() { + if (this.currentModalQR && this.currentModalQR.destination_url) { + navigator.clipboard + .writeText(this.currentModalQR.destination_url) + .then(() => { + this.showToast("QR destination link copied to clipboard!", "success"); + }) + .catch(() => { + this.showToast("Failed to copy QR link", "error"); + }); + } else { + this.showToast("No QR destination link available", "warning"); + } + } + + // FIXED: Copy QR Code URL + async copyQRUrl(qrId) { + try { + const response = await fetch(`/qr-codes/${qrId}/copy-url`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": (window.qrConfig && window.qrConfig.csrfToken) || "", + }, + }); + + 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", + "X-CSRF-Token": (window.qrConfig && window.qrConfig.csrfToken) || "", + }, + }); + + const data = await response.json(); + + if (data.success && data.url) { + // Open in new tab + window.open(data.url, "_blank"); + this.showToast("QR code link opened!", "success"); + + // Log the action + console.log(`QR link opened for ID: ${qrId}`); + } else { + this.showToast("Failed to open link", "error"); + } + } catch (error) { + console.error("Open link error:", error); + this.showToast("Failed to open link", "error"); + } + } + + fallbackCopyText(text) { + const textArea = document.createElement("textarea"); + textArea.value = text; + document.body.appendChild(textArea); + textArea.select(); + try { + document.execCommand("copy"); + this.showToast("QR code information copied to clipboard!", "success"); + } catch (err) { + this.showToast("Failed to copy to clipboard", "error"); + } + document.body.removeChild(textArea); + } + + // Image Lightbox Functions + openImageLightbox(previewElement, qrName) { + console.log("Opening lightbox for:", qrName); // Debug log + + const img = previewElement.querySelector("img"); + + if (img && img.src) { + const lightbox = document.getElementById("imageLightbox"); + const lightboxImage = document.getElementById("lightboxImage"); + const lightboxInfo = document.getElementById("lightboxInfo"); + + console.log( + "Lightbox elements found:", + !!lightbox, + !!lightboxImage, + !!lightboxInfo + ); // Debug log + + if (lightbox && lightboxImage && lightboxInfo) { + lightboxImage.src = img.src; + lightboxImage.alt = img.alt; + lightboxInfo.textContent = `QR Code: ${qrName}`; + + lightbox.style.display = "flex"; + console.log("Lightbox should be visible now"); // Debug log + + // Add keyboard listener for ESC key + document.addEventListener( + "keydown", + this.handleLightboxKeydown.bind(this) + ); + } else { + console.error("Lightbox elements not found"); + } + } else { + console.error("Image element not found or no src"); + } + } + + closeImageLightbox() { + console.log("Closing lightbox"); // Debug log + const lightbox = document.getElementById("imageLightbox"); + if (lightbox) { + lightbox.style.display = "none"; + + // Remove keyboard listener + document.removeEventListener( + "keydown", + this.handleLightboxKeydown.bind(this) + ); + } + } + + handleLightboxKeydown(event) { + if (event.key === "Escape") { + this.closeImageLightbox(); + } + } + + // QR Item Toggle functionality (for selection) + toggleQRItem(element) { + const qrId = element.dataset.qrId; + if (this.selectedQRCodes.has(qrId)) { + this.selectedQRCodes.delete(qrId); + element.classList.remove("selected"); + } else { + this.selectedQRCodes.add(qrId); + element.classList.add("selected"); + } + + // Update bulk action buttons if they exist + this.updateBulkActionButtons(); + } + + updateBulkActionButtons() { + const bulkActions = document.querySelector(".bulk-actions"); + if (bulkActions) { + bulkActions.style.display = + this.selectedQRCodes.size > 0 ? "flex" : "none"; + } + } + + // Copy QR data functionality + copyQRData(name, location, address, event) { + const data = `QR Code: ${name}\nLocation: ${location}\nAddress: ${address}\nEvent: ${event}`; + + if (navigator.clipboard) { + navigator.clipboard + .writeText(data) + .then(() => + this.showToast("QR code information copied to clipboard!", "success") + ) + .catch(() => this.fallbackCopyText(data)); + } else { + this.fallbackCopyText(data); + } + } + + // Delete QR Code + async deleteQRCode(qrId, qrName) { + if (!confirm(`Delete "${qrName}"? This cannot be undone.`)) return; + + try { + // Show loading state + const deleteBtn = document.querySelector( + `[onclick*="deleteQRCode(${qrId}"]` + ); + if (deleteBtn) { + deleteBtn.disabled = true; + deleteBtn.innerHTML = + ' Deleting...'; + } + + const response = await fetch(`/qr-codes/${qrId}/delete`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-CSRF-Token": (window.qrConfig && window.qrConfig.csrfToken) || "", + }, + }); + + if (response.ok) { + // Remove QR item from page immediately + const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`); + if (qrItem) { + qrItem.style.transition = "opacity 0.3s"; + qrItem.style.opacity = "0"; + setTimeout(() => { + qrItem.remove(); + }, 300); + } + + // Show success message + this.showToast(`QR code "${qrName}" deleted successfully!`, "success"); + } else { + throw new Error(`Server error: ${response.status}`); + } + } catch (error) { + console.error("Delete error:", error); + + // Restore button if there was an error + const deleteBtn = document.querySelector( + `[onclick*="deleteQRCode(${qrId}"]` + ); + if (deleteBtn) { + deleteBtn.disabled = false; + deleteBtn.innerHTML = ''; + } + + // Show error message + this.showToast("Failed to delete QR code. Please try again.", "error"); + } + } + + // Toast notification system + showToast(message, type = "info") { + const toast = document.createElement("div"); + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${ + type === "success" + ? "#10b981" + : type === "error" + ? "#ef4444" + : type === "warning" + ? "#f59e0b" + : "#3b82f6" + }; + color: white; + padding: 12px 16px; + border-radius: 8px; + z-index: 9999; + font-weight: 500; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + transition: all 0.3s ease; + opacity: 0; + transform: translateX(100%); + `; + + toast.textContent = message; + document.body.appendChild(toast); + + // Animate in + setTimeout(() => { + toast.style.opacity = "1"; + toast.style.transform = "translateX(0)"; + }, 100); + + // Animate out + setTimeout(() => { + toast.style.opacity = "0"; + toast.style.transform = "translateX(100%)"; + setTimeout(() => { + if (document.body.contains(toast)) { + toast.remove(); + } + }, 300); + }, 3000); + } + + // Expand/collapse functionality + toggleExpandAll() { + this.allExpanded = !this.allExpanded; + const qrItems = document.querySelectorAll(".qr-item"); + const expandToggle = document.getElementById("expandAllToggle"); + + qrItems.forEach((item) => { + const details = item.querySelector(".qr-details"); + if (details) { + if (this.allExpanded) { + details.style.display = "block"; + item.classList.add("expanded"); + } else { + details.style.display = "none"; + item.classList.remove("expanded"); + } + } + }); + + if (expandToggle) { + expandToggle.innerHTML = this.allExpanded + ? ' Collapse All' + : ' Expand All'; + } + } +} + +// Global variable to hold dashboard manager instance +let dashboardManager; + +// Initialize dashboard when DOM is loaded +document.addEventListener("DOMContentLoaded", function () { + dashboardManager = new ProjectDashboardManager(); + + // Register all global functions for template compatibility + window.toggleProject = (projectId) => + dashboardManager.toggleProject(projectId); + window.toggleQRCodeStatus = (qrId) => + dashboardManager.toggleQRCodeStatus(qrId); + window.openQRModalFromData = (element) => + dashboardManager.openQRModalFromData(element); + window.openQRModal = (qrData) => dashboardManager.openQRModal(qrData); + window.closeQRModal = () => dashboardManager.closeQRModal(); + window.downloadModalQR = () => dashboardManager.downloadModalQR(); + window.copyModalQRData = () => dashboardManager.copyModalQRData(); + window.copyQRDestination = () => dashboardManager.copyQRDestination(); + window.downloadQRFromCard = (button) => + dashboardManager.downloadQRFromCard(button); + window.openImageLightbox = (element, qrName) => + dashboardManager.openImageLightbox(element, qrName); + window.closeImageLightbox = () => dashboardManager.closeImageLightbox(); + window.toggleQRItem = (element) => dashboardManager.toggleQRItem(element); + window.copyQRData = (name, location, address, event) => + dashboardManager.copyQRData(name, location, address, event); + window.deleteQRCode = (qrId, qrName) => + dashboardManager.deleteQRCode(qrId, qrName); + + // FIXED: Global functions for copy/open link functionality + window.copyQRUrl = function (qrId) { + if (dashboardManager && dashboardManager.copyQRUrl) { + dashboardManager.copyQRUrl(qrId); + } else { + console.error( + "ProjectDashboardManager not initialized or copyQRUrl method missing" + ); + } + }; + + window.openQRLink = function (qrId) { + if (dashboardManager && dashboardManager.openQRLink) { + dashboardManager.openQRLink(qrId); + } else { + console.error( + "ProjectDashboardManager not initialized or openQRLink method missing" + ); + } + }; + + console.log( + "Project Dashboard initialized successfully with copy/open link functionality" + ); + console.log("Dashboard keyboard shortcuts:"); + console.log("Ctrl/Cmd + F: Focus search"); + console.log("Escape: Close modal"); +}); diff --git a/static/js/script.js b/static/js/script.js index e8661ad..1b903b0 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -447,10 +447,13 @@ class QRManager { // AJAX Helper async makeRequest(url, options = {}) { + // Read the CSRF token injected by Flask into window.qrConfig + const csrfToken = (window.qrConfig && window.qrConfig.csrfToken) || ''; const defaultOptions = { headers: { 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest' + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-Token': csrfToken }, ...options }; diff --git a/static/js/users.js b/static/js/users.js index ee7b3a1..058bb87 100644 --- a/static/js/users.js +++ b/static/js/users.js @@ -1,739 +1,747 @@ -/** - * Users management JavaScript functionality - * static/js/users.js - */ - -class UsersManager { - constructor() { - this.selectedUsers = new Set(); - this.init(); - } - - init() { - this.initializeSearch(); - this.initializeFilters(); - this.initializeBulkActions(); - this.setupEventListeners(); - this.initializeModals(); - } - - // Initialize search functionality - initializeSearch() { - const searchInput = document.getElementById("searchUsers"); - if (!searchInput) return; - - let searchTimeout; - searchInput.addEventListener("input", () => { - clearTimeout(searchTimeout); - searchTimeout = setTimeout(() => { - this.filterUsers(); - }, 300); - }); - } - - // Initialize filter functionality - initializeFilters() { - const filters = ["roleFilter", "statusFilter"]; - - filters.forEach((filterId) => { - const filter = document.getElementById(filterId); - if (filter) { - filter.addEventListener("change", () => { - this.filterUsers(); - }); - } - }); - } - - // Initialize bulk actions - initializeBulkActions() { - const selectAllCheckbox = document.getElementById("selectAllUsers"); - if (selectAllCheckbox) { - selectAllCheckbox.addEventListener("change", (e) => { - this.toggleSelectAll(e.target.checked); - }); - } - - // Individual checkbox handlers - const userCheckboxes = document.querySelectorAll(".user-checkbox"); - userCheckboxes.forEach((checkbox) => { - checkbox.addEventListener("change", (e) => { - this.handleUserSelection(e.target); - }); - }); - - // Bulk action buttons - this.setupBulkActionButtons(); - } - - setupBulkActionButtons() { - const bulkDeactivateBtn = document.getElementById("bulkDeactivateBtn"); - const bulkActivateBtn = document.getElementById("bulkActivateBtn"); - const bulkDeleteBtn = document.getElementById("bulkDeleteBtn"); - - if (bulkDeactivateBtn) { - bulkDeactivateBtn.addEventListener("click", () => { - this.bulkDeactivateUsers(); - }); - } - - if (bulkActivateBtn) { - bulkActivateBtn.addEventListener("click", () => { - this.bulkActivateUsers(); - }); - } - - if (bulkDeleteBtn) { - bulkDeleteBtn.addEventListener("click", () => { - this.bulkDeleteUsers(); - }); - } - } - - // Setup event listeners - setupEventListeners() { - // Keyboard shortcuts - document.addEventListener("keydown", (e) => { - // ESC to close modals - if (e.key === "Escape") { - this.closeAllModals(); - } - - // Ctrl/Cmd + F to focus search - if ((e.ctrlKey || e.metaKey) && e.key === "f") { - e.preventDefault(); - const searchInput = document.getElementById("searchUsers"); - if (searchInput) searchInput.focus(); - } - }); - - // Click outside dropdowns to close - document.addEventListener("click", (e) => { - if (!e.target.closest(".dropdown")) { - this.closeAllDropdowns(); - } - }); - } - - // Initialize modal functionality - initializeModals() { - const modals = document.querySelectorAll(".modal"); - modals.forEach((modal) => { - modal.addEventListener("click", (e) => { - if (e.target === modal) { - this.closeModal(modal); - } - }); - }); - } - - // Filter users based on search and filters - filterUsers() { - const searchTerm = - document.getElementById("searchUsers")?.value.toLowerCase() || ""; - const roleFilter = document.getElementById("roleFilter")?.value || ""; - const statusFilter = document.getElementById("statusFilter")?.value || ""; - - const userRows = document.querySelectorAll(".user-row"); - let visibleCount = 0; - - userRows.forEach((row) => { - const name = row.dataset.name?.toLowerCase() || ""; - const email = row.dataset.email?.toLowerCase() || ""; - const username = row.dataset.username?.toLowerCase() || ""; - const role = row.dataset.role || ""; - const status = row.dataset.status || ""; - - const matchesSearch = - !searchTerm || - name.includes(searchTerm) || - email.includes(searchTerm) || - username.includes(searchTerm); - - const matchesRole = !roleFilter || role === roleFilter; - const matchesStatus = !statusFilter || status === statusFilter; - - if (matchesSearch && matchesRole && matchesStatus) { - this.showUserRow(row); - visibleCount++; - } else { - this.hideUserRow(row); - } - }); - - this.updateResultsCount(visibleCount); - } - - showUserRow(row) { - row.style.display = "table-row"; - row.classList.remove("fade-out"); - row.classList.add("fade-in"); - } - - hideUserRow(row) { - row.classList.remove("fade-in"); - row.classList.add("fade-out"); - setTimeout(() => { - if (row.classList.contains("fade-out")) { - row.style.display = "none"; - } - }, 300); - } - - updateResultsCount(count) { - const counter = document.querySelector(".results-counter"); - if (counter) { - counter.textContent = `${count} users found`; - } - } - - // Dropdown management - toggleDropdown(event, button) { - event.stopPropagation(); - - const dropdown = button.closest(".dropdown"); - const menu = dropdown.querySelector(".dropdown-menu"); - - // Close all other dropdowns - this.closeAllDropdowns(); - - // Toggle current dropdown - menu.classList.toggle("show"); - } - - closeAllDropdowns() { - const openMenus = document.querySelectorAll(".dropdown-menu.show"); - openMenus.forEach((menu) => { - menu.classList.remove("show"); - }); - } - - // User Actions - async deactivateUser(userId, userName) { - const confirmed = await this.showConfirmation( - "Deactivate User", - `Are you sure you want to deactivate ${userName}?`, - "This will disable their login access but preserve their data." - ); - - if (!confirmed) return; - - try { - const response = await fetch(`/users/${userId}/delete`, { - method: "GET", - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - }); - - if (response.ok) { - // Update UI - this.updateUserStatus(userId, "inactive"); - window.showToast( - `User ${userName} deactivated successfully`, - "success" - ); - } else { - throw new Error("Failed to deactivate user"); - } - } catch (error) { - console.error("Deactivation error:", error); - window.showToast("Failed to deactivate user", "error"); - } - } - - async reactivateUser(userId, userName) { - try { - const response = await fetch(`/users/${userId}/reactivate`, { - method: "GET", - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - }); - - if (response.ok) { - this.updateUserStatus(userId, "active"); - window.showToast( - `User ${userName} reactivated successfully`, - "success" - ); - } else { - throw new Error("Failed to reactivate user"); - } - } catch (error) { - console.error("Reactivation error:", error); - window.showToast("Failed to reactivate user", "error"); - } - } - - async promoteUser(userId, userName) { - const confirmed = await this.showConfirmation( - "Promote to Admin", - `Promote ${userName} to admin?`, - "This will give them full system access including user management and system settings." - ); - - if (!confirmed) return; - - try { - const response = await fetch(`/users/${userId}/promote`, { - method: "GET", - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - }); - - if (response.ok) { - this.updateUserRole(userId, "admin"); - window.showToast( - `${userName} promoted to admin successfully`, - "success" - ); - } else { - throw new Error("Failed to promote user"); - } - } catch (error) { - console.error("Promotion error:", error); - window.showToast("Failed to promote user", "error"); - } - } - - async demoteUser(userId, userName) { - const confirmed = await this.showConfirmation( - "Demote from Admin", - `Demote ${userName} from admin to staff?`, - "This will remove their admin privileges and limit access to QR code management only." - ); - - if (!confirmed) return; - - try { - const response = await fetch(`/users/${userId}/demote`, { - method: "GET", - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - }); - - if (response.ok) { - this.updateUserRole(userId, "staff"); - window.showToast( - `${userName} demoted to staff successfully`, - "success" - ); - } else { - throw new Error("Failed to demote user"); - } - } catch (error) { - console.error("Demotion error:", error); - window.showToast("Failed to demote user", "error"); - } - } - - async permanentlyDeleteUser(userId, userName) { - const confirmed = await this.showConfirmation( - "Permanently Delete User", - `⚠️ PERMANENTLY DELETE ${userName}?`, - "This action CANNOT be undone and will permanently remove the user account and all associated QR codes.", - "danger" - ); - - if (!confirmed) return; - - try { - const response = await fetch(`/users/${userId}/permanently-delete`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Requested-With": "XMLHttpRequest", - }, - }); - - if (response.ok) { - // Remove user row from table - const userRow = document.querySelector(`[data-user-id="${userId}"]`); - if (userRow) { - userRow.classList.add("fade-out"); - setTimeout(() => userRow.remove(), 300); - } - - window.showToast(`User ${userName} permanently deleted`, "success"); - } else { - throw new Error("Failed to delete user"); - } - } catch (error) { - console.error("Deletion error:", error); - window.showToast("Failed to delete user", "error"); - } - } - - // Update UI after user actions - updateUserStatus(userId, newStatus) { - const userRow = document.querySelector(`[data-user-id="${userId}"]`); - if (!userRow) return; - - userRow.dataset.status = newStatus; - - const statusBadge = userRow.querySelector(".user-status"); - if (statusBadge) { - statusBadge.className = `user-status ${newStatus}`; - statusBadge.innerHTML = ` - - ${newStatus === "active" ? "Active" : "Inactive"} - `; - } - - // Update action buttons in dropdown - this.updateUserActions(userId, newStatus); - } - - updateUserRole(userId, newRole) { - const userRow = document.querySelector(`[data-user-id="${userId}"]`); - if (!userRow) return; - - userRow.dataset.role = newRole; - - const roleBadge = userRow.querySelector(".user-role"); - if (roleBadge) { - roleBadge.className = `user-role ${newRole}`; - roleBadge.textContent = newRole; - } - - // Update action buttons - this.updateUserActions(userId, null, newRole); - } - - updateUserActions(userId, status = null, role = null) { - const userRow = document.querySelector(`[data-user-id="${userId}"]`); - if (!userRow) return; - - const currentStatus = status || userRow.dataset.status; - const currentRole = role || userRow.dataset.role; - - // Update dropdown menu items - const dropdownMenu = userRow.querySelector(".dropdown-menu"); - if (dropdownMenu) { - // This would update the dropdown items based on new status/role - // Implementation depends on your dropdown structure - } - } - - // Bulk Actions - toggleSelectAll(checked) { - const userCheckboxes = document.querySelectorAll(".user-checkbox"); - userCheckboxes.forEach((checkbox) => { - checkbox.checked = checked; - this.handleUserSelection(checkbox); - }); - } - - handleUserSelection(checkbox) { - const userId = checkbox.value; - - if (checkbox.checked) { - this.selectedUsers.add(userId); - } else { - this.selectedUsers.delete(userId); - } - - this.updateBulkActionsBar(); - this.updateSelectAllState(); - } - - updateBulkActionsBar() { - const bulkActionsBar = document.getElementById("bulkActionsBar"); - const selectedCount = document.getElementById("selectedCount"); - - if (bulkActionsBar && selectedCount) { - if (this.selectedUsers.size > 0) { - bulkActionsBar.classList.add("show"); - selectedCount.textContent = this.selectedUsers.size; - } else { - bulkActionsBar.classList.remove("show"); - } - } - } - - updateSelectAllState() { - const selectAllCheckbox = document.getElementById("selectAllUsers"); - const userCheckboxes = document.querySelectorAll(".user-checkbox"); - - if (selectAllCheckbox && userCheckboxes.length > 0) { - const checkedCount = Array.from(userCheckboxes).filter( - (cb) => cb.checked - ).length; - selectAllCheckbox.checked = checkedCount === userCheckboxes.length; - selectAllCheckbox.indeterminate = - checkedCount > 0 && checkedCount < userCheckboxes.length; - } - } - - async bulkDeactivateUsers() { - if (this.selectedUsers.size === 0) return; - - const confirmed = await this.showConfirmation( - "Bulk Deactivate Users", - `Deactivate ${this.selectedUsers.size} selected users?`, - "This will disable their login access but preserve their data." - ); - - if (!confirmed) return; - - try { - const response = await fetch("/users/bulk/deactivate", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Requested-With": "XMLHttpRequest", - }, - body: JSON.stringify({ - user_ids: Array.from(this.selectedUsers), - }), - }); - - const result = await response.json(); - - if (result.success) { - // Update UI for deactivated users - this.selectedUsers.forEach((userId) => { - this.updateUserStatus(userId, "inactive"); - }); - - this.clearSelection(); - window.showToast(result.message, "success"); - } else { - throw new Error(result.message); - } - } catch (error) { - console.error("Bulk deactivation error:", error); - window.showToast("Failed to deactivate users", "error"); - } - } - - async bulkActivateUsers() { - if (this.selectedUsers.size === 0) return; - - const confirmed = await this.showConfirmation( - "Bulk Activate Users", - `Activate ${this.selectedUsers.size} selected users?`, - "This will restore their login access." - ); - - if (!confirmed) return; - - try { - const response = await fetch("/users/bulk/activate", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Requested-With": "XMLHttpRequest", - }, - body: JSON.stringify({ - user_ids: Array.from(this.selectedUsers), - }), - }); - - const result = await response.json(); - - if (result.success) { - this.selectedUsers.forEach((userId) => { - this.updateUserStatus(userId, "active"); - }); - - this.clearSelection(); - window.showToast(result.message, "success"); - } else { - throw new Error(result.message); - } - } catch (error) { - console.error("Bulk activation error:", error); - window.showToast("Failed to activate users", "error"); - } - } - - async bulkDeleteUsers() { - if (this.selectedUsers.size === 0) return; - - const confirmed = await this.showConfirmation( - "Permanently Delete Users", - `⚠️ PERMANENTLY DELETE ${this.selectedUsers.size} selected users?`, - "This action CANNOT be undone and will permanently remove all user accounts and their associated data.", - "danger" - ); - - if (!confirmed) return; - - try { - const response = await fetch("/users/bulk/permanently-delete", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Requested-With": "XMLHttpRequest", - }, - body: JSON.stringify({ - user_ids: Array.from(this.selectedUsers), - }), - }); - - const result = await response.json(); - - if (result.success) { - // Remove users from table - this.selectedUsers.forEach((userId) => { - const userRow = document.querySelector(`[data-user-id="${userId}"]`); - if (userRow) { - userRow.classList.add("fade-out"); - setTimeout(() => userRow.remove(), 300); - } - }); - - this.clearSelection(); - window.showToast(result.message, "success"); - } else { - throw new Error(result.message); - } - } catch (error) { - console.error("Bulk deletion error:", error); - window.showToast("Failed to delete users", "error"); - } - } - - clearSelection() { - this.selectedUsers.clear(); - const userCheckboxes = document.querySelectorAll(".user-checkbox"); - userCheckboxes.forEach((checkbox) => { - checkbox.checked = false; - }); - this.updateBulkActionsBar(); - this.updateSelectAllState(); - } - - // Modal and confirmation dialogs - showConfirmation(title, message, details = "", type = "warning") { - return new Promise((resolve) => { - const modal = document.createElement("div"); - modal.className = "modal"; - modal.style.display = "flex"; - modal.innerHTML = ` - - `; - - document.body.appendChild(modal); - - const cancelBtn = modal.querySelector(".cancel-btn"); - const confirmBtn = modal.querySelector(".confirm-btn"); - - const cleanup = () => modal.remove(); - - cancelBtn.addEventListener("click", () => { - cleanup(); - resolve(false); - }); - - confirmBtn.addEventListener("click", () => { - cleanup(); - resolve(true); - }); - - modal.addEventListener("click", (e) => { - if (e.target === modal) { - cleanup(); - resolve(false); - } - }); - }); - } - - closeModal(modal) { - modal.style.display = "none"; - } - - closeAllModals() { - const modals = document.querySelectorAll('.modal[style*="flex"]'); - modals.forEach((modal) => this.closeModal(modal)); - } - - // User details modal - showUserDetails(userId) { - // Implementation for showing user details modal - const modal = document.getElementById("userDetailsModal"); - if (modal) { - // Populate modal with user data - modal.style.display = "flex"; - } - } - - closeUserDetailsModal() { - const modal = document.getElementById("userDetailsModal"); - if (modal) { - modal.style.display = "none"; - } - } - - // Password reset modal - showPasswordResetModal(userId) { - const modal = document.getElementById("passwordResetModal"); - if (modal) { - modal.style.display = "flex"; - } - } - - closePasswordResetModal() { - const modal = document.getElementById("passwordResetModal"); - if (modal) { - modal.style.display = "none"; - } - } -} - -// Initialize users manager when DOM is loaded -document.addEventListener("DOMContentLoaded", function () { - window.usersManager = new UsersManager(); - - // Global functions for inline event handlers - window.toggleDropdown = (event, button) => - window.usersManager.toggleDropdown(event, button); - window.deactivateUser = (userId, userName) => - window.usersManager.deactivateUser(userId, userName); - window.reactivateUser = (userId, userName) => - window.usersManager.reactivateUser(userId, userName); - window.promoteUser = (userId, userName) => - window.usersManager.promoteUser(userId, userName); - window.demoteUser = (userId, userName) => - window.usersManager.demoteUser(userId, userName); - window.permanentlyDeleteUser = (userId, userName) => - window.usersManager.permanentlyDeleteUser(userId, userName); - window.showUserDetails = (userId) => - window.usersManager.showUserDetails(userId); - window.closeUserDetailsModal = () => - window.usersManager.closeUserDetailsModal(); - window.showPasswordResetModal = (userId) => - window.usersManager.showPasswordResetModal(userId); - window.closePasswordResetModal = () => - window.usersManager.closePasswordResetModal(); -}); +/** + * Users management JavaScript functionality + * static/js/users.js + */ + +class UsersManager { + constructor() { + this.selectedUsers = new Set(); + this.init(); + } + + init() { + this.initializeSearch(); + this.initializeFilters(); + this.initializeBulkActions(); + this.setupEventListeners(); + this.initializeModals(); + } + + // Initialize search functionality + initializeSearch() { + const searchInput = document.getElementById("searchUsers"); + if (!searchInput) return; + + let searchTimeout; + searchInput.addEventListener("input", () => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + this.filterUsers(); + }, 300); + }); + } + + // Initialize filter functionality + initializeFilters() { + const filters = ["roleFilter", "statusFilter"]; + + filters.forEach((filterId) => { + const filter = document.getElementById(filterId); + if (filter) { + filter.addEventListener("change", () => { + this.filterUsers(); + }); + } + }); + } + + // Initialize bulk actions + initializeBulkActions() { + const selectAllCheckbox = document.getElementById("selectAllUsers"); + if (selectAllCheckbox) { + selectAllCheckbox.addEventListener("change", (e) => { + this.toggleSelectAll(e.target.checked); + }); + } + + // Individual checkbox handlers + const userCheckboxes = document.querySelectorAll(".user-checkbox"); + userCheckboxes.forEach((checkbox) => { + checkbox.addEventListener("change", (e) => { + this.handleUserSelection(e.target); + }); + }); + + // Bulk action buttons + this.setupBulkActionButtons(); + } + + setupBulkActionButtons() { + const bulkDeactivateBtn = document.getElementById("bulkDeactivateBtn"); + const bulkActivateBtn = document.getElementById("bulkActivateBtn"); + const bulkDeleteBtn = document.getElementById("bulkDeleteBtn"); + + if (bulkDeactivateBtn) { + bulkDeactivateBtn.addEventListener("click", () => { + this.bulkDeactivateUsers(); + }); + } + + if (bulkActivateBtn) { + bulkActivateBtn.addEventListener("click", () => { + this.bulkActivateUsers(); + }); + } + + if (bulkDeleteBtn) { + bulkDeleteBtn.addEventListener("click", () => { + this.bulkDeleteUsers(); + }); + } + } + + // Setup event listeners + setupEventListeners() { + // Keyboard shortcuts + document.addEventListener("keydown", (e) => { + // ESC to close modals + if (e.key === "Escape") { + this.closeAllModals(); + } + + // Ctrl/Cmd + F to focus search + if ((e.ctrlKey || e.metaKey) && e.key === "f") { + e.preventDefault(); + const searchInput = document.getElementById("searchUsers"); + if (searchInput) searchInput.focus(); + } + }); + + // Click outside dropdowns to close + document.addEventListener("click", (e) => { + if (!e.target.closest(".dropdown")) { + this.closeAllDropdowns(); + } + }); + } + + // Initialize modal functionality + initializeModals() { + const modals = document.querySelectorAll(".modal"); + modals.forEach((modal) => { + modal.addEventListener("click", (e) => { + if (e.target === modal) { + this.closeModal(modal); + } + }); + }); + } + + // Filter users based on search and filters + filterUsers() { + const searchTerm = + document.getElementById("searchUsers")?.value.toLowerCase() || ""; + const roleFilter = document.getElementById("roleFilter")?.value || ""; + const statusFilter = document.getElementById("statusFilter")?.value || ""; + + const userRows = document.querySelectorAll(".user-row"); + let visibleCount = 0; + + userRows.forEach((row) => { + const name = row.dataset.name?.toLowerCase() || ""; + const email = row.dataset.email?.toLowerCase() || ""; + const username = row.dataset.username?.toLowerCase() || ""; + const role = row.dataset.role || ""; + const status = row.dataset.status || ""; + + const matchesSearch = + !searchTerm || + name.includes(searchTerm) || + email.includes(searchTerm) || + username.includes(searchTerm); + + const matchesRole = !roleFilter || role === roleFilter; + const matchesStatus = !statusFilter || status === statusFilter; + + if (matchesSearch && matchesRole && matchesStatus) { + this.showUserRow(row); + visibleCount++; + } else { + this.hideUserRow(row); + } + }); + + this.updateResultsCount(visibleCount); + } + + showUserRow(row) { + row.style.display = "table-row"; + row.classList.remove("fade-out"); + row.classList.add("fade-in"); + } + + hideUserRow(row) { + row.classList.remove("fade-in"); + row.classList.add("fade-out"); + setTimeout(() => { + if (row.classList.contains("fade-out")) { + row.style.display = "none"; + } + }, 300); + } + + updateResultsCount(count) { + const counter = document.querySelector(".results-counter"); + if (counter) { + counter.textContent = `${count} users found`; + } + } + + // Dropdown management + toggleDropdown(event, button) { + event.stopPropagation(); + + const dropdown = button.closest(".dropdown"); + const menu = dropdown.querySelector(".dropdown-menu"); + + // Close all other dropdowns + this.closeAllDropdowns(); + + // Toggle current dropdown + menu.classList.toggle("show"); + } + + closeAllDropdowns() { + const openMenus = document.querySelectorAll(".dropdown-menu.show"); + openMenus.forEach((menu) => { + menu.classList.remove("show"); + }); + } + + // User Actions + async deactivateUser(userId, userName) { + const confirmed = await this.showConfirmation( + "Deactivate User", + `Are you sure you want to deactivate ${userName}?`, + "This will disable their login access but preserve their data." + ); + + if (!confirmed) return; + + try { + const response = await fetch(`/users/${userId}/delete`, { + method: "GET", + headers: { + "X-Requested-With": "XMLHttpRequest", + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', + }, + }); + + if (response.ok) { + // Update UI + this.updateUserStatus(userId, "inactive"); + window.showToast( + `User ${userName} deactivated successfully`, + "success" + ); + } else { + throw new Error("Failed to deactivate user"); + } + } catch (error) { + console.error("Deactivation error:", error); + window.showToast("Failed to deactivate user", "error"); + } + } + + async reactivateUser(userId, userName) { + try { + const response = await fetch(`/users/${userId}/reactivate`, { + method: "GET", + headers: { + "X-Requested-With": "XMLHttpRequest", + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', + }, + }); + + if (response.ok) { + this.updateUserStatus(userId, "active"); + window.showToast( + `User ${userName} reactivated successfully`, + "success" + ); + } else { + throw new Error("Failed to reactivate user"); + } + } catch (error) { + console.error("Reactivation error:", error); + window.showToast("Failed to reactivate user", "error"); + } + } + + async promoteUser(userId, userName) { + const confirmed = await this.showConfirmation( + "Promote to Admin", + `Promote ${userName} to admin?`, + "This will give them full system access including user management and system settings." + ); + + if (!confirmed) return; + + try { + const response = await fetch(`/users/${userId}/promote`, { + method: "GET", + headers: { + "X-Requested-With": "XMLHttpRequest", + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', + }, + }); + + if (response.ok) { + this.updateUserRole(userId, "admin"); + window.showToast( + `${userName} promoted to admin successfully`, + "success" + ); + } else { + throw new Error("Failed to promote user"); + } + } catch (error) { + console.error("Promotion error:", error); + window.showToast("Failed to promote user", "error"); + } + } + + async demoteUser(userId, userName) { + const confirmed = await this.showConfirmation( + "Demote from Admin", + `Demote ${userName} from admin to staff?`, + "This will remove their admin privileges and limit access to QR code management only." + ); + + if (!confirmed) return; + + try { + const response = await fetch(`/users/${userId}/demote`, { + method: "GET", + headers: { + "X-Requested-With": "XMLHttpRequest", + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', + }, + }); + + if (response.ok) { + this.updateUserRole(userId, "staff"); + window.showToast( + `${userName} demoted to staff successfully`, + "success" + ); + } else { + throw new Error("Failed to demote user"); + } + } catch (error) { + console.error("Demotion error:", error); + window.showToast("Failed to demote user", "error"); + } + } + + async permanentlyDeleteUser(userId, userName) { + const confirmed = await this.showConfirmation( + "Permanently Delete User", + `⚠️ PERMANENTLY DELETE ${userName}?`, + "This action CANNOT be undone and will permanently remove the user account and all associated QR codes.", + "danger" + ); + + if (!confirmed) return; + + try { + const response = await fetch(`/users/${userId}/permanently-delete`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', + }, + }); + + if (response.ok) { + // Remove user row from table + const userRow = document.querySelector(`[data-user-id="${userId}"]`); + if (userRow) { + userRow.classList.add("fade-out"); + setTimeout(() => userRow.remove(), 300); + } + + window.showToast(`User ${userName} permanently deleted`, "success"); + } else { + throw new Error("Failed to delete user"); + } + } catch (error) { + console.error("Deletion error:", error); + window.showToast("Failed to delete user", "error"); + } + } + + // Update UI after user actions + updateUserStatus(userId, newStatus) { + const userRow = document.querySelector(`[data-user-id="${userId}"]`); + if (!userRow) return; + + userRow.dataset.status = newStatus; + + const statusBadge = userRow.querySelector(".user-status"); + if (statusBadge) { + statusBadge.className = `user-status ${newStatus}`; + statusBadge.innerHTML = ` + + ${newStatus === "active" ? "Active" : "Inactive"} + `; + } + + // Update action buttons in dropdown + this.updateUserActions(userId, newStatus); + } + + updateUserRole(userId, newRole) { + const userRow = document.querySelector(`[data-user-id="${userId}"]`); + if (!userRow) return; + + userRow.dataset.role = newRole; + + const roleBadge = userRow.querySelector(".user-role"); + if (roleBadge) { + roleBadge.className = `user-role ${newRole}`; + roleBadge.textContent = newRole; + } + + // Update action buttons + this.updateUserActions(userId, null, newRole); + } + + updateUserActions(userId, status = null, role = null) { + const userRow = document.querySelector(`[data-user-id="${userId}"]`); + if (!userRow) return; + + const currentStatus = status || userRow.dataset.status; + const currentRole = role || userRow.dataset.role; + + // Update dropdown menu items + const dropdownMenu = userRow.querySelector(".dropdown-menu"); + if (dropdownMenu) { + // This would update the dropdown items based on new status/role + // Implementation depends on your dropdown structure + } + } + + // Bulk Actions + toggleSelectAll(checked) { + const userCheckboxes = document.querySelectorAll(".user-checkbox"); + userCheckboxes.forEach((checkbox) => { + checkbox.checked = checked; + this.handleUserSelection(checkbox); + }); + } + + handleUserSelection(checkbox) { + const userId = checkbox.value; + + if (checkbox.checked) { + this.selectedUsers.add(userId); + } else { + this.selectedUsers.delete(userId); + } + + this.updateBulkActionsBar(); + this.updateSelectAllState(); + } + + updateBulkActionsBar() { + const bulkActionsBar = document.getElementById("bulkActionsBar"); + const selectedCount = document.getElementById("selectedCount"); + + if (bulkActionsBar && selectedCount) { + if (this.selectedUsers.size > 0) { + bulkActionsBar.classList.add("show"); + selectedCount.textContent = this.selectedUsers.size; + } else { + bulkActionsBar.classList.remove("show"); + } + } + } + + updateSelectAllState() { + const selectAllCheckbox = document.getElementById("selectAllUsers"); + const userCheckboxes = document.querySelectorAll(".user-checkbox"); + + if (selectAllCheckbox && userCheckboxes.length > 0) { + const checkedCount = Array.from(userCheckboxes).filter( + (cb) => cb.checked + ).length; + selectAllCheckbox.checked = checkedCount === userCheckboxes.length; + selectAllCheckbox.indeterminate = + checkedCount > 0 && checkedCount < userCheckboxes.length; + } + } + + async bulkDeactivateUsers() { + if (this.selectedUsers.size === 0) return; + + const confirmed = await this.showConfirmation( + "Bulk Deactivate Users", + `Deactivate ${this.selectedUsers.size} selected users?`, + "This will disable their login access but preserve their data." + ); + + if (!confirmed) return; + + try { + const response = await fetch("/users/bulk/deactivate", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', + }, + body: JSON.stringify({ + user_ids: Array.from(this.selectedUsers), + }), + }); + + const result = await response.json(); + + if (result.success) { + // Update UI for deactivated users + this.selectedUsers.forEach((userId) => { + this.updateUserStatus(userId, "inactive"); + }); + + this.clearSelection(); + window.showToast(result.message, "success"); + } else { + throw new Error(result.message); + } + } catch (error) { + console.error("Bulk deactivation error:", error); + window.showToast("Failed to deactivate users", "error"); + } + } + + async bulkActivateUsers() { + if (this.selectedUsers.size === 0) return; + + const confirmed = await this.showConfirmation( + "Bulk Activate Users", + `Activate ${this.selectedUsers.size} selected users?`, + "This will restore their login access." + ); + + if (!confirmed) return; + + try { + const response = await fetch("/users/bulk/activate", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', + }, + body: JSON.stringify({ + user_ids: Array.from(this.selectedUsers), + }), + }); + + const result = await response.json(); + + if (result.success) { + this.selectedUsers.forEach((userId) => { + this.updateUserStatus(userId, "active"); + }); + + this.clearSelection(); + window.showToast(result.message, "success"); + } else { + throw new Error(result.message); + } + } catch (error) { + console.error("Bulk activation error:", error); + window.showToast("Failed to activate users", "error"); + } + } + + async bulkDeleteUsers() { + if (this.selectedUsers.size === 0) return; + + const confirmed = await this.showConfirmation( + "Permanently Delete Users", + `⚠️ PERMANENTLY DELETE ${this.selectedUsers.size} selected users?`, + "This action CANNOT be undone and will permanently remove all user accounts and their associated data.", + "danger" + ); + + if (!confirmed) return; + + try { + const response = await fetch("/users/bulk/permanently-delete", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', + }, + body: JSON.stringify({ + user_ids: Array.from(this.selectedUsers), + }), + }); + + const result = await response.json(); + + if (result.success) { + // Remove users from table + this.selectedUsers.forEach((userId) => { + const userRow = document.querySelector(`[data-user-id="${userId}"]`); + if (userRow) { + userRow.classList.add("fade-out"); + setTimeout(() => userRow.remove(), 300); + } + }); + + this.clearSelection(); + window.showToast(result.message, "success"); + } else { + throw new Error(result.message); + } + } catch (error) { + console.error("Bulk deletion error:", error); + window.showToast("Failed to delete users", "error"); + } + } + + clearSelection() { + this.selectedUsers.clear(); + const userCheckboxes = document.querySelectorAll(".user-checkbox"); + userCheckboxes.forEach((checkbox) => { + checkbox.checked = false; + }); + this.updateBulkActionsBar(); + this.updateSelectAllState(); + } + + // Modal and confirmation dialogs + showConfirmation(title, message, details = "", type = "warning") { + return new Promise((resolve) => { + const modal = document.createElement("div"); + modal.className = "modal"; + modal.style.display = "flex"; + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + + const cancelBtn = modal.querySelector(".cancel-btn"); + const confirmBtn = modal.querySelector(".confirm-btn"); + + const cleanup = () => modal.remove(); + + cancelBtn.addEventListener("click", () => { + cleanup(); + resolve(false); + }); + + confirmBtn.addEventListener("click", () => { + cleanup(); + resolve(true); + }); + + modal.addEventListener("click", (e) => { + if (e.target === modal) { + cleanup(); + resolve(false); + } + }); + }); + } + + closeModal(modal) { + modal.style.display = "none"; + } + + closeAllModals() { + const modals = document.querySelectorAll('.modal[style*="flex"]'); + modals.forEach((modal) => this.closeModal(modal)); + } + + // User details modal + showUserDetails(userId) { + // Implementation for showing user details modal + const modal = document.getElementById("userDetailsModal"); + if (modal) { + // Populate modal with user data + modal.style.display = "flex"; + } + } + + closeUserDetailsModal() { + const modal = document.getElementById("userDetailsModal"); + if (modal) { + modal.style.display = "none"; + } + } + + // Password reset modal + showPasswordResetModal(userId) { + const modal = document.getElementById("passwordResetModal"); + if (modal) { + modal.style.display = "flex"; + } + } + + closePasswordResetModal() { + const modal = document.getElementById("passwordResetModal"); + if (modal) { + modal.style.display = "none"; + } + } +} + +// Initialize users manager when DOM is loaded +document.addEventListener("DOMContentLoaded", function () { + window.usersManager = new UsersManager(); + + // Global functions for inline event handlers + window.toggleDropdown = (event, button) => + window.usersManager.toggleDropdown(event, button); + window.deactivateUser = (userId, userName) => + window.usersManager.deactivateUser(userId, userName); + window.reactivateUser = (userId, userName) => + window.usersManager.reactivateUser(userId, userName); + window.promoteUser = (userId, userName) => + window.usersManager.promoteUser(userId, userName); + window.demoteUser = (userId, userName) => + window.usersManager.demoteUser(userId, userName); + window.permanentlyDeleteUser = (userId, userName) => + window.usersManager.permanentlyDeleteUser(userId, userName); + window.showUserDetails = (userId) => + window.usersManager.showUserDetails(userId); + window.closeUserDetailsModal = () => + window.usersManager.closeUserDetailsModal(); + window.showPasswordResetModal = (userId) => + window.usersManager.showPasswordResetModal(userId); + window.closePasswordResetModal = () => + window.usersManager.closePasswordResetModal(); +}); diff --git a/templates/add_manual_attendance.html b/templates/add_manual_attendance.html index 7c389f9..c799831 100644 --- a/templates/add_manual_attendance.html +++ b/templates/add_manual_attendance.html @@ -213,6 +213,7 @@
+
+
@@ -234,6 +235,7 @@

All {{ validation_result.valid_rows }} records are valid and ready to import.

+ - -
- -
-
- - - +
+
+
+

+ + Location Details +

+

Update the complete address and event information

+ +
+ + + Include all address details for accurate location + identification +
+ {{ qr_code.location_address|length }}/500 +
+ + +
+
+ +

Address Coordinates

+
+ +
+
+
+
+ {% if qr_code.has_coordinates %} + {{ "%.6f"|format(qr_code.address_latitude) }} + {% else %} + ---.---------- + {% endif %} +
+
Latitude
+
+
+
+ {% if qr_code.has_coordinates %} + {{ "%.6f"|format(qr_code.address_longitude) }} + {% else %} + ---.---------- + {% endif %} +
+
Longitude
+
+
+ +
+ + +
+ +
+
+
+
+ +
@@ -799,14 +816,20 @@ Event or Purpose * - - - + + - Select the event type for this QR code + Select the event type for this QR code @@ -830,9 +853,13 @@ - + + Color of the QR code modules @@ -863,10 +901,21 @@ Background Color
- - + +
Background color of the QR code @@ -880,8 +929,15 @@ Module Size
- + {{ qr_code.box_size or 10 }}px
Size of each QR code module (affects overall size) @@ -893,8 +949,15 @@ Border Size
- + {{ qr_code.border or 4 }} modules
White border around the QR code @@ -908,11 +971,10 @@ Error Correction Level Higher levels allow QR code to work even if partially damaged @@ -947,12 +1009,24 @@ - - - + + +
@@ -1037,11 +1111,9 @@
Colors: - + on - +
@@ -1054,11 +1126,11 @@ function updateCharacterCount(inputId, counterId, maxLength) { const input = document.getElementById(inputId); const counter = document.getElementById(counterId); - + if (input && counter) { const length = input.value.length; counter.textContent = `${length}/${maxLength}`; - + if (length > maxLength * 0.9) { counter.classList.add('warning'); } else { @@ -1071,7 +1143,7 @@ function applyStylePreset() { const styleSelect = document.getElementById('style_id'); const selectedOption = styleSelect.options[styleSelect.selectedIndex]; - + if (selectedOption.value) { // Apply preset values const fillColor = selectedOption.getAttribute('data-fill'); @@ -1079,7 +1151,7 @@ const boxSize = selectedOption.getAttribute('data-box-size'); const border = selectedOption.getAttribute('data-border'); const errorCorrection = selectedOption.getAttribute('data-error-correction'); - + // Update form inputs document.getElementById('fill_color').value = fillColor; document.getElementById('fill_color_text').value = fillColor; @@ -1088,14 +1160,14 @@ document.getElementById('box_size').value = boxSize; document.getElementById('border').value = border; document.getElementById('error_correction').value = errorCorrection; - + // Update range value displays updateRangeValue('box_size'); updateRangeValue('border'); - + // Update preview updatePreview(); - + // Show toast notification showToast(`Applied "${selectedOption.text}" style preset`, 'success'); } @@ -1104,14 +1176,14 @@ function syncColorInput(type) { const colorInput = document.getElementById(`${type}_color`); const textInput = document.getElementById(`${type}_color_text`); - + let value = textInput.value.trim().toUpperCase(); - + // Add # if missing if (!value.startsWith('#')) { value = '#' + value; } - + // Validate hex format if (/^#[0-9A-F]{6}$/i.test(value)) { colorInput.value = value; @@ -1127,7 +1199,7 @@ function updateRangeValue(inputId) { const input = document.getElementById(inputId); const valueDisplay = document.getElementById(`${inputId}_value`); - + if (inputId === 'box_size') { valueDisplay.textContent = `${input.value}px`; } else if (inputId === 'border') { @@ -1140,31 +1212,31 @@ const backColor = document.getElementById('back_color').value; const boxSize = document.getElementById('box_size').value; const border = document.getElementById('border').value; - + // Update preview QR modules const qrSample = document.getElementById('qr_sample'); const qrModules = qrSample.querySelectorAll('.qr-module:not(.empty)'); - + // Apply colors to QR modules qrModules.forEach(module => { module.style.backgroundColor = fillColor; }); - + // Apply background color qrSample.style.backgroundColor = backColor; - + // Apply border (padding represents border) const borderPx = Math.max(4, parseInt(border) * 2); qrSample.style.padding = `${borderPx}px`; - + // Apply module size effect (simulate with scale) const scale = Math.max(0.7, Math.min(1.3, parseInt(boxSize) / 10)); qrSample.style.transform = `scale(${scale})`; - + // Sync text inputs with color inputs document.getElementById('fill_color_text').value = fillColor.toUpperCase(); document.getElementById('back_color_text').value = backColor.toUpperCase(); - + // Add animation class const preview = document.getElementById('qr_preview'); preview.style.animation = 'none'; @@ -1182,11 +1254,11 @@ document.getElementById('box_size').value = '10'; document.getElementById('border').value = '4'; document.getElementById('error_correction').value = 'L'; - + updateRangeValue('box_size'); updateRangeValue('border'); updatePreview(); - + showToast('Reset to default QR code styling', 'info'); } @@ -1200,7 +1272,7 @@ ${message} `; - + toast.style.cssText = ` position: fixed; top: 20px; @@ -1216,14 +1288,14 @@ border-left: 4px solid ${getToastColor(type)}; max-width: 350px; `; - + document.body.appendChild(toast); - + setTimeout(() => { toast.style.opacity = '1'; toast.style.transform = 'translateX(0)'; }, 100); - + setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(100%)'; @@ -1259,57 +1331,58 @@ function validateQRCustomization() { const fillColor = document.getElementById('fill_color_text').value; const backColor = document.getElementById('back_color_text').value; - + // Check if colors are too similar if (fillColor.toLowerCase() === backColor.toLowerCase()) { showToast('QR code color and background color cannot be the same', 'error'); return false; } - + return true; } // Geocoding functionality function geocodeAddress(address) { showStatus('info', 'Getting coordinates...'); - + // Using server API which prefers Google Maps with OpenStreetMap fallback const encodedAddress = encodeURIComponent(address); - + // First try the server API fetch('/api/geocode', { method: 'POST', headers: { 'Content-Type': 'application/json', + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', }, body: JSON.stringify({ address: address }) }) - .then(response => response.json()) - .then(data => { - if (data.success) { - const lat = data.data.latitude; - const lng = data.data.longitude; - - updateCoordinates(lat, lng, 'geocoded'); - showStatus('success', data.message || 'Coordinates updated successfully'); - } else { - showStatus('warning', data.message || 'No coordinates found for this address'); - } - }) - .catch(error => { - console.error('Geocoding error:', error); - showStatus('error', 'Failed to get coordinates. Please try again.'); - }); + .then(response => response.json()) + .then(data => { + if (data.success) { + const lat = data.data.latitude; + const lng = data.data.longitude; + + updateCoordinates(lat, lng, 'geocoded'); + showStatus('success', data.message || 'Coordinates updated successfully'); + } else { + showStatus('warning', data.message || 'No coordinates found for this address'); + } + }) + .catch(error => { + console.error('Geocoding error:', error); + showStatus('error', 'Failed to get coordinates. Please try again.'); + }); } function updateCoordinates(lat, lng, accuracy) { document.getElementById('address_latitude').value = lat; document.getElementById('address_longitude').value = lng; document.getElementById('coordinate_accuracy').value = accuracy; - + document.getElementById('latitudeDisplay').textContent = lat.toFixed(10); document.getElementById('longitudeDisplay').textContent = lng.toFixed(10); - + document.getElementById('clearCoordinatesBtn').style.display = 'inline-flex'; document.getElementById('geocodeBtn').innerHTML = ' Update Coordinates'; } @@ -1318,13 +1391,13 @@ document.getElementById('address_latitude').value = ''; document.getElementById('address_longitude').value = ''; document.getElementById('coordinate_accuracy').value = ''; - + document.getElementById('latitudeDisplay').textContent = '---.----------'; document.getElementById('longitudeDisplay').textContent = '---.----------'; - + document.getElementById('clearCoordinatesBtn').style.display = 'none'; document.getElementById('geocodeBtn').innerHTML = ' Get Coordinates'; - + showStatus('info', 'Coordinates cleared'); } @@ -1332,7 +1405,7 @@ const statusDiv = document.getElementById('coordinateStatus'); if (statusDiv) { statusDiv.innerHTML = `
${message}
`; - + setTimeout(() => { statusDiv.innerHTML = ''; }, 5000); @@ -1347,8 +1420,12 @@ } // Initialize on page load - document.addEventListener('DOMContentLoaded', function () { + document.addEventListener('DOMContentLoaded', function() { // Set up character counters + document.getElementById('name').addEventListener('input', () => { + updateCharacterCount('name', 'nameCounter', 100); + updateCurrentInfo(); + }); document.getElementById('location').addEventListener('input', () => { updateCharacterCount('location', 'locationCounter', 100); updateCurrentInfo(); @@ -1357,33 +1434,33 @@ updateCharacterCount('location_address', 'addressCounter', 500); updateCurrentInfo(); }); - + // Set up event listener for location_event document.getElementById('location_event').addEventListener('change', updateCurrentInfo); - + // Set up initial values for QR customization updateRangeValue('box_size'); updateRangeValue('border'); updatePreview(); - + // Add form validation const form = document.getElementById('editQRForm'); if (form) { - form.addEventListener('submit', function (e) { + form.addEventListener('submit', function(e) { if (!validateQRCustomization()) { e.preventDefault(); return false; } }); } - + // Add color input synchronization - document.getElementById('fill_color').addEventListener('change', function () { + document.getElementById('fill_color').addEventListener('change', function() { document.getElementById('fill_color_text').value = this.value.toUpperCase(); updatePreview(); }); - - document.getElementById('back_color').addEventListener('change', function () { + + document.getElementById('back_color').addEventListener('change', function() { document.getElementById('back_color_text').value = this.value.toUpperCase(); updatePreview(); }); @@ -1392,7 +1469,7 @@ const geocodeBtn = document.getElementById('geocodeBtn'); const clearBtn = document.getElementById('clearCoordinatesBtn'); - geocodeBtn.addEventListener('click', function () { + geocodeBtn.addEventListener('click', function() { const address = document.getElementById('location_address').value.trim(); if (address.length > 10) { geocodeAddress(address); @@ -1408,26 +1485,26 @@ diff --git a/templates/edit_user.html b/templates/edit_user.html index 2050d91..1d63f02 100644 --- a/templates/edit_user.html +++ b/templates/edit_user.html @@ -15,6 +15,7 @@
+

@@ -470,6 +471,7 @@ method: 'POST', headers: { 'Content-Type': 'application/json', + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', }, body: JSON.stringify({ project_ids: projectIds }) }); diff --git a/templates/employees.html b/templates/employees.html index efe108e..2362ab7 100644 --- a/templates/employees.html +++ b/templates/employees.html @@ -304,6 +304,7 @@ extra_head %} - - -
-
-
-

- - Update Profile Information -

-

- Keep your profile information up to date for better collaboration -

-
- -
- - -
-
- - - Your display name in the system -
- -
- - - Used for notifications and account recovery -
-
- -
- - - Username cannot be changed. Contact admin if needed. -
- -
- - -
-
-
-
- - -
-
-
-

- - Change Password -

-

Ensure your account security with a strong password

-
- -
- - -
- - - Enter your current password for verification -
- -
- - -
- Minimum 6 characters with mixed case, numbers, and - symbols -
- -
- - -
-
- -
-

- - Password Security Tips -

-
    -
  • Use at least 8 characters
  • -
  • Include uppercase and lowercase letters
  • -
  • Add numbers and special characters
  • -
  • Avoid common words or personal information
  • -
  • Don't reuse passwords from other accounts
  • -
-
- -
- - -
-
-
-
- - -
-
-
-

- - Account Information -

-

View your account details and system information

-
- -
-
-
- - Account ID -
-
{{ user.id }}
-
- -
-
- - Role -
-
- - {{ user.role.title() }} - -
-
- -
-
- - Account Created -
-
- {{ user.created_date.strftime('%B %d, %Y at %H:%M') }} -
-
- - {% if user.creator %} -
-
- - Created By -
-
{{ user.creator.full_name }}
-
- {% endif %} - -
-
- - Account Status -
-
- - - {{ 'Active' if user.active_status else 'Inactive' }} - -
-
- - {% if user.last_login_date %} -
-
- - Last Login -
-
- {{ user.last_login_date.strftime('%B %d, %Y at %H:%M') }} ({{ - user.last_login_date|time_ago }}) -
-
- {% endif %} -
- - - -
-
-

-
- -{% endblock %} {% block extra_scripts %} - - - -{% endblock %} +{% extends "base_authenticated.html" %} {% block title %}My Profile - QR Code Management{% +endblock %} {% block content %} +
+
+
+
+ +
+
+ +
+
+ +
+

{{ user.full_name }}

+
+
+ + {{ user.email }} +
+
+ + @{{ user.username }} +
+
+ + + {{ user.role.title() }} + +
+
+ + Member since {{ user.created_date.strftime('%B %Y') }} +
+ {% if user.last_login_date %} +
+ + Last login {{ user.last_login_date.strftime('%Y-%m-%d %H:%M') + }} +
+ {% endif %} +
+
+
+ +
+ +
+
+
+ +
+
+

{{ user.created_qr_codes.count() }}

+

QR Codes Created

+
+
+ +
+
+ +
+
+

+ {{ user.created_qr_codes.filter_by(active_status=True).count() }} +

+

Active QR Codes

+
+
+ +
+
+ +
+
+

{{ user.created_date|days_since }}

+

Days as Member

+
+
+
+ + +
+
+ + + +
+ + +
+
+
+

+ + Update Profile Information +

+

+ Keep your profile information up to date for better collaboration +

+
+ +
+ + + +
+
+ + + Your display name in the system +
+ +
+ + + Used for notifications and account recovery +
+
+ +
+ + + Username cannot be changed. Contact admin if needed. +
+ +
+ + +
+
+
+
+ + +
+
+
+

+ + Change Password +

+

Ensure your account security with a strong password

+
+ +
+ + + +
+ + + Enter your current password for verification +
+ +
+ + +
+ Minimum 6 characters with mixed case, numbers, and + symbols +
+ +
+ + +
+
+ +
+

+ + Password Security Tips +

+
    +
  • Use at least 8 characters
  • +
  • Include uppercase and lowercase letters
  • +
  • Add numbers and special characters
  • +
  • Avoid common words or personal information
  • +
  • Don't reuse passwords from other accounts
  • +
+
+ +
+ + +
+
+
+
+ + +
+
+
+

+ + Account Information +

+

View your account details and system information

+
+ +
+
+
+ + Account ID +
+
{{ user.id }}
+
+ +
+
+ + Role +
+
+ + {{ user.role.title() }} + +
+
+ +
+
+ + Account Created +
+
+ {{ user.created_date.strftime('%B %d, %Y at %H:%M') }} +
+
+ + {% if user.creator %} +
+
+ + Created By +
+
{{ user.creator.full_name }}
+
+ {% endif %} + +
+
+ + Account Status +
+
+ + + {{ 'Active' if user.active_status else 'Inactive' }} + +
+
+ + {% if user.last_login_date %} +
+
+ + Last Login +
+
+ {{ user.last_login_date.strftime('%B %d, %Y at %H:%M') }} ({{ + user.last_login_date|time_ago }}) +
+
+ {% endif %} +
+ + + +
+
+
+
+
+{% endblock %} {% block extra_scripts %} + + + +{% endblock %} diff --git a/templates/projects.html b/templates/projects.html index fd8f85f..1f10383 100644 --- a/templates/projects.html +++ b/templates/projects.html @@ -114,6 +114,7 @@ {% if analysis.duplicate_records > 0 %}
+ diff --git a/templates/time_attendance_import.html b/templates/time_attendance_import.html index 0457bcf..1846193 100644 --- a/templates/time_attendance_import.html +++ b/templates/time_attendance_import.html @@ -343,6 +343,7 @@
+
@@ -885,6 +886,7 @@ importForm.addEventListener('submit', async (e) => { try { const resp = await fetch('{{ url_for("time_attendance.start_import_job") }}', { method: 'POST', + headers: { 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '' }, body: formData }); const data = await resp.json(); diff --git a/templates/time_attendance_import_progress.html b/templates/time_attendance_import_progress.html index fe161d5..b75470a 100644 --- a/templates/time_attendance_import_progress.html +++ b/templates/time_attendance_import_progress.html @@ -243,6 +243,7 @@ fetch('/time-attendance/import/execute', { method: 'POST', headers: { 'Content-Type': 'application/json', + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', }, body: JSON.stringify({ batch_id: batchId, diff --git a/templates/time_attendance_invalid_review.html b/templates/time_attendance_invalid_review.html index 59d15de..990cee3 100644 --- a/templates/time_attendance_invalid_review.html +++ b/templates/time_attendance_invalid_review.html @@ -321,6 +321,7 @@ {% if analysis.invalid_rows > 0 %} + diff --git a/templates/time_attendance_record_detail.html b/templates/time_attendance_record_detail.html index b45f549..7776f1e 100644 --- a/templates/time_attendance_record_detail.html +++ b/templates/time_attendance_record_detail.html @@ -867,7 +867,7 @@ function submitDelete() { } // Add CSRF token - const sessionCsrfToken = '{{ session.get("csrf_token", "") }}'; + const sessionCsrfToken = (window.qrConfig && window.qrConfig.csrfToken) || ''; if (sessionCsrfToken) { const csrfInput = document.createElement('input'); csrfInput.type = 'hidden'; diff --git a/templates/time_attendance_records.html b/templates/time_attendance_records.html index 3b6027b..b79f2cb 100644 --- a/templates/time_attendance_records.html +++ b/templates/time_attendance_records.html @@ -630,7 +630,7 @@ function confirmDelete(recordId, employeeName, date) { } // Add CSRF token if available - const sessionCsrfToken = '{{ session.get("csrf_token", "") }}'; + const sessionCsrfToken = (window.qrConfig && window.qrConfig.csrfToken) || ''; if (sessionCsrfToken) { const csrfInput = document.createElement('input'); csrfInput.type = 'hidden'; diff --git a/templates/users.html b/templates/users.html index 412a788..df1810d 100644 --- a/templates/users.html +++ b/templates/users.html @@ -442,7 +442,9 @@ Code Management{% endblock %} {% block extra_head %} method: "POST", headers: { "Content-Type": "application/json", + "X-CSRF-Token": (window.qrConfig && window.qrConfig.csrfToken) || "", "X-Requested-With": "XMLHttpRequest", + "X-CSRF-Token": (window.qrConfig && window.qrConfig.csrfToken) || "", }, body: JSON.stringify({ new_status: newStatus, diff --git a/templates/verification_review.html b/templates/verification_review.html index 049c3a2..5cfe962 100644 --- a/templates/verification_review.html +++ b/templates/verification_review.html @@ -817,6 +817,7 @@ method: 'POST', headers: { 'Content-Type': 'application/json', + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', }, body: JSON.stringify({ status: status, diff --git a/templates/verification_review_detail.html b/templates/verification_review_detail.html index 3e38a52..305dd06 100644 --- a/templates/verification_review_detail.html +++ b/templates/verification_review_detail.html @@ -497,6 +497,7 @@ QR Code Management{% endblock %} {% block extra_head %} method: 'POST', headers: { 'Content-Type': 'application/json', + 'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '', }, body: JSON.stringify({ status: status,