freealberta/map/app/public/js/admin-core.js

410 lines
15 KiB
JavaScript
Raw 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() {
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');
if (menuToggle && sidebar) {
// Toggle menu
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active');
menuToggle.classList.toggle('active');
document.body.classList.toggle('sidebar-open');
});
// Close sidebar button
if (closeSidebar) {
closeSidebar.addEventListener('click', () => {
sidebar.classList.remove('active');
menuToggle.classList.remove('active');
document.body.classList.remove('sidebar-open');
});
}
// Close sidebar when clicking outside
document.addEventListener('click', (e) => {
if (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');
}
});
// Close sidebar when navigation link is clicked on mobile
adminNavLinks.forEach(link => {
link.addEventListener('click', () => {
if (window.innerWidth <= 768) {
sidebar.classList.remove('active');
menuToggle.classList.remove('active');
document.body.classList.remove('sidebar-open');
}
});
});
}
}
// 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':
if (typeof loadUsers === 'function') {
loadUsers();
}
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() {
// Set initial viewport dimensions and listen for resize events
setAdminViewportDimensions();
window.addEventListener('resize', setAdminViewportDimensions);
window.addEventListener('orientationchange', () => {
// Add a small delay for orientation change to complete
setTimeout(setAdminViewportDimensions, 100);
});
setupNavigation();
setupMobileMenu();
// Check if URL has a hash to show specific section
const hash = window.location.hash;
if (hash === '#walk-sheet') {
showSection('walk-sheet');
} else if (hash === '#convert-data') {
showSection('convert-data');
} else if (hash === '#cuts') {
showSection('cuts');
} else {
// Default to dashboard
showSection('dashboard');
}
}
// Export functions for use by other modules
window.adminCore = {
showSection,
showStatus,
escapeHtml,
debounce,
createLocalDate,
initializeAdminCore,
loadDashboardDataFromDashboardModule,
adminAppState
};