Updated: change menu bar to the left
This commit is contained in:
+847
-667
File diff suppressed because it is too large
Load Diff
+438
-388
@@ -1,68 +1,423 @@
|
|||||||
/**
|
// Enhanced JavaScript for Left Sidebar Navigation
|
||||||
* Main JavaScript functionality for QR Code Management System
|
class QRManager {
|
||||||
* static/js/script.js
|
constructor() {
|
||||||
*/
|
this.initializeApp();
|
||||||
|
}
|
||||||
|
|
||||||
// Global configuration
|
initializeApp() {
|
||||||
const QRManager = {
|
this.initSidebar();
|
||||||
config: {
|
this.initModals();
|
||||||
modalCloseDelay: 300,
|
this.initDropdowns();
|
||||||
searchDelay: 300,
|
this.initActiveNavigation();
|
||||||
animationDuration: 300,
|
this.initFlashMessages();
|
||||||
toastDuration: 5000,
|
}
|
||||||
},
|
|
||||||
|
|
||||||
// Utility functions
|
// Sidebar Management
|
||||||
utils: {
|
initSidebar() {
|
||||||
// Show toast notification
|
const sidebar = document.getElementById('sidebar');
|
||||||
showToast(message, type = "info") {
|
const sidebarToggle = document.getElementById('sidebarToggle');
|
||||||
const toast = document.createElement("div");
|
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
|
||||||
toast.className = `toast toast-${type}`;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility Methods
|
||||||
|
showToast(message, type = 'info', duration = 3000) {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `alert alert-${type}`;
|
||||||
toast.innerHTML = `
|
toast.innerHTML = `
|
||||||
<div class="toast-content">
|
<i class="fas fa-info-circle"></i>
|
||||||
<i class="fas ${this.getToastIcon(type)}"></i>
|
${message}
|
||||||
<span>${message}</span>
|
<button class="alert-close">
|
||||||
<button onclick="this.parentElement.parentElement.remove()" class="toast-close">
|
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(toast);
|
const container = document.querySelector('.flash-messages') || document.body;
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
// Auto remove after delay
|
// Trigger animation
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (toast.parentElement) {
|
toast.classList.add('show');
|
||||||
toast.remove();
|
}, 10);
|
||||||
|
|
||||||
|
// Auto dismiss
|
||||||
|
setTimeout(() => {
|
||||||
|
this.dismissAlert(toast);
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
return toast;
|
||||||
}
|
}
|
||||||
}, QRManager.config.toastDuration);
|
|
||||||
},
|
|
||||||
|
|
||||||
getToastIcon(type) {
|
// Form Validation Helper
|
||||||
const icons = {
|
validateForm(formElement) {
|
||||||
success: "fa-check-circle",
|
const requiredFields = formElement.querySelectorAll('[required]');
|
||||||
error: "fa-exclamation-circle",
|
let isValid = true;
|
||||||
warning: "fa-exclamation-triangle",
|
|
||||||
info: "fa-info-circle",
|
|
||||||
};
|
|
||||||
return icons[type] || icons.info;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Debounce function
|
requiredFields.forEach(field => {
|
||||||
debounce(func, wait) {
|
if (!field.value.trim()) {
|
||||||
let timeout;
|
this.showFieldError(field, 'This field is required');
|
||||||
return function executedFunction(...args) {
|
isValid = false;
|
||||||
const later = () => {
|
} else {
|
||||||
clearTimeout(timeout);
|
this.clearFieldError(field);
|
||||||
func(...args);
|
}
|
||||||
};
|
});
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(later, wait);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Format date
|
return isValid;
|
||||||
formatDate(dateString) {
|
}
|
||||||
|
|
||||||
|
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 = {}) {
|
||||||
|
const defaultOptions = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
...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);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString("en-US", {
|
return date.toLocaleDateString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
@@ -71,18 +426,27 @@ const QRManager = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Format time
|
formatDateTime: (dateString) => {
|
||||||
formatTime(dateString) {
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime: (dateString) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleTimeString("en-US", {
|
return date.toLocaleTimeString("en-US", {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigation functionality
|
// Legacy Navigation Manager for backward compatibility
|
||||||
class NavigationManager {
|
class NavigationManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.initMobileMenu();
|
this.initMobileMenu();
|
||||||
@@ -99,7 +463,6 @@ class NavigationManager {
|
|||||||
navMenu.classList.toggle("active");
|
navMenu.classList.toggle("active");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close menu when clicking nav links
|
|
||||||
const navLinks = navMenu.querySelectorAll(".nav-link");
|
const navLinks = navMenu.querySelectorAll(".nav-link");
|
||||||
navLinks.forEach((link) => {
|
navLinks.forEach((link) => {
|
||||||
link.addEventListener("click", () => {
|
link.addEventListener("click", () => {
|
||||||
@@ -125,7 +488,6 @@ class NavigationManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close dropdowns when clicking outside
|
|
||||||
document.addEventListener("click", () => {
|
document.addEventListener("click", () => {
|
||||||
this.closeAllDropdowns();
|
this.closeAllDropdowns();
|
||||||
});
|
});
|
||||||
@@ -150,338 +512,26 @@ class NavigationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal management
|
// Initialize the application
|
||||||
class ModalManager {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
constructor() {
|
// Initialize main app if sidebar exists (authenticated users)
|
||||||
this.initModals();
|
if (document.getElementById('sidebar')) {
|
||||||
}
|
window.qrManager = new QRManager();
|
||||||
|
|
||||||
initModals() {
|
|
||||||
const modals = document.querySelectorAll(".modal");
|
|
||||||
|
|
||||||
modals.forEach((modal) => {
|
|
||||||
// Close button functionality
|
|
||||||
const closeBtn = modal.querySelector(".modal-close");
|
|
||||||
if (closeBtn) {
|
|
||||||
closeBtn.addEventListener("click", () => this.closeModal(modal));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click outside to close
|
|
||||||
modal.addEventListener("click", (e) => {
|
|
||||||
if (e.target === modal) {
|
|
||||||
this.closeModal(modal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Escape key to close all modals
|
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
this.closeAllModals();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
closeModal(modal) {
|
|
||||||
modal.classList.remove("show");
|
|
||||||
setTimeout(() => {
|
|
||||||
modal.style.display = "none";
|
|
||||||
}, QRManager.config.modalCloseDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAllModals() {
|
|
||||||
const openModals = document.querySelectorAll('.modal[style*="flex"]');
|
|
||||||
openModals.forEach((modal) => this.closeModal(modal));
|
|
||||||
}
|
|
||||||
|
|
||||||
openModal(modalId) {
|
|
||||||
const modal = document.getElementById(modalId);
|
|
||||||
if (modal) {
|
|
||||||
modal.style.display = "flex";
|
|
||||||
setTimeout(() => {
|
|
||||||
modal.classList.add("show");
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search functionality
|
|
||||||
class SearchManager {
|
|
||||||
constructor(searchInputId, resultsContainerId) {
|
|
||||||
this.searchInput = document.getElementById(searchInputId);
|
|
||||||
this.resultsContainer = document.getElementById(resultsContainerId);
|
|
||||||
this.originalItems = [];
|
|
||||||
|
|
||||||
if (this.searchInput && this.resultsContainer) {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// Store original items
|
|
||||||
this.originalItems = Array.from(this.resultsContainer.children);
|
|
||||||
|
|
||||||
// Add search event listener with debouncing
|
|
||||||
this.searchInput.addEventListener(
|
|
||||||
"input",
|
|
||||||
QRManager.utils.debounce(
|
|
||||||
() => this.performSearch(),
|
|
||||||
QRManager.config.searchDelay
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
performSearch() {
|
|
||||||
const searchTerm = this.searchInput.value.toLowerCase().trim();
|
|
||||||
|
|
||||||
this.originalItems.forEach((item) => {
|
|
||||||
const searchableText = this.getSearchableText(item);
|
|
||||||
const matches = searchableText.includes(searchTerm);
|
|
||||||
|
|
||||||
if (matches || searchTerm === "") {
|
|
||||||
this.showItem(item);
|
|
||||||
} else {
|
} else {
|
||||||
this.hideItem(item);
|
// Initialize legacy navigation for non-authenticated pages
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateResultsCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
getSearchableText(item) {
|
|
||||||
// Get text content from data attributes or text content
|
|
||||||
const name = item.dataset.name || "";
|
|
||||||
const location = item.dataset.location || "";
|
|
||||||
const textContent = item.textContent || "";
|
|
||||||
|
|
||||||
return (name + " " + location + " " + textContent).toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
showItem(item) {
|
|
||||||
item.style.display = "block";
|
|
||||||
item.classList.remove("fade-out");
|
|
||||||
item.classList.add("fade-in");
|
|
||||||
}
|
|
||||||
|
|
||||||
hideItem(item) {
|
|
||||||
item.classList.remove("fade-in");
|
|
||||||
item.classList.add("fade-out");
|
|
||||||
setTimeout(() => {
|
|
||||||
if (item.classList.contains("fade-out")) {
|
|
||||||
item.style.display = "none";
|
|
||||||
}
|
|
||||||
}, QRManager.config.animationDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateResultsCount() {
|
|
||||||
const visibleItems = this.originalItems.filter(
|
|
||||||
(item) => item.style.display !== "none"
|
|
||||||
);
|
|
||||||
|
|
||||||
const counter = document.querySelector(".results-counter");
|
|
||||||
if (counter) {
|
|
||||||
counter.textContent = `${visibleItems.length} results`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download functionality
|
|
||||||
class DownloadManager {
|
|
||||||
static 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);
|
|
||||||
|
|
||||||
QRManager.utils.showToast("QR code downloaded successfully!", "success");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Download error:", error);
|
|
||||||
QRManager.utils.showToast("Failed to download QR code", "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static downloadModalQR() {
|
|
||||||
if (window.currentModalQR) {
|
|
||||||
const base64Data = window.currentModalQR.image.split("base64,")[1];
|
|
||||||
this.downloadQR(base64Data, window.currentModalQR.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theme management
|
|
||||||
class ThemeManager {
|
|
||||||
constructor() {
|
|
||||||
this.initThemeToggle();
|
|
||||||
this.loadSavedTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
initThemeToggle() {
|
|
||||||
const themeToggle = document.getElementById("themeToggle");
|
|
||||||
if (themeToggle) {
|
|
||||||
themeToggle.addEventListener("click", () => {
|
|
||||||
this.toggleTheme();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTheme() {
|
|
||||||
const currentTheme = document.documentElement.getAttribute("data-theme");
|
|
||||||
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
|
||||||
|
|
||||||
document.documentElement.setAttribute("data-theme", newTheme);
|
|
||||||
localStorage.setItem("theme", newTheme);
|
|
||||||
|
|
||||||
this.updateThemeIcon(newTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSavedTheme() {
|
|
||||||
const savedTheme = localStorage.getItem("theme") || "light";
|
|
||||||
document.documentElement.setAttribute("data-theme", savedTheme);
|
|
||||||
this.updateThemeIcon(savedTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateThemeIcon(theme) {
|
|
||||||
const themeIcon = document.querySelector("#themeToggle i");
|
|
||||||
if (themeIcon) {
|
|
||||||
themeIcon.className = theme === "dark" ? "fas fa-sun" : "fas fa-moon";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form validation
|
|
||||||
class FormValidator {
|
|
||||||
constructor(formId) {
|
|
||||||
this.form = document.getElementById(formId);
|
|
||||||
if (this.form) {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.form.addEventListener("submit", (e) => {
|
|
||||||
if (!this.validateForm()) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Real-time validation
|
|
||||||
const inputs = this.form.querySelectorAll("input, select, textarea");
|
|
||||||
inputs.forEach((input) => {
|
|
||||||
input.addEventListener("blur", () => this.validateField(input));
|
|
||||||
input.addEventListener("input", () => this.clearFieldError(input));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
validateForm() {
|
|
||||||
const inputs = this.form.querySelectorAll(
|
|
||||||
"input[required], select[required], textarea[required]"
|
|
||||||
);
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
inputs.forEach((input) => {
|
|
||||||
if (!this.validateField(input)) {
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateField(field) {
|
|
||||||
const value = field.value.trim();
|
|
||||||
const isRequired = field.hasAttribute("required");
|
|
||||||
const fieldType = field.type;
|
|
||||||
|
|
||||||
// Clear previous errors
|
|
||||||
this.clearFieldError(field);
|
|
||||||
|
|
||||||
// Required field validation
|
|
||||||
if (isRequired && !value) {
|
|
||||||
this.showFieldError(field, "This field is required");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Email validation
|
|
||||||
if (fieldType === "email" && value) {
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
if (!emailRegex.test(value)) {
|
|
||||||
this.showFieldError(field, "Please enter a valid email address");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Password validation
|
|
||||||
if (fieldType === "password" && value) {
|
|
||||||
if (value.length < 6) {
|
|
||||||
this.showFieldError(
|
|
||||||
field,
|
|
||||||
"Password must be at least 6 characters long"
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
showFieldError(field, message) {
|
|
||||||
field.classList.add("error");
|
|
||||||
|
|
||||||
// Remove existing error message
|
|
||||||
const existingError = field.parentNode.querySelector(".field-error");
|
|
||||||
if (existingError) {
|
|
||||||
existingError.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new error message
|
|
||||||
const errorElement = document.createElement("div");
|
|
||||||
errorElement.className = "field-error";
|
|
||||||
errorElement.textContent = message;
|
|
||||||
field.parentNode.appendChild(errorElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearFieldError(field) {
|
|
||||||
field.classList.remove("error");
|
|
||||||
const errorElement = field.parentNode.querySelector(".field-error");
|
|
||||||
if (errorElement) {
|
|
||||||
errorElement.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize when DOM is loaded
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
// Initialize managers
|
|
||||||
window.navigationManager = new NavigationManager();
|
window.navigationManager = new NavigationManager();
|
||||||
window.modalManager = new ModalManager();
|
|
||||||
window.themeManager = new ThemeManager();
|
|
||||||
|
|
||||||
// Initialize search if search input exists
|
|
||||||
const searchInput =
|
|
||||||
document.getElementById("searchInput") ||
|
|
||||||
document.getElementById("qrSearch") ||
|
|
||||||
document.getElementById("searchUsers");
|
|
||||||
|
|
||||||
if (searchInput) {
|
|
||||||
const containerId = searchInput.dataset.container || "searchResults";
|
|
||||||
window.searchManager = new SearchManager(searchInput.id, containerId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize form validation for forms with validation class
|
|
||||||
const forms = document.querySelectorAll(".validate-form");
|
|
||||||
forms.forEach((form) => {
|
|
||||||
new FormValidator(form.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Global function assignments for inline event handlers
|
|
||||||
window.downloadQR = DownloadManager.downloadQR;
|
|
||||||
window.downloadModalQR = DownloadManager.downloadModalQR;
|
|
||||||
window.showToast = QRManager.utils.showToast;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Global utility functions
|
// Export for use in other scripts
|
||||||
window.QRManager = QRManager;
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base_authenticated.html" %}
|
||||||
|
|
||||||
{% block title %}Attendance Report - QR Code Management{% endblock %}
|
{% block title %}Attendance Report - QR Code Management{% endblock %}
|
||||||
|
|
||||||
|
|||||||
+5
-58
@@ -27,63 +27,8 @@
|
|||||||
href="{{ url_for('static', filename='favicon.ico') }}"
|
href="{{ url_for('static', filename='favicon.ico') }}"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="login-layout">
|
||||||
<!-- Navigation Bar -->
|
<!-- Non-authenticated Layout (Login, Register, etc.) -->
|
||||||
{% if session.user_id %}
|
|
||||||
<nav class="navbar">
|
|
||||||
<div class="nav-container">
|
|
||||||
<div class="nav-brand">
|
|
||||||
<i class="fas fa-qrcode"></i>
|
|
||||||
<span>QR Manager</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-menu" id="navMenu">
|
|
||||||
<a href="{{ url_for('dashboard') }}" class="nav-link">
|
|
||||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% if session.role == 'admin' %}
|
|
||||||
<a href="{{ url_for('users') }}" class="nav-link">
|
|
||||||
<i class="fas fa-users"></i> Users
|
|
||||||
</a>
|
|
||||||
<a href="{{ url_for('attendance_report') }}" class="nav-link">
|
|
||||||
<i class="fas fa-chart-line"></i> Reports
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<a href="{{ url_for('create_qr_code') }}" class="nav-link">
|
|
||||||
<i class="fas fa-plus"></i> Create QR
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{{ url_for('profile') }}" class="nav-link">
|
|
||||||
<i class="fas fa-user"></i> Profile
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Theme Toggle -->
|
|
||||||
<button
|
|
||||||
id="themeToggle"
|
|
||||||
class="nav-link"
|
|
||||||
style="background: none; border: none"
|
|
||||||
>
|
|
||||||
<i class="fas fa-moon"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="{{ url_for('logout') }}" class="nav-link logout">
|
|
||||||
<i class="fas fa-sign-out-alt"></i> Logout
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mobile Menu Button -->
|
|
||||||
<div class="nav-toggle" id="mobile-menu">
|
|
||||||
<span class="bar"></span>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<span class="bar"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<!-- Flash Messages -->
|
<!-- Flash Messages -->
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %} {% if
|
{% with messages = get_flashed_messages(with_categories=true) %} {% if
|
||||||
@@ -107,7 +52,9 @@
|
|||||||
{% endif %} {% endwith %}
|
{% endif %} {% endwith %}
|
||||||
|
|
||||||
<!-- Page Content -->
|
<!-- Page Content -->
|
||||||
<div class="container">{% block content %}{% endblock %}</div>
|
<div class="container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{% block title %}QR Code Management System{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- Main CSS -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('static', filename='css/style.css') }}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Page-specific CSS -->
|
||||||
|
{% block extra_head %}{% endblock %}
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/x-icon"
|
||||||
|
href="{{ url_for('static', filename='favicon.ico') }}"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body class="has-sidebar">
|
||||||
|
<!-- Authenticated Layout with Sidebar -->
|
||||||
|
<div class="app-layout">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="sidebar" id="sidebar">
|
||||||
|
<!-- Brand Section -->
|
||||||
|
<div class="sidebar-brand">
|
||||||
|
<div class="brand-content">
|
||||||
|
<i class="fas fa-qrcode"></i>
|
||||||
|
<span class="brand-text">QR Manager</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation Menu -->
|
||||||
|
<div class="sidebar-menu">
|
||||||
|
<div class="menu-section">
|
||||||
|
<div class="menu-items">
|
||||||
|
<a href="{{ url_for('dashboard') }}" class="menu-item">
|
||||||
|
<i class="fas fa-tachometer-alt"></i>
|
||||||
|
<span class="menu-text">Dashboard</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ url_for('create_qr_code') }}" class="menu-item">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
<span class="menu-text">Create QR</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% if session.role == 'admin' %}
|
||||||
|
<a href="{{ url_for('users') }}" class="menu-item">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
<span class="menu-text">Users</span>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('attendance_report') }}" class="menu-item">
|
||||||
|
<i class="fas fa-chart-line"></i>
|
||||||
|
<span class="menu-text">Reports</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="{{ url_for('profile') }}" class="menu-item">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
<span class="menu-text">Profile</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Section -->
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="menu-items">
|
||||||
|
<a href="{{ url_for('logout') }}" class="menu-item logout">
|
||||||
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
|
<span class="menu-text">Logout</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar Toggle Button -->
|
||||||
|
<button class="sidebar-toggle" id="sidebarToggle">
|
||||||
|
<i class="fas fa-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Mobile Overlay -->
|
||||||
|
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||||
|
|
||||||
|
<!-- Main Content Area -->
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<!-- Top Header Bar -->
|
||||||
|
<header class="top-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<button class="mobile-menu-btn" id="mobileMenuBtn">
|
||||||
|
<span class="hamburger-line"></span>
|
||||||
|
<span class="hamburger-line"></span>
|
||||||
|
<span class="hamburger-line"></span>
|
||||||
|
</button>
|
||||||
|
<h1 class="page-title">{% block page_title %}Dashboard{% endblock %}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="user-info">
|
||||||
|
<span class="user-name">{{ session.full_name or 'User' }}</span>
|
||||||
|
<span class="user-role">{{ session.role|title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- Flash Messages -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %} {% if
|
||||||
|
messages %}
|
||||||
|
<div class="flash-messages">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">
|
||||||
|
<i
|
||||||
|
class="fas {% if category == 'success' %}fa-check-circle{% elif category == 'error' %}fa-exclamation-circle{% elif category == 'info' %}fa-info-circle{% else %}fa-exclamation-triangle{% endif %}"
|
||||||
|
></i>
|
||||||
|
{{ message }}
|
||||||
|
<button
|
||||||
|
class="alert-close"
|
||||||
|
onclick="this.parentElement.style.display='none'"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %} {% endwith %}
|
||||||
|
|
||||||
|
<!-- Page Content -->
|
||||||
|
<div class="container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-content">
|
||||||
|
<p>© 2025 QR Code Management System. All rights reserved.</p>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="#" class="footer-link">Privacy Policy</a>
|
||||||
|
<a href="#" class="footer-link">Terms of Service</a>
|
||||||
|
<a href="#" class="footer-link">Support</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main JavaScript -->
|
||||||
|
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- Page-specific JavaScript -->
|
||||||
|
{% block extra_scripts %}{% endblock %}
|
||||||
|
|
||||||
|
<!-- Global JavaScript Variables -->
|
||||||
|
<script>
|
||||||
|
// Pass Flask session data to JavaScript
|
||||||
|
window.qrConfig = {
|
||||||
|
currentUserId: {{ session.user_id|default('null') }},
|
||||||
|
currentUserRole: '{{ session.role|default('') }}',
|
||||||
|
currentUserName: '{{ session.full_name|default('') }}',
|
||||||
|
csrfToken: '{{ csrf_token() if csrf_token else '' }}'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends "base.html" %} {% block title %}Dashboard - QR Code Management{%
|
{% extends "base_authenticated.html" %} {% block title %}Dashboard - QR Code Management{%
|
||||||
endblock %} {% block extra_head %}
|
endblock %} {% block extra_head %}
|
||||||
<!-- Dashboard-specific CSS -->
|
<!-- Dashboard-specific CSS -->
|
||||||
<link
|
<link
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends "base.html" %} {% block title %}My Profile - QR Code Management{%
|
{% extends "base_authenticated.html" %} {% block title %}My Profile - QR Code Management{%
|
||||||
endblock %} {% block content %}
|
endblock %} {% block content %}
|
||||||
<div class="profile-page">
|
<div class="profile-page">
|
||||||
<div class="profile-header">
|
<div class="profile-header">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends "base.html" %} {% block title %}User Management - QR Code
|
{% extends "base_authenticated.html" %} {% block title %}User Management - QR Code
|
||||||
Management{% endblock %} {% block content %}
|
Management{% endblock %} {% block content %}
|
||||||
<div class="users-page">
|
<div class="users-page">
|
||||||
<div class="users-header">
|
<div class="users-header">
|
||||||
|
|||||||
Reference in New Issue
Block a user