Sep 11 - Reupload the code
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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");
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user