410 lines
15 KiB
JavaScript
410 lines
15 KiB
JavaScript
/**
|
||
* 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
|
||
};
|