freealberta/map/app/public/js/admin-core.js
2025-09-11 13:42:07 -06:00

511 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Admin Core Module
* Contains core utilities, navigation, viewport management, and initialization coordination
*/
// Global admin state
let adminAppState = {
currentSection: 'dashboard',
dashboardLoading: false
};
// Utility function to create a local date from YYYY-MM-DD string
// This prevents timezone issues when displaying dates
function createLocalDate(dateString) {
if (!dateString) return null;
const parts = dateString.split('-');
if (parts.length !== 3) return new Date(dateString); // fallback to original behavior
// Create date using local timezone (year, month-1, day)
return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
}
// A function to set viewport dimensions for admin page
function setAdminViewportDimensions() {
const doc = document.documentElement;
// Set height and width
doc.style.setProperty('--app-height', `${window.innerHeight}px`);
doc.style.setProperty('--app-width', `${window.innerWidth}px`);
// Handle safe area insets for devices with notches or home indicators
if (CSS.supports('padding: env(safe-area-inset-top)')) {
doc.style.setProperty('--safe-area-top', 'env(safe-area-inset-top)');
doc.style.setProperty('--safe-area-bottom', 'env(safe-area-inset-bottom)');
doc.style.setProperty('--safe-area-left', 'env(safe-area-inset-left)');
doc.style.setProperty('--safe-area-right', 'env(safe-area-inset-right)');
} else {
doc.style.setProperty('--safe-area-top', '0px');
doc.style.setProperty('--safe-area-bottom', '0px');
doc.style.setProperty('--safe-area-left', '0px');
doc.style.setProperty('--safe-area-right', '0px');
}
}
// Add mobile menu functionality
function setupMobileMenu() {
console.log('🔧 Setting up mobile menu...');
const menuToggle = document.getElementById('mobile-menu-toggle');
const sidebar = document.getElementById('admin-sidebar');
const closeSidebar = document.getElementById('close-sidebar');
const adminNavLinks = document.querySelectorAll('.admin-nav a');
console.log('📱 Mobile menu elements found:', {
menuToggle: !!menuToggle,
sidebar: !!sidebar,
closeSidebar: !!closeSidebar,
adminNavLinks: adminNavLinks.length
});
if (menuToggle && sidebar) {
console.log('✅ Setting up mobile menu event listeners...');
// Remove any existing listeners to prevent duplicates
const newMenuToggle = menuToggle.cloneNode(true);
menuToggle.parentNode.replaceChild(newMenuToggle, menuToggle);
// Toggle menu function
const toggleMobileMenu = (e) => {
console.log('🔄 Mobile menu toggle triggered!', e.type);
e.preventDefault();
e.stopPropagation();
const sidebar = document.getElementById('admin-sidebar');
const menuToggle = document.getElementById('mobile-menu-toggle');
if (!sidebar || !menuToggle) {
console.error('❌ Sidebar or menu toggle not found during toggle');
return;
}
const isActive = sidebar.classList.contains('active');
console.log('📱 Current menu state:', isActive ? 'open' : 'closed');
if (isActive) {
sidebar.classList.remove('active');
menuToggle.classList.remove('active');
document.body.classList.remove('sidebar-open');
console.log('✅ Menu closed');
} else {
sidebar.classList.add('active');
menuToggle.classList.add('active');
document.body.classList.add('sidebar-open');
console.log('✅ Menu opened');
}
};
// Use pointer events for better mobile support
const toggleButton = document.getElementById('mobile-menu-toggle');
// Add click event for all devices
toggleButton.addEventListener('click', toggleMobileMenu, { passive: false });
// Add pointer events for better mobile support
toggleButton.addEventListener('pointerdown', (e) => {
// Visual feedback
e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.2)';
}, { passive: true });
toggleButton.addEventListener('pointerup', (e) => {
// Remove visual feedback
e.currentTarget.style.backgroundColor = '';
}, { passive: true });
// Close sidebar button
if (closeSidebar) {
closeSidebar.addEventListener('click', () => {
const sidebar = document.getElementById('admin-sidebar');
const menuToggle = document.getElementById('mobile-menu-toggle');
if (sidebar && menuToggle) {
sidebar.classList.remove('active');
menuToggle.classList.remove('active');
document.body.classList.remove('sidebar-open');
console.log('✅ Sidebar closed via close button');
}
});
}
// Close sidebar when clicking outside
document.addEventListener('click', (e) => {
const sidebar = document.getElementById('admin-sidebar');
const menuToggle = document.getElementById('mobile-menu-toggle');
if (sidebar && menuToggle && sidebar.classList.contains('active') &&
!sidebar.contains(e.target) &&
!menuToggle.contains(e.target)) {
sidebar.classList.remove('active');
menuToggle.classList.remove('active');
document.body.classList.remove('sidebar-open');
console.log('✅ Sidebar closed by clicking outside');
}
});
// Close sidebar when navigation link is clicked on mobile
const navLinks = document.querySelectorAll('.admin-nav a');
navLinks.forEach(link => {
link.addEventListener('click', () => {
if (window.innerWidth <= 768) {
const sidebar = document.getElementById('admin-sidebar');
const menuToggle = document.getElementById('mobile-menu-toggle');
if (sidebar && menuToggle) {
sidebar.classList.remove('active');
menuToggle.classList.remove('active');
document.body.classList.remove('sidebar-open');
console.log('✅ Sidebar closed after navigation');
}
}
});
});
console.log('✅ Mobile menu setup complete!');
} else {
console.error('❌ Mobile menu elements not found:', {
menuToggle: !!menuToggle,
sidebar: !!sidebar
});
}
}
// Setup navigation between admin sections
function setupNavigation() {
const navLinks = document.querySelectorAll('.admin-nav a');
const sections = document.querySelectorAll('.admin-section');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetId = link.getAttribute('href').substring(1);
// Update active nav
navLinks.forEach(l => l.classList.remove('active'));
link.classList.add('active');
// Show target section
sections.forEach(section => {
section.style.display = section.id === targetId ? 'block' : 'none';
});
// Update URL hash and app state
window.location.hash = targetId;
adminAppState.currentSection = targetId;
// Load section-specific data
loadSectionData(targetId);
// Close mobile menu if open
const sidebar = document.getElementById('admin-sidebar');
if (sidebar && sidebar.classList.contains('open')) {
sidebar.classList.remove('open');
}
});
});
// Set initial active state based on current hash or default
const currentHash = window.location.hash || '#dashboard';
const activeLink = document.querySelector(`.admin-nav a[href="${currentHash}"]`);
if (activeLink) {
activeLink.classList.add('active');
}
// Check for initial hash routing
const hash = window.location.hash;
if (hash) {
const sectionId = hash.substring(1);
adminAppState.currentSection = sectionId;
if (sectionId === 'shifts') {
showSection('shifts');
}
}
}
// Helper function to show a specific section
function showSection(sectionId) {
const sections = document.querySelectorAll('.admin-section');
const navLinks = document.querySelectorAll('.admin-nav a');
// Hide all sections
sections.forEach(section => {
section.style.display = section.id === sectionId ? 'block' : 'none';
});
// Update active nav
navLinks.forEach(link => {
const linkTarget = link.getAttribute('href').substring(1);
link.classList.toggle('active', linkTarget === sectionId);
});
// Update app state
adminAppState.currentSection = sectionId;
// Load section-specific data
loadSectionData(sectionId);
}
// Load section-specific data based on current section
function loadSectionData(sectionId) {
switch(sectionId) {
case 'walk-sheet':
if (typeof checkAndLoadWalkSheetConfig === 'function') {
checkAndLoadWalkSheetConfig();
}
break;
case 'dashboard':
// Always use our guarded dashboard loading function
loadDashboardDataFromDashboardModule();
break;
case 'shifts':
if (typeof loadAdminShifts === 'function') {
console.log('Loading shifts for admin panel...');
loadAdminShifts();
}
break;
case 'users':
// Load users and set up event listeners
if (window.adminUsers && typeof window.adminUsers.loadUsers === 'function') {
if (!window.adminUsers.isInitialized) {
console.log('Loading users for first time...');
window.adminUsers.loadUsers();
} else {
console.log('Users already initialized');
// Ensure event listeners are set up even if already initialized
if (typeof window.adminUsers.setupUserEventListeners === 'function') {
window.adminUsers.setupUserEventListeners();
}
}
}
break;
case 'start-location':
// Initialize or refresh map when start location section becomes visible
setTimeout(() => {
if (window.adminMap && window.adminMap.initializeAdminMap) {
console.log('Ensuring admin map is initialized for start-location section...');
// This will either initialize the map (if not done) or invalidate size (if already initialized)
window.adminMap.initializeAdminMap();
// Also load the current start location data
if (window.adminMap.loadCurrentStartLocation) {
window.adminMap.loadCurrentStartLocation();
}
}
}, 100);
break;
case 'convert-data':
// Initialize data convert event listeners when section is shown
setTimeout(() => {
if (typeof window.setupDataConvertEventListeners === 'function') {
console.log('Setting up data convert event listeners...');
window.setupDataConvertEventListeners();
} else {
console.error('setupDataConvertEventListeners not found');
}
}, 100);
break;
case 'cuts':
// Initialize admin cuts manager when section is shown
setTimeout(() => {
if (typeof window.adminCutsManager === 'object' && window.adminCutsManager.initialize) {
if (!window.adminCutsManager.isInitialized) {
console.log('Initializing admin cuts manager from showSection...');
window.adminCutsManager.initialize().catch(error => {
console.error('Failed to initialize cuts manager:', error);
});
} else {
console.log('Admin cuts manager already initialized, refreshing map...');
// Refresh map size when section becomes visible
if (window.adminCutsManager.refreshMapSize) {
window.adminCutsManager.refreshMapSize();
}
}
} else {
console.error('adminCutsManager not found in showSection');
}
}, 100);
break;
}
}
// Show status message
function showStatus(message, type = 'info') {
let container = document.getElementById('status-container');
// Create container if it doesn't exist
if (!container) {
container = document.createElement('div');
container.id = 'status-container';
container.className = 'status-container';
// Ensure proper z-index even if CSS hasn't loaded
container.style.zIndex = '12300';
document.body.appendChild(container);
}
const messageDiv = document.createElement('div');
messageDiv.className = `status-message ${type}`;
messageDiv.textContent = message;
// Add click to dismiss functionality
messageDiv.addEventListener('click', () => {
messageDiv.remove();
});
// Add a small close button for better UX
const closeBtn = document.createElement('span');
closeBtn.innerHTML = ' ×';
closeBtn.style.float = 'right';
closeBtn.style.fontWeight = 'bold';
closeBtn.style.marginLeft = '10px';
closeBtn.style.cursor = 'pointer';
closeBtn.setAttribute('title', 'Click to dismiss');
messageDiv.appendChild(closeBtn);
container.appendChild(messageDiv);
// Auto-remove after 5 seconds
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.remove();
}
}, 5000);
}
// Escape HTML
function escapeHtml(text) {
if (text === null || text === undefined) {
return '';
}
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
// Debounce function for input events
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Fallback function to load dashboard data directly from dashboard.js module
async function loadDashboardDataFromDashboardModule() {
// Prevent multiple simultaneous loads
if (adminAppState.dashboardLoading) {
console.log('Dashboard already loading, skipping duplicate request');
return;
}
adminAppState.dashboardLoading = true;
console.log('Loading dashboard data from dashboard module');
try {
// Show loading state
const cards = document.querySelectorAll('.card-value');
cards.forEach(card => {
card.textContent = '...';
card.style.opacity = '0.6';
});
const response = await fetch('/api/admin/dashboard/stats');
const result = await response.json();
if (result.success) {
// Update summary cards
const data = result.data;
document.getElementById('total-locations').textContent = data.totalLocations.toLocaleString();
document.getElementById('overall-score').textContent = data.overallScore;
document.getElementById('sign-delivered').textContent = data.signDelivered.toLocaleString();
document.getElementById('total-users').textContent = data.totalUsers.toLocaleString();
// Clear loading state
cards.forEach(card => {
card.style.opacity = '1';
});
// Create charts if functions are available globally
if (typeof createSupportLevelChart === 'function') {
createSupportLevelChart(data.supportLevels);
}
if (typeof createSignSizesChart === 'function') {
createSignSizesChart(data.signSizes);
}
if (typeof createEntriesChart === 'function') {
createEntriesChart(data.dailyEntries);
}
console.log('Dashboard data loaded successfully');
} else {
showStatus('Failed to load dashboard data', 'error');
}
} catch (error) {
console.error('Dashboard loading error:', error);
showStatus('Error loading dashboard', 'error');
// Clear loading state
const cards = document.querySelectorAll('.card-value');
cards.forEach(card => {
card.style.opacity = '1';
card.textContent = 'Error';
});
} finally {
adminAppState.dashboardLoading = false;
}
}
// Initialize the admin core when DOM is loaded
function initializeAdminCore() {
console.log('🚀 Initializing Admin Core...');
// Set initial viewport dimensions and listen for resize events
setAdminViewportDimensions();
window.addEventListener('resize', setAdminViewportDimensions);
window.addEventListener('orientationchange', () => {
setTimeout(setAdminViewportDimensions, 100);
});
// Setup navigation first
setupNavigation();
// Setup mobile menu with a small delay to ensure DOM is ready
setTimeout(() => {
setupMobileMenu();
}, 100);
// Check if URL has a hash to show specific section
const hash = window.location.hash;
if (hash === '#walk-sheet') {
console.log('Direct navigation to walk-sheet section');
showSection('walk-sheet');
} else if (hash) {
const sectionId = hash.substring(1);
showSection(sectionId);
}
console.log('✅ Admin Core initialized');
}
// Make sure we wait for DOM to be fully loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeAdminCore);
} else {
// DOM is already loaded
initializeAdminCore();
}
// Export functions for use by other modules
window.adminCore = {
showSection,
showStatus,
escapeHtml,
debounce,
createLocalDate,
initializeAdminCore,
loadDashboardDataFromDashboardModule,
adminAppState
};