/** * MSPE Admin Panel JavaScript */ // Lock admin UI until auth verification completes (except login/reset pages) document.documentElement.classList.add('auth-pending'); const authStyle = document.createElement('style'); authStyle.textContent = ` html.auth-pending body { visibility: hidden; } html.auth-pending::after { content: ''; position: fixed; inset: 0; z-index: 99999; background: #0f172a; display: flex; align-items: center; justify-content: center; } html.auth-pending::before { content: ''; position: fixed; top: 50%; left: 50%; z-index: 100000; width: 36px; height: 36px; margin: -18px 0 0 -18px; border: 3px solid rgba(59,130,246,0.2); border-top-color: #3b82f6; border-radius: 50%; animation: authSpin .7s linear infinite; } @keyframes authSpin { to { transform: rotate(360deg) } } `; document.head.appendChild(authStyle); document.addEventListener('DOMContentLoaded', function() { if (isAuthExemptPage()) { unlockAdminUi(); return; } // Initialize components initSidebar(); initDropdowns(); initLogout(); checkAuth(); }); /** * Sidebar Toggle */ function initSidebar() { const sidebar = document.getElementById('sidebar'); const mobileToggle = document.getElementById('mobile-toggle'); const sidebarToggle = document.getElementById('sidebar-toggle'); if (mobileToggle) { mobileToggle.addEventListener('click', function() { sidebar.classList.toggle('active'); }); } if (sidebarToggle) { sidebarToggle.addEventListener('click', function() { sidebar.classList.toggle('active'); }); } // Close sidebar when clicking outside on mobile document.addEventListener('click', function(e) { if (window.innerWidth <= 1024) { if (!sidebar.contains(e.target) && !mobileToggle?.contains(e.target)) { sidebar.classList.remove('active'); } } }); } /** * Dropdowns */ function initDropdowns() { const dropdowns = document.querySelectorAll('.user-dropdown'); dropdowns.forEach(dropdown => { const toggle = dropdown.querySelector('.dropdown-toggle'); if (toggle) { toggle.addEventListener('click', function(e) { e.stopPropagation(); dropdown.classList.toggle('active'); }); } }); // Close dropdowns when clicking outside document.addEventListener('click', function() { dropdowns.forEach(dropdown => { dropdown.classList.remove('active'); }); }); } /** * Logout Handler */ function initLogout() { const logoutBtn = document.getElementById('logout-btn'); if (logoutBtn) { logoutBtn.addEventListener('click', async function(e) { e.preventDefault(); try { await fetch('/api/auth.php', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'logout' }) }); } catch (error) { console.log('Logout request failed, proceeding with local logout'); } // Redirect to login window.location.href = 'index.html'; }); } } /** * Check Authentication */ function checkAuth() { fetch('/api/auth.php', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'verify' }) }) .then(r => r.json()) .then(data => { if (data.success) { applyUserToHeader(data.user || {}); unlockAdminUi(); startSessionWatchdog(); } else { window.location.href = 'index.html'; } }) .catch(() => { window.location.href = 'index.html'; }); } function isAuthExemptPage() { return window.location.pathname.includes('index.html') || window.location.pathname.endsWith('/admin/') || window.location.pathname.includes('reset-password.html'); } function unlockAdminUi() { document.documentElement.classList.remove('auth-pending'); } /** * Periodic Session Re-Verification (every 5 minutes) * Catches expired / revoked sessions proactively. */ let _sessionInterval = null; function startSessionWatchdog() { if (_sessionInterval) return; _sessionInterval = setInterval(() => { fetch('/api/auth.php', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'verify' }) }) .then(r => r.json()) .then(data => { if (!data.success) { clearInterval(_sessionInterval); showNotification('Session expired — redirecting to login.', 'warning'); setTimeout(() => { window.location.href = 'index.html'; }, 2000); } }) .catch(() => { /* network hiccup, retry next cycle */ }); }, 5 * 60 * 1000); } function applyUserToHeader(user) { const nameSpans = document.querySelectorAll('.user-name'); nameSpans.forEach(el => { el.textContent = user.username || 'Admin'; }); } /** * API Helper Functions */ const API = { baseUrl: '/api', async request(endpoint, options = {}) { const defaultOptions = { credentials: 'include', headers: { 'Content-Type': 'application/json' } }; const response = await fetch(`${this.baseUrl}/${endpoint}`, { ...defaultOptions, ...options, headers: { ...defaultOptions.headers, ...options.headers } }); if (response.status === 401) { window.location.href = 'index.html'; return; } if (response.status === 403) { return response.json(); } return response.json(); }, async get(endpoint) { return this.request(endpoint); }, async post(endpoint, data) { return this.request(endpoint, { method: 'POST', body: JSON.stringify(data) }); }, async put(endpoint, data) { return this.request(endpoint, { method: 'PUT', body: JSON.stringify(data) }); }, async delete(endpoint) { return this.request(endpoint, { method: 'DELETE' }); }, async upload(endpoint, formData) { const response = await fetch(`${this.baseUrl}/${endpoint}`, { method: 'POST', credentials: 'include', body: formData }); if (response.status === 401) { window.location.href = 'index.html'; return; } return response.json(); } }; /** * Notification Helper */ function showNotification(message, type = 'success') { const iconMap = { success: 'check-circle', error: 'exclamation-circle', info: 'info-circle', warning: 'exclamation-triangle' }; const icon = iconMap[type] || 'info-circle'; const notification = document.createElement('div'); notification.className = `notification ${type}`; const iconEl = document.createElement('i'); iconEl.className = `fas fa-${icon}`; const spanEl = document.createElement('span'); spanEl.textContent = message; notification.appendChild(iconEl); notification.appendChild(spanEl); // Add styles if not present if (!document.querySelector('.notification-styles')) { const styles = document.createElement('style'); styles.className = 'notification-styles'; styles.textContent = ` .notification { position: fixed; top: 20px; right: 20px; padding: 1rem 1.5rem; border-radius: 10px; display: flex; align-items: center; gap: 0.75rem; font-weight: 500; z-index: 9999; animation: slideIn 0.3s ease; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); } .notification.success { background: #10b981; color: white; } .notification.error { background: #ef4444; color: white; } .notification.info { background: #3b82f6; color: white; } .notification.warning { background: #f59e0b; color: white; } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } `; document.head.appendChild(styles); } document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideIn 0.3s ease reverse'; setTimeout(() => notification.remove(), 300); }, 3000); } /** * Format Date Helper */ function formatDate(dateString) { const options = { year: 'numeric', month: 'short', day: 'numeric' }; return new Date(dateString).toLocaleDateString('en-US', options); } /** * Confirm Dialog Helper */ function confirmDialog(message) { return new Promise((resolve) => { const modal = document.createElement('div'); modal.className = 'modal active'; modal.innerHTML = ` `; modal.querySelector('.confirm-msg').textContent = message; document.body.appendChild(modal); modal.querySelector('#confirm-ok').addEventListener('click', () => { modal.remove(); resolve(true); }); modal.querySelector('#confirm-cancel').addEventListener('click', () => { modal.remove(); resolve(false); }); modal.querySelector('.modal-close').addEventListener('click', () => { modal.remove(); resolve(false); }); modal.querySelector('.modal-overlay').addEventListener('click', () => { modal.remove(); resolve(false); }); }); } /** * Debounce Helper */ function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Export for use in other scripts window.MSPE = { API, showNotification, formatDate, confirmDialog, debounce };