Sep 11 - Reupload the code

This commit is contained in:
2026-09-11 22:42:45 -04:00
commit d92cff81e5
130 changed files with 73508 additions and 0 deletions
+445
View File
@@ -0,0 +1,445 @@
/**
* Android Location Fix - GPS + IP Geolocation Only
* File: static/js/android_location_handler.js
*
* This version includes only precise location methods:
* 1. Progressive GPS fallback (4 attempts)
* 2. IP-based geolocation (3 services) - 5-50km accuracy
* No timezone analysis (removed 100km accuracy method)
*/
// Android-specific geolocation configuration
const ANDROID_LOCATION_CONFIG = {
// Primary attempt - High accuracy with reasonable timeout
highAccuracy: {
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 60000
},
// Fallback attempt - Network-based location
networkBased: {
enableHighAccuracy: false,
timeout: 20000,
maximumAge: 300000
},
// Final attempt - Any available location
anyLocation: {
enableHighAccuracy: false,
timeout: 30000,
maximumAge: 600000
}
};
// Global variables for location state
let locationAttemptInProgress = false;
let currentLocationMethod = '';
// Enhanced location request with GPS + IP fallback only
function requestAndroidEnhancedLocation() {
console.log("📱 Starting Android location request (GPS + IP methods only)...");
if (locationAttemptInProgress) {
console.log("📍 Location attempt already in progress, skipping");
return;
}
if (typeof locationRequestActive !== 'undefined' && locationRequestActive) {
console.log("📍 Location request already active, skipping");
return;
}
if (!navigator.geolocation) {
console.log("❌ Geolocation not supported, trying IP-based location");
attemptIPBasedLocation();
return;
}
locationAttemptInProgress = true;
// Set active flags
if (typeof locationRequestActive !== 'undefined') {
locationRequestActive = true;
}
if (typeof locationCaptureActive !== 'undefined') {
locationCaptureActive = true;
}
console.log("📱 Attempting GPS-based location sequence...");
attemptAndroidLocationSequence();
}
// GPS-based sequence (Steps 1-4)
function attemptAndroidLocationSequence() {
console.log("🔄 Step 1/5: High Accuracy GPS");
currentLocationMethod = 'gps_high_accuracy';
navigator.geolocation.getCurrentPosition(
(position) => {
console.log("✅ GPS high-accuracy success!");
handleAndroidLocationSuccess(position, "gps_high_accuracy");
},
(error) => {
console.log(`❌ High accuracy failed (${error.message}), trying network-based...`);
attemptNetworkBasedLocation();
},
ANDROID_LOCATION_CONFIG.highAccuracy
);
}
function attemptNetworkBasedLocation() {
console.log("🔄 Step 2/5: Network-based GPS");
currentLocationMethod = 'network';
navigator.geolocation.getCurrentPosition(
(position) => {
console.log("✅ Network-based GPS success!");
handleAndroidLocationSuccess(position, "network");
},
(error) => {
console.log(`❌ Network-based failed (${error.message}), trying any location...`);
attemptAnyAvailableLocation();
},
ANDROID_LOCATION_CONFIG.networkBased
);
}
function attemptAnyAvailableLocation() {
console.log("🔄 Step 3/5: Any available GPS");
currentLocationMethod = 'any';
navigator.geolocation.getCurrentPosition(
(position) => {
console.log("✅ Any-location GPS success!");
handleAndroidLocationSuccess(position, "any");
},
(error) => {
console.log(`❌ Any location failed (${error.message}), trying watchPosition...`);
attemptWatchPosition();
},
ANDROID_LOCATION_CONFIG.anyLocation
);
}
function attemptWatchPosition() {
console.log("🔄 Step 4/5: Watch Position (persistent)");
currentLocationMethod = 'watch';
let watchId = null;
let watchTimeout = null;
watchTimeout = setTimeout(() => {
if (watchId !== null) {
navigator.geolocation.clearWatch(watchId);
}
console.log("❌ Watch position timed out, trying IP-based location...");
attemptIPBasedLocation();
}, 25000);
watchId = navigator.geolocation.watchPosition(
(position) => {
console.log("✅ Watch position success!");
navigator.geolocation.clearWatch(watchId);
clearTimeout(watchTimeout);
handleAndroidLocationSuccess(position, "watch");
},
(error) => {
console.log(`❌ Watch position error: ${error.message}`);
},
{
enableHighAccuracy: false,
timeout: 20000,
maximumAge: 0
}
);
}
// IP-based geolocation (Step 5) - Final fallback
function attemptIPBasedLocation() {
console.log("🔄 Step 5/5: IP-based Geolocation (Final Fallback)");
currentLocationMethod = 'ip_geolocation';
// Try multiple IP geolocation services for better accuracy
const ipLocationServices = [
{
url: 'https://ipinfo.io/json',
parseResponse: (data) => {
if (data.loc) {
const [lat, lng] = data.loc.split(',');
return {
lat: parseFloat(lat),
lng: parseFloat(lng),
city: data.city,
region: data.region,
country: data.country,
accuracy: data.city ? 15000 : 50000 // Better accuracy if city is available
};
}
return null;
}
},
{
url: 'https://ipapi.co/json/',
parseResponse: (data) => ({
lat: data.latitude,
lng: data.longitude,
city: data.city,
region: data.region,
country: data.country_name,
accuracy: data.city ? 10000 : 50000 // Better accuracy if city is available
})
},
];
let serviceIndex = 0;
function tryNextIPService() {
if (serviceIndex >= ipLocationServices.length) {
console.log("❌ All IP geolocation services failed - location detection complete");
handleAndroidLocationError("All GPS and IP geolocation methods failed");
return;
}
const service = ipLocationServices[serviceIndex];
console.log(`🌐 Trying IP geolocation service ${serviceIndex + 1}: ${service.url}`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
fetch(service.url, {
method: 'GET',
signal: controller.signal,
headers: {
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
clearTimeout(timeoutId);
console.log(`🌐 IP service ${serviceIndex + 1} response:`, data);
const parsed = service.parseResponse(data);
if (parsed && parsed.lat && parsed.lng && !isNaN(parsed.lat) && !isNaN(parsed.lng)) {
// Validate coordinates are reasonable
if (parsed.lat >= -90 && parsed.lat <= 90 && parsed.lng >= -180 && parsed.lng <= 180) {
console.log(`✅ IP-based location success: ${parsed.lat}, ${parsed.lng}`);
console.log(`📊 Location: ${parsed.city}, ${parsed.region}, ${parsed.country}`);
console.log(`📊 Estimated accuracy: ${parsed.accuracy}m (~${Math.round(parsed.accuracy/1000)}km)`);
const ipLocationData = {
coords: {
latitude: parsed.lat,
longitude: parsed.lng,
accuracy: parsed.accuracy,
altitude: null
},
locationInfo: {
city: parsed.city,
region: parsed.region,
country: parsed.country,
source: `IP Service ${serviceIndex + 1}`,
serviceUrl: service.url
}
};
handleAndroidLocationSuccess(ipLocationData, "ip_geolocation");
return;
}
}
console.log(`❌ Invalid or missing coordinates from service ${serviceIndex + 1}, trying next...`);
serviceIndex++;
tryNextIPService();
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
console.log(`❌ IP service ${serviceIndex + 1} timed out (10s), trying next...`);
} else {
console.log(`❌ IP service ${serviceIndex + 1} failed: ${error.message}, trying next...`);
}
serviceIndex++;
tryNextIPService();
});
}
tryNextIPService();
}
// Enhanced success handler for GPS and IP location methods
function handleAndroidLocationSuccess(position, source) {
console.log(`✅ Android location obtained successfully via ${source}`);
let locationData;
if (source === "ip_geolocation") {
// Handle IP-based location
locationData = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
timestamp: new Date(),
source: source,
address: null,
locationInfo: position.locationInfo || null
};
console.log(`📍 IP-estimated coordinates: ${position.coords.latitude}, ${position.coords.longitude}`);
console.log(`📊 IP-estimated accuracy: ${position.coords.accuracy}m (~${Math.round(position.coords.accuracy/1000)}km)`);
if (position.locationInfo) {
console.log(`🏢 Location info: ${position.locationInfo.city}, ${position.locationInfo.region}`);
}
} else {
// Handle GPS-based location
locationData = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
timestamp: new Date(),
source: source,
address: null,
};
console.log(`📍 GPS coordinates: ${position.coords.latitude}, ${position.coords.longitude}`);
console.log(`📊 GPS accuracy: ${position.coords.accuracy}m`);
}
// Update global location variables
if (typeof userLocation !== 'undefined') {
Object.assign(userLocation, locationData);
console.log("📍 Updated userLocation with location data");
// Trigger reverse geocoding if we have coordinates but no address
if (typeof reverseGeocode === 'function' && locationData.latitude && locationData.longitude && !locationData.address) {
reverseGeocode(locationData.latitude, locationData.longitude);
}
}
if (typeof currentUserLocation !== 'undefined') {
Object.assign(currentUserLocation, locationData);
console.log("📍 Updated currentUserLocation with location data");
// Trigger enhanced reverse geocoding if available
if (typeof reverseGeocodeEnhanced === 'function' && locationData.latitude && locationData.longitude && !locationData.address) {
reverseGeocodeEnhanced(locationData.latitude, locationData.longitude);
}
}
// Clear active flags
if (typeof locationRequestActive !== 'undefined') {
locationRequestActive = false;
}
if (typeof locationCaptureActive !== 'undefined') {
locationCaptureActive = false;
}
locationAttemptInProgress = false;
// Console logging
console.log(`📊 LOCATION SUCCESS LOG:`, {
method: source,
success: true,
coordinates: `${locationData.latitude},${locationData.longitude}`,
accuracy: `${locationData.accuracy}m`,
accuracyKm: `~${Math.round(locationData.accuracy/1000)}km`,
locationInfo: locationData.locationInfo,
timestamp: new Date().toISOString()
});
}
function handleAndroidLocationError(errorMessage) {
console.log(`❌ All Android location methods failed: ${errorMessage}`);
// Update global location variables to indicate manual source
if (typeof userLocation !== 'undefined') {
userLocation.source = "manual";
}
if (typeof currentUserLocation !== 'undefined') {
currentUserLocation.source = "manual";
}
// Clear active flags
if (typeof locationRequestActive !== 'undefined') {
locationRequestActive = false;
}
if (typeof locationCaptureActive !== 'undefined') {
locationCaptureActive = false;
}
locationAttemptInProgress = false;
console.log(`📊 LOCATION ERROR LOG:`, {
error: errorMessage,
method: currentLocationMethod,
finalResult: 'manual_entry_required',
gpsAttempts: 4,
ipAttempts: 3,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
});
}
// Device detection functions
function isAndroidDevice() {
const userAgent = navigator.userAgent.toLowerCase();
return userAgent.includes('android');
}
function isAndroidChrome() {
const userAgent = navigator.userAgent.toLowerCase();
return userAgent.includes('android') && userAgent.includes('chrome') && !userAgent.includes('edg');
}
// Main initialization function
function initializeAndroidLocation() {
console.log("📱 Initializing Android location services (GPS + IP only)...");
if (isAndroidDevice()) {
console.log("📱 Android device detected, using GPS + IP location methods (5 steps)");
requestAndroidEnhancedLocation();
} else {
console.log("📱 Non-Android device, using standard location request");
if (typeof requestLocationData === 'function') {
requestLocationData();
} else if (typeof requestEnhancedLocation === 'function') {
requestEnhancedLocation();
}
}
}
// Override standard location initialization for Android devices
document.addEventListener('DOMContentLoaded', function() {
setTimeout(() => {
if (isAndroidDevice()) {
console.log("📱 Android detected - overriding with GPS + IP location handler");
if (typeof initializeLocation === 'function') {
window.initializeLocation = function() {
console.log("📱 Using GPS + IP Android location initialization");
initializeAndroidLocation();
};
}
if (typeof requestEnhancedLocation === 'function') {
window.requestEnhancedLocation = function() {
console.log("📱 Using GPS + IP Android location request");
initializeAndroidLocation();
};
}
}
}, 100);
});
// Export functions
if (typeof window !== 'undefined') {
window.AndroidLocationHandler = {
requestAndroidEnhancedLocation,
isAndroidDevice,
isAndroidChrome,
initializeAndroidLocation,
attemptIPBasedLocation
};
}
+471
View File
@@ -0,0 +1,471 @@
/**
* Attendance Fullscreen JavaScript
* Handles fullscreen toggle and optimization for iPad viewing
* static/js/attendance_fullscreen.js
*/
// Fullscreen state management
let isFullscreen = false;
// Touch event handlers
let touchStartY = 0;
let touchStartX = 0;
/**
* Toggle fullscreen mode for attendance report
*/
function toggleFullscreen() {
const container = document.getElementById('attendanceReportContainer');
const icon = document.getElementById('fullscreenIcon');
const body = document.body;
if (!container || !icon) {
console.error('Fullscreen elements not found');
return;
}
isFullscreen = !isFullscreen;
if (isFullscreen) {
enterFullscreen(container, icon, body);
} else {
exitFullscreen(container, icon, body);
}
// Log fullscreen action
logFullscreenAction(isFullscreen ? 'enter' : 'exit');
}
/**
* Prevent swipe gestures from interfering with fullscreen on touch devices
*/
function preventSwipeGestures(container, enable) {
if (enable) {
// Prevent pull-to-refresh and other touch gestures
container.addEventListener('touchstart', handleTouchStart, { passive: false });
container.addEventListener('touchmove', handleTouchMove, { passive: false });
container.addEventListener('touchend', handleTouchEnd, { passive: false });
// Prevent overscroll
document.body.style.overscrollBehavior = 'none';
container.style.overscrollBehavior = 'none';
console.log('Swipe gestures prevented for fullscreen mode');
} else {
// Re-enable normal touch behavior
container.removeEventListener('touchstart', handleTouchStart);
container.removeEventListener('touchmove', handleTouchMove);
container.removeEventListener('touchend', handleTouchEnd);
// Restore overscroll
document.body.style.overscrollBehavior = '';
container.style.overscrollBehavior = '';
console.log('Swipe gestures re-enabled');
}
}
function handleTouchStart(e) {
touchStartY = e.touches[0].clientY;
touchStartX = e.touches[0].clientX;
}
function handleTouchMove(e) {
if (!isFullscreen) return;
const touchY = e.touches[0].clientY;
const touchX = e.touches[0].clientX;
const deltaY = touchY - touchStartY;
const deltaX = touchX - touchStartX;
const container = document.getElementById('attendanceReportContainer');
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
const isAtTop = scrollTop === 0;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
// Prevent pull-down-to-refresh when at top
if (isAtTop && deltaY > 0) {
e.preventDefault();
return false;
}
// Prevent overscroll at bottom
if (isAtBottom && deltaY < 0) {
e.preventDefault();
return false;
}
// Allow normal scrolling within the container
}
function handleTouchEnd(e) {
touchStartY = 0;
touchStartX = 0;
}
/**
* Enter fullscreen mode
*/
function enterFullscreen(container, icon, body) {
// Add fullscreen classes
container.classList.add('fullscreen-mode');
body.classList.add('fullscreen-active');
// Change icon
icon.classList.remove('fa-expand');
icon.classList.add('fa-compress');
// Update button title
const button = document.getElementById('fullscreenToggle');
if (button) {
button.setAttribute('title', 'Exit Fullscreen');
}
// Detect if device is touch-enabled (iPad/tablet)
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// Only use native fullscreen API on non-touch devices
// This prevents swipe-down gesture from exiting fullscreen on iPad
if (!isTouchDevice) {
// Try to use native fullscreen API for desktop browsers
if (container.requestFullscreen) {
container.requestFullscreen().catch(err => {
console.log('Native fullscreen not available, using CSS fullscreen');
});
} else if (container.webkitRequestFullscreen) {
container.webkitRequestFullscreen().catch(err => {
console.log('Native fullscreen not available, using CSS fullscreen');
});
} else if (container.mozRequestFullScreen) {
container.mozRequestFullScreen().catch(err => {
console.log('Native fullscreen not available, using CSS fullscreen');
});
} else if (container.msRequestFullscreen) {
container.msRequestFullscreen().catch(err => {
console.log('Native fullscreen not available, using CSS fullscreen');
});
}
} else {
console.log('Touch device detected - using CSS-only fullscreen to prevent swipe-down exit');
}
// Adjust table layout for better viewing
adjustTableForFullscreen(true);
// Save fullscreen preference
saveFullscreenPreference(true);
// Prevent default touch behaviors that might interfere
preventSwipeGestures(container, true);
}
/**
* Exit fullscreen mode
*/
function exitFullscreen(container, icon, body) {
// Remove fullscreen classes
container.classList.remove('fullscreen-mode');
body.classList.remove('fullscreen-active');
// Change icon back
icon.classList.remove('fa-compress');
icon.classList.add('fa-expand');
// Update button title
const button = document.getElementById('fullscreenToggle');
if (button) {
button.setAttribute('title', 'Toggle Fullscreen');
}
// Exit native fullscreen if active (only for desktop)
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (!isTouchDevice) {
if (document.exitFullscreen) {
document.exitFullscreen().catch(err => {
console.log('Native fullscreen exit not needed');
});
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen().catch(err => {
console.log('Native fullscreen exit not needed');
});
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen().catch(err => {
console.log('Native fullscreen exit not needed');
});
} else if (document.msExitFullscreen) {
document.msExitFullscreen().catch(err => {
console.log('Native fullscreen exit not needed');
});
}
}
// Restore table layout
adjustTableForFullscreen(false);
// Save fullscreen preference
saveFullscreenPreference(false);
// Re-enable default touch behaviors
preventSwipeGestures(container, false);
}
/**
* Adjust table columns visibility based on fullscreen and device
*/
function adjustTableForFullscreen(isFullscreen) {
const table = document.getElementById('attendanceTable');
if (!table) return;
const viewport = {
width: window.innerWidth,
height: window.innerHeight,
orientation: window.innerWidth > window.innerHeight ? 'landscape' : 'portrait'
};
// Log viewport info for debugging
console.log('Adjusting table for fullscreen:', {
isFullscreen,
viewport
});
// Additional optimizations can be added here
// The CSS already handles most responsive adjustments
}
/**
* Save fullscreen preference to localStorage and URL
*/
function saveFullscreenPreference(isFullscreen) {
try {
localStorage.setItem('attendance_fullscreen_preference', isFullscreen ? 'true' : 'false');
// Also update the hidden form field if it exists
const fullscreenInput = document.getElementById('fullscreenState');
if (fullscreenInput) {
fullscreenInput.value = isFullscreen ? '1' : '';
}
console.log('Fullscreen preference saved:', isFullscreen);
} catch (e) {
console.warn('Could not save fullscreen preference:', e);
}
}
/**
* Load fullscreen preference from localStorage
*/
function loadFullscreenPreference() {
try {
const preference = localStorage.getItem('attendance_fullscreen_preference');
return preference === 'true';
} catch (e) {
console.warn('Could not load fullscreen preference:', e);
return false;
}
}
/**
* Log fullscreen action for analytics
*/
function logFullscreenAction(action) {
const logData = {
action: action,
timestamp: new Date().toISOString(),
viewport: {
width: window.innerWidth,
height: window.innerHeight,
orientation: window.innerWidth > window.innerHeight ? 'landscape' : 'portrait'
},
userAgent: navigator.userAgent,
isIPad: /iPad/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
};
console.log('Fullscreen action logged:', logData);
// You can send this to your backend for analytics if needed
// fetch('/api/log-fullscreen', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(logData)
// });
}
/**
* Handle native fullscreen change events (only for desktop)
*/
function handleFullscreenChange() {
// Skip handling on touch devices since we're not using native fullscreen there
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
return;
}
const isNativeFullscreen = !!(
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
);
// Sync our state with native fullscreen (desktop only)
if (!isNativeFullscreen && isFullscreen) {
// User exited native fullscreen, update our state
const container = document.getElementById('attendanceReportContainer');
const icon = document.getElementById('fullscreenIcon');
const body = document.body;
if (container && icon) {
isFullscreen = false;
exitFullscreen(container, icon, body);
}
}
}
/**
* Handle keyboard shortcuts
*/
function handleKeyboardShortcuts(event) {
// F11 or F for fullscreen toggle
if (event.key === 'F11' || (event.key === 'f' && event.ctrlKey)) {
event.preventDefault();
toggleFullscreen();
}
// Escape to exit fullscreen
if (event.key === 'Escape' && isFullscreen) {
toggleFullscreen();
}
}
/**
* Detect iPad and adjust UI accordingly
*/
function detectAndOptimizeForIPad() {
const isIPad = /iPad/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 0);
if (isIPad) {
console.log('iPad detected - optimizing UI');
document.body.classList.add('ipad-device');
// Add iPad-specific optimizations
const container = document.getElementById('attendanceReportContainer');
if (container) {
container.classList.add('ipad-optimized');
}
}
}
/**
* Restore fullscreen state from URL (CSS-only, no native fullscreen)
*/
function restoreFullscreenFromURL() {
const urlParams = new URLSearchParams(window.location.search);
const fullscreenParam = urlParams.get('fullscreen');
if (fullscreenParam === '1' && !isFullscreen) {
console.log('Restoring fullscreen mode from URL parameter (CSS-only)');
// Use CSS-only fullscreen (no native fullscreen API call)
const container = document.getElementById('attendanceReportContainer');
const icon = document.getElementById('fullscreenIcon');
const body = document.body;
if (container && icon) {
// Manually set fullscreen state without calling native API
isFullscreen = true;
container.classList.add('fullscreen-mode');
body.classList.add('fullscreen-active');
// Update icon
icon.classList.remove('fa-expand');
icon.classList.add('fa-compress');
// Update button
const button = document.getElementById('fullscreenToggle');
if (button) {
button.setAttribute('title', 'Exit Fullscreen');
}
// Apply touch gesture prevention
preventSwipeGestures(container, true);
// Save preference
saveFullscreenPreference(true);
console.log('Fullscreen restored successfully (CSS-only mode)');
}
}
}
/**
* Initialize fullscreen functionality
*/
function initializeFullscreen() {
console.log('Initializing fullscreen functionality');
// Detect iPad
detectAndOptimizeForIPad();
// Add event listeners for native fullscreen changes (desktop only)
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
document.addEventListener('MSFullscreenChange', handleFullscreenChange);
// Add keyboard shortcuts
document.addEventListener('keydown', handleKeyboardShortcuts);
// Intercept filter form submission to preserve fullscreen state
const filterForm = document.getElementById('filterForm');
if (filterForm) {
filterForm.addEventListener('submit', function(e) {
const fullscreenInput = document.getElementById('fullscreenState');
if (fullscreenInput) {
fullscreenInput.value = isFullscreen ? '1' : '';
console.log('Filter form submitted with fullscreen state:', isFullscreen);
}
});
}
// Handle orientation changes
window.addEventListener('orientationchange', function() {
console.log('Orientation changed');
if (isFullscreen) {
adjustTableForFullscreen(true);
}
});
// Handle window resize
let resizeTimeout;
window.addEventListener('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function() {
if (isFullscreen) {
adjustTableForFullscreen(true);
}
}, 250);
});
// Restore fullscreen state from URL if present
// Use setTimeout to ensure DOM is fully loaded
setTimeout(function() {
restoreFullscreenFromURL();
}, 100);
console.log('Fullscreen functionality initialized');
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeFullscreen);
} else {
initializeFullscreen();
}
// Export functions for external use
window.toggleFullscreen = toggleFullscreen;
window.isAttendanceFullscreen = function() { return isFullscreen; };
File diff suppressed because it is too large Load Diff
+703
View File
@@ -0,0 +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",
'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 = `
<i class="fas fa-external-link-alt"></i>
${destinationUrl}
`;
}
} else if (modalQRDestination) {
modalQRDestination.innerHTML =
'<span style="color: var(--gray-500); font-style: italic;">No destination URL available</span>';
}
this.currentModalQR = {
name: qrData.name,
image: qrData.image,
location: qrData.location,
address: qrData.address,
event: qrData.event,
qr_url: qrData.qr_url,
destination_url: qrData.qr_url
? `${window.location.origin}/qr/${qrData.qr_url}`
: null,
};
modal.style.display = "flex";
document.addEventListener("keydown", this.handleModalKeydown.bind(this));
}
}
closeQRModal() {
const modal = document.getElementById("qrModal");
if (modal) {
modal.style.display = "none";
this.currentModalQR = null;
document.removeEventListener(
"keydown",
this.handleModalKeydown.bind(this)
);
}
}
handleModalKeydown(event) {
if (event.key === "Escape") {
this.closeQRModal();
}
}
// Download Functions
downloadModalQR() {
if (this.currentModalQR) {
const base64Data = this.currentModalQR.image.includes("base64,")
? this.currentModalQR.image.split("base64,")[1]
: this.currentModalQR.image;
this.downloadQR(base64Data, this.currentModalQR.name);
}
}
downloadQRFromCard(button) {
const qrCard = button.closest(".qr-card") || button.closest(".qr-item");
const img = qrCard.querySelector("img");
const qrName =
qrCard.dataset.qrName ||
qrCard.querySelector(".qr-name")?.textContent ||
"qr_code";
if (img && img.src) {
const base64Data = img.src.includes("base64,")
? img.src.split("base64,")[1]
: img.src;
this.downloadQR(base64Data, qrName);
}
}
downloadQR(base64Image, filename) {
try {
const base64Data = base64Image.includes("base64,")
? base64Image.split("base64,")[1]
: base64Image;
const link = document.createElement("a");
link.href = `data:image/png;base64,${base64Data}`;
link.download = `${filename
.replace(/[^a-z0-9]/gi, "_")
.toLowerCase()}_qr_code.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.showToast("QR code downloaded successfully!", "success");
} catch (error) {
console.error("Download error:", error);
this.showToast("Failed to download QR code", "error");
}
}
// Copy Functions
copyModalQRData() {
if (this.currentModalQR) {
const data = `QR Code: ${this.currentModalQR.name}\nLocation: ${
this.currentModalQR.location
}\nAddress: ${this.currentModalQR.address}\nEvent: ${
this.currentModalQR.event
}${
this.currentModalQR.destination_url
? `\nQR Link: ${this.currentModalQR.destination_url}`
: ""
}`;
navigator.clipboard
.writeText(data)
.then(() => {
this.showToast("QR code information copied to clipboard!", "success");
})
.catch(() => {
this.fallbackCopyText(data);
});
}
}
copyQRDestination() {
if (this.currentModalQR && this.currentModalQR.destination_url) {
navigator.clipboard
.writeText(this.currentModalQR.destination_url)
.then(() => {
this.showToast("QR destination link copied to clipboard!", "success");
})
.catch(() => {
this.showToast("Failed to copy QR link", "error");
});
} else {
this.showToast("No QR destination link available", "warning");
}
}
// FIXED: Copy QR Code URL
async copyQRUrl(qrId) {
try {
const response = await fetch(`/qr-codes/${qrId}/copy-url`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"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 =
'<i class="fas fa-spinner fa-spin"></i> 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 = '<i class="fas fa-trash"></i>';
}
// 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
? '<i class="fas fa-compress-alt"></i> Collapse All'
: '<i class="fas fa-expand-alt"></i> 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");
});
+862
View File
@@ -0,0 +1,862 @@
/**
* Enhanced Export Configuration JavaScript with Drag & Drop
* Handles column selection, preview updates, drag & drop reordering, and preference management
*/
// Default column configuration
const DEFAULT_COLUMN_ORDER = [
'employee_id', // ID
'device_info', // Platform
'check_in_date', // Date
'check_in_time', // Time
'location_name', // Location Name
'status', // Action Description
'qr_address', // Event Description
'address', // Recorded Address
'location_accuracy' // Distance
];
const DEFAULT_COLUMNS = {
'employee_id': { selected: true, name: 'ID' },
'location_name': { selected: true, name: 'Location Name' },
'status': { selected: true, name: 'Action Description' },
'check_in_date': { selected: true, name: 'Date' },
'check_in_time': { selected: true, name: 'Time' },
'qr_address': { selected: true, name: 'Event Description' },
'address': { selected: true, name: 'Recorded Address' },
'device_info': { selected: true, name: 'Platform' },
'location_accuracy': { selected: true, name: 'Distance' }
};
// Global variables
let availableColumns = [];
let sortableInstance = null;
let savedColumnOrder = [];
function toggleSection(sectionId) {
try {
const section = document.getElementById(sectionId);
const icon = document.getElementById(sectionId + 'Icon');
if (!section) return;
if (section.classList.contains('collapsed')) {
section.classList.remove('collapsed');
if (icon) icon.classList.remove('rotated');
} else {
section.classList.add('collapsed');
if (icon) icon.classList.add('rotated');
}
// Save state
try {
const collapsedSections = JSON.parse(localStorage.getItem('collapsedSections') || '{}');
collapsedSections[sectionId] = section.classList.contains('collapsed');
localStorage.setItem('collapsedSections', JSON.stringify(collapsedSections));
} catch (e) {}
} catch (error) {
console.error('Error toggling section:', error);
}
}
function restoreCollapsedState() {
try {
const collapsedSections = JSON.parse(localStorage.getItem('collapsedSections') || '{}');
Object.keys(collapsedSections).forEach(sectionId => {
if (collapsedSections[sectionId]) {
const section = document.getElementById(sectionId);
const icon = document.getElementById(sectionId + 'Icon');
if (section) {
section.classList.add('collapsed');
if (icon) icon.classList.add('rotated');
}
}
});
} catch (error) {}
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('Enhanced Export Configuration with Drag & Drop initialized');
// Initialize available columns data
initializeColumnsData();
// Set up event listeners
setupEventListeners();
// Load saved preferences if available
loadSavedPreferences();
// Update preview on load
updatePreview();
// Initialize drag & drop
initializeDragDrop();
});
function initializeColumnsData() {
try {
// Extract column data from the form
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
availableColumns = Array.from(checkboxes).map(cb => {
const columnKey = cb.value;
const label = cb.parentElement.querySelector('label').textContent.trim();
const nameInput = document.getElementById('name_' + columnKey);
return {
key: columnKey,
label: label,
defaultName: nameInput ? nameInput.value : label,
enabled: cb.checked
};
});
console.log('Initialized columns data:', availableColumns);
} catch (error) {
console.error('Error initializing columns data:', error);
}
}
function setupEventListeners() {
try {
// Add change listeners to all column checkboxes
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
toggleColumnName(this.value);
updateSelectedColumnsList();
updatePreview();
// Add visual feedback
const columnItem = this.closest('.column-item');
if (this.checked) {
columnItem.classList.add('selected');
} else {
columnItem.classList.remove('selected');
}
});
});
// Add input listeners to all column name inputs
const nameInputs = document.querySelectorAll('input[id^="name_"]');
nameInputs.forEach(input => {
input.addEventListener('input', debounce(() => {
updateSelectedColumnsList();
updatePreview();
}, 300));
});
// Form validation before submit
const form = document.getElementById('exportForm');
if (form) {
form.addEventListener('submit', function(e) {
if (!validateForm()) {
e.preventDefault();
}
});
}
} catch (error) {
console.error('Error setting up event listeners:', error);
}
}
function toggleColumnName(columnKey) {
try {
const checkbox = document.getElementById('col_' + columnKey);
const nameGroup = document.getElementById('name_group_' + columnKey);
if (checkbox && nameGroup) {
if (checkbox.checked) {
nameGroup.style.display = 'block';
nameGroup.style.opacity = '0';
setTimeout(() => {
nameGroup.style.opacity = '1';
}, 10);
} else {
nameGroup.style.opacity = '0';
setTimeout(() => {
nameGroup.style.display = 'none';
}, 300);
}
}
} catch (error) {
console.error('Error toggling column name:', error);
}
}
/**
* Toggle edit mode for column name input
* @param {string} columnKey - The column key
* @param {boolean} enableEdit - Whether to enable editing
*/
function toggleEditMode(columnKey, enableEdit) {
try {
const nameInput = document.getElementById('name_' + columnKey);
if (nameInput) {
if (enableEdit) {
// Enable editing
nameInput.removeAttribute('readonly');
nameInput.focus();
nameInput.select();
console.log(`✏️ Editing enabled for column: ${columnKey}`);
} else {
// Disable editing
nameInput.setAttribute('readonly', 'readonly');
console.log(`🔒 Editing disabled for column: ${columnKey}`);
}
// Update preview when name changes
updateSelectedColumnsList();
updatePreview();
}
} catch (error) {
console.error('Error toggling edit mode:', error);
}
}
function initializeDragDrop() {
try {
const selectedColumnsList = document.getElementById('selectedColumnsList');
if (selectedColumnsList) {
sortableInstance = Sortable.create(selectedColumnsList, {
animation: 200,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
handle: '.column-drag-handle',
onStart: function(evt) {
console.log('Drag started:', evt.oldIndex);
},
onEnd: function(evt) {
console.log('Drag ended:', evt.oldIndex, '->', evt.newIndex);
updateColumnOrderNumbers();
updatePreview();
// Save the new order
savePreferences();
}
});
console.log('Drag & drop initialized successfully');
}
} catch (error) {
console.error('Error initializing drag & drop:', error);
}
}
function updateSelectedColumnsList() {
try {
const selectedColumnsList = document.getElementById('selectedColumnsList');
const selectedColumnsSection = document.getElementById('selectedColumnsSection');
if (!selectedColumnsList || !selectedColumnsSection) return;
// Get currently selected columns
const selectedColumns = Array.from(document.querySelectorAll('input[name="selected_columns"]:checked'));
if (selectedColumns.length === 0) {
selectedColumnsSection.style.display = 'none';
return;
}
selectedColumnsSection.style.display = 'block';
selectedColumnsSection.classList.add('has-columns');
// Get current order if exists, otherwise use selection order
let orderedColumns = [];
if (savedColumnOrder.length > 0) {
// Use saved order, but only include currently selected columns
orderedColumns = savedColumnOrder.filter(key =>
selectedColumns.some(cb => cb.value === key)
);
// Add any newly selected columns that weren't in saved order
selectedColumns.forEach(cb => {
if (!orderedColumns.includes(cb.value)) {
orderedColumns.push(cb.value);
}
});
} else {
orderedColumns = selectedColumns.map(cb => cb.value);
}
// Build the selected columns list HTML
let listHTML = '';
orderedColumns.forEach((columnKey, index) => {
const nameInput = document.getElementById('name_' + columnKey);
const columnData = availableColumns.find(col => col.key === columnKey);
const customName = nameInput ? nameInput.value : (columnData ? columnData.label : columnKey);
listHTML += `
<div class="selected-column-item" data-column-key="${columnKey}">
<div class="selected-column-info">
<div class="column-drag-handle" title="Drag to reorder">
<i class="fas fa-grip-vertical"></i>
</div>
<div class="selected-column-details">
<div class="selected-column-name">${columnData ? columnData.label : columnKey}</div>
<div class="selected-column-export-name">Export as: "${customName}"</div>
</div>
</div>
<div class="column-order-number">${index + 1}</div>
</div>
`;
});
if (listHTML === '') {
listHTML = `
<div class="selected-columns-empty">
<i class="fas fa-hand-point-up"></i>
<p>Select columns above to see them here for reordering</p>
</div>
`;
}
selectedColumnsList.innerHTML = listHTML;
// Re-initialize sortable after updating content
if (sortableInstance) {
sortableInstance.destroy();
}
initializeDragDrop();
console.log(`Updated selected columns list with ${orderedColumns.length} columns`);
} catch (error) {
console.error('Error updating selected columns list:', error);
}
}
function updateColumnOrderNumbers() {
try {
const orderNumbers = document.querySelectorAll('.column-order-number');
orderNumbers.forEach((element, index) => {
element.textContent = index + 1;
});
} catch (error) {
console.error('Error updating column order numbers:', error);
}
}
function getCurrentColumnOrder() {
try {
const selectedItems = document.querySelectorAll('.selected-column-item');
return Array.from(selectedItems).map(item => item.dataset.columnKey);
} catch (error) {
console.error('Error getting current column order:', error);
return [];
}
}
function updateColumnOrderField() {
try {
const columnOrderField = document.getElementById('column_order');
const currentOrder = getCurrentColumnOrder();
if (columnOrderField) {
columnOrderField.value = JSON.stringify(currentOrder);
}
} catch (error) {
console.error('Error updating column order field:', error);
}
}
function updatePreview() {
try {
const previewHeader = document.getElementById('previewHeader');
const previewTable = document.querySelector('.preview-table tbody');
if (!previewHeader || !previewTable) {
console.warn('Preview elements not found');
return;
}
// Get selected columns in their current order
const currentOrder = getCurrentColumnOrder();
if (currentOrder.length === 0) {
// No columns selected
previewHeader.innerHTML = '';
previewTable.innerHTML = `
<tr>
<td colspan="100%" class="preview-placeholder">
<i class="fas fa-exclamation-triangle"></i>
No columns selected - please select at least one column to export
</td>
</tr>
`;
updateGenerateButton(0);
return;
}
// Build header with order numbers
let headerHTML = '';
currentOrder.forEach((columnKey, index) => {
const nameInput = document.getElementById('name_' + columnKey);
const customName = nameInput ? nameInput.value : columnKey;
headerHTML += `<th data-order="${index + 1}">${customName}</th>`;
});
previewHeader.innerHTML = headerHTML;
// Build sample data row
let sampleRowHTML = '<tr>';
currentOrder.forEach(columnKey => {
let sampleData = getSampleData(columnKey);
// Special handling for address column to show the logic
if (columnKey === 'address') {
sampleData = `<span title="If location accuracy ≤ 0.5 miles: shows QR address, otherwise: shows actual check-in address">123 Business St, City*</span>`;
}
sampleRowHTML += `<td>${sampleData}</td>`;
});
sampleRowHTML += '</tr>';
// Add explanation row if address column is selected
const hasAddressColumn = currentOrder.includes('address');
if (hasAddressColumn) {
sampleRowHTML += `
<tr style="background-color: #f8f9fa; font-size: 0.85em; color: #6c757d;">
<td colspan="${currentOrder.length}" style="text-align: center; padding: 0.75rem; font-style: italic;">
<i class="fas fa-info-circle"></i>
* Check-in Address: Shows QR address when location accuracy ≤ 0.5 miles, otherwise shows actual GPS address
</td>
</tr>
`;
}
previewTable.innerHTML = sampleRowHTML;
// Update generate button
updateGenerateButton(currentOrder.length);
console.log(`Preview updated with ${currentOrder.length} columns in order:`, currentOrder);
} catch (error) {
console.error('Error updating preview:', error);
}
}
function getSampleData(columnKey) {
// Return sample data for each column type
const sampleData = {
'employee_id': 'EMP001',
'location_name': 'Main Office',
'status': 'Check In',
'check_in_date': '2025-08-14',
'check_in_time': '09:30:00',
'qr_address': '123 Business St, City',
'address': '123 Business St, City',
'device_info': 'iPhone 14 Pro',
'ip_address': '192.168.1.100',
'user_agent': 'Mobile Safari',
'latitude': '40.7128',
'longitude': '-74.0060',
'accuracy': '5.2',
'location_accuracy': '0.003'
};
return sampleData[columnKey] || 'Sample Data';
}
function selectAllColumns() {
try {
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
checkboxes.forEach(checkbox => {
if (!checkbox.checked) {
checkbox.checked = true;
checkbox.closest('.column-item').classList.add('selected');
toggleColumnName(checkbox.value);
}
});
updateSelectedColumnsList();
updatePreview();
console.log('All columns selected');
} catch (error) {
console.error('Error selecting all columns:', error);
}
}
function deselectAllColumns() {
try {
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
checkboxes.forEach(checkbox => {
if (checkbox.checked) {
checkbox.checked = false;
checkbox.closest('.column-item').classList.remove('selected');
toggleColumnName(checkbox.value);
}
});
updateSelectedColumnsList();
updatePreview();
console.log('All columns deselected');
} catch (error) {
console.error('Error deselecting all columns:', error);
}
}
function resetToDefaults() {
try {
console.log('🔄 Resetting to default settings...');
// First, deselect all columns and reset to defaults
const allCheckboxes = document.querySelectorAll('input[name="selected_columns"]');
allCheckboxes.forEach(checkbox => {
const key = checkbox.value;
const nameInput = document.getElementById('name_' + key);
const editCheckbox = document.getElementById('edit_' + key);
const columnItem = checkbox.closest('.column-item');
// Check if this column should be selected by default
const isDefaultSelected = DEFAULT_COLUMNS.hasOwnProperty(key) && DEFAULT_COLUMNS[key].selected;
// Set checkbox state
checkbox.checked = isDefaultSelected;
// Update visual state
if (isDefaultSelected) {
columnItem?.classList.add('selected');
} else {
columnItem?.classList.remove('selected');
}
// Set the export name and reset to readonly
if (nameInput) {
if (DEFAULT_COLUMNS[key]) {
nameInput.value = DEFAULT_COLUMNS[key].name;
} else {
// Use the original default name from availableColumns if not in DEFAULT_COLUMNS
const columnData = availableColumns.find(col => col.key === key);
nameInput.value = columnData ? columnData.defaultName : nameInput.value;
}
// Reset to readonly mode
nameInput.setAttribute('readonly', 'readonly');
}
// Uncheck edit checkbox
if (editCheckbox) {
editCheckbox.checked = false;
}
// Toggle visibility
toggleColumnName(key);
});
// Set the saved order to the default order
savedColumnOrder = [...DEFAULT_COLUMN_ORDER];
// Update the display with the default order
updateSelectedColumnsList();
// Apply the default order to the selected columns list
applyDefaultOrder();
// Update preview
updatePreview();
// Clear saved preferences from localStorage
try {
localStorage.removeItem('exportPreferences');
} catch (storageError) {
console.warn('Could not clear saved preferences:', storageError);
}
console.log('✅ Reset to default settings complete');
} catch (error) {
console.error('❌ Error resetting to defaults:', error);
}
}
/**
* Apply the default column order to the selected columns list
*/
function applyDefaultOrder() {
try {
const selectedList = document.getElementById('selectedColumnsList');
if (!selectedList) return;
const items = Array.from(selectedList.children);
if (items.length === 0) return;
// Sort items based on DEFAULT_COLUMN_ORDER
items.sort((a, b) => {
const keyA = a.dataset.columnKey;
const keyB = b.dataset.columnKey;
const indexA = DEFAULT_COLUMN_ORDER.indexOf(keyA);
const indexB = DEFAULT_COLUMN_ORDER.indexOf(keyB);
// If key not in default order, put it at the end
if (indexA === -1 && indexB === -1) return 0;
if (indexA === -1) return 1;
if (indexB === -1) return -1;
return indexA - indexB;
});
// Clear and re-append in sorted order
selectedList.innerHTML = '';
items.forEach(item => selectedList.appendChild(item));
// Update order numbers
updateColumnOrderNumbers();
// Update the hidden column order field
updateColumnOrderField();
console.log('📋 Applied default column order');
} catch (error) {
console.error('Error applying default order:', error);
}
}
function updateGenerateButton(columnCount) {
try {
const generateBtn = document.getElementById('generateBtn');
if (!generateBtn) return;
if (columnCount === 0) {
generateBtn.disabled = true;
generateBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Select columns to export';
generateBtn.classList.add('btn-disabled');
} else {
generateBtn.disabled = false;
generateBtn.innerHTML = `<i class="fas fa-download"></i> Generate Excel Export (${columnCount} columns)`;
generateBtn.classList.remove('btn-disabled');
}
} catch (error) {
console.error('Error updating generate button:', error);
}
}
function validateForm() {
try {
const selectedColumns = document.querySelectorAll('input[name="selected_columns"]:checked');
if (selectedColumns.length === 0) {
alert('Please select at least one column to export.');
return false;
}
// Validate that all selected columns have names
let hasEmptyNames = false;
selectedColumns.forEach(checkbox => {
const nameInput = document.getElementById('name_' + checkbox.value);
if (nameInput && nameInput.value.trim() === '') {
hasEmptyNames = true;
nameInput.style.borderColor = '#e53e3e';
nameInput.focus();
} else if (nameInput) {
nameInput.style.borderColor = '#e2e8f0';
}
});
if (hasEmptyNames) {
alert('Please provide names for all selected columns.');
return false;
}
// Update column order field before submitting
updateColumnOrderField();
// Save preferences before submitting
savePreferences();
return true;
} catch (error) {
console.error('Error validating form:', error);
return false;
}
}
function savePreferences() {
try {
const selectedColumns = Array.from(document.querySelectorAll('input[name="selected_columns"]:checked'))
.map(cb => cb.value);
const columnNames = {};
const editStates = {};
selectedColumns.forEach(col => {
const input = document.getElementById('name_' + col);
const editCheckbox = document.getElementById('edit_' + col);
if (input) {
columnNames[col] = input.value.trim();
}
if (editCheckbox) {
editStates[col] = editCheckbox.checked;
}
});
// Get current column order
const columnOrder = getCurrentColumnOrder();
const prefs = {
selected_columns: selectedColumns,
column_names: columnNames,
edit_states: editStates,
column_order: columnOrder,
timestamp: new Date().toISOString()
};
localStorage.setItem('exportPreferences', JSON.stringify(prefs));
console.log('💾 Preferences saved with column order and edit states:', prefs);
} catch (e) {
console.warn('Could not save preferences:', e);
}
}
function loadSavedPreferences() {
try {
const savedPrefs = localStorage.getItem('exportPreferences');
if (!savedPrefs) {
console.log('No saved preferences found');
// Check if there are already selected columns on page load and update preview
const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked');
if (alreadySelected.length > 0) {
console.log('Found pre-selected columns, updating preview');
updateSelectedColumnsList();
updatePreview();
}
return;
}
const prefs = JSON.parse(savedPrefs);
// Check if preferences are not too old (30 days)
const savedDate = new Date(prefs.timestamp || 0);
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
if (savedDate < thirtyDaysAgo) {
localStorage.removeItem('exportPreferences');
console.log('Saved preferences are too old, removed');
// Check if there are already selected columns on page load and update preview
const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked');
if (alreadySelected.length > 0) {
console.log('Found pre-selected columns, updating preview');
updateSelectedColumnsList();
updatePreview();
}
return;
}
// Apply saved column selections
if (prefs.selected_columns) {
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
checkboxes.forEach(cb => {
const shouldBeChecked = prefs.selected_columns.includes(cb.value);
if (cb.checked !== shouldBeChecked) {
cb.checked = shouldBeChecked;
const columnItem = cb.closest('.column-item');
if (shouldBeChecked) {
columnItem?.classList.add('selected');
} else {
columnItem?.classList.remove('selected');
}
toggleColumnName(cb.value);
}
});
}
// Apply saved column names
if (prefs.column_names) {
Object.keys(prefs.column_names).forEach(key => {
const input = document.getElementById('name_' + key);
if (input && prefs.column_names[key]) {
input.value = prefs.column_names[key];
}
});
}
// Apply saved edit states
if (prefs.edit_states) {
Object.keys(prefs.edit_states).forEach(key => {
const editCheckbox = document.getElementById('edit_' + key);
const nameInput = document.getElementById('name_' + key);
if (editCheckbox && prefs.edit_states[key]) {
editCheckbox.checked = true;
if (nameInput) {
nameInput.removeAttribute('readonly');
}
} else if (nameInput) {
nameInput.setAttribute('readonly', 'readonly');
}
});
}
// Save column order for later use
if (prefs.column_order) {
savedColumnOrder = prefs.column_order;
}
console.log('📋 Preferences loaded:', prefs);
// Update preview after loading preferences
updateSelectedColumnsList();
updatePreview();
} catch (e) {
console.warn('Could not load saved preferences:', e);
try {
localStorage.removeItem('exportPreferences');
} catch (removeError) {
console.warn('Could not remove invalid preferences:', removeError);
}
// Check if there are already selected columns on page load and update preview
const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked');
if (alreadySelected.length > 0) {
console.log('Found pre-selected columns after preference error, updating preview');
updateSelectedColumnsList();
updatePreview();
}
}
}
// Utility function for debouncing
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Global functions for template usage
window.selectAllColumns = selectAllColumns;
window.deselectAllColumns = deselectAllColumns;
window.resetToDefaults = resetToDefaults;
window.toggleColumnName = toggleColumnName;
window.toggleEditMode = toggleEditMode;
window.updateSelectedColumnsList = updateSelectedColumnsList;
window.getCurrentColumnOrder = getCurrentColumnOrder;
window.updateColumnOrderField = updateColumnOrderField;
window.savePreferences = savePreferences;
// Add CSS for disabled button
const style = document.createElement('style');
style.textContent = `
.btn-disabled {
opacity: 0.6 !important;
cursor: not-allowed !important;
background: #a0aec0 !important;
pointer-events: none;
}
.btn-disabled:hover {
transform: none !important;
box-shadow: none !important;
}
`;
document.head.appendChild(style);
File diff suppressed because it is too large Load Diff
+613
View File
@@ -0,0 +1,613 @@
// Enhanced JavaScript for Left Sidebar Navigation
class QRManager {
constructor() {
this.initializeApp();
}
initializeApp() {
this.initSidebar();
this.initModals();
this.initDropdowns();
this.initActiveNavigation();
this.initFlashMessages();
}
// Sidebar Management
initSidebar() {
const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebarToggle');
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const sidebarOverlay = document.getElementById('sidebarOverlay');
if (!sidebar) return;
// Desktop sidebar toggle
if (sidebarToggle) {
sidebarToggle.addEventListener('click', () => {
this.toggleSidebar();
});
}
// Mobile menu toggle
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', () => {
this.toggleMobileSidebar();
});
}
// Close mobile sidebar when clicking overlay
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', () => {
this.closeMobileSidebar();
});
}
// Close mobile sidebar when clicking menu items
const menuItems = sidebar.querySelectorAll('.menu-item');
menuItems.forEach(item => {
item.addEventListener('click', () => {
if (window.innerWidth <= 768) {
this.closeMobileSidebar();
}
});
});
// Handle window resize
window.addEventListener('resize', () => {
this.handleResize();
});
// Initialize sidebar state based on screen size
this.handleResize();
}
toggleSidebar() {
const sidebar = document.getElementById('sidebar');
if (sidebar) {
sidebar.classList.toggle('collapsed');
this.saveSidebarState();
}
}
toggleMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
const mobileBtn = document.getElementById('mobileMenuBtn');
if (sidebar && overlay && mobileBtn) {
const isOpen = sidebar.classList.contains('mobile-open');
if (isOpen) {
this.closeMobileSidebar();
} else {
this.openMobileSidebar();
}
}
}
openMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
const mobileBtn = document.getElementById('mobileMenuBtn');
if (sidebar && overlay && mobileBtn) {
sidebar.classList.add('mobile-open');
overlay.classList.add('active');
mobileBtn.classList.add('active');
document.body.style.overflow = 'hidden';
}
}
closeMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
const mobileBtn = document.getElementById('mobileMenuBtn');
if (sidebar && overlay && mobileBtn) {
sidebar.classList.remove('mobile-open');
overlay.classList.remove('active');
mobileBtn.classList.remove('active');
document.body.style.overflow = '';
}
}
handleResize() {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
if (window.innerWidth <= 768) {
// Mobile: ensure sidebar is hidden and mobile menu is available
this.closeMobileSidebar();
} else if (window.innerWidth <= 1024) {
// Tablet: auto-collapse sidebar
sidebar.classList.add('collapsed');
this.closeMobileSidebar();
} else {
// Desktop: restore saved state
this.restoreSidebarState();
this.closeMobileSidebar();
}
}
saveSidebarState() {
const sidebar = document.getElementById('sidebar');
if (sidebar && window.innerWidth > 1024) {
const isCollapsed = sidebar.classList.contains('collapsed');
localStorage.setItem('sidebarCollapsed', isCollapsed);
}
}
restoreSidebarState() {
const sidebar = document.getElementById('sidebar');
if (sidebar && window.innerWidth > 1024) {
const isCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
sidebar.classList.toggle('collapsed', isCollapsed);
}
}
// Active Navigation Highlighting
initActiveNavigation() {
const menuItems = document.querySelectorAll('.menu-item[href]');
const currentPath = window.location.pathname;
menuItems.forEach(item => {
const href = item.getAttribute('href');
if (href === currentPath || (currentPath.startsWith(href) && href !== '/')) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
// Theme Management
initTheme() {
const themeToggle = document.getElementById('themeToggle');
if (!themeToggle) return;
// Load saved theme
const savedTheme = localStorage.getItem('theme') || 'light';
this.setTheme(savedTheme);
themeToggle.addEventListener('click', () => {
const currentTheme = document.body.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
this.setTheme(newTheme);
localStorage.setItem('theme', newTheme);
});
}
setTheme(theme) {
document.body.setAttribute('data-theme', theme);
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
const icon = themeToggle.querySelector('i');
const text = themeToggle.querySelector('.menu-text');
if (theme === 'dark') {
icon.className = 'fas fa-sun';
if (text) text.textContent = 'Light Mode';
} else {
icon.className = 'fas fa-moon';
if (text) text.textContent = 'Dark Mode';
}
}
}
// Modal Management
initModals() {
// Close modal when clicking outside
document.addEventListener('click', (e) => {
if (e.target.classList.contains('modal')) {
this.closeModal(e.target);
}
});
// Close modal with close button
document.addEventListener('click', (e) => {
if (e.target.classList.contains('modal-close') ||
e.target.closest('.modal-close')) {
const modal = e.target.closest('.modal');
if (modal) {
this.closeModal(modal);
}
}
});
// Close modal with Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const openModal = document.querySelector('.modal.show');
if (openModal) {
this.closeModal(openModal);
}
}
});
}
showModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('show');
document.body.style.overflow = 'hidden';
// Focus first focusable element
const focusableElement = modal.querySelector('input, textarea, select, button, [tabindex]:not([tabindex="-1"])');
if (focusableElement) {
setTimeout(() => focusableElement.focus(), 100);
}
}
}
closeModal(modal) {
if (modal) {
modal.classList.remove('show');
document.body.style.overflow = '';
}
}
// Dropdown Management
initDropdowns() {
const dropdowns = document.querySelectorAll('.dropdown');
dropdowns.forEach(dropdown => {
const trigger = dropdown.querySelector('.dropdown-trigger');
const menu = dropdown.querySelector('.dropdown-menu');
if (trigger && menu) {
trigger.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleDropdown(dropdown);
});
}
});
// Close dropdowns when clicking outside
document.addEventListener('click', () => {
this.closeAllDropdowns();
});
// Close dropdowns with Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.closeAllDropdowns();
}
});
}
toggleDropdown(dropdown) {
const menu = dropdown.querySelector('.dropdown-menu');
const isOpen = menu.classList.contains('show');
this.closeAllDropdowns();
if (!isOpen) {
menu.classList.add('show');
}
}
closeAllDropdowns() {
const openMenus = document.querySelectorAll('.dropdown-menu.show');
openMenus.forEach(menu => {
menu.classList.remove('show');
});
}
// Flash Messages
initFlashMessages() {
const alerts = document.querySelectorAll('.alert');
alerts.forEach(alert => {
// Auto-dismiss after 5 seconds
setTimeout(() => {
this.dismissAlert(alert);
}, 5000);
// Manual dismiss
const closeBtn = alert.querySelector('.alert-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
this.dismissAlert(alert);
});
}
});
}
dismissAlert(alert) {
alert.style.opacity = '0';
alert.style.transform = 'translateX(100%)';
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 300);
}
// Download QR Code functionality
downloadQR(base64Image, filename) {
try {
const link = document.createElement("a");
link.href = `data:image/png;base64,${base64Image}`;
link.download = `${filename.replace(/[^a-z0-9]/gi, "_").toLowerCase()}_qr_code.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Show success toast
this.showToast("QR code downloaded successfully!", "success");
} catch (error) {
this.showToast("Failed to download QR code", "error");
console.error("Download error:", error);
}
}
// Download from modal
downloadModalQR() {
if (window.currentModalQR) {
const base64Data = window.currentModalQR.image.split("base64,")[1];
this.downloadQR(base64Data, window.currentModalQR.name);
}
}
// QR Modal functionality
openQRModal(qrData, qrName) {
const modal = document.getElementById("qrModal");
const modalTitle = document.getElementById("modalTitle");
const modalImage = document.getElementById("modalQRImage");
if (modal && modalTitle && modalImage) {
modalTitle.textContent = `${qrName} - QR Code`;
modalImage.src = `data:image/png;base64,${qrData}`;
modalImage.alt = `QR Code for ${qrName}`;
// Store current modal QR for download
window.currentModalQR = {
name: qrName,
image: `data:image/png;base64,${qrData}`,
};
modal.classList.add('show');
}
}
closeQRModal() {
const modal = document.getElementById("qrModal");
if (modal) {
modal.classList.remove('show');
window.currentModalQR = null;
}
}
// Utility Methods
showToast(message, type = 'info', duration = 3000) {
const toast = document.createElement('div');
toast.className = `alert alert-${type}`;
toast.innerHTML = `
<i class="fas fa-info-circle"></i>
${message}
<button class="alert-close">
<i class="fas fa-times"></i>
</button>
`;
const container = document.querySelector('.flash-messages') || document.body;
container.appendChild(toast);
// Trigger animation
setTimeout(() => {
toast.classList.add('show');
}, 10);
// Auto dismiss
setTimeout(() => {
this.dismissAlert(toast);
}, duration);
return toast;
}
// Form Validation Helper
validateForm(formElement) {
const requiredFields = formElement.querySelectorAll('[required]');
let isValid = true;
requiredFields.forEach(field => {
if (!field.value.trim()) {
this.showFieldError(field, 'This field is required');
isValid = false;
} else {
this.clearFieldError(field);
}
});
return isValid;
}
showFieldError(field, message) {
this.clearFieldError(field);
field.classList.add('error');
const errorElement = document.createElement('div');
errorElement.className = 'field-error';
errorElement.textContent = message;
field.parentNode.appendChild(errorElement);
}
clearFieldError(field) {
field.classList.remove('error');
const existingError = field.parentNode.querySelector('.field-error');
if (existingError) {
existingError.remove();
}
}
// 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-CSRF-Token': csrfToken
},
...options
};
try {
const response = await fetch(url, defaultOptions);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Request failed:', error);
this.showToast('An error occurred. Please try again.', 'error');
throw error;
}
}
}
// Keep existing date/time utilities
const DateTimeUtils = {
formatDate: (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
},
formatDateTime: (dateString) => {
const date = new Date(dateString);
return date.toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
},
formatTime: (dateString) => {
const date = new Date(dateString);
return date.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
});
},
};
// Legacy Navigation Manager for backward compatibility
class NavigationManager {
constructor() {
this.initMobileMenu();
this.initDropdowns();
}
initMobileMenu() {
const mobileMenuBtn = document.getElementById("mobile-menu");
const navMenu = document.getElementById("navMenu");
if (mobileMenuBtn && navMenu) {
mobileMenuBtn.addEventListener("click", () => {
mobileMenuBtn.classList.toggle("active");
navMenu.classList.toggle("active");
});
const navLinks = navMenu.querySelectorAll(".nav-link");
navLinks.forEach((link) => {
link.addEventListener("click", () => {
mobileMenuBtn.classList.remove("active");
navMenu.classList.remove("active");
});
});
}
}
initDropdowns() {
const dropdowns = document.querySelectorAll(".dropdown");
dropdowns.forEach((dropdown) => {
const trigger = dropdown.querySelector(".dropdown-trigger");
const menu = dropdown.querySelector(".dropdown-menu");
if (trigger && menu) {
trigger.addEventListener("click", (e) => {
e.stopPropagation();
this.toggleDropdown(dropdown);
});
}
});
document.addEventListener("click", () => {
this.closeAllDropdowns();
});
}
toggleDropdown(dropdown) {
const menu = dropdown.querySelector(".dropdown-menu");
const isOpen = menu.classList.contains("show");
this.closeAllDropdowns();
if (!isOpen) {
menu.classList.add("show");
}
}
closeAllDropdowns() {
const openMenus = document.querySelectorAll(".dropdown-menu.show");
openMenus.forEach((menu) => {
menu.classList.remove("show");
});
}
}
// Initialize the application
document.addEventListener('DOMContentLoaded', () => {
// Initialize main app if sidebar exists (authenticated users)
if (document.getElementById('sidebar')) {
window.qrManager = new QRManager();
// Make functions globally available for inline event handlers
window.downloadQR = (base64Image, filename) => {
window.qrManager.downloadQR(base64Image, filename);
};
window.downloadModalQR = () => {
window.qrManager.downloadModalQR();
};
window.openQRModal = (qrData, qrName) => {
window.qrManager.openQRModal(qrData, qrName);
};
window.closeQRModal = () => {
window.qrManager.closeQRModal();
};
} else {
// Initialize legacy navigation for non-authenticated pages
window.navigationManager = new NavigationManager();
}
});
// Export for use in other scripts
window.DateTimeUtils = DateTimeUtils;
// Keep any existing global functions for backward compatibility
if (typeof showConfirmation === 'undefined') {
window.showConfirmation = async function(title, message, description = '') {
return new Promise((resolve) => {
const confirmed = confirm(`${title}\n\n${message}\n${description}`);
resolve(confirmed);
});
};
}
+747
View File
@@ -0,0 +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",
'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 = `
<i class="fas ${
newStatus === "active" ? "fa-check-circle" : "fa-times-circle"
}"></i>
${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 = `
<div class="modal-content confirmation-modal">
<div class="modal-header">
<h3>
<i class="fas ${
type === "danger"
? "fa-exclamation-triangle text-danger"
: "fa-question-circle text-warning"
}"></i>
${title}
</h3>
</div>
<div class="modal-body">
<p><strong>${message}</strong></p>
${details ? `<p class="text-muted">${details}</p>` : ""}
</div>
<div class="modal-footer">
<button class="btn btn-secondary cancel-btn">Cancel</button>
<button class="btn btn-${
type === "danger" ? "danger" : "warning"
} confirm-btn">
<i class="fas fa-check"></i> Confirm
</button>
</div>
</div>
`;
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();
});