Files
MSPE/admin/js/admin.js
T

441 lines
12 KiB
JavaScript
Executable File

/**
* 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 = `
<div class="modal-overlay"></div>
<div class="modal-content">
<div class="modal-header">
<h3>Confirm</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<p class="confirm-msg"></p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="confirm-cancel">Cancel</button>
<button class="btn btn-danger" id="confirm-ok">Confirm</button>
</div>
</div>
`;
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
};