📝 Commit Details:
This commit is contained in:
584
backend/static/js/admin-dashboard.js
Normal file
584
backend/static/js/admin-dashboard.js
Normal file
@@ -0,0 +1,584 @@
|
||||
/**
|
||||
* MYP Admin Dashboard
|
||||
* Core JavaScript für das Admin-Dashboard
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize navigation
|
||||
initNavigation();
|
||||
|
||||
// Initialize modal events
|
||||
initModals();
|
||||
|
||||
// Load initial data
|
||||
loadDashboardData();
|
||||
});
|
||||
|
||||
/**
|
||||
* Navigation Initialization
|
||||
*/
|
||||
function initNavigation() {
|
||||
// Desktop navigation
|
||||
const desktopNavItems = document.querySelectorAll('.admin-nav-item');
|
||||
desktopNavItems.forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const section = this.getAttribute('data-section');
|
||||
activateSection(section);
|
||||
updateActiveNavItem(this, desktopNavItems);
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile navigation
|
||||
const mobileNavItems = document.querySelectorAll('.mobile-nav-item');
|
||||
mobileNavItems.forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const section = this.getAttribute('data-section');
|
||||
activateSection(section);
|
||||
updateActiveNavItem(this, mobileNavItems);
|
||||
closeMobileNav();
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile menu toggle
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const closeMobileNavButton = document.getElementById('close-mobile-nav');
|
||||
|
||||
if (mobileMenuButton) {
|
||||
mobileMenuButton.addEventListener('click', openMobileNav);
|
||||
}
|
||||
|
||||
if (closeMobileNavButton) {
|
||||
closeMobileNavButton.addEventListener('click', closeMobileNav);
|
||||
}
|
||||
|
||||
// Setup hash navigation
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
if (window.location.hash) {
|
||||
handleHashChange();
|
||||
}
|
||||
}
|
||||
|
||||
function activateSection(section) {
|
||||
// Hide all sections
|
||||
document.querySelectorAll('.admin-section').forEach(el => {
|
||||
el.classList.remove('active');
|
||||
el.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Show selected section
|
||||
const targetSection = document.getElementById(`${section}-section`);
|
||||
if (targetSection) {
|
||||
targetSection.classList.remove('hidden');
|
||||
targetSection.classList.add('active');
|
||||
|
||||
// Load section data if needed
|
||||
switch(section) {
|
||||
case 'dashboard':
|
||||
loadDashboardData();
|
||||
break;
|
||||
case 'users':
|
||||
loadUsers();
|
||||
break;
|
||||
case 'printers':
|
||||
loadPrinters();
|
||||
break;
|
||||
case 'scheduler':
|
||||
loadSchedulerStatus();
|
||||
break;
|
||||
case 'logs':
|
||||
loadLogs();
|
||||
break;
|
||||
}
|
||||
|
||||
// Update URL hash
|
||||
window.location.hash = section;
|
||||
}
|
||||
}
|
||||
|
||||
function updateActiveNavItem(activeItem, allItems) {
|
||||
// Remove active class from all items
|
||||
allItems.forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// Add active class to selected item
|
||||
activeItem.classList.add('active');
|
||||
}
|
||||
|
||||
function handleHashChange() {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash) {
|
||||
const navItem = document.querySelector(`.admin-nav-item[data-section="${hash}"]`);
|
||||
if (navItem) {
|
||||
activateSection(hash);
|
||||
updateActiveNavItem(navItem, document.querySelectorAll('.admin-nav-item'));
|
||||
|
||||
// Also update mobile nav
|
||||
const mobileNavItem = document.querySelector(`.mobile-nav-item[data-section="${hash}"]`);
|
||||
if (mobileNavItem) {
|
||||
updateActiveNavItem(mobileNavItem, document.querySelectorAll('.mobile-nav-item'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openMobileNav() {
|
||||
const mobileNav = document.getElementById('mobile-nav');
|
||||
if (mobileNav) {
|
||||
mobileNav.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeMobileNav() {
|
||||
const mobileNav = document.getElementById('mobile-nav');
|
||||
if (mobileNav) {
|
||||
mobileNav.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal Initialization
|
||||
*/
|
||||
function initModals() {
|
||||
// Delete modal
|
||||
const deleteModal = document.getElementById('delete-modal');
|
||||
const closeDeleteModalBtn = document.getElementById('close-delete-modal');
|
||||
const cancelDeleteBtn = document.getElementById('cancel-delete-btn');
|
||||
|
||||
if (closeDeleteModalBtn) {
|
||||
closeDeleteModalBtn.addEventListener('click', closeDeleteModal);
|
||||
}
|
||||
|
||||
if (cancelDeleteBtn) {
|
||||
cancelDeleteBtn.addEventListener('click', closeDeleteModal);
|
||||
}
|
||||
|
||||
// Toast notification
|
||||
const closeToastBtn = document.getElementById('close-toast');
|
||||
if (closeToastBtn) {
|
||||
closeToastBtn.addEventListener('click', closeToast);
|
||||
}
|
||||
|
||||
// Global refresh button
|
||||
const refreshAllBtn = document.getElementById('refresh-all-btn');
|
||||
if (refreshAllBtn) {
|
||||
refreshAllBtn.addEventListener('click', refreshAllData);
|
||||
}
|
||||
}
|
||||
|
||||
function showDeleteModal(message, onConfirm) {
|
||||
const modal = document.getElementById('delete-modal');
|
||||
const messageEl = document.getElementById('delete-message');
|
||||
const confirmBtn = document.getElementById('confirm-delete-btn');
|
||||
|
||||
if (modal && messageEl && confirmBtn) {
|
||||
messageEl.textContent = message;
|
||||
modal.classList.add('modal-show');
|
||||
|
||||
// Setup confirm button action
|
||||
confirmBtn.onclick = function() {
|
||||
closeDeleteModal();
|
||||
if (typeof onConfirm === 'function') {
|
||||
onConfirm();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
const modal = document.getElementById('delete-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('modal-show');
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.getElementById('toast-notification');
|
||||
const messageEl = document.getElementById('toast-message');
|
||||
const iconEl = document.getElementById('toast-icon');
|
||||
|
||||
if (toast && messageEl && iconEl) {
|
||||
messageEl.textContent = message;
|
||||
|
||||
// Set icon based on type
|
||||
const iconSvg = getToastIcon(type);
|
||||
iconEl.innerHTML = iconSvg;
|
||||
|
||||
// Show toast
|
||||
toast.classList.add('toast-show');
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(closeToast, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function closeToast() {
|
||||
const toast = document.getElementById('toast-notification');
|
||||
if (toast) {
|
||||
toast.classList.remove('toast-show');
|
||||
}
|
||||
}
|
||||
|
||||
function getToastIcon(type) {
|
||||
switch(type) {
|
||||
case 'success':
|
||||
return '<svg class="h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>';
|
||||
case 'error':
|
||||
return '<svg class="h-6 w-6 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>';
|
||||
case 'warning':
|
||||
return '<svg class="h-6 w-6 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>';
|
||||
case 'info':
|
||||
default:
|
||||
return '<svg class="h-6 w-6 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard Data Loading
|
||||
*/
|
||||
function loadDashboardData() {
|
||||
// Load dashboard stats
|
||||
loadStats();
|
||||
|
||||
// Load recent activity
|
||||
loadRecentActivity();
|
||||
|
||||
// Load system status
|
||||
loadSystemStatus();
|
||||
|
||||
// Setup refresh buttons
|
||||
const refreshActivityBtn = document.getElementById('refresh-activity-btn');
|
||||
if (refreshActivityBtn) {
|
||||
refreshActivityBtn.addEventListener('click', loadRecentActivity);
|
||||
}
|
||||
|
||||
const refreshSystemBtn = document.getElementById('refresh-system-btn');
|
||||
if (refreshSystemBtn) {
|
||||
refreshSystemBtn.addEventListener('click', loadSystemStatus);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const response = await fetch('/api/stats');
|
||||
const data = await response.json();
|
||||
|
||||
// Update dashboard counters
|
||||
document.getElementById('total-users-count').textContent = data.total_users || 0;
|
||||
document.getElementById('total-printers-count').textContent = data.total_printers || 0;
|
||||
document.getElementById('active-jobs-count').textContent = data.active_jobs || 0;
|
||||
|
||||
// Update scheduler status
|
||||
updateSchedulerStatusIndicator(data.scheduler_status || false);
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error);
|
||||
showToast('Fehler beim Laden der Statistiken', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function updateSchedulerStatusIndicator(isRunning) {
|
||||
const statusText = document.getElementById('scheduler-status');
|
||||
const indicator = document.getElementById('scheduler-indicator');
|
||||
|
||||
if (statusText && indicator) {
|
||||
if (isRunning) {
|
||||
statusText.textContent = 'Aktiv';
|
||||
statusText.classList.add('text-green-600', 'dark:text-green-400');
|
||||
statusText.classList.remove('text-red-600', 'dark:text-red-400');
|
||||
|
||||
indicator.classList.add('bg-green-500');
|
||||
indicator.classList.remove('bg-red-500', 'bg-gray-300');
|
||||
} else {
|
||||
statusText.textContent = 'Inaktiv';
|
||||
statusText.classList.add('text-red-600', 'dark:text-red-400');
|
||||
statusText.classList.remove('text-green-600', 'dark:text-green-400');
|
||||
|
||||
indicator.classList.add('bg-red-500');
|
||||
indicator.classList.remove('bg-green-500', 'bg-gray-300');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecentActivity() {
|
||||
const container = document.getElementById('recent-activity-container');
|
||||
if (!container) return;
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = `
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<svg class="animate-spin h-8 w-8 text-accent-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/activity/recent');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.activities && data.activities.length > 0) {
|
||||
const activities = data.activities;
|
||||
const html = activities.map(activity => `
|
||||
<div class="p-3 rounded-lg bg-light-surface dark:bg-dark-surface border border-light-border dark:border-dark-border">
|
||||
<div class="flex items-start">
|
||||
<div class="w-2 h-2 rounded-full bg-accent-primary mt-2 mr-3 flex-shrink-0"></div>
|
||||
<div>
|
||||
<p class="text-sm text-light-text dark:text-dark-text">${activity.description}</p>
|
||||
<p class="text-xs text-light-text-muted dark:text-dark-text-muted mt-1">
|
||||
${formatDateTime(activity.timestamp)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = html;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<p class="text-light-text-muted dark:text-dark-text-muted">Keine Aktivitäten gefunden</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading activities:', error);
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<p class="text-red-600 dark:text-red-400">Fehler beim Laden der Aktivitäten</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSystemStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/stats');
|
||||
const data = await response.json();
|
||||
|
||||
// Update system stats
|
||||
document.getElementById('total-print-time').textContent =
|
||||
formatPrintTime(data.total_print_time_hours);
|
||||
document.getElementById('completed-jobs-count').textContent =
|
||||
data.total_jobs_completed || 0;
|
||||
document.getElementById('total-material-used').textContent =
|
||||
formatMaterialUsed(data.total_material_used);
|
||||
document.getElementById('last-updated-time').textContent =
|
||||
data.last_updated ? formatDateTime(data.last_updated) : '-';
|
||||
} catch (error) {
|
||||
console.error('Error loading system status:', error);
|
||||
showToast('Fehler beim Laden des Systemstatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function formatPrintTime(hours) {
|
||||
if (!hours) return '-';
|
||||
return `${hours} Stunden`;
|
||||
}
|
||||
|
||||
function formatMaterialUsed(grams) {
|
||||
if (!grams) return '-';
|
||||
return `${grams} g`;
|
||||
}
|
||||
|
||||
function formatDateTime(dateString) {
|
||||
if (!dateString) return '-';
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Global refresh function
|
||||
*/
|
||||
function refreshAllData() {
|
||||
// Get active section
|
||||
const activeSection = document.querySelector('.admin-section.active');
|
||||
if (activeSection) {
|
||||
const sectionId = activeSection.id;
|
||||
const section = sectionId.replace('-section', '');
|
||||
|
||||
// Reload data based on active section
|
||||
switch(section) {
|
||||
case 'dashboard':
|
||||
loadDashboardData();
|
||||
break;
|
||||
case 'users':
|
||||
loadUsers();
|
||||
break;
|
||||
case 'printers':
|
||||
loadPrinters();
|
||||
break;
|
||||
case 'scheduler':
|
||||
loadSchedulerStatus();
|
||||
break;
|
||||
case 'logs':
|
||||
loadLogs();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
showToast('Daten aktualisiert', 'success');
|
||||
}
|
||||
/**
|
||||
* Benutzer laden und anzeigen
|
||||
*/
|
||||
function loadUsers() {
|
||||
const usersContainer = document.getElementById('users-container');
|
||||
if (!usersContainer) return;
|
||||
|
||||
// Lade-Animation anzeigen
|
||||
usersContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 dark:border-indigo-400"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Benutzer vom Server laden
|
||||
fetch('/api/users')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Fehler beim Laden der Benutzer');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
renderUsers(data.users);
|
||||
updateUserStatistics(data.users);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Benutzer:', error);
|
||||
usersContainer.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-red-600 dark:text-red-400 text-xl mb-2">Fehler beim Laden der Benutzer</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">${error.message}</p>
|
||||
<button onclick="loadUsers()" class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Drucker laden und anzeigen
|
||||
*/
|
||||
function loadPrinters() {
|
||||
const printersContainer = document.getElementById('printers-container');
|
||||
if (!printersContainer) return;
|
||||
|
||||
// Lade-Animation anzeigen
|
||||
printersContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 dark:border-indigo-400"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Drucker vom Server laden
|
||||
fetch('/api/printers')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Fehler beim Laden der Drucker');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
renderPrinters(data.printers);
|
||||
updatePrinterStatistics(data.printers);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Drucker:', error);
|
||||
printersContainer.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-red-600 dark:text-red-400 text-xl mb-2">Fehler beim Laden der Drucker</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">${error.message}</p>
|
||||
<button onclick="loadPrinters()" class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduler-Status laden und anzeigen
|
||||
*/
|
||||
function loadSchedulerStatus() {
|
||||
const schedulerContainer = document.getElementById('scheduler-container');
|
||||
if (!schedulerContainer) return;
|
||||
|
||||
// Lade-Animation anzeigen
|
||||
schedulerContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 dark:border-indigo-400"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Scheduler-Status vom Server laden
|
||||
fetch('/api/scheduler/status')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Fehler beim Laden des Scheduler-Status');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
renderSchedulerStatus(data);
|
||||
updateSchedulerControls(data.active);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden des Scheduler-Status:', error);
|
||||
schedulerContainer.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-red-600 dark:text-red-400 text-xl mb-2">Fehler beim Laden des Scheduler-Status</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">${error.message}</p>
|
||||
<button onclick="loadSchedulerStatus()" class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs laden und anzeigen
|
||||
*/
|
||||
function loadLogs() {
|
||||
const logsContainer = document.getElementById('logs-container');
|
||||
if (!logsContainer) return;
|
||||
|
||||
// Lade-Animation anzeigen
|
||||
logsContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 dark:border-indigo-400"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Logs vom Server laden
|
||||
fetch('/api/logs')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Fehler beim Laden der Logs');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
window.logsData = data.logs;
|
||||
window.filteredLogs = [...data.logs];
|
||||
renderLogs();
|
||||
updateLogStatistics();
|
||||
scrollLogsToBottom();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Logs:', error);
|
||||
logsContainer.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-red-600 dark:text-red-400 text-xl mb-2">Fehler beim Laden der Logs</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">${error.message}</p>
|
||||
<button onclick="loadLogs()" class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user