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 += `
-
- `;
-
- 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 += `
+
+ `;
+
+ 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 = `
-
-
-
-
${message}
- ${details ? `
${details}
` : ""}
-
-
-
- `;
-
- 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 = `
+
+
+
+
${message}
+ ${details ? `
${details}
` : ""}
+
+
+
+ `;
+
+ 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 @@