📝 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>
|
||||
`;
|
||||
});
|
||||
}
|
875
backend/static/js/admin-guest-requests.js
Normal file
875
backend/static/js/admin-guest-requests.js
Normal file
@ -0,0 +1,875 @@
|
||||
/**
|
||||
* Mercedes-Benz MYP Admin Guest Requests Management
|
||||
* Moderne Verwaltung von Gastaufträgen mit Live-Updates
|
||||
*/
|
||||
|
||||
// Globale Variablen
|
||||
let currentRequests = [];
|
||||
let filteredRequests = [];
|
||||
let currentPage = 0;
|
||||
let totalPages = 0;
|
||||
let totalRequests = 0;
|
||||
let refreshInterval = null;
|
||||
let csrfToken = '';
|
||||
|
||||
// API Base URL Detection - Korrigierte Version für CSP-Kompatibilität
|
||||
function detectApiBaseUrl() {
|
||||
// Für lokale Entwicklung und CSP-Kompatibilität immer relative URLs verwenden
|
||||
// Das verhindert CSP-Probleme mit connect-src
|
||||
return ''; // Leerer String für relative URLs
|
||||
}
|
||||
|
||||
const API_BASE_URL = detectApiBaseUrl();
|
||||
|
||||
// Initialisierung beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// CSRF Token abrufen
|
||||
csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
||||
|
||||
// Event Listeners initialisieren
|
||||
initEventListeners();
|
||||
|
||||
// Daten initial laden
|
||||
loadGuestRequests();
|
||||
|
||||
// Auto-Refresh starten
|
||||
startAutoRefresh();
|
||||
|
||||
console.log('🎯 Admin Guest Requests Management geladen');
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Listeners initialisieren
|
||||
*/
|
||||
function initEventListeners() {
|
||||
// Search Input
|
||||
const searchInput = document.getElementById('search-requests');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', debounce(handleSearch, 300));
|
||||
}
|
||||
|
||||
// Status Filter
|
||||
const statusFilter = document.getElementById('status-filter');
|
||||
if (statusFilter) {
|
||||
statusFilter.addEventListener('change', handleFilterChange);
|
||||
}
|
||||
|
||||
// Sort Order
|
||||
const sortOrder = document.getElementById('sort-order');
|
||||
if (sortOrder) {
|
||||
sortOrder.addEventListener('change', handleSortChange);
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
loadGuestRequests();
|
||||
showNotification('🔄 Gastaufträge aktualisiert', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
const exportBtn = document.getElementById('export-btn');
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', handleExport);
|
||||
}
|
||||
|
||||
const bulkActionsBtn = document.getElementById('bulk-actions-btn');
|
||||
if (bulkActionsBtn) {
|
||||
bulkActionsBtn.addEventListener('click', showBulkActionsModal);
|
||||
}
|
||||
|
||||
// Select All Checkbox
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', handleSelectAll);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gastaufträge von der API laden
|
||||
*/
|
||||
async function loadGuestRequests() {
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/admin/guest-requests`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
currentRequests = data.requests || [];
|
||||
totalRequests = data.total || 0;
|
||||
|
||||
// Statistiken aktualisieren
|
||||
updateStats(data.stats || {});
|
||||
|
||||
// Tabelle aktualisieren
|
||||
applyFiltersAndSort();
|
||||
|
||||
console.log(`✅ ${currentRequests.length} Gastaufträge geladen`);
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Laden der Gastaufträge');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Gastaufträge:', error);
|
||||
showNotification('❌ Fehler beim Laden der Gastaufträge: ' + error.message, 'error');
|
||||
showEmptyState();
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiken aktualisieren
|
||||
*/
|
||||
function updateStats(stats) {
|
||||
const elements = {
|
||||
'pending-count': stats.pending || 0,
|
||||
'approved-count': stats.approved || 0,
|
||||
'rejected-count': stats.rejected || 0,
|
||||
'total-count': stats.total || 0
|
||||
};
|
||||
|
||||
Object.entries(elements).forEach(([id, value]) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
animateCounter(element, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Counter mit Animation
|
||||
*/
|
||||
function animateCounter(element, targetValue) {
|
||||
const currentValue = parseInt(element.textContent) || 0;
|
||||
const difference = targetValue - currentValue;
|
||||
const steps = 20;
|
||||
const stepValue = difference / steps;
|
||||
|
||||
let step = 0;
|
||||
const interval = setInterval(() => {
|
||||
step++;
|
||||
const value = Math.round(currentValue + (stepValue * step));
|
||||
element.textContent = value;
|
||||
|
||||
if (step >= steps) {
|
||||
clearInterval(interval);
|
||||
element.textContent = targetValue;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter und Sortierung anwenden
|
||||
*/
|
||||
function applyFiltersAndSort() {
|
||||
let requests = [...currentRequests];
|
||||
|
||||
// Status Filter
|
||||
const statusFilter = document.getElementById('status-filter')?.value;
|
||||
if (statusFilter && statusFilter !== 'all') {
|
||||
requests = requests.filter(req => req.status === statusFilter);
|
||||
}
|
||||
|
||||
// Such-Filter
|
||||
const searchTerm = document.getElementById('search-requests')?.value.toLowerCase();
|
||||
if (searchTerm) {
|
||||
requests = requests.filter(req =>
|
||||
req.name?.toLowerCase().includes(searchTerm) ||
|
||||
req.email?.toLowerCase().includes(searchTerm) ||
|
||||
req.file_name?.toLowerCase().includes(searchTerm) ||
|
||||
req.reason?.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
// Sortierung
|
||||
const sortOrder = document.getElementById('sort-order')?.value;
|
||||
requests.sort((a, b) => {
|
||||
switch (sortOrder) {
|
||||
case 'oldest':
|
||||
return new Date(a.created_at) - new Date(b.created_at);
|
||||
case 'priority':
|
||||
return getPriorityValue(b) - getPriorityValue(a);
|
||||
case 'newest':
|
||||
default:
|
||||
return new Date(b.created_at) - new Date(a.created_at);
|
||||
}
|
||||
});
|
||||
|
||||
filteredRequests = requests;
|
||||
renderRequestsTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prioritätswert für Sortierung berechnen
|
||||
*/
|
||||
function getPriorityValue(request) {
|
||||
const now = new Date();
|
||||
const created = new Date(request.created_at);
|
||||
const hoursOld = (now - created) / (1000 * 60 * 60);
|
||||
|
||||
let priority = 0;
|
||||
|
||||
// Status-basierte Priorität
|
||||
if (request.status === 'pending') priority += 10;
|
||||
else if (request.status === 'approved') priority += 5;
|
||||
|
||||
// Alter-basierte Priorität
|
||||
if (hoursOld > 24) priority += 5;
|
||||
else if (hoursOld > 8) priority += 3;
|
||||
else if (hoursOld > 2) priority += 1;
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests-Tabelle rendern
|
||||
*/
|
||||
function renderRequestsTable() {
|
||||
const tableBody = document.getElementById('requests-table-body');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
|
||||
if (!tableBody) return;
|
||||
|
||||
if (filteredRequests.length === 0) {
|
||||
tableBody.innerHTML = '';
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
hideEmptyState();
|
||||
|
||||
const requestsHtml = filteredRequests.map(request => createRequestRow(request)).join('');
|
||||
tableBody.innerHTML = requestsHtml;
|
||||
|
||||
// Event Listeners für neue Rows hinzufügen
|
||||
addRowEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request Row HTML erstellen
|
||||
*/
|
||||
function createRequestRow(request) {
|
||||
const statusColor = getStatusColor(request.status);
|
||||
const priorityLevel = getPriorityLevel(request);
|
||||
const timeAgo = getTimeAgo(request.created_at);
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors duration-200" data-request-id="${request.id}">
|
||||
<td class="px-6 py-4">
|
||||
<input type="checkbox" class="request-checkbox rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
||||
value="${request.id}">
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<div class="h-10 w-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">
|
||||
${request.name ? request.name[0].toUpperCase() : 'G'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-slate-900 dark:text-white">${escapeHtml(request.name || 'Unbekannt')}</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">${escapeHtml(request.email || 'Keine E-Mail')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-slate-900 dark:text-white font-medium">${escapeHtml(request.file_name || 'Keine Datei')}</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||
${request.duration_minutes ? `${request.duration_minutes} Min.` : 'Unbekannte Dauer'}
|
||||
${request.copies ? ` • ${request.copies} Kopien` : ''}
|
||||
</div>
|
||||
${request.reason ? `<div class="text-xs text-slate-400 dark:text-slate-500 mt-1 truncate max-w-xs">${escapeHtml(request.reason)}</div>` : ''}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full ${statusColor}">
|
||||
<span class="w-2 h-2 mr-1 rounded-full ${getStatusDot(request.status)}"></span>
|
||||
${getStatusText(request.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-slate-900 dark:text-white">${timeAgo}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">${formatDateTime(request.created_at)}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center">
|
||||
${getPriorityBadge(priorityLevel)}
|
||||
${request.is_urgent ? '<span class="ml-2 text-red-500 text-xs">🔥 Dringend</span>' : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<button onclick="showRequestDetail(${request.id})"
|
||||
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
|
||||
title="Details anzeigen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
${request.status === 'pending' ? `
|
||||
<button onclick="approveRequest(${request.id})"
|
||||
class="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300 transition-colors"
|
||||
title="Genehmigen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button onclick="rejectRequest(${request.id})"
|
||||
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||
title="Ablehnen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
<button onclick="deleteRequest(${request.id})"
|
||||
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 transition-colors"
|
||||
title="Löschen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status-Helper-Funktionen
|
||||
*/
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
'pending': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300',
|
||||
'approved': 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
|
||||
'rejected': 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
|
||||
'expired': 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300'
|
||||
};
|
||||
return colors[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300';
|
||||
}
|
||||
|
||||
function getStatusDot(status) {
|
||||
const dots = {
|
||||
'pending': 'bg-yellow-400 dark:bg-yellow-300',
|
||||
'approved': 'bg-green-400 dark:bg-green-300',
|
||||
'rejected': 'bg-red-400 dark:bg-red-300',
|
||||
'expired': 'bg-gray-400 dark:bg-gray-300'
|
||||
};
|
||||
return dots[status] || 'bg-gray-400 dark:bg-gray-300';
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
const texts = {
|
||||
'pending': 'Wartend',
|
||||
'approved': 'Genehmigt',
|
||||
'rejected': 'Abgelehnt',
|
||||
'expired': 'Abgelaufen'
|
||||
};
|
||||
return texts[status] || status;
|
||||
}
|
||||
|
||||
function getPriorityLevel(request) {
|
||||
const priority = getPriorityValue(request);
|
||||
if (priority >= 15) return 'high';
|
||||
if (priority >= 8) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
function getPriorityBadge(level) {
|
||||
const badges = {
|
||||
'high': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300">🔴 Hoch</span>',
|
||||
'medium': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300">🟡 Mittel</span>',
|
||||
'low': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">🟢 Niedrig</span>'
|
||||
};
|
||||
return badges[level] || badges['low'];
|
||||
}
|
||||
|
||||
/**
|
||||
* CRUD-Operationen
|
||||
*/
|
||||
async function approveRequest(requestId) {
|
||||
if (!confirm('Möchten Sie diesen Gastauftrag wirklich genehmigen?')) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${requestId}/approve`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({}) // Leeres JSON-Objekt senden
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('✅ Gastauftrag erfolgreich genehmigt', 'success');
|
||||
loadGuestRequests();
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Genehmigen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Genehmigen:', error);
|
||||
showNotification('❌ Fehler beim Genehmigen: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectRequest(requestId) {
|
||||
const reason = prompt('Grund für die Ablehnung:');
|
||||
if (!reason) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${requestId}/reject`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ reason })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('✅ Gastauftrag erfolgreich abgelehnt', 'success');
|
||||
loadGuestRequests();
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Ablehnen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Ablehnen:', error);
|
||||
showNotification('❌ Fehler beim Ablehnen: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRequest(requestId) {
|
||||
if (!confirm('Möchten Sie diesen Gastauftrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${requestId}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('✅ Gastauftrag erfolgreich gelöscht', 'success');
|
||||
loadGuestRequests();
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Löschen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen:', error);
|
||||
showNotification('❌ Fehler beim Löschen: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail-Modal Funktionen
|
||||
*/
|
||||
function showRequestDetail(requestId) {
|
||||
const request = currentRequests.find(req => req.id === requestId);
|
||||
if (!request) return;
|
||||
|
||||
const modal = document.getElementById('detail-modal');
|
||||
const content = document.getElementById('modal-content');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Gastauftrag Details</h3>
|
||||
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Antragsteller</h4>
|
||||
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
|
||||
<p><strong>Name:</strong> ${escapeHtml(request.name || 'Unbekannt')}</p>
|
||||
<p><strong>E-Mail:</strong> ${escapeHtml(request.email || 'Keine E-Mail')}</p>
|
||||
<p><strong>Erstellt am:</strong> ${formatDateTime(request.created_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Auftrag Details</h4>
|
||||
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
|
||||
<p><strong>Datei:</strong> ${escapeHtml(request.file_name || 'Keine Datei')}</p>
|
||||
<p><strong>Dauer:</strong> ${request.duration_minutes || 'Unbekannt'} Minuten</p>
|
||||
<p><strong>Kopien:</strong> ${request.copies || 1}</p>
|
||||
<p><strong>Status:</strong> ${getStatusText(request.status)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${request.reason ? `
|
||||
<div class="mt-6">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Begründung</h4>
|
||||
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">${escapeHtml(request.reason)}</p>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="mt-8 flex justify-end space-x-3">
|
||||
${request.status === 'pending' ? `
|
||||
<button onclick="approveRequest(${request.id}); closeDetailModal();"
|
||||
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
||||
Genehmigen
|
||||
</button>
|
||||
<button onclick="rejectRequest(${request.id}); closeDetailModal();"
|
||||
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
|
||||
Ablehnen
|
||||
</button>
|
||||
` : ''}
|
||||
<button onclick="closeDetailModal()"
|
||||
class="px-4 py-2 bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors">
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeDetailModal() {
|
||||
const modal = document.getElementById('detail-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk Actions
|
||||
*/
|
||||
function showBulkActionsModal() {
|
||||
const selectedIds = getSelectedRequestIds();
|
||||
if (selectedIds.length === 0) {
|
||||
showNotification('⚠️ Bitte wählen Sie mindestens einen Gastauftrag aus', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('bulk-modal');
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeBulkModal() {
|
||||
const modal = document.getElementById('bulk-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
async function performBulkAction(action) {
|
||||
const selectedIds = getSelectedRequestIds();
|
||||
if (selectedIds.length === 0) return;
|
||||
|
||||
const confirmMessages = {
|
||||
'approve': `Möchten Sie ${selectedIds.length} Gastaufträge genehmigen?`,
|
||||
'reject': `Möchten Sie ${selectedIds.length} Gastaufträge ablehnen?`,
|
||||
'delete': `Möchten Sie ${selectedIds.length} Gastaufträge löschen?`
|
||||
};
|
||||
|
||||
if (!confirm(confirmMessages[action])) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
closeBulkModal();
|
||||
|
||||
const promises = selectedIds.map(async (id) => {
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${id}/${action}`;
|
||||
const method = action === 'delete' ? 'DELETE' : 'POST';
|
||||
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value.ok).length;
|
||||
|
||||
showNotification(`✅ ${successCount} von ${selectedIds.length} Aktionen erfolgreich`, 'success');
|
||||
loadGuestRequests();
|
||||
|
||||
// Alle Checkboxen zurücksetzen
|
||||
document.getElementById('select-all').checked = false;
|
||||
document.querySelectorAll('.request-checkbox').forEach(cb => cb.checked = false);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler bei Bulk-Aktion:', error);
|
||||
showNotification('❌ Fehler bei der Bulk-Aktion: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedRequestIds() {
|
||||
const checkboxes = document.querySelectorAll('.request-checkbox:checked');
|
||||
return Array.from(checkboxes).map(cb => parseInt(cb.value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event Handlers
|
||||
*/
|
||||
function handleSearch() {
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
|
||||
function handleFilterChange() {
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
|
||||
function handleSortChange() {
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
|
||||
function handleSelectAll(event) {
|
||||
const checkboxes = document.querySelectorAll('.request-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = event.target.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
const selectedIds = getSelectedRequestIds();
|
||||
const exportData = selectedIds.length > 0 ?
|
||||
filteredRequests.filter(req => selectedIds.includes(req.id)) :
|
||||
filteredRequests;
|
||||
|
||||
if (exportData.length === 0) {
|
||||
showNotification('⚠️ Keine Daten zum Exportieren verfügbar', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
exportToCSV(exportData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export-Funktionen
|
||||
*/
|
||||
function exportToCSV(data) {
|
||||
const headers = ['ID', 'Name', 'E-Mail', 'Datei', 'Status', 'Erstellt', 'Dauer (Min)', 'Kopien', 'Begründung'];
|
||||
const rows = data.map(req => [
|
||||
req.id,
|
||||
req.name || '',
|
||||
req.email || '',
|
||||
req.file_name || '',
|
||||
getStatusText(req.status),
|
||||
formatDateTime(req.created_at),
|
||||
req.duration_minutes || '',
|
||||
req.copies || '',
|
||||
req.reason || ''
|
||||
]);
|
||||
|
||||
const csvContent = [headers, ...rows]
|
||||
.map(row => row.map(field => `"${String(field).replace(/"/g, '""')}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
|
||||
if (link.download !== undefined) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `gastauftraege_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
showNotification('📄 CSV-Export erfolgreich erstellt', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Refresh
|
||||
*/
|
||||
function startAutoRefresh() {
|
||||
// Refresh alle 30 Sekunden
|
||||
refreshInterval = setInterval(() => {
|
||||
loadGuestRequests();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility-Funktionen
|
||||
*/
|
||||
function addRowEventListeners() {
|
||||
// Falls notwendig, können hier zusätzliche Event Listener hinzugefügt werden
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
const loadingElement = document.getElementById('table-loading');
|
||||
const tableBody = document.getElementById('requests-table-body');
|
||||
|
||||
if (loadingElement) {
|
||||
loadingElement.classList.toggle('hidden', !show);
|
||||
}
|
||||
|
||||
if (show && tableBody) {
|
||||
tableBody.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function showEmptyState() {
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function hideEmptyState() {
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 px-6 py-4 rounded-xl shadow-2xl z-50 transform transition-all duration-500 translate-x-full ${
|
||||
type === 'success' ? 'bg-green-500 text-white' :
|
||||
type === 'error' ? 'bg-red-500 text-white' :
|
||||
type === 'warning' ? 'bg-yellow-500 text-black' :
|
||||
'bg-blue-500 text-white'
|
||||
}`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="text-lg">
|
||||
${type === 'success' ? '✅' :
|
||||
type === 'error' ? '❌' :
|
||||
type === 'warning' ? '⚠️' : 'ℹ️'}
|
||||
</span>
|
||||
<span class="font-medium">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animation einblenden
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// Nach 5 Sekunden entfernen
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-x-full');
|
||||
setTimeout(() => notification.remove(), 5000);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text ? String(text).replace(/[&<>"']/g, m => map[m]) : '';
|
||||
}
|
||||
|
||||
function formatDateTime(dateString) {
|
||||
if (!dateString) return 'Unbekannt';
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeAgo(dateString) {
|
||||
if (!dateString) return 'Unbekannt';
|
||||
|
||||
const now = new Date();
|
||||
const date = new Date(dateString);
|
||||
const diffMs = now - date;
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffDays > 0) {
|
||||
return `vor ${diffDays} Tag${diffDays === 1 ? '' : 'en'}`;
|
||||
} else if (diffHours > 0) {
|
||||
return `vor ${diffHours} Stunde${diffHours === 1 ? '' : 'n'}`;
|
||||
} else {
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
||||
return `vor ${Math.max(1, diffMinutes)} Minute${diffMinutes === 1 ? '' : 'n'}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Funktionen für onclick-Handler
|
||||
window.showRequestDetail = showRequestDetail;
|
||||
window.approveRequest = approveRequest;
|
||||
window.rejectRequest = rejectRequest;
|
||||
window.deleteRequest = deleteRequest;
|
||||
window.closeDetailModal = closeDetailModal;
|
||||
window.closeBulkModal = closeBulkModal;
|
||||
window.performBulkAction = performBulkAction;
|
||||
|
||||
console.log('📋 Admin Guest Requests JavaScript vollständig geladen');
|
585
backend/static/js/admin-live.js
Normal file
585
backend/static/js/admin-live.js
Normal file
@ -0,0 +1,585 @@
|
||||
/**
|
||||
* Mercedes-Benz MYP Admin Live Dashboard
|
||||
* Echtzeit-Updates für das Admin Panel mit echten Daten
|
||||
*/
|
||||
|
||||
class AdminLiveDashboard {
|
||||
constructor() {
|
||||
this.isLive = false;
|
||||
this.updateInterval = null;
|
||||
this.retryCount = 0;
|
||||
this.maxRetries = 3;
|
||||
|
||||
// Dynamische API-Base-URL-Erkennung
|
||||
this.apiBaseUrl = this.detectApiBaseUrl();
|
||||
console.log('🔗 API Base URL erkannt:', this.apiBaseUrl);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
detectApiBaseUrl() {
|
||||
const currentHost = window.location.hostname;
|
||||
const currentProtocol = window.location.protocol;
|
||||
const currentPort = window.location.port;
|
||||
|
||||
console.log('🔍 Live Dashboard API URL Detection:', { currentHost, currentProtocol, currentPort });
|
||||
|
||||
// Wenn wir bereits auf dem richtigen Port sind, verwende relative URLs
|
||||
if (currentPort === '443' || !currentPort) {
|
||||
console.log('✅ Verwende relative URLs (HTTPS Port 443)');
|
||||
return '';
|
||||
}
|
||||
|
||||
// Für alle anderen Fälle, verwende HTTPS auf Port 443
|
||||
const fallbackUrl = `https://${currentHost}`;
|
||||
console.log('🔄 Fallback zu HTTPS:443:', fallbackUrl);
|
||||
|
||||
return fallbackUrl;
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log('🚀 Mercedes-Benz MYP Admin Live Dashboard gestartet');
|
||||
|
||||
// Live-Status anzeigen
|
||||
this.updateLiveTime();
|
||||
this.startLiveUpdates();
|
||||
|
||||
// Event Listeners
|
||||
this.bindEvents();
|
||||
|
||||
// Initial Load
|
||||
this.loadLiveStats();
|
||||
|
||||
// Error Monitoring System
|
||||
this.initErrorMonitoring();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Quick Action Buttons
|
||||
const systemStatusBtn = document.getElementById('system-status-btn');
|
||||
const analyticsBtn = document.getElementById('analytics-btn');
|
||||
const maintenanceBtn = document.getElementById('maintenance-btn');
|
||||
|
||||
if (systemStatusBtn) {
|
||||
systemStatusBtn.addEventListener('click', () => this.showSystemStatus());
|
||||
}
|
||||
|
||||
if (analyticsBtn) {
|
||||
analyticsBtn.addEventListener('click', () => this.showAnalytics());
|
||||
}
|
||||
|
||||
if (maintenanceBtn) {
|
||||
maintenanceBtn.addEventListener('click', () => this.showMaintenance());
|
||||
}
|
||||
|
||||
// Page Visibility API für optimierte Updates
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
this.pauseLiveUpdates();
|
||||
} else {
|
||||
this.resumeLiveUpdates();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startLiveUpdates() {
|
||||
this.isLive = true;
|
||||
this.updateLiveIndicator(true);
|
||||
|
||||
// Live Stats alle 30 Sekunden aktualisieren
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.loadLiveStats();
|
||||
}, 30000);
|
||||
|
||||
// Zeit jede Sekunde aktualisieren
|
||||
setInterval(() => {
|
||||
this.updateLiveTime();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
pauseLiveUpdates() {
|
||||
this.isLive = false;
|
||||
this.updateLiveIndicator(false);
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
}
|
||||
|
||||
resumeLiveUpdates() {
|
||||
if (!this.isLive) {
|
||||
this.startLiveUpdates();
|
||||
this.loadLiveStats(); // Sofortiges Update beim Fortsetzen
|
||||
}
|
||||
}
|
||||
|
||||
updateLiveIndicator(isLive) {
|
||||
const indicator = document.getElementById('live-indicator');
|
||||
if (indicator) {
|
||||
if (isLive) {
|
||||
indicator.className = 'w-2 h-2 bg-green-400 rounded-full animate-pulse';
|
||||
} else {
|
||||
indicator.className = 'w-2 h-2 bg-gray-400 rounded-full';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateLiveTime() {
|
||||
const timeElement = document.getElementById('live-time');
|
||||
if (timeElement) {
|
||||
const now = new Date();
|
||||
timeElement.textContent = now.toLocaleTimeString('de-DE');
|
||||
}
|
||||
}
|
||||
|
||||
async loadLiveStats() {
|
||||
try {
|
||||
const url = `${this.apiBaseUrl}/api/admin/stats/live`;
|
||||
console.log('🔄 Lade Live-Statistiken von:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCSRFToken()
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.updateStatsDisplay(data);
|
||||
this.retryCount = 0; // Reset retry count on success
|
||||
|
||||
// Success notification (optional)
|
||||
this.showQuietNotification('Live-Daten aktualisiert', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || 'Unbekannter Fehler beim Laden der Live-Statistiken');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Live-Statistiken:', error);
|
||||
|
||||
this.retryCount++;
|
||||
if (this.retryCount <= this.maxRetries) {
|
||||
console.log(`Versuche erneut... (${this.retryCount}/${this.maxRetries})`);
|
||||
setTimeout(() => this.loadLiveStats(), 5000); // Retry nach 5 Sekunden
|
||||
} else {
|
||||
this.handleConnectionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStatsDisplay(data) {
|
||||
// Benutzer Stats
|
||||
this.updateCounter('live-users-count', data.total_users);
|
||||
this.updateProgress('users-progress', Math.min((data.total_users / 20) * 100, 100)); // Max 20 users = 100%
|
||||
|
||||
// Drucker Stats
|
||||
this.updateCounter('live-printers-count', data.total_printers);
|
||||
this.updateElement('live-printers-online', `${data.online_printers} online`);
|
||||
if (data.total_printers > 0) {
|
||||
this.updateProgress('printers-progress', (data.online_printers / data.total_printers) * 100);
|
||||
}
|
||||
|
||||
// Jobs Stats
|
||||
this.updateCounter('live-jobs-active', data.active_jobs);
|
||||
this.updateElement('live-jobs-queued', `${data.queued_jobs} in Warteschlange`);
|
||||
this.updateProgress('jobs-progress', Math.min(data.active_jobs * 20, 100)); // Max 5 jobs = 100%
|
||||
|
||||
// Erfolgsrate Stats
|
||||
this.updateCounter('live-success-rate', `${data.success_rate}%`);
|
||||
this.updateProgress('success-progress', data.success_rate);
|
||||
|
||||
// Trend Analysis
|
||||
this.updateSuccessTrend(data.success_rate);
|
||||
|
||||
console.log('📊 Live-Statistiken aktualisiert:', data);
|
||||
}
|
||||
|
||||
updateCounter(elementId, newValue) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
const currentValue = parseInt(element.textContent) || 0;
|
||||
if (currentValue !== newValue) {
|
||||
this.animateCounter(element, currentValue, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animateCounter(element, from, to) {
|
||||
const duration = 1000; // 1 Sekunde
|
||||
const increment = (to - from) / (duration / 16); // 60 FPS
|
||||
let current = from;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if ((increment > 0 && current >= to) || (increment < 0 && current <= to)) {
|
||||
current = to;
|
||||
clearInterval(timer);
|
||||
}
|
||||
element.textContent = Math.round(current);
|
||||
}, 16);
|
||||
}
|
||||
|
||||
updateElement(elementId, newValue) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element && element.textContent !== newValue) {
|
||||
element.textContent = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(elementId, percentage) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.style.width = `${Math.max(0, Math.min(100, percentage))}%`;
|
||||
}
|
||||
}
|
||||
|
||||
updateSuccessTrend(successRate) {
|
||||
const trendElement = document.getElementById('success-trend');
|
||||
if (trendElement) {
|
||||
let trendText = 'Stabil';
|
||||
let trendClass = 'text-green-500';
|
||||
let trendIcon = 'M5 10l7-7m0 0l7 7m-7-7v18'; // Up arrow
|
||||
|
||||
if (successRate >= 95) {
|
||||
trendText = 'Excellent';
|
||||
trendClass = 'text-green-600';
|
||||
} else if (successRate >= 80) {
|
||||
trendText = 'Gut';
|
||||
trendClass = 'text-green-500';
|
||||
} else if (successRate >= 60) {
|
||||
trendText = 'Mittel';
|
||||
trendClass = 'text-yellow-500';
|
||||
trendIcon = 'M5 12h14'; // Horizontal line
|
||||
} else {
|
||||
trendText = 'Niedrig';
|
||||
trendClass = 'text-red-500';
|
||||
trendIcon = 'M19 14l-7 7m0 0l-7-7m7 7V3'; // Down arrow
|
||||
}
|
||||
|
||||
trendElement.className = `text-sm ${trendClass}`;
|
||||
trendElement.innerHTML = `
|
||||
<span class="inline-flex items-center">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${trendIcon}"/>
|
||||
</svg>
|
||||
${trendText}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
showSystemStatus() {
|
||||
// System Status Modal oder Navigation
|
||||
console.log('🔧 System Status angezeigt');
|
||||
this.showNotification('System Status wird geladen...', 'info');
|
||||
|
||||
// Hier könnten weitere System-Details geladen werden
|
||||
const url = `${this.apiBaseUrl}/api/admin/system/status`;
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// System Status anzeigen
|
||||
console.log('System Status:', data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden des System Status:', error);
|
||||
});
|
||||
}
|
||||
|
||||
showAnalytics() {
|
||||
console.log('📈 Live Analytics angezeigt');
|
||||
this.showNotification('Analytics werden geladen...', 'info');
|
||||
|
||||
// Analytics Tab aktivieren oder Modal öffnen
|
||||
const analyticsTab = document.querySelector('a[href*="tab=system"]');
|
||||
if (analyticsTab) {
|
||||
analyticsTab.click();
|
||||
}
|
||||
}
|
||||
|
||||
showMaintenance() {
|
||||
console.log('🛠️ Wartung angezeigt');
|
||||
this.showNotification('Wartungsoptionen werden geladen...', 'info');
|
||||
|
||||
// Wartungs-Tab aktivieren oder Modal öffnen
|
||||
const systemTab = document.querySelector('a[href*="tab=system"]');
|
||||
if (systemTab) {
|
||||
systemTab.click();
|
||||
}
|
||||
}
|
||||
|
||||
handleConnectionError() {
|
||||
console.error('🔴 Verbindung zu Live-Updates verloren');
|
||||
this.updateLiveIndicator(false);
|
||||
this.showNotification('Verbindung zu Live-Updates verloren. Versuche erneut...', 'error');
|
||||
|
||||
// Auto-Recovery nach 30 Sekunden
|
||||
setTimeout(() => {
|
||||
this.retryCount = 0;
|
||||
this.loadLiveStats();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
// Erstelle oder aktualisiere Notification
|
||||
let notification = document.getElementById('live-notification');
|
||||
if (!notification) {
|
||||
notification = document.createElement('div');
|
||||
notification.id = 'live-notification';
|
||||
notification.className = 'fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm';
|
||||
document.body.appendChild(notification);
|
||||
}
|
||||
|
||||
const colors = {
|
||||
success: 'bg-green-500 text-white',
|
||||
error: 'bg-red-500 text-white',
|
||||
info: 'bg-blue-500 text-white',
|
||||
warning: 'bg-yellow-500 text-white'
|
||||
};
|
||||
|
||||
notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm ${colors[type]} transform transition-all duration-300 translate-x-0`;
|
||||
notification.textContent = message;
|
||||
|
||||
// Auto-Hide nach 3 Sekunden
|
||||
setTimeout(() => {
|
||||
if (notification) {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (notification && notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
showQuietNotification(message, type) {
|
||||
// Nur in der Konsole loggen für nicht-störende Updates
|
||||
const emoji = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️';
|
||||
console.log(`${emoji} ${message}`);
|
||||
}
|
||||
|
||||
getCSRFToken() {
|
||||
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||
return meta ? meta.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
// Error Monitoring System
|
||||
initErrorMonitoring() {
|
||||
// Check system health every 30 seconds
|
||||
this.checkSystemHealth();
|
||||
setInterval(() => this.checkSystemHealth(), 30000);
|
||||
|
||||
// Setup error alert event handlers
|
||||
this.setupErrorAlertHandlers();
|
||||
}
|
||||
|
||||
async checkSystemHealth() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/system-health');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.updateHealthDisplay(data);
|
||||
this.updateErrorAlerts(data);
|
||||
} else {
|
||||
console.error('System health check failed:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking system health:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateHealthDisplay(data) {
|
||||
// Update database health status
|
||||
const statusIndicator = document.getElementById('db-status-indicator');
|
||||
const statusText = document.getElementById('db-status-text');
|
||||
const lastMigration = document.getElementById('last-migration');
|
||||
const schemaIntegrity = document.getElementById('schema-integrity');
|
||||
const recentErrorsCount = document.getElementById('recent-errors-count');
|
||||
|
||||
if (statusIndicator && statusText) {
|
||||
if (data.health_status === 'critical') {
|
||||
statusIndicator.className = 'w-3 h-3 bg-red-500 rounded-full animate-pulse';
|
||||
statusText.textContent = 'Kritisch';
|
||||
statusText.className = 'text-sm font-medium text-red-600 dark:text-red-400';
|
||||
} else if (data.health_status === 'warning') {
|
||||
statusIndicator.className = 'w-3 h-3 bg-yellow-500 rounded-full animate-pulse';
|
||||
statusText.textContent = 'Warnung';
|
||||
statusText.className = 'text-sm font-medium text-yellow-600 dark:text-yellow-400';
|
||||
} else {
|
||||
statusIndicator.className = 'w-3 h-3 bg-green-400 rounded-full animate-pulse';
|
||||
statusText.textContent = 'Gesund';
|
||||
statusText.className = 'text-sm font-medium text-green-600 dark:text-green-400';
|
||||
}
|
||||
}
|
||||
|
||||
if (lastMigration) {
|
||||
lastMigration.textContent = data.last_migration || 'Unbekannt';
|
||||
}
|
||||
|
||||
if (schemaIntegrity) {
|
||||
schemaIntegrity.textContent = data.schema_integrity || 'Prüfung';
|
||||
if (data.schema_integrity === 'FEHLER') {
|
||||
schemaIntegrity.className = 'text-lg font-semibold text-red-600 dark:text-red-400';
|
||||
} else {
|
||||
schemaIntegrity.className = 'text-lg font-semibold text-green-600 dark:text-green-400';
|
||||
}
|
||||
}
|
||||
|
||||
if (recentErrorsCount) {
|
||||
const errorCount = data.recent_errors_count || 0;
|
||||
recentErrorsCount.textContent = errorCount;
|
||||
if (errorCount > 0) {
|
||||
recentErrorsCount.className = 'text-lg font-semibold text-red-600 dark:text-red-400';
|
||||
} else {
|
||||
recentErrorsCount.className = 'text-lg font-semibold text-green-600 dark:text-green-400';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateErrorAlerts(data) {
|
||||
const alertContainer = document.getElementById('critical-errors-alert');
|
||||
const errorList = document.getElementById('error-list');
|
||||
|
||||
if (!alertContainer || !errorList) return;
|
||||
|
||||
const allErrors = [...(data.critical_errors || []), ...(data.warnings || [])];
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
// Show alert container
|
||||
alertContainer.classList.remove('hidden');
|
||||
|
||||
// Clear previous errors
|
||||
errorList.innerHTML = '';
|
||||
|
||||
// Add each error
|
||||
allErrors.forEach(error => {
|
||||
const errorElement = document.createElement('div');
|
||||
errorElement.className = `p-3 rounded-lg border-l-4 ${
|
||||
error.severity === 'critical' ? 'bg-red-50 dark:bg-red-900/30 border-red-500' :
|
||||
error.severity === 'high' ? 'bg-orange-50 dark:bg-orange-900/30 border-orange-500' :
|
||||
'bg-yellow-50 dark:bg-yellow-900/30 border-yellow-500'
|
||||
}`;
|
||||
|
||||
errorElement.innerHTML = `
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium ${
|
||||
error.severity === 'critical' ? 'text-red-800 dark:text-red-200' :
|
||||
error.severity === 'high' ? 'text-orange-800 dark:text-orange-200' :
|
||||
'text-yellow-800 dark:text-yellow-200'
|
||||
}">${error.message}</h4>
|
||||
<p class="text-sm mt-1 ${
|
||||
error.severity === 'critical' ? 'text-red-600 dark:text-red-300' :
|
||||
error.severity === 'high' ? 'text-orange-600 dark:text-orange-300' :
|
||||
'text-yellow-600 dark:text-yellow-300'
|
||||
}">💡 ${error.suggested_fix}</p>
|
||||
<p class="text-xs mt-1 text-gray-500 dark:text-gray-400">
|
||||
📅 ${new Date(error.timestamp).toLocaleString('de-DE')}
|
||||
</p>
|
||||
</div>
|
||||
<span class="ml-2 px-2 py-1 text-xs font-medium rounded-full ${
|
||||
error.severity === 'critical' ? 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100' :
|
||||
error.severity === 'high' ? 'bg-orange-100 text-orange-800 dark:bg-orange-800 dark:text-orange-100' :
|
||||
'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100'
|
||||
}">
|
||||
${error.severity.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
errorList.appendChild(errorElement);
|
||||
});
|
||||
} else {
|
||||
// Hide alert container
|
||||
alertContainer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
setupErrorAlertHandlers() {
|
||||
// Fix errors button
|
||||
const fixErrorsBtn = document.getElementById('fix-errors-btn');
|
||||
if (fixErrorsBtn) {
|
||||
fixErrorsBtn.addEventListener('click', async () => {
|
||||
await this.fixErrors();
|
||||
});
|
||||
}
|
||||
|
||||
// Dismiss errors button
|
||||
const dismissErrorsBtn = document.getElementById('dismiss-errors-btn');
|
||||
if (dismissErrorsBtn) {
|
||||
dismissErrorsBtn.addEventListener('click', () => {
|
||||
const alertContainer = document.getElementById('critical-errors-alert');
|
||||
if (alertContainer) {
|
||||
alertContainer.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// View details button
|
||||
const viewDetailsBtn = document.getElementById('view-error-details-btn');
|
||||
if (viewDetailsBtn) {
|
||||
viewDetailsBtn.addEventListener('click', () => {
|
||||
// Redirect to logs tab
|
||||
window.location.href = '/admin-dashboard?tab=logs';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fixErrors() {
|
||||
const fixBtn = document.getElementById('fix-errors-btn');
|
||||
if (!fixBtn) return;
|
||||
|
||||
// Show loading state
|
||||
const originalText = fixBtn.innerHTML;
|
||||
fixBtn.innerHTML = '🔄 Repariere...';
|
||||
fixBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/fix-errors', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Show success message
|
||||
this.showNotification('✅ Automatische Reparatur erfolgreich durchgeführt!', 'success');
|
||||
|
||||
// Refresh health check
|
||||
setTimeout(() => {
|
||||
this.checkSystemHealth();
|
||||
}, 2000);
|
||||
} else {
|
||||
// Show error message
|
||||
this.showNotification(`❌ Reparatur fehlgeschlagen: ${data.error}`, 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fixing errors:', error);
|
||||
this.showNotification('❌ Fehler bei der automatischen Reparatur', 'error');
|
||||
} finally {
|
||||
// Restore button
|
||||
fixBtn.innerHTML = originalText;
|
||||
fixBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
new AdminLiveDashboard();
|
||||
});
|
||||
|
||||
// Export for global access
|
||||
window.AdminLiveDashboard = AdminLiveDashboard;
|
1080
backend/static/js/admin-panel.js
Normal file
1080
backend/static/js/admin-panel.js
Normal file
File diff suppressed because it is too large
Load Diff
350
backend/static/js/admin-system.js
Normal file
350
backend/static/js/admin-system.js
Normal file
@ -0,0 +1,350 @@
|
||||
/**
|
||||
* Admin System Management JavaScript
|
||||
* Funktionen für System-Wartung und -Konfiguration
|
||||
*/
|
||||
|
||||
// CSRF Token für AJAX-Anfragen
|
||||
function getCsrfToken() {
|
||||
const token = document.querySelector('meta[name="csrf-token"]');
|
||||
return token ? token.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
// Hilfsfunktion für API-Aufrufe
|
||||
async function makeApiCall(url, method = 'GET', data = null) {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
};
|
||||
|
||||
if (data) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showNotification(result.message || 'Aktion erfolgreich ausgeführt', 'success');
|
||||
return result;
|
||||
} else {
|
||||
showNotification(result.error || 'Ein Fehler ist aufgetreten', 'error');
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Logs laden und anzeigen
|
||||
async 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>
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/logs');
|
||||
if (!response.ok) throw new Error('Fehler beim Laden der Logs');
|
||||
|
||||
const data = await response.json();
|
||||
window.logsData = data.logs || [];
|
||||
window.filteredLogs = [...window.logsData];
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Logs rendern
|
||||
function renderLogs() {
|
||||
const logsContainer = document.getElementById('logs-container');
|
||||
if (!logsContainer || !window.filteredLogs) return;
|
||||
|
||||
if (window.filteredLogs.length === 0) {
|
||||
logsContainer.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<p class="text-gray-600 dark:text-gray-400">Keine Logs gefunden</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const logsHtml = window.filteredLogs.map(log => {
|
||||
const levelColor = getLogLevelColor(log.level);
|
||||
return `
|
||||
<div class="bg-white/40 dark:bg-slate-700/40 rounded-lg p-4 border ${levelColor.border}">
|
||||
<div class="flex items-start space-x-3">
|
||||
<span class="inline-block px-2 py-1 text-xs font-semibold rounded-full ${levelColor.bg} ${levelColor.text}">
|
||||
${log.level}
|
||||
</span>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-sm font-medium text-slate-600 dark:text-slate-400">${log.category}</span>
|
||||
<span class="text-xs text-slate-500 dark:text-slate-500">${log.timestamp}</span>
|
||||
</div>
|
||||
<p class="text-sm text-slate-900 dark:text-white break-all">${log.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
logsContainer.innerHTML = logsHtml;
|
||||
}
|
||||
|
||||
// Log-Level-Farben bestimmen
|
||||
function getLogLevelColor(level) {
|
||||
const colors = {
|
||||
'ERROR': {
|
||||
bg: 'bg-red-100 dark:bg-red-900/30',
|
||||
text: 'text-red-800 dark:text-red-200',
|
||||
border: 'border-red-200 dark:border-red-700'
|
||||
},
|
||||
'WARNING': {
|
||||
bg: 'bg-yellow-100 dark:bg-yellow-900/30',
|
||||
text: 'text-yellow-800 dark:text-yellow-200',
|
||||
border: 'border-yellow-200 dark:border-yellow-700'
|
||||
},
|
||||
'INFO': {
|
||||
bg: 'bg-blue-100 dark:bg-blue-900/30',
|
||||
text: 'text-blue-800 dark:text-blue-200',
|
||||
border: 'border-blue-200 dark:border-blue-700'
|
||||
},
|
||||
'DEBUG': {
|
||||
bg: 'bg-gray-100 dark:bg-gray-900/30',
|
||||
text: 'text-gray-800 dark:text-gray-200',
|
||||
border: 'border-gray-200 dark:border-gray-700'
|
||||
}
|
||||
};
|
||||
|
||||
return colors[level.toUpperCase()] || colors['INFO'];
|
||||
}
|
||||
|
||||
// Log-Statistiken aktualisieren
|
||||
function updateLogStatistics() {
|
||||
if (!window.logsData) return;
|
||||
|
||||
const stats = {
|
||||
total: window.logsData.length,
|
||||
errors: window.logsData.filter(log => log.level.toUpperCase() === 'ERROR').length,
|
||||
warnings: window.logsData.filter(log => log.level.toUpperCase() === 'WARNING').length,
|
||||
info: window.logsData.filter(log => log.level.toUpperCase() === 'INFO').length
|
||||
};
|
||||
|
||||
// Aktualisiere Statistik-Anzeigen falls vorhanden
|
||||
const totalElement = document.getElementById('log-stats-total');
|
||||
const errorsElement = document.getElementById('log-stats-errors');
|
||||
const warningsElement = document.getElementById('log-stats-warnings');
|
||||
const infoElement = document.getElementById('log-stats-info');
|
||||
|
||||
if (totalElement) totalElement.textContent = stats.total;
|
||||
if (errorsElement) errorsElement.textContent = stats.errors;
|
||||
if (warningsElement) warningsElement.textContent = stats.warnings;
|
||||
if (infoElement) infoElement.textContent = stats.info;
|
||||
}
|
||||
|
||||
// Zum Ende der Logs scrollen
|
||||
function scrollLogsToBottom() {
|
||||
const logsContainer = document.getElementById('logs-container');
|
||||
if (logsContainer) {
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Notification anzeigen
|
||||
function showNotification(message, type = 'info') {
|
||||
// Erstelle Notification-Element falls nicht vorhanden
|
||||
let notification = document.getElementById('admin-notification');
|
||||
if (!notification) {
|
||||
notification = document.createElement('div');
|
||||
notification.id = 'admin-notification';
|
||||
notification.className = 'fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm transition-all duration-300 transform translate-x-full';
|
||||
document.body.appendChild(notification);
|
||||
}
|
||||
|
||||
// Setze Farbe basierend auf Typ
|
||||
const colors = {
|
||||
success: 'bg-green-500 text-white',
|
||||
error: 'bg-red-500 text-white',
|
||||
warning: 'bg-yellow-500 text-white',
|
||||
info: 'bg-blue-500 text-white'
|
||||
};
|
||||
|
||||
notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm transition-all duration-300 ${colors[type] || colors.info}`;
|
||||
notification.textContent = message;
|
||||
|
||||
// Zeige Notification
|
||||
notification.style.transform = 'translateX(0)';
|
||||
|
||||
// Verstecke nach 5 Sekunden
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Cache leeren
|
||||
async function clearCache() {
|
||||
if (confirm('Möchten Sie wirklich den Cache leeren?')) {
|
||||
showNotification('Cache wird geleert...', 'info');
|
||||
const result = await makeApiCall('/api/admin/cache/clear', 'POST');
|
||||
if (result) {
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Datenbank optimieren
|
||||
async function optimizeDatabase() {
|
||||
if (confirm('Möchten Sie wirklich die Datenbank optimieren? Dies kann einige Minuten dauern.')) {
|
||||
showNotification('Datenbank wird optimiert...', 'info');
|
||||
const result = await makeApiCall('/api/admin/database/optimize', 'POST');
|
||||
if (result) {
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backup erstellen
|
||||
async function createBackup() {
|
||||
if (confirm('Möchten Sie wirklich ein Backup erstellen?')) {
|
||||
showNotification('Backup wird erstellt...', 'info');
|
||||
const result = await makeApiCall('/api/admin/backup/create', 'POST');
|
||||
}
|
||||
}
|
||||
|
||||
// Drucker aktualisieren
|
||||
async function updatePrinters() {
|
||||
if (confirm('Möchten Sie alle Drucker-Verbindungen aktualisieren?')) {
|
||||
showNotification('Drucker werden aktualisiert...', 'info');
|
||||
const result = await makeApiCall('/api/admin/printers/update', 'POST');
|
||||
if (result) {
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// System neustarten
|
||||
async function restartSystem() {
|
||||
if (confirm('WARNUNG: Möchten Sie wirklich das System neustarten? Alle aktiven Verbindungen werden getrennt.')) {
|
||||
const result = await makeApiCall('/api/admin/system/restart', 'POST');
|
||||
if (result) {
|
||||
showNotification('System wird neugestartet...', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Einstellungen bearbeiten
|
||||
function editSettings() {
|
||||
window.location.href = '/settings';
|
||||
}
|
||||
|
||||
// Systemstatus automatisch aktualisieren
|
||||
async function updateSystemStatus() {
|
||||
if (window.location.search.includes('tab=system')) {
|
||||
const result = await makeApiCall('/api/admin/system/status');
|
||||
if (result) {
|
||||
// Aktualisiere die Anzeige
|
||||
updateStatusDisplay('cpu_usage', result.cpu_usage + '%');
|
||||
updateStatusDisplay('memory_usage', result.memory_usage + '%');
|
||||
updateStatusDisplay('disk_usage', result.disk_usage + '%');
|
||||
updateStatusDisplay('uptime', result.uptime);
|
||||
updateStatusDisplay('db_size', result.db_size);
|
||||
updateStatusDisplay('scheduler_jobs', result.scheduler_jobs);
|
||||
updateStatusDisplay('next_job', result.next_job);
|
||||
|
||||
// Scheduler-Status aktualisieren
|
||||
const schedulerStatus = document.querySelector('.scheduler-status');
|
||||
if (schedulerStatus) {
|
||||
if (result.scheduler_running) {
|
||||
schedulerStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-blue-400 animate-pulse"></span>Läuft';
|
||||
schedulerStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
|
||||
} else {
|
||||
schedulerStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-red-400"></span>Gestoppt';
|
||||
schedulerStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hilfsfunktion zum Aktualisieren der Status-Anzeige
|
||||
function updateStatusDisplay(key, value) {
|
||||
const element = document.querySelector(`[data-status="${key}"]`);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Datenbankstatus aktualisieren
|
||||
async function updateDatabaseStatus() {
|
||||
if (window.location.search.includes('tab=system')) {
|
||||
const result = await makeApiCall('/api/admin/database/status');
|
||||
if (result) {
|
||||
const dbStatus = document.querySelector('.database-status');
|
||||
if (dbStatus) {
|
||||
if (result.connected) {
|
||||
dbStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-green-400 animate-pulse"></span>Verbunden';
|
||||
dbStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||
} else {
|
||||
dbStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-red-400"></span>Getrennt';
|
||||
dbStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusDisplay('db_size', result.size);
|
||||
updateStatusDisplay('db_connections', result.connected ? 'Aktiv' : 'Getrennt');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-Update alle 30 Sekunden
|
||||
setInterval(() => {
|
||||
updateSystemStatus();
|
||||
updateDatabaseStatus();
|
||||
}, 30000);
|
||||
|
||||
// Initial load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateSystemStatus();
|
||||
updateDatabaseStatus();
|
||||
});
|
||||
|
||||
// Export für globale Verwendung
|
||||
window.adminSystem = {
|
||||
clearCache,
|
||||
optimizeDatabase,
|
||||
createBackup,
|
||||
updatePrinters,
|
||||
restartSystem,
|
||||
editSettings,
|
||||
updateSystemStatus,
|
||||
updateDatabaseStatus,
|
||||
loadLogs,
|
||||
renderLogs,
|
||||
updateLogStatistics,
|
||||
scrollLogsToBottom
|
||||
};
|
1580
backend/static/js/admin.js
Normal file
1580
backend/static/js/admin.js
Normal file
File diff suppressed because it is too large
Load Diff
752
backend/static/js/advanced-components.js
Normal file
752
backend/static/js/advanced-components.js
Normal file
@ -0,0 +1,752 @@
|
||||
/**
|
||||
* MYP Platform Advanced UI Components
|
||||
* Erweiterte Komponenten: Progress-Bars, File-Upload, Datepicker
|
||||
* Version: 2.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Namespace erweitern
|
||||
window.MYP = window.MYP || {};
|
||||
window.MYP.Advanced = window.MYP.Advanced || {};
|
||||
|
||||
/**
|
||||
* Progress Bar Component
|
||||
*/
|
||||
class ProgressBar {
|
||||
constructor(container, options = {}) {
|
||||
this.container = typeof container === 'string' ? document.querySelector(container) : container;
|
||||
this.options = {
|
||||
value: 0,
|
||||
max: 100,
|
||||
showLabel: true,
|
||||
showPercentage: true,
|
||||
animated: true,
|
||||
color: 'blue',
|
||||
size: 'md',
|
||||
striped: false,
|
||||
...options
|
||||
};
|
||||
|
||||
this.currentValue = this.options.value;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.container) {
|
||||
console.error('ProgressBar: Container nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const percentage = Math.round((this.currentValue / this.options.max) * 100);
|
||||
const sizeClass = this.getSizeClass();
|
||||
const colorClass = this.getColorClass();
|
||||
|
||||
this.container.innerHTML = `
|
||||
<div class="progress-bar-container ${sizeClass}">
|
||||
${this.options.showLabel ? `
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
${this.options.label || 'Fortschritt'}
|
||||
</span>
|
||||
${this.options.showPercentage ? `
|
||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
${percentage}%
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="progress-bar-track ${sizeClass}">
|
||||
<div class="progress-bar-fill ${colorClass} ${this.options.animated ? 'animated' : ''} ${this.options.striped ? 'striped' : ''}"
|
||||
style="width: ${percentage}%"
|
||||
role="progressbar"
|
||||
aria-valuenow="${this.currentValue}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="${this.options.max}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getSizeClass() {
|
||||
const sizes = {
|
||||
'sm': 'h-2',
|
||||
'md': 'h-3',
|
||||
'lg': 'h-4',
|
||||
'xl': 'h-6'
|
||||
};
|
||||
return sizes[this.options.size] || sizes.md;
|
||||
}
|
||||
|
||||
getColorClass() {
|
||||
const colors = {
|
||||
'blue': 'bg-blue-500',
|
||||
'green': 'bg-green-500',
|
||||
'red': 'bg-red-500',
|
||||
'yellow': 'bg-yellow-500',
|
||||
'purple': 'bg-purple-500',
|
||||
'indigo': 'bg-indigo-500'
|
||||
};
|
||||
return colors[this.options.color] || colors.blue;
|
||||
}
|
||||
|
||||
setValue(value, animate = true) {
|
||||
const oldValue = this.currentValue;
|
||||
this.currentValue = Math.max(0, Math.min(this.options.max, value));
|
||||
|
||||
if (animate) {
|
||||
this.animateToValue(oldValue, this.currentValue);
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
animateToValue(from, to) {
|
||||
const duration = 500; // ms
|
||||
const steps = 60;
|
||||
const stepValue = (to - from) / steps;
|
||||
let currentStep = 0;
|
||||
|
||||
const animate = () => {
|
||||
if (currentStep < steps) {
|
||||
this.currentValue = from + (stepValue * currentStep);
|
||||
this.render();
|
||||
currentStep++;
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
this.currentValue = to;
|
||||
this.render();
|
||||
}
|
||||
};
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
increment(amount = 1) {
|
||||
this.setValue(this.currentValue + amount);
|
||||
}
|
||||
|
||||
decrement(amount = 1) {
|
||||
this.setValue(this.currentValue - amount);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setValue(0);
|
||||
}
|
||||
|
||||
complete() {
|
||||
this.setValue(this.options.max);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced File Upload Component
|
||||
*/
|
||||
class FileUpload {
|
||||
constructor(container, options = {}) {
|
||||
this.container = typeof container === 'string' ? document.querySelector(container) : container;
|
||||
this.options = {
|
||||
multiple: false,
|
||||
accept: '*/*',
|
||||
maxSize: 50 * 1024 * 1024, // 50MB
|
||||
maxFiles: 10,
|
||||
dragDrop: true,
|
||||
showProgress: true,
|
||||
showPreview: true,
|
||||
uploadUrl: '/api/upload',
|
||||
chunkSize: 1024 * 1024, // 1MB chunks
|
||||
...options
|
||||
};
|
||||
|
||||
this.files = [];
|
||||
this.uploads = new Map();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.container) {
|
||||
console.error('FileUpload: Container nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.container.innerHTML = `
|
||||
<div class="file-upload-area" id="fileUploadArea">
|
||||
<div class="file-upload-dropzone ${this.options.dragDrop ? 'drag-enabled' : ''}" id="dropzone">
|
||||
<div class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-slate-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div class="mt-4">
|
||||
<label for="fileInput" class="cursor-pointer">
|
||||
<span class="mt-2 block text-sm font-medium text-slate-900 dark:text-white">
|
||||
${this.options.dragDrop ? 'Dateien hierher ziehen oder' : ''}
|
||||
<span class="text-blue-600 dark:text-blue-400 hover:text-blue-500">durchsuchen</span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="file"
|
||||
id="fileInput"
|
||||
name="files"
|
||||
${this.options.multiple ? 'multiple' : ''}
|
||||
accept="${this.options.accept}"
|
||||
class="sr-only">
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
|
||||
${this.getFileTypeText()} • Max. ${this.formatFileSize(this.options.maxSize)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-list mt-4" id="fileList"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const fileInput = this.container.querySelector('#fileInput');
|
||||
const dropzone = this.container.querySelector('#dropzone');
|
||||
|
||||
// File Input Change
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
this.handleFiles(Array.from(e.target.files));
|
||||
});
|
||||
|
||||
if (this.options.dragDrop) {
|
||||
// Drag and Drop Events
|
||||
dropzone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.add('drag-over');
|
||||
});
|
||||
|
||||
dropzone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
dropzone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove('drag-over');
|
||||
this.handleFiles(Array.from(e.dataTransfer.files));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleFiles(fileList) {
|
||||
for (const file of fileList) {
|
||||
if (this.validateFile(file)) {
|
||||
this.addFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
this.renderFileList();
|
||||
}
|
||||
|
||||
validateFile(file) {
|
||||
// Dateigröße prüfen
|
||||
if (file.size > this.options.maxSize) {
|
||||
this.showError(`Datei "${file.name}" ist zu groß. Maximum: ${this.formatFileSize(this.options.maxSize)}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Anzahl Dateien prüfen
|
||||
if (!this.options.multiple && this.files.length > 0) {
|
||||
this.files = []; // Ersetze einzelne Datei
|
||||
} else if (this.files.length >= this.options.maxFiles) {
|
||||
this.showError(`Maximal ${this.options.maxFiles} Dateien erlaubt`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
addFile(file) {
|
||||
const fileData = {
|
||||
id: this.generateId(),
|
||||
file: file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
error: null
|
||||
};
|
||||
|
||||
this.files.push(fileData);
|
||||
|
||||
// Preview generieren
|
||||
if (this.options.showPreview && file.type.startsWith('image/')) {
|
||||
this.generatePreview(fileData);
|
||||
}
|
||||
}
|
||||
|
||||
generatePreview(fileData) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
fileData.preview = e.target.result;
|
||||
this.renderFileList();
|
||||
};
|
||||
reader.readAsDataURL(fileData.file);
|
||||
}
|
||||
|
||||
renderFileList() {
|
||||
const fileListContainer = this.container.querySelector('#fileList');
|
||||
|
||||
if (this.files.length === 0) {
|
||||
fileListContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
fileListContainer.innerHTML = this.files.map(fileData => `
|
||||
<div class="file-item" data-file-id="${fileData.id}">
|
||||
<div class="flex items-center space-x-4 p-4 bg-slate-50 dark:bg-slate-800 rounded-lg">
|
||||
${fileData.preview ? `
|
||||
<img src="${fileData.preview}" class="w-12 h-12 object-cover rounded" alt="Preview">
|
||||
` : `
|
||||
<div class="w-12 h-12 bg-slate-200 dark:bg-slate-700 rounded flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-slate-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-medium text-slate-900 dark:text-white truncate">
|
||||
${fileData.name}
|
||||
</p>
|
||||
<button class="remove-file text-slate-400 hover:text-red-500" data-file-id="${fileData.id}">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||
${this.formatFileSize(fileData.size)} • ${this.getStatusText(fileData.status)}
|
||||
</p>
|
||||
|
||||
${this.options.showProgress && fileData.status === 'uploading' ? `
|
||||
<div class="mt-2">
|
||||
<div class="w-full bg-slate-200 dark:bg-slate-600 rounded-full h-2">
|
||||
<div class="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
||||
style="width: ${fileData.progress}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${fileData.error ? `
|
||||
<p class="text-xs text-red-500 mt-1">${fileData.error}</p>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Event Listeners für Remove-Buttons
|
||||
fileListContainer.querySelectorAll('.remove-file').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
const fileId = e.target.closest('.remove-file').dataset.fileId;
|
||||
this.removeFile(fileId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeFile(fileId) {
|
||||
this.files = this.files.filter(f => f.id !== fileId);
|
||||
this.renderFileList();
|
||||
}
|
||||
|
||||
async uploadFiles() {
|
||||
const pendingFiles = this.files.filter(f => f.status === 'pending');
|
||||
|
||||
for (const fileData of pendingFiles) {
|
||||
await this.uploadFile(fileData);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(fileData) {
|
||||
fileData.status = 'uploading';
|
||||
fileData.progress = 0;
|
||||
this.renderFileList();
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileData.file);
|
||||
|
||||
const response = await fetch(this.options.uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
fileData.status = 'completed';
|
||||
fileData.progress = 100;
|
||||
const result = await response.json();
|
||||
fileData.url = result.url;
|
||||
} else {
|
||||
throw new Error(`Upload fehlgeschlagen: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fileData.status = 'error';
|
||||
fileData.error = error.message;
|
||||
}
|
||||
|
||||
this.renderFileList();
|
||||
}
|
||||
|
||||
getFileTypeText() {
|
||||
if (this.options.accept === '*/*') return 'Alle Dateitypen';
|
||||
if (this.options.accept.includes('image/')) return 'Bilder';
|
||||
if (this.options.accept.includes('.pdf')) return 'PDF-Dateien';
|
||||
return 'Spezifische Dateitypen';
|
||||
}
|
||||
|
||||
getStatusText(status) {
|
||||
const statusTexts = {
|
||||
'pending': 'Wartend',
|
||||
'uploading': 'Wird hochgeladen...',
|
||||
'completed': 'Abgeschlossen',
|
||||
'error': 'Fehler'
|
||||
};
|
||||
return statusTexts[status] || status;
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
generateId() {
|
||||
return 'file_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
if (window.showToast) {
|
||||
window.showToast(message, 'error');
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
getFiles() {
|
||||
return this.files;
|
||||
}
|
||||
|
||||
getCompletedFiles() {
|
||||
return this.files.filter(f => f.status === 'completed');
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.files = [];
|
||||
this.renderFileList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple Datepicker Component
|
||||
*/
|
||||
class DatePicker {
|
||||
constructor(input, options = {}) {
|
||||
this.input = typeof input === 'string' ? document.querySelector(input) : input;
|
||||
this.options = {
|
||||
format: 'dd.mm.yyyy',
|
||||
minDate: null,
|
||||
maxDate: null,
|
||||
disabledDates: [],
|
||||
language: 'de',
|
||||
closeOnSelect: true,
|
||||
showWeekNumbers: false,
|
||||
...options
|
||||
};
|
||||
|
||||
this.isOpen = false;
|
||||
this.currentDate = new Date();
|
||||
this.selectedDate = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.input) {
|
||||
console.error('DatePicker: Input-Element nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupInput();
|
||||
this.createCalendar();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupInput() {
|
||||
this.input.setAttribute('readonly', 'true');
|
||||
this.input.classList.add('datepicker-input');
|
||||
|
||||
// Container für Input und Calendar
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = 'datepicker-container relative';
|
||||
this.input.parentNode.insertBefore(this.container, this.input);
|
||||
this.container.appendChild(this.input);
|
||||
}
|
||||
|
||||
createCalendar() {
|
||||
this.calendar = document.createElement('div');
|
||||
this.calendar.className = 'datepicker-calendar absolute top-full left-0 mt-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-lg shadow-lg z-50 hidden';
|
||||
this.calendar.innerHTML = this.renderCalendar();
|
||||
this.container.appendChild(this.calendar);
|
||||
}
|
||||
|
||||
renderCalendar() {
|
||||
const year = this.currentDate.getFullYear();
|
||||
const month = this.currentDate.getMonth();
|
||||
const monthName = this.getMonthName(month);
|
||||
|
||||
return `
|
||||
<div class="datepicker-header p-4 border-b border-slate-200 dark:border-slate-600">
|
||||
<div class="flex items-center justify-between">
|
||||
<button type="button" class="prev-month p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="text-sm font-medium text-slate-900 dark:text-white">
|
||||
${monthName} ${year}
|
||||
</div>
|
||||
<button type="button" class="next-month p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="datepicker-body p-4">
|
||||
<div class="grid grid-cols-7 gap-1 mb-2">
|
||||
${this.getWeekdayHeaders()}
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
${this.getDaysOfMonth(year, month)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getWeekdayHeaders() {
|
||||
const weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
return weekdays.map(day =>
|
||||
`<div class="text-xs font-medium text-slate-500 dark:text-slate-400 text-center p-1">${day}</div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
getDaysOfMonth(year, month) {
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const startDate = new Date(firstDay);
|
||||
startDate.setDate(startDate.getDate() - ((firstDay.getDay() + 6) % 7));
|
||||
|
||||
const days = [];
|
||||
const current = new Date(startDate);
|
||||
|
||||
while (current <= lastDay || current.getMonth() === month) {
|
||||
const isCurrentMonth = current.getMonth() === month;
|
||||
const isToday = this.isToday(current);
|
||||
const isSelected = this.isSelectedDate(current);
|
||||
const isDisabled = this.isDisabledDate(current);
|
||||
|
||||
const classes = [
|
||||
'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center transition-colors',
|
||||
isCurrentMonth ? 'text-slate-900 dark:text-white' : 'text-slate-400 dark:text-slate-600',
|
||||
isToday ? 'bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100' : '',
|
||||
isSelected ? 'bg-blue-500 text-white' : '',
|
||||
!isDisabled && isCurrentMonth ? 'hover:bg-slate-100 dark:hover:bg-slate-700' : '',
|
||||
isDisabled ? 'cursor-not-allowed opacity-50' : ''
|
||||
].filter(Boolean);
|
||||
|
||||
days.push(`
|
||||
<div class="${classes.join(' ')}"
|
||||
data-date="${this.formatDateForData(current)}"
|
||||
${isDisabled ? '' : 'data-selectable="true"'}>
|
||||
${current.getDate()}
|
||||
</div>
|
||||
`);
|
||||
|
||||
current.setDate(current.getDate() + 1);
|
||||
|
||||
if (days.length >= 42) break; // Max 6 Wochen
|
||||
}
|
||||
|
||||
return days.join('');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Input click
|
||||
this.input.addEventListener('click', () => {
|
||||
this.toggle();
|
||||
});
|
||||
|
||||
// Calendar clicks
|
||||
this.calendar.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('prev-month')) {
|
||||
this.previousMonth();
|
||||
} else if (e.target.classList.contains('next-month')) {
|
||||
this.nextMonth();
|
||||
} else if (e.target.dataset.selectable) {
|
||||
this.selectDate(new Date(e.target.dataset.date));
|
||||
}
|
||||
});
|
||||
|
||||
// Click outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.container.contains(e.target)) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
this.calendar.classList.remove('hidden');
|
||||
this.isOpen = true;
|
||||
this.updateCalendar();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.calendar.classList.add('hidden');
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
selectDate(date) {
|
||||
this.selectedDate = new Date(date);
|
||||
this.input.value = this.formatDate(date);
|
||||
|
||||
// Custom Event
|
||||
this.input.dispatchEvent(new CustomEvent('dateselected', {
|
||||
detail: { date: new Date(date) }
|
||||
}));
|
||||
|
||||
if (this.options.closeOnSelect) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
previousMonth() {
|
||||
this.currentDate.setMonth(this.currentDate.getMonth() - 1);
|
||||
this.updateCalendar();
|
||||
}
|
||||
|
||||
nextMonth() {
|
||||
this.currentDate.setMonth(this.currentDate.getMonth() + 1);
|
||||
this.updateCalendar();
|
||||
}
|
||||
|
||||
updateCalendar() {
|
||||
this.calendar.innerHTML = this.renderCalendar();
|
||||
}
|
||||
|
||||
isToday(date) {
|
||||
const today = new Date();
|
||||
return date.toDateString() === today.toDateString();
|
||||
}
|
||||
|
||||
isSelectedDate(date) {
|
||||
return this.selectedDate && date.toDateString() === this.selectedDate.toDateString();
|
||||
}
|
||||
|
||||
isDisabledDate(date) {
|
||||
if (this.options.minDate && date < this.options.minDate) return true;
|
||||
if (this.options.maxDate && date > this.options.maxDate) return true;
|
||||
return this.options.disabledDates.some(disabled =>
|
||||
date.toDateString() === disabled.toDateString()
|
||||
);
|
||||
}
|
||||
|
||||
formatDate(date) {
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
|
||||
return this.options.format
|
||||
.replace('dd', day)
|
||||
.replace('mm', month)
|
||||
.replace('yyyy', year);
|
||||
}
|
||||
|
||||
formatDateForData(date) {
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
getMonthName(monthIndex) {
|
||||
const months = [
|
||||
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
|
||||
];
|
||||
return months[monthIndex];
|
||||
}
|
||||
|
||||
setValue(date) {
|
||||
if (date) {
|
||||
this.selectDate(new Date(date));
|
||||
}
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.selectedDate;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.selectedDate = null;
|
||||
this.input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Globale API
|
||||
window.MYP.Advanced = {
|
||||
ProgressBar,
|
||||
FileUpload,
|
||||
DatePicker,
|
||||
|
||||
// Convenience Functions
|
||||
createProgressBar: (container, options) => new ProgressBar(container, options),
|
||||
createFileUpload: (container, options) => new FileUpload(container, options),
|
||||
createDatePicker: (input, options) => new DatePicker(input, options)
|
||||
};
|
||||
|
||||
// Auto-Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Auto-initialize datepickers
|
||||
document.querySelectorAll('[data-datepicker]').forEach(input => {
|
||||
const options = JSON.parse(input.dataset.datepicker || '{}');
|
||||
new DatePicker(input, options);
|
||||
});
|
||||
|
||||
// Auto-initialize file uploads
|
||||
document.querySelectorAll('[data-file-upload]').forEach(container => {
|
||||
const options = JSON.parse(container.dataset.fileUpload || '{}');
|
||||
new FileUpload(container, options);
|
||||
});
|
||||
|
||||
console.log('🚀 MYP Advanced Components geladen');
|
||||
});
|
||||
|
||||
})();
|
143
backend/static/js/auto-logout.js
Normal file
143
backend/static/js/auto-logout.js
Normal file
@ -0,0 +1,143 @@
|
||||
class AutoLogoutManager {
|
||||
constructor() {
|
||||
this.timer = null;
|
||||
this.warningTimer = null;
|
||||
this.timeout = 60; // Standard: 60 Minuten
|
||||
this.warningTime = 5; // Warnung 5 Minuten vor Logout
|
||||
this.isWarningShown = false;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadSettings();
|
||||
this.setupActivityListeners();
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/user/settings');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.settings.privacy?.auto_logout) {
|
||||
const timeout = parseInt(data.settings.privacy.auto_logout);
|
||||
if (timeout > 0 && timeout !== 'never') {
|
||||
this.timeout = timeout;
|
||||
} else {
|
||||
this.timeout = 0; // Deaktiviert
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Auto-Logout-Einstellungen konnten nicht geladen werden:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setupActivityListeners() {
|
||||
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
|
||||
|
||||
events.forEach(event => {
|
||||
document.addEventListener(event, () => this.resetTimer(), { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
if (this.timeout <= 0) return;
|
||||
|
||||
this.clearTimers();
|
||||
|
||||
const timeoutMs = this.timeout * 60 * 1000;
|
||||
const warningMs = this.warningTime * 60 * 1000;
|
||||
|
||||
this.warningTimer = setTimeout(() => this.showWarning(), timeoutMs - warningMs);
|
||||
this.timer = setTimeout(() => this.performLogout(), timeoutMs);
|
||||
}
|
||||
|
||||
resetTimer() {
|
||||
if (this.isWarningShown) {
|
||||
this.closeWarning();
|
||||
}
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
clearTimers() {
|
||||
if (this.timer) clearTimeout(this.timer);
|
||||
if (this.warningTimer) clearTimeout(this.warningTimer);
|
||||
}
|
||||
|
||||
showWarning() {
|
||||
if (this.isWarningShown) return;
|
||||
this.isWarningShown = true;
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'auto-logout-warning';
|
||||
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
||||
modal.innerHTML = `
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 max-w-md mx-4 shadow-xl">
|
||||
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-4">Automatische Abmeldung</h3>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300 mb-4">
|
||||
Sie werden in ${this.warningTime} Minuten aufgrund von Inaktivität abgemeldet.
|
||||
</p>
|
||||
<div class="flex space-x-3">
|
||||
<button id="stay-logged-in" class="bg-blue-600 text-white px-4 py-2 rounded-lg">
|
||||
Angemeldet bleiben
|
||||
</button>
|
||||
<button id="logout-now" class="bg-gray-300 text-slate-700 px-4 py-2 rounded-lg">
|
||||
Jetzt abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
document.getElementById('stay-logged-in').onclick = () => {
|
||||
this.closeWarning();
|
||||
this.sendKeepAlive();
|
||||
this.resetTimer();
|
||||
};
|
||||
|
||||
document.getElementById('logout-now').onclick = () => {
|
||||
this.performLogout();
|
||||
};
|
||||
}
|
||||
|
||||
closeWarning() {
|
||||
const modal = document.getElementById('auto-logout-warning');
|
||||
if (modal) modal.remove();
|
||||
this.isWarningShown = false;
|
||||
}
|
||||
|
||||
async sendKeepAlive() {
|
||||
try {
|
||||
await fetch('/api/auth/keep-alive', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCSRFToken()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Keep-Alive fehlgeschlagen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getCSRFToken() {
|
||||
const metaTag = document.querySelector('meta[name="csrf-token"]');
|
||||
return metaTag ? metaTag.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
async performLogout() {
|
||||
this.closeWarning();
|
||||
this.clearTimers();
|
||||
window.location.href = '/auth/logout';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisierung
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.location.pathname.includes('/login')) {
|
||||
window.autoLogoutManager = new AutoLogoutManager();
|
||||
}
|
||||
});
|
413
backend/static/js/charts.js
Normal file
413
backend/static/js/charts.js
Normal file
@ -0,0 +1,413 @@
|
||||
/**
|
||||
* Charts.js - Diagramm-Management mit Chart.js für MYP Platform
|
||||
*
|
||||
* Verwaltet alle Diagramme auf der Statistiken-Seite.
|
||||
* Unterstützt Dark Mode und Live-Updates.
|
||||
*/
|
||||
|
||||
// Chart.js Instanzen Global verfügbar machen
|
||||
window.statsCharts = {};
|
||||
|
||||
// Chart.js Konfiguration für Dark/Light Theme
|
||||
function getChartTheme() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
|
||||
return {
|
||||
isDark: isDark,
|
||||
backgroundColor: isDark ? 'rgba(30, 41, 59, 0.8)' : 'rgba(255, 255, 255, 0.8)',
|
||||
textColor: isDark ? '#e2e8f0' : '#374151',
|
||||
gridColor: isDark ? 'rgba(148, 163, 184, 0.1)' : 'rgba(156, 163, 175, 0.2)',
|
||||
borderColor: isDark ? 'rgba(148, 163, 184, 0.3)' : 'rgba(156, 163, 175, 0.5)'
|
||||
};
|
||||
}
|
||||
|
||||
// Standard Chart.js Optionen
|
||||
function getDefaultChartOptions() {
|
||||
const theme = getChartTheme();
|
||||
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: theme.textColor,
|
||||
font: {
|
||||
family: 'Inter, sans-serif'
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
titleColor: theme.textColor,
|
||||
bodyColor: theme.textColor,
|
||||
borderColor: theme.borderColor,
|
||||
borderWidth: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
color: theme.textColor,
|
||||
font: {
|
||||
family: 'Inter, sans-serif'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: theme.gridColor
|
||||
}
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
color: theme.textColor,
|
||||
font: {
|
||||
family: 'Inter, sans-serif'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: theme.gridColor
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Job Status Doughnut Chart
|
||||
async function createJobStatusChart() {
|
||||
try {
|
||||
const response = await fetch('/api/stats/charts/job-status');
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Fehler beim Laden der Job-Status-Daten');
|
||||
}
|
||||
|
||||
const ctx = document.getElementById('job-status-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
// Vorhandenes Chart zerstören falls vorhanden
|
||||
if (window.statsCharts.jobStatus) {
|
||||
window.statsCharts.jobStatus.destroy();
|
||||
}
|
||||
|
||||
const theme = getChartTheme();
|
||||
|
||||
window.statsCharts.jobStatus = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
color: theme.textColor,
|
||||
font: {
|
||||
family: 'Inter, sans-serif',
|
||||
size: 12
|
||||
},
|
||||
padding: 15
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
titleColor: theme.textColor,
|
||||
bodyColor: theme.textColor,
|
||||
borderColor: theme.borderColor,
|
||||
borderWidth: 1,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
|
||||
return `${label}: ${value} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
cutout: '60%'
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Job-Status-Charts:', error);
|
||||
showChartError('job-status-chart', 'Fehler beim Laden der Job-Status-Daten');
|
||||
}
|
||||
}
|
||||
|
||||
// Drucker-Nutzung Bar Chart
|
||||
async function createPrinterUsageChart() {
|
||||
try {
|
||||
const response = await fetch('/api/stats/charts/printer-usage');
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Fehler beim Laden der Drucker-Nutzung-Daten');
|
||||
}
|
||||
|
||||
const ctx = document.getElementById('printer-usage-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
// Vorhandenes Chart zerstören falls vorhanden
|
||||
if (window.statsCharts.printerUsage) {
|
||||
window.statsCharts.printerUsage.destroy();
|
||||
}
|
||||
|
||||
const options = getDefaultChartOptions();
|
||||
options.scales.y.title = {
|
||||
display: true,
|
||||
text: 'Anzahl Jobs',
|
||||
color: getChartTheme().textColor,
|
||||
font: {
|
||||
family: 'Inter, sans-serif'
|
||||
}
|
||||
};
|
||||
|
||||
window.statsCharts.printerUsage = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: data,
|
||||
options: options
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Drucker-Nutzung-Charts:', error);
|
||||
showChartError('printer-usage-chart', 'Fehler beim Laden der Drucker-Nutzung-Daten');
|
||||
}
|
||||
}
|
||||
|
||||
// Jobs Timeline Line Chart
|
||||
async function createJobsTimelineChart() {
|
||||
try {
|
||||
const response = await fetch('/api/stats/charts/jobs-timeline');
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Fehler beim Laden der Jobs-Timeline-Daten');
|
||||
}
|
||||
|
||||
const ctx = document.getElementById('jobs-timeline-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
// Vorhandenes Chart zerstören falls vorhanden
|
||||
if (window.statsCharts.jobsTimeline) {
|
||||
window.statsCharts.jobsTimeline.destroy();
|
||||
}
|
||||
|
||||
const options = getDefaultChartOptions();
|
||||
options.scales.y.title = {
|
||||
display: true,
|
||||
text: 'Jobs pro Tag',
|
||||
color: getChartTheme().textColor,
|
||||
font: {
|
||||
family: 'Inter, sans-serif'
|
||||
}
|
||||
};
|
||||
options.scales.x.title = {
|
||||
display: true,
|
||||
text: 'Datum (letzte 30 Tage)',
|
||||
color: getChartTheme().textColor,
|
||||
font: {
|
||||
family: 'Inter, sans-serif'
|
||||
}
|
||||
};
|
||||
|
||||
window.statsCharts.jobsTimeline = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Jobs-Timeline-Charts:', error);
|
||||
showChartError('jobs-timeline-chart', 'Fehler beim Laden der Jobs-Timeline-Daten');
|
||||
}
|
||||
}
|
||||
|
||||
// Benutzer-Aktivität Bar Chart
|
||||
async function createUserActivityChart() {
|
||||
try {
|
||||
const response = await fetch('/api/stats/charts/user-activity');
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Fehler beim Laden der Benutzer-Aktivität-Daten');
|
||||
}
|
||||
|
||||
const ctx = document.getElementById('user-activity-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
// Vorhandenes Chart zerstören falls vorhanden
|
||||
if (window.statsCharts.userActivity) {
|
||||
window.statsCharts.userActivity.destroy();
|
||||
}
|
||||
|
||||
const options = getDefaultChartOptions();
|
||||
options.indexAxis = 'y'; // Horizontales Balkendiagramm
|
||||
options.scales.x.title = {
|
||||
display: true,
|
||||
text: 'Anzahl Jobs',
|
||||
color: getChartTheme().textColor,
|
||||
font: {
|
||||
family: 'Inter, sans-serif'
|
||||
}
|
||||
};
|
||||
options.scales.y.title = {
|
||||
display: true,
|
||||
text: 'Benutzer',
|
||||
color: getChartTheme().textColor,
|
||||
font: {
|
||||
family: 'Inter, sans-serif'
|
||||
}
|
||||
};
|
||||
|
||||
window.statsCharts.userActivity = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: data,
|
||||
options: options
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Benutzer-Aktivität-Charts:', error);
|
||||
showChartError('user-activity-chart', 'Fehler beim Laden der Benutzer-Aktivität-Daten');
|
||||
}
|
||||
}
|
||||
|
||||
// Fehleranzeige in Chart-Container
|
||||
function showChartError(chartId, message) {
|
||||
const container = document.getElementById(chartId);
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div class="text-center">
|
||||
<svg class="h-12 w-12 mx-auto text-red-500 mb-4" 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-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
<p class="text-red-500 font-medium">${message}</p>
|
||||
<button onclick="refreshAllCharts()" class="mt-2 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Alle Charts erstellen
|
||||
async function initializeAllCharts() {
|
||||
// Loading-Indikatoren anzeigen
|
||||
showChartLoading();
|
||||
|
||||
// Charts parallel erstellen
|
||||
await Promise.allSettled([
|
||||
createJobStatusChart(),
|
||||
createPrinterUsageChart(),
|
||||
createJobsTimelineChart(),
|
||||
createUserActivityChart()
|
||||
]);
|
||||
}
|
||||
|
||||
// Loading-Indikatoren anzeigen
|
||||
function showChartLoading() {
|
||||
const chartIds = ['job-status-chart', 'printer-usage-chart', 'jobs-timeline-chart', 'user-activity-chart'];
|
||||
|
||||
chartIds.forEach(chartId => {
|
||||
const container = document.getElementById(chartId);
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm">Diagramm wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Alle Charts aktualisieren
|
||||
async function refreshAllCharts() {
|
||||
console.log('Aktualisiere alle Diagramme...');
|
||||
|
||||
// Bestehende Charts zerstören
|
||||
Object.values(window.statsCharts).forEach(chart => {
|
||||
if (chart && typeof chart.destroy === 'function') {
|
||||
chart.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Charts neu erstellen
|
||||
await initializeAllCharts();
|
||||
|
||||
console.log('Alle Diagramme aktualisiert');
|
||||
}
|
||||
|
||||
// Theme-Wechsel handhaben
|
||||
function updateChartsTheme() {
|
||||
// Alle Charts mit neuem Theme aktualisieren
|
||||
refreshAllCharts();
|
||||
}
|
||||
|
||||
// Auto-refresh (alle 5 Minuten)
|
||||
let chartRefreshInterval;
|
||||
|
||||
function startChartAutoRefresh() {
|
||||
// Bestehenden Interval stoppen
|
||||
if (chartRefreshInterval) {
|
||||
clearInterval(chartRefreshInterval);
|
||||
}
|
||||
|
||||
// Neuen Interval starten (5 Minuten)
|
||||
chartRefreshInterval = setInterval(() => {
|
||||
refreshAllCharts();
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
function stopChartAutoRefresh() {
|
||||
if (chartRefreshInterval) {
|
||||
clearInterval(chartRefreshInterval);
|
||||
chartRefreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup beim Verlassen der Seite
|
||||
function cleanup() {
|
||||
stopChartAutoRefresh();
|
||||
|
||||
// Alle Charts zerstören
|
||||
Object.values(window.statsCharts).forEach(chart => {
|
||||
if (chart && typeof chart.destroy === 'function') {
|
||||
chart.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
window.statsCharts = {};
|
||||
}
|
||||
|
||||
// Globale Funktionen verfügbar machen
|
||||
window.refreshAllCharts = refreshAllCharts;
|
||||
window.updateChartsTheme = updateChartsTheme;
|
||||
window.startChartAutoRefresh = startChartAutoRefresh;
|
||||
window.stopChartAutoRefresh = stopChartAutoRefresh;
|
||||
window.cleanup = cleanup;
|
||||
|
||||
// Event Listeners
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Charts initialisieren wenn auf Stats-Seite
|
||||
if (document.getElementById('job-status-chart')) {
|
||||
initializeAllCharts();
|
||||
startChartAutoRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
// Dark Mode Event Listener
|
||||
if (typeof window.addEventListener !== 'undefined') {
|
||||
window.addEventListener('darkModeChanged', function(e) {
|
||||
updateChartsTheme();
|
||||
});
|
||||
}
|
||||
|
||||
// Page unload cleanup
|
||||
window.addEventListener('beforeunload', cleanup);
|
14
backend/static/js/charts/apexcharts.min.js
vendored
Normal file
14
backend/static/js/charts/apexcharts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
291
backend/static/js/charts/chart-adapter.js
Normal file
291
backend/static/js/charts/chart-adapter.js
Normal file
@ -0,0 +1,291 @@
|
||||
/**
|
||||
* MYP Platform Chart-Adapter
|
||||
* Verbindet bestehende API/Logik mit ApexCharts
|
||||
* Version: 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Überbrückt die bestehende renderChart Funktion mit der neuen ApexCharts Implementation
|
||||
* @param {HTMLElement} container - Der Diagramm-Container
|
||||
* @param {Object} data - Die Diagrammdaten vom API-Endpunkt
|
||||
*/
|
||||
function renderChart(container, data) {
|
||||
// Überprüfen, ob die Daten vorhanden sind
|
||||
if (!data || (Array.isArray(data) && data.length === 0) || Object.keys(data).length === 0) {
|
||||
showEmptyState(container, 'Keine Daten', 'Es sind keine Daten für dieses Diagramm verfügbar.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Container-ID überprüfen und ggf. generieren
|
||||
if (!container.id) {
|
||||
container.id = 'chart-' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
// Chart-Typ basierend auf Container-ID oder Attributen bestimmen
|
||||
let chartType = container.getAttribute('data-chart-type');
|
||||
if (!chartType) {
|
||||
if (container.id === 'job-status-chart') {
|
||||
chartType = 'pie';
|
||||
container.setAttribute('data-chart-type', 'pie');
|
||||
} else if (container.id === 'printer-usage-chart') {
|
||||
chartType = 'bar';
|
||||
container.setAttribute('data-chart-type', 'bar');
|
||||
} else if (container.id.includes('line')) {
|
||||
chartType = 'line';
|
||||
container.setAttribute('data-chart-type', 'line');
|
||||
} else if (container.id.includes('area')) {
|
||||
chartType = 'area';
|
||||
container.setAttribute('data-chart-type', 'area');
|
||||
} else if (container.id.includes('bar')) {
|
||||
chartType = 'bar';
|
||||
container.setAttribute('data-chart-type', 'bar');
|
||||
} else if (container.id.includes('pie')) {
|
||||
chartType = 'pie';
|
||||
container.setAttribute('data-chart-type', 'pie');
|
||||
} else if (container.id.includes('donut')) {
|
||||
chartType = 'donut';
|
||||
container.setAttribute('data-chart-type', 'donut');
|
||||
} else {
|
||||
// Standard-Typ
|
||||
chartType = 'line';
|
||||
container.setAttribute('data-chart-type', 'line');
|
||||
}
|
||||
}
|
||||
|
||||
// Daten für das Diagramm vorbereiten
|
||||
let chartData = {};
|
||||
|
||||
// Daten-Transformation basierend auf Container-ID
|
||||
if (container.id === 'job-status-chart') {
|
||||
chartData = transformJobStatusData(data);
|
||||
} else if (container.id === 'printer-usage-chart') {
|
||||
chartData = transformPrinterUsageData(data);
|
||||
} else {
|
||||
// Generischer Ansatz für andere Diagrammtypen
|
||||
chartData = transformGenericData(data, chartType);
|
||||
}
|
||||
|
||||
// Existierendes Chart zerstören, falls vorhanden
|
||||
if (activeCharts[container.id]) {
|
||||
destroyChart(container.id);
|
||||
}
|
||||
|
||||
// Daten als Attribut am Container speichern
|
||||
container.setAttribute('data-chart-data', JSON.stringify(chartData));
|
||||
|
||||
// Neues Chart erstellen
|
||||
createChart(container, chartType, chartData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformiert Job-Status-Daten für Pie-Chart
|
||||
* @param {Object} data - Rohdaten vom API-Endpunkt
|
||||
* @returns {Object} - Formatierte Daten für ApexCharts
|
||||
*/
|
||||
function transformJobStatusData(data) {
|
||||
// Werte für Pie-Chart extrahieren
|
||||
const series = [
|
||||
data.scheduled || 0,
|
||||
data.active || 0,
|
||||
data.completed || 0,
|
||||
data.cancelled || 0
|
||||
];
|
||||
|
||||
// Labels für die Diagramm-Segmente
|
||||
const labels = ['Geplant', 'Aktiv', 'Abgeschlossen', 'Abgebrochen'];
|
||||
|
||||
// Benutzerdefinierte Farben
|
||||
const colors = [
|
||||
MYP_CHART_COLORS.info, // Blau für geplant
|
||||
MYP_CHART_COLORS.primary, // Primär für aktiv
|
||||
MYP_CHART_COLORS.success, // Grün für abgeschlossen
|
||||
MYP_CHART_COLORS.warning // Gelb für abgebrochen
|
||||
];
|
||||
|
||||
// Zusätzliche Optionen
|
||||
const options = {
|
||||
colors: colors,
|
||||
chart: {
|
||||
height: 320
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '0%' // Vollständiger Kreis (kein Donut)
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
series: series,
|
||||
labels: labels,
|
||||
options: options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformiert Drucker-Nutzungsdaten für Bar-Chart
|
||||
* @param {Object} data - Rohdaten vom API-Endpunkt
|
||||
* @returns {Object} - Formatierte Daten für ApexCharts
|
||||
*/
|
||||
function transformPrinterUsageData(data) {
|
||||
// Prüfen, ob Daten ein Array sind
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('Drucker-Nutzungsdaten müssen ein Array sein:', data);
|
||||
return {
|
||||
series: [],
|
||||
categories: [],
|
||||
options: {}
|
||||
};
|
||||
}
|
||||
|
||||
// Druckernamen für X-Achse extrahieren
|
||||
const categories = data.map(item => item.printer_name || 'Unbekannt');
|
||||
|
||||
// Datenreihen für Jobs und Stunden erstellen
|
||||
const jobsSeries = {
|
||||
name: 'Jobs',
|
||||
data: data.map(item => item.job_count || 0)
|
||||
};
|
||||
|
||||
const hoursSeries = {
|
||||
name: 'Stunden',
|
||||
data: data.map(item => Math.round((item.print_hours || 0) * 10) / 10)
|
||||
};
|
||||
|
||||
// Zusätzliche Optionen
|
||||
const options = {
|
||||
colors: [MYP_CHART_COLORS.primary, MYP_CHART_COLORS.success],
|
||||
chart: {
|
||||
height: 320
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: '60%',
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
xaxis: {
|
||||
categories: categories
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
series: [jobsSeries, hoursSeries],
|
||||
categories: categories,
|
||||
options: options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformiert generische Daten für verschiedene Diagrammtypen
|
||||
* @param {Object|Array} data - Rohdaten vom API-Endpunkt
|
||||
* @param {string} chartType - Art des Diagramms
|
||||
* @returns {Object} - Formatierte Daten für ApexCharts
|
||||
*/
|
||||
function transformGenericData(data, chartType) {
|
||||
// Standard-Ergebnisobjekt
|
||||
const result = {
|
||||
series: [],
|
||||
categories: [],
|
||||
labels: [],
|
||||
options: {}
|
||||
};
|
||||
|
||||
// Arrays verarbeiten
|
||||
if (Array.isArray(data)) {
|
||||
if (chartType === 'pie' || chartType === 'donut' || chartType === 'radial') {
|
||||
// Für Kreisdiagramme
|
||||
result.series = data.map(item => item.value || 0);
|
||||
result.labels = data.map(item => item.name || item.label || 'Unbekannt');
|
||||
} else {
|
||||
// Für Linien-, Flächen- und Balkendiagramme
|
||||
// Annahme: Erste Datenreihe
|
||||
result.series = [{
|
||||
name: 'Werte',
|
||||
data: data.map(item => item.value || 0)
|
||||
}];
|
||||
result.categories = data.map(item => item.name || item.label || item.date || 'Unbekannt');
|
||||
}
|
||||
}
|
||||
// Objekte mit Datenreihen verarbeiten
|
||||
else if (data.series && Array.isArray(data.series)) {
|
||||
result.series = data.series;
|
||||
|
||||
if (data.categories) {
|
||||
result.categories = data.categories;
|
||||
}
|
||||
|
||||
if (data.labels) {
|
||||
result.labels = data.labels;
|
||||
}
|
||||
|
||||
if (data.options) {
|
||||
result.options = data.options;
|
||||
}
|
||||
}
|
||||
// Einfache Objekte verarbeiten
|
||||
else {
|
||||
// Für Kreisdiagramme: Alle Eigenschaften als Segmente verwenden
|
||||
if (chartType === 'pie' || chartType === 'donut' || chartType === 'radial') {
|
||||
const seriesData = [];
|
||||
const labelData = [];
|
||||
|
||||
Object.keys(data).forEach(key => {
|
||||
if (typeof data[key] === 'number') {
|
||||
seriesData.push(data[key]);
|
||||
labelData.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
result.series = seriesData;
|
||||
result.labels = labelData;
|
||||
}
|
||||
// Für andere Diagrammtypen: Versuchen, Zeit-/Wertepaare zu finden
|
||||
else {
|
||||
const timeKeys = [];
|
||||
const values = [];
|
||||
|
||||
Object.keys(data).forEach(key => {
|
||||
if (typeof data[key] === 'number') {
|
||||
timeKeys.push(key);
|
||||
values.push(data[key]);
|
||||
}
|
||||
});
|
||||
|
||||
result.series = [{
|
||||
name: 'Werte',
|
||||
data: values
|
||||
}];
|
||||
result.categories = timeKeys;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt einen leeren Status-Container an
|
||||
* @param {HTMLElement} container - Der Diagramm-Container
|
||||
* @param {string} title - Titel der Meldung
|
||||
* @param {string} message - Meldungstext
|
||||
*/
|
||||
function showEmptyState(container, title, message) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<svg class="w-12 h-12 mx-auto text-slate-500 dark:text-slate-400 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-slate-700 dark:text-slate-300">${title}</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 italic mt-1">${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
431
backend/static/js/charts/chart-config.js
Normal file
431
backend/static/js/charts/chart-config.js
Normal file
@ -0,0 +1,431 @@
|
||||
/**
|
||||
* MYP Platform Chart-Konfigurationen
|
||||
* Basierend auf ApexCharts Bibliothek
|
||||
* Version: 1.0.0
|
||||
*/
|
||||
|
||||
// Standard Farben für Diagramme
|
||||
const MYP_CHART_COLORS = {
|
||||
primary: '#3b82f6', // Blau
|
||||
secondary: '#8b5cf6', // Lila
|
||||
success: '#10b981', // Grün
|
||||
warning: '#f59e0b', // Orange
|
||||
danger: '#ef4444', // Rot
|
||||
info: '#06b6d4', // Türkis
|
||||
gray: '#6b7280', // Grau
|
||||
gradient: {
|
||||
blue: ['#3b82f6', '#93c5fd'],
|
||||
purple: ['#8b5cf6', '#c4b5fd'],
|
||||
green: ['#10b981', '#6ee7b7'],
|
||||
red: ['#ef4444', '#fca5a5'],
|
||||
orange: ['#f59e0b', '#fcd34d'],
|
||||
}
|
||||
};
|
||||
|
||||
// Gemeinsame Grundeinstellungen für alle Diagramme
|
||||
const getBaseChartOptions = () => {
|
||||
return {
|
||||
chart: {
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
},
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'easeinout',
|
||||
speed: 800,
|
||||
animateGradually: {
|
||||
enabled: true,
|
||||
delay: 150
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: true,
|
||||
speed: 350
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
theme: 'dark',
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
show: true,
|
||||
borderColor: '#334155',
|
||||
strokeDashArray: 4,
|
||||
position: 'back',
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
horizontalAlign: 'center',
|
||||
offsetY: 8,
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
markers: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
strokeWidth: 0,
|
||||
radius: 4
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 10,
|
||||
vertical: 0
|
||||
}
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 3
|
||||
},
|
||||
xaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
colors: '#94a3b8'
|
||||
}
|
||||
},
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
axisTicks: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
colors: '#94a3b8'
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 768,
|
||||
options: {
|
||||
chart: {
|
||||
height: '300px'
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
offsetY: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Liniendiagramm Konfiguration
|
||||
* @param {Array} series - Datenreihen
|
||||
* @param {Array} categories - X-Achsen Kategorien
|
||||
* @param {Object} customOptions - Benutzerdefinierte Optionen
|
||||
* @returns {Object} Konfigurationsobjekt für ApexCharts
|
||||
*/
|
||||
function getLineChartConfig(series, categories, customOptions = {}) {
|
||||
const baseOptions = getBaseChartOptions();
|
||||
|
||||
const defaultOptions = {
|
||||
chart: {
|
||||
...baseOptions.chart,
|
||||
type: 'line',
|
||||
height: 350
|
||||
},
|
||||
colors: [
|
||||
MYP_CHART_COLORS.primary,
|
||||
MYP_CHART_COLORS.secondary,
|
||||
MYP_CHART_COLORS.success
|
||||
],
|
||||
series: series || [],
|
||||
xaxis: {
|
||||
...baseOptions.xaxis,
|
||||
categories: categories || []
|
||||
}
|
||||
};
|
||||
|
||||
// Optionen zusammenführen
|
||||
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flächendiagramm Konfiguration
|
||||
* @param {Array} series - Datenreihen
|
||||
* @param {Array} categories - X-Achsen Kategorien
|
||||
* @param {Object} customOptions - Benutzerdefinierte Optionen
|
||||
* @returns {Object} Konfigurationsobjekt für ApexCharts
|
||||
*/
|
||||
function getAreaChartConfig(series, categories, customOptions = {}) {
|
||||
const baseOptions = getBaseChartOptions();
|
||||
|
||||
const defaultOptions = {
|
||||
chart: {
|
||||
...baseOptions.chart,
|
||||
type: 'area',
|
||||
height: 350
|
||||
},
|
||||
colors: [
|
||||
MYP_CHART_COLORS.primary,
|
||||
MYP_CHART_COLORS.success
|
||||
],
|
||||
series: series || [],
|
||||
xaxis: {
|
||||
...baseOptions.xaxis,
|
||||
categories: categories || []
|
||||
},
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.7,
|
||||
opacityTo: 0.3,
|
||||
stops: [0, 90, 100]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Optionen zusammenführen
|
||||
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Balkendiagramm Konfiguration
|
||||
* @param {Array} series - Datenreihen
|
||||
* @param {Array} categories - X-Achsen Kategorien
|
||||
* @param {Object} customOptions - Benutzerdefinierte Optionen
|
||||
* @returns {Object} Konfigurationsobjekt für ApexCharts
|
||||
*/
|
||||
function getBarChartConfig(series, categories, customOptions = {}) {
|
||||
const baseOptions = getBaseChartOptions();
|
||||
|
||||
const defaultOptions = {
|
||||
chart: {
|
||||
...baseOptions.chart,
|
||||
type: 'bar',
|
||||
height: 350
|
||||
},
|
||||
colors: [
|
||||
MYP_CHART_COLORS.primary,
|
||||
MYP_CHART_COLORS.secondary
|
||||
],
|
||||
series: series || [],
|
||||
xaxis: {
|
||||
...baseOptions.xaxis,
|
||||
categories: categories || []
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: '70%',
|
||||
borderRadius: 6,
|
||||
dataLabels: {
|
||||
position: 'top'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Optionen zusammenführen
|
||||
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Kreisdiagramm Konfiguration
|
||||
* @param {Array} series - Datenreihen (Werte)
|
||||
* @param {Array} labels - Beschriftungen
|
||||
* @param {Object} customOptions - Benutzerdefinierte Optionen
|
||||
* @returns {Object} Konfigurationsobjekt für ApexCharts
|
||||
*/
|
||||
function getPieChartConfig(series, labels, customOptions = {}) {
|
||||
const baseOptions = getBaseChartOptions();
|
||||
|
||||
const defaultOptions = {
|
||||
chart: {
|
||||
...baseOptions.chart,
|
||||
type: 'pie',
|
||||
height: 350
|
||||
},
|
||||
colors: [
|
||||
MYP_CHART_COLORS.primary,
|
||||
MYP_CHART_COLORS.success,
|
||||
MYP_CHART_COLORS.warning,
|
||||
MYP_CHART_COLORS.danger,
|
||||
MYP_CHART_COLORS.info
|
||||
],
|
||||
series: series || [],
|
||||
labels: labels || [],
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 480,
|
||||
options: {
|
||||
chart: {
|
||||
width: 300
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Optionen zusammenführen
|
||||
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Donut-Diagramm Konfiguration
|
||||
* @param {Array} series - Datenreihen (Werte)
|
||||
* @param {Array} labels - Beschriftungen
|
||||
* @param {Object} customOptions - Benutzerdefinierte Optionen
|
||||
* @returns {Object} Konfigurationsobjekt für ApexCharts
|
||||
*/
|
||||
function getDonutChartConfig(series, labels, customOptions = {}) {
|
||||
const baseOptions = getBaseChartOptions();
|
||||
|
||||
const defaultOptions = {
|
||||
chart: {
|
||||
...baseOptions.chart,
|
||||
type: 'donut',
|
||||
height: 350
|
||||
},
|
||||
colors: [
|
||||
MYP_CHART_COLORS.primary,
|
||||
MYP_CHART_COLORS.success,
|
||||
MYP_CHART_COLORS.warning,
|
||||
MYP_CHART_COLORS.danger,
|
||||
MYP_CHART_COLORS.info
|
||||
],
|
||||
series: series || [],
|
||||
labels: labels || [],
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '70%',
|
||||
labels: {
|
||||
show: true,
|
||||
name: {
|
||||
show: true
|
||||
},
|
||||
value: {
|
||||
show: true,
|
||||
formatter: function(val) {
|
||||
return val;
|
||||
}
|
||||
},
|
||||
total: {
|
||||
show: true,
|
||||
formatter: function(w) {
|
||||
return w.globals.seriesTotals.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Optionen zusammenführen
|
||||
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Radial-Diagramm Konfiguration
|
||||
* @param {Array} series - Datenreihen (Werte)
|
||||
* @param {Array} labels - Beschriftungen
|
||||
* @param {Object} customOptions - Benutzerdefinierte Optionen
|
||||
* @returns {Object} Konfigurationsobjekt für ApexCharts
|
||||
*/
|
||||
function getRadialChartConfig(series, labels, customOptions = {}) {
|
||||
const baseOptions = getBaseChartOptions();
|
||||
|
||||
const defaultOptions = {
|
||||
chart: {
|
||||
...baseOptions.chart,
|
||||
type: 'radialBar',
|
||||
height: 350
|
||||
},
|
||||
colors: [
|
||||
MYP_CHART_COLORS.primary,
|
||||
MYP_CHART_COLORS.success,
|
||||
MYP_CHART_COLORS.warning
|
||||
],
|
||||
series: series || [],
|
||||
labels: labels || [],
|
||||
plotOptions: {
|
||||
radialBar: {
|
||||
dataLabels: {
|
||||
name: {
|
||||
fontSize: '22px',
|
||||
},
|
||||
value: {
|
||||
fontSize: '16px',
|
||||
},
|
||||
total: {
|
||||
show: true,
|
||||
label: 'Gesamt',
|
||||
formatter: function(w) {
|
||||
return w.globals.seriesTotals.reduce((a, b) => a + b, 0) + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Optionen zusammenführen
|
||||
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper-Funktion zum tiefen Zusammenführen von Objekten
|
||||
*/
|
||||
function mergeDeep(target, source) {
|
||||
const isObject = obj => obj && typeof obj === 'object';
|
||||
|
||||
if (!isObject(target) || !isObject(source)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
const output = { ...target };
|
||||
|
||||
Object.keys(source).forEach(key => {
|
||||
if (isObject(source[key])) {
|
||||
if (!(key in target)) {
|
||||
output[key] = source[key];
|
||||
} else {
|
||||
output[key] = mergeDeep(target[key], source[key]);
|
||||
}
|
||||
} else {
|
||||
output[key] = source[key];
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
400
backend/static/js/charts/chart-renderer.js
Normal file
400
backend/static/js/charts/chart-renderer.js
Normal file
@ -0,0 +1,400 @@
|
||||
/**
|
||||
* MYP Platform Chart-Renderer
|
||||
* Erstellt und verwaltet Diagramme mit ApexCharts
|
||||
* Version: 1.0.0
|
||||
*/
|
||||
|
||||
// Speicher für aktive Chart-Instanzen
|
||||
const activeCharts = {};
|
||||
|
||||
/**
|
||||
* Initialisiert alle Diagramme auf der Seite
|
||||
*/
|
||||
function initCharts() {
|
||||
// Prüfen, ob ApexCharts verfügbar ist
|
||||
if (typeof ApexCharts === 'undefined') {
|
||||
console.error('ApexCharts ist nicht geladen. Bitte ApexCharts vor chart-renderer.js einbinden.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Alle Diagramm-Container mit data-chart-type Attribut finden
|
||||
const chartContainers = document.querySelectorAll('[data-chart-type]');
|
||||
|
||||
// Für jeden Container ein Diagramm erstellen
|
||||
chartContainers.forEach(container => {
|
||||
const chartId = container.id;
|
||||
const chartType = container.getAttribute('data-chart-type');
|
||||
|
||||
// Prüfen, ob Container eine ID hat
|
||||
if (!chartId) {
|
||||
console.error('Chart-Container benötigt eine ID:', container);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bereits erstellte Charts nicht neu initialisieren
|
||||
if (activeCharts[chartId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Daten aus data-chart-data Attribut laden (als JSON)
|
||||
let chartData = {};
|
||||
try {
|
||||
const dataAttr = container.getAttribute('data-chart-data');
|
||||
if (dataAttr) {
|
||||
chartData = JSON.parse(dataAttr);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Fehler beim Parsen der Chart-Daten für ${chartId}:`, error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chart basierend auf Typ erstellen
|
||||
createChart(container, chartType, chartData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein einzelnes Diagramm
|
||||
* @param {HTMLElement} container - Der Container für das Diagramm
|
||||
* @param {string} chartType - Typ des Diagramms (line, area, bar, pie, donut, radial)
|
||||
* @param {Object} chartData - Daten und Optionen für das Diagramm
|
||||
*/
|
||||
function createChart(container, chartType, chartData = {}) {
|
||||
const chartId = container.id;
|
||||
let chartOptions = {};
|
||||
|
||||
// Diagramm-Typ-spezifische Konfiguration laden
|
||||
switch(chartType.toLowerCase()) {
|
||||
case 'line':
|
||||
chartOptions = getLineChartConfig(
|
||||
chartData.series || [],
|
||||
chartData.categories || [],
|
||||
chartData.options || {}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'area':
|
||||
chartOptions = getAreaChartConfig(
|
||||
chartData.series || [],
|
||||
chartData.categories || [],
|
||||
chartData.options || {}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'bar':
|
||||
chartOptions = getBarChartConfig(
|
||||
chartData.series || [],
|
||||
chartData.categories || [],
|
||||
chartData.options || {}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'pie':
|
||||
chartOptions = getPieChartConfig(
|
||||
chartData.series || [],
|
||||
chartData.labels || [],
|
||||
chartData.options || {}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'donut':
|
||||
chartOptions = getDonutChartConfig(
|
||||
chartData.series || [],
|
||||
chartData.labels || [],
|
||||
chartData.options || {}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'radial':
|
||||
chartOptions = getRadialChartConfig(
|
||||
chartData.series || [],
|
||||
chartData.labels || [],
|
||||
chartData.options || {}
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unbekannter Chart-Typ: ${chartType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dark Mode Anpassungen
|
||||
updateChartTheme(chartOptions);
|
||||
|
||||
// Chart erstellen und speichern
|
||||
try {
|
||||
const chart = new ApexCharts(container, chartOptions);
|
||||
chart.render();
|
||||
|
||||
// Referenz speichern
|
||||
activeCharts[chartId] = {
|
||||
instance: chart,
|
||||
type: chartType,
|
||||
lastData: chartData
|
||||
};
|
||||
|
||||
return chart;
|
||||
} catch (error) {
|
||||
console.error(`Fehler beim Erstellen des Charts ${chartId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert ein bestehendes Diagramm mit neuen Daten
|
||||
* @param {string} chartId - ID des Diagramm-Containers
|
||||
* @param {Object} newData - Neue Daten für das Diagramm
|
||||
*/
|
||||
function updateChart(chartId, newData) {
|
||||
const chartInfo = activeCharts[chartId];
|
||||
|
||||
if (!chartInfo) {
|
||||
console.error(`Chart mit ID ${chartId} nicht gefunden.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const chart = chartInfo.instance;
|
||||
|
||||
// Aktualisieren basierend auf Chart-Typ
|
||||
if (chartInfo.type === 'pie' || chartInfo.type === 'donut' || chartInfo.type === 'radial') {
|
||||
// Für Pie/Donut/Radial-Charts
|
||||
chart.updateSeries(newData.series || []);
|
||||
if (newData.labels) {
|
||||
chart.updateOptions({
|
||||
labels: newData.labels
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Für Line/Area/Bar-Charts
|
||||
chart.updateSeries(newData.series || []);
|
||||
if (newData.categories) {
|
||||
chart.updateOptions({
|
||||
xaxis: {
|
||||
categories: newData.categories
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Zusätzliche Optionen aktualisieren
|
||||
if (newData.options) {
|
||||
chart.updateOptions(newData.options);
|
||||
}
|
||||
|
||||
// Gespeicherte Daten aktualisieren
|
||||
chartInfo.lastData = {
|
||||
...chartInfo.lastData,
|
||||
...newData
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Diagrammdaten über AJAX und aktualisiert das Diagramm
|
||||
* @param {string} chartId - ID des Diagramm-Containers
|
||||
* @param {string} url - URL zur Datenbeschaffung
|
||||
* @param {Function} successCallback - Callback nach erfolgreicher Aktualisierung
|
||||
*/
|
||||
function loadChartData(chartId, url, successCallback) {
|
||||
const container = document.getElementById(chartId);
|
||||
|
||||
if (!container) {
|
||||
console.error(`Container mit ID ${chartId} nicht gefunden.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lade-Animation anzeigen
|
||||
container.classList.add('chart-loading');
|
||||
|
||||
// Daten vom Server laden
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Netzwerkantwort war nicht ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Lade-Animation entfernen
|
||||
container.classList.remove('chart-loading');
|
||||
|
||||
const chartInfo = activeCharts[chartId];
|
||||
|
||||
// Chart erstellen, falls nicht vorhanden
|
||||
if (!chartInfo) {
|
||||
const chartType = container.getAttribute('data-chart-type');
|
||||
if (chartType) {
|
||||
createChart(container, chartType, data);
|
||||
} else {
|
||||
console.error(`Kein Chart-Typ für ${chartId} definiert.`);
|
||||
}
|
||||
} else {
|
||||
// Bestehendes Chart aktualisieren
|
||||
updateChart(chartId, data);
|
||||
}
|
||||
|
||||
// Callback aufrufen, falls vorhanden
|
||||
if (typeof successCallback === 'function') {
|
||||
successCallback(data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Chart-Daten:', error);
|
||||
container.classList.remove('chart-loading');
|
||||
|
||||
// Fehlermeldung im Container anzeigen
|
||||
container.innerHTML = `
|
||||
<div class="chart-error">
|
||||
<svg class="w-10 h-10 text-red-500 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium">Fehler beim Laden</h3>
|
||||
<p class="text-sm text-gray-500">Die Diagrammdaten konnten nicht geladen werden.</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Farbthemen basierend auf dem Dark Mode
|
||||
* @param {Object} chartOptions - Chart-Optionen Objekt
|
||||
*/
|
||||
function updateChartTheme(chartOptions) {
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
|
||||
// Theme anpassen
|
||||
if (isDarkMode) {
|
||||
// Dark Mode Einstellungen
|
||||
chartOptions.theme = {
|
||||
mode: 'dark',
|
||||
palette: 'palette1'
|
||||
};
|
||||
|
||||
chartOptions.grid = {
|
||||
...chartOptions.grid,
|
||||
borderColor: '#334155'
|
||||
};
|
||||
|
||||
// Text Farben anpassen
|
||||
chartOptions.xaxis = {
|
||||
...chartOptions.xaxis,
|
||||
labels: {
|
||||
...chartOptions.xaxis?.labels,
|
||||
style: {
|
||||
...chartOptions.xaxis?.labels?.style,
|
||||
colors: '#94a3b8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
chartOptions.yaxis = {
|
||||
...chartOptions.yaxis,
|
||||
labels: {
|
||||
...chartOptions.yaxis?.labels,
|
||||
style: {
|
||||
...chartOptions.yaxis?.labels?.style,
|
||||
colors: '#94a3b8'
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Light Mode Einstellungen
|
||||
chartOptions.theme = {
|
||||
mode: 'light',
|
||||
palette: 'palette1'
|
||||
};
|
||||
|
||||
chartOptions.grid = {
|
||||
...chartOptions.grid,
|
||||
borderColor: '#e2e8f0'
|
||||
};
|
||||
|
||||
// Text Farben anpassen
|
||||
chartOptions.xaxis = {
|
||||
...chartOptions.xaxis,
|
||||
labels: {
|
||||
...chartOptions.xaxis?.labels,
|
||||
style: {
|
||||
...chartOptions.xaxis?.labels?.style,
|
||||
colors: '#64748b'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
chartOptions.yaxis = {
|
||||
...chartOptions.yaxis,
|
||||
labels: {
|
||||
...chartOptions.yaxis?.labels,
|
||||
style: {
|
||||
...chartOptions.yaxis?.labels?.style,
|
||||
colors: '#64748b'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return chartOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event-Listener für Dark Mode Änderungen
|
||||
*/
|
||||
function setupDarkModeListener() {
|
||||
window.addEventListener('darkModeChanged', function(event) {
|
||||
const isDark = event.detail?.isDark;
|
||||
|
||||
// Alle aktiven Charts aktualisieren
|
||||
Object.keys(activeCharts).forEach(chartId => {
|
||||
const chartInfo = activeCharts[chartId];
|
||||
const chart = chartInfo.instance;
|
||||
|
||||
// Theme aktualisieren
|
||||
const updatedOptions = updateChartTheme({
|
||||
grid: chart.opts.grid,
|
||||
xaxis: chart.opts.xaxis,
|
||||
yaxis: chart.opts.yaxis
|
||||
});
|
||||
|
||||
// Chart aktualisieren
|
||||
chart.updateOptions({
|
||||
theme: updatedOptions.theme,
|
||||
grid: updatedOptions.grid,
|
||||
xaxis: updatedOptions.xaxis,
|
||||
yaxis: updatedOptions.yaxis
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt alle Chart-Instanzen
|
||||
*/
|
||||
function destroyAllCharts() {
|
||||
Object.keys(activeCharts).forEach(chartId => {
|
||||
const chartInfo = activeCharts[chartId];
|
||||
if (chartInfo && chartInfo.instance) {
|
||||
chartInfo.instance.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Aktive Charts zurücksetzen
|
||||
Object.keys(activeCharts).forEach(key => delete activeCharts[key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt eine spezifische Chart-Instanz
|
||||
* @param {string} chartId - ID des Diagramm-Containers
|
||||
*/
|
||||
function destroyChart(chartId) {
|
||||
const chartInfo = activeCharts[chartId];
|
||||
|
||||
if (chartInfo && chartInfo.instance) {
|
||||
chartInfo.instance.destroy();
|
||||
delete activeCharts[chartId];
|
||||
}
|
||||
}
|
||||
|
||||
// DOM bereit Event-Listener
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initCharts();
|
||||
setupDarkModeListener();
|
||||
});
|
283
backend/static/js/csp-violation-handler.js
Normal file
283
backend/static/js/csp-violation-handler.js
Normal file
@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Mercedes-Benz MYP Platform - CSP Violation Handler
|
||||
* Protokolliert und behandelt Content Security Policy Verletzungen
|
||||
*/
|
||||
|
||||
class CSPViolationHandler {
|
||||
constructor() {
|
||||
this.violations = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// CSP Violation Event Listener
|
||||
document.addEventListener('securitypolicyviolation', this.handleViolation.bind(this));
|
||||
|
||||
// Report-To API fallback
|
||||
if ('ReportingObserver' in window) {
|
||||
const observer = new ReportingObserver((reports, observer) => {
|
||||
for (const report of reports) {
|
||||
if (report.type === 'csp-violation') {
|
||||
this.handleViolation(report.body);
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe();
|
||||
}
|
||||
|
||||
console.log('🛡️ CSP Violation Handler initialisiert');
|
||||
}
|
||||
|
||||
/**
|
||||
* CSP-Verletzung behandeln
|
||||
*/
|
||||
handleViolation(violationEvent) {
|
||||
const violation = {
|
||||
timestamp: new Date().toISOString(),
|
||||
blockedURI: violationEvent.blockedURI || 'unknown',
|
||||
violatedDirective: violationEvent.violatedDirective || 'unknown',
|
||||
originalPolicy: violationEvent.originalPolicy || 'unknown',
|
||||
documentURI: violationEvent.documentURI || window.location.href,
|
||||
sourceFile: violationEvent.sourceFile || 'unknown',
|
||||
lineNumber: violationEvent.lineNumber || 0,
|
||||
columnNumber: violationEvent.columnNumber || 0,
|
||||
sample: violationEvent.sample || '',
|
||||
disposition: violationEvent.disposition || 'enforce'
|
||||
};
|
||||
|
||||
this.violations.push(violation);
|
||||
this.logViolation(violation);
|
||||
this.suggestFix(violation);
|
||||
|
||||
// Violation an Server senden (falls API verfügbar)
|
||||
this.reportViolation(violation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verletzung protokollieren
|
||||
*/
|
||||
logViolation(violation) {
|
||||
console.group('🚨 CSP Violation detected');
|
||||
console.error('Blocked URI:', violation.blockedURI);
|
||||
console.error('Violated Directive:', violation.violatedDirective);
|
||||
console.error('Source:', `${violation.sourceFile}:${violation.lineNumber}:${violation.columnNumber}`);
|
||||
console.error('Sample:', violation.sample);
|
||||
console.error('Full Policy:', violation.originalPolicy);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lösungsvorschlag basierend auf Verletzungstyp
|
||||
*/
|
||||
suggestFix(violation) {
|
||||
const directive = violation.violatedDirective;
|
||||
const blockedURI = violation.blockedURI;
|
||||
|
||||
console.group('💡 Lösungsvorschlag');
|
||||
|
||||
if (directive.includes('script-src')) {
|
||||
if (blockedURI === 'inline') {
|
||||
console.log('Problem: Inline-Script blockiert');
|
||||
console.log('Lösung 1: Script in externe .js-Datei auslagern');
|
||||
console.log('Lösung 2: data-action Attribute für Event-Handler verwenden');
|
||||
console.log('Lösung 3: Nonce verwenden (nicht empfohlen für Entwicklung)');
|
||||
console.log('Beispiel: <button data-action="refresh-dashboard">Aktualisieren</button>');
|
||||
} else if (blockedURI.includes('eval')) {
|
||||
console.log('Problem: eval() oder ähnliche Funktionen blockiert');
|
||||
console.log('Lösung: Verwende sichere Alternativen zu eval()');
|
||||
} else {
|
||||
console.log(`Problem: Externes Script von ${blockedURI} blockiert`);
|
||||
console.log('Lösung: URL zur CSP script-src Richtlinie hinzufügen');
|
||||
}
|
||||
} else if (directive.includes('style-src')) {
|
||||
console.log('Problem: Style blockiert');
|
||||
console.log('Lösung: CSS in externe .css-Datei auslagern oder CSP erweitern');
|
||||
} else if (directive.includes('connect-src')) {
|
||||
console.log(`Problem: Verbindung zu ${blockedURI} blockiert`);
|
||||
console.log('Lösung: URL zur CSP connect-src Richtlinie hinzufügen');
|
||||
console.log('Tipp: Für API-Calls relative URLs verwenden');
|
||||
} else if (directive.includes('img-src')) {
|
||||
console.log(`Problem: Bild von ${blockedURI} blockiert`);
|
||||
console.log('Lösung: URL zur CSP img-src Richtlinie hinzufügen');
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verletzung an Server senden
|
||||
*/
|
||||
async reportViolation(violation) {
|
||||
try {
|
||||
// Nur in Produktion an Server senden
|
||||
if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
|
||||
await fetch('/api/security/csp-violation', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(violation)
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Fehler beim Senden der CSP-Verletzung:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Verletzungen abrufen
|
||||
*/
|
||||
getViolations() {
|
||||
return this.violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verletzungsstatistiken
|
||||
*/
|
||||
getStats() {
|
||||
const stats = {
|
||||
total: this.violations.length,
|
||||
byDirective: {},
|
||||
byURI: {},
|
||||
recent: this.violations.slice(-10)
|
||||
};
|
||||
|
||||
this.violations.forEach(violation => {
|
||||
// Nach Direktive gruppieren
|
||||
const directive = violation.violatedDirective;
|
||||
stats.byDirective[directive] = (stats.byDirective[directive] || 0) + 1;
|
||||
|
||||
// Nach URI gruppieren
|
||||
const uri = violation.blockedURI;
|
||||
stats.byURI[uri] = (stats.byURI[uri] || 0) + 1;
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entwickler-Debugging-Tools
|
||||
*/
|
||||
enableDebugMode() {
|
||||
// Debug-Panel erstellen
|
||||
this.createDebugPanel();
|
||||
|
||||
// Konsolen-Hilfe ausgeben
|
||||
console.log('🔧 CSP Debug Mode aktiviert');
|
||||
console.log('Verfügbare Befehle:');
|
||||
console.log('- cspHandler.getViolations() - Alle Verletzungen anzeigen');
|
||||
console.log('- cspHandler.getStats() - Statistiken anzeigen');
|
||||
console.log('- cspHandler.clearViolations() - Verletzungen löschen');
|
||||
console.log('- cspHandler.exportViolations() - Als JSON exportieren');
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug-Panel erstellen
|
||||
*/
|
||||
createDebugPanel() {
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'csp-debug-panel';
|
||||
panel.style.cssText = `
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 300px;
|
||||
max-height: 400px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
z-index: 10000;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
`;
|
||||
|
||||
panel.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
|
||||
<strong>CSP Violations</strong>
|
||||
<button onclick="this.parentElement.parentElement.style.display='none'"
|
||||
style="background: none; border: none; color: white; cursor: pointer;">×</button>
|
||||
</div>
|
||||
<div id="csp-violations-list"></div>
|
||||
<div style="margin-top: 10px;">
|
||||
<button onclick="cspHandler.clearViolations()"
|
||||
style="background: #333; color: white; border: none; padding: 5px; margin-right: 5px; cursor: pointer;">Clear</button>
|
||||
<button onclick="cspHandler.exportViolations()"
|
||||
style="background: #333; color: white; border: none; padding: 5px; cursor: pointer;">Export</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(panel);
|
||||
|
||||
// Shortcut zum Anzeigen/Verstecken
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.ctrlKey && event.shiftKey && event.key === 'C') {
|
||||
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
||||
this.updateDebugPanel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug-Panel aktualisieren
|
||||
*/
|
||||
updateDebugPanel() {
|
||||
const list = document.getElementById('csp-violations-list');
|
||||
if (!list) return;
|
||||
|
||||
const recent = this.violations.slice(-5);
|
||||
list.innerHTML = recent.map(v => `
|
||||
<div style="margin-bottom: 5px; padding: 5px; background: rgba(255, 255, 255, 0.1);">
|
||||
<div><strong>${v.violatedDirective}</strong></div>
|
||||
<div style="color: #ff6b6b;">${v.blockedURI}</div>
|
||||
<div style="color: #ffd93d; font-size: 10px;">${v.timestamp}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verletzungen löschen
|
||||
*/
|
||||
clearViolations() {
|
||||
this.violations = [];
|
||||
this.updateDebugPanel();
|
||||
console.log('🗑️ CSP Violations gelöscht');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verletzungen exportieren
|
||||
*/
|
||||
exportViolations() {
|
||||
const data = {
|
||||
timestamp: new Date().toISOString(),
|
||||
stats: this.getStats(),
|
||||
violations: this.violations
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `csp-violations-${new Date().toISOString().split('T')[0]}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log('📄 CSP Violations exportiert');
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Instanz erstellen
|
||||
const cspHandler = new CSPViolationHandler();
|
||||
|
||||
// In Entwicklungsumgebung Debug-Mode aktivieren
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
cspHandler.enableDebugMode();
|
||||
console.log('🔍 CSP Debug Mode aktiv - Drücken Sie Ctrl+Shift+C für Debug-Panel');
|
||||
}
|
||||
|
||||
// Global verfügbar machen
|
||||
window.cspHandler = cspHandler;
|
||||
|
||||
console.log('🛡️ CSP Violation Handler geladen');
|
194
backend/static/js/dark-mode-fix.js
Normal file
194
backend/static/js/dark-mode-fix.js
Normal file
@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Dark Mode Toggle Fix - Premium Edition
|
||||
* Diese Datei stellt sicher, dass der neue Premium Dark Mode Toggle Button korrekt funktioniert
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Dark Mode Toggle Button (Premium Design)
|
||||
const darkModeToggle = document.getElementById('darkModeToggle');
|
||||
const html = document.documentElement;
|
||||
|
||||
// Local Storage Key
|
||||
const STORAGE_KEY = 'myp-dark-mode';
|
||||
|
||||
/**
|
||||
* Aktuellen Dark Mode Status aus Local Storage oder Systemeinstellung abrufen
|
||||
*/
|
||||
function isDarkMode() {
|
||||
const savedMode = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedMode !== null) {
|
||||
return savedMode === 'true';
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icons im Premium Toggle-Button aktualisieren
|
||||
*/
|
||||
function updateIcons(isDark) {
|
||||
if (!darkModeToggle) return;
|
||||
|
||||
// Finde die Premium-Icons
|
||||
const sunIcon = darkModeToggle.querySelector('.sun-icon');
|
||||
const moonIcon = darkModeToggle.querySelector('.moon-icon');
|
||||
|
||||
if (!sunIcon || !moonIcon) {
|
||||
console.warn('Premium Dark Mode Icons nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Animation für Übergänge
|
||||
if (isDark) {
|
||||
// Dark Mode aktiviert - zeige Mond
|
||||
sunIcon.style.opacity = '0';
|
||||
sunIcon.style.transform = 'scale(0.75) rotate(90deg)';
|
||||
moonIcon.style.opacity = '1';
|
||||
moonIcon.style.transform = 'scale(1) rotate(0deg)';
|
||||
|
||||
// CSS-Klassen für Dark Mode
|
||||
sunIcon.classList.add('opacity-0', 'dark:opacity-0', 'scale-75', 'dark:scale-75', 'rotate-90', 'dark:rotate-90');
|
||||
sunIcon.classList.remove('opacity-100', 'scale-100', 'rotate-0');
|
||||
moonIcon.classList.add('opacity-100', 'dark:opacity-100', 'scale-100', 'dark:scale-100', 'rotate-0', 'dark:rotate-0');
|
||||
moonIcon.classList.remove('opacity-0', 'scale-75', 'rotate-90');
|
||||
} else {
|
||||
// Light Mode aktiviert - zeige Sonne
|
||||
sunIcon.style.opacity = '1';
|
||||
sunIcon.style.transform = 'scale(1) rotate(0deg)';
|
||||
moonIcon.style.opacity = '0';
|
||||
moonIcon.style.transform = 'scale(0.75) rotate(-90deg)';
|
||||
|
||||
// CSS-Klassen für Light Mode
|
||||
sunIcon.classList.add('opacity-100', 'scale-100', 'rotate-0');
|
||||
sunIcon.classList.remove('opacity-0', 'dark:opacity-0', 'scale-75', 'dark:scale-75', 'rotate-90', 'dark:rotate-90');
|
||||
moonIcon.classList.add('opacity-0', 'dark:opacity-100', 'scale-75', 'dark:scale-100', 'rotate-90', 'dark:rotate-0');
|
||||
moonIcon.classList.remove('opacity-100', 'scale-100', 'rotate-0');
|
||||
}
|
||||
|
||||
// Icon-Animationen hinzufügen
|
||||
sunIcon.classList.toggle('icon-enter', !isDark);
|
||||
moonIcon.classList.toggle('icon-enter', isDark);
|
||||
}
|
||||
|
||||
/**
|
||||
* Premium Dark Mode aktivieren/deaktivieren
|
||||
*/
|
||||
function setDarkMode(enable) {
|
||||
console.log(`🎨 Setze Premium Dark Mode auf: ${enable ? 'Aktiviert' : 'Deaktiviert'}`);
|
||||
|
||||
if (enable) {
|
||||
html.classList.add('dark');
|
||||
html.setAttribute('data-theme', 'dark');
|
||||
html.style.colorScheme = 'dark';
|
||||
|
||||
if (darkModeToggle) {
|
||||
darkModeToggle.setAttribute('aria-pressed', 'true');
|
||||
darkModeToggle.setAttribute('title', 'Light Mode aktivieren');
|
||||
// Premium Button-Icons aktualisieren
|
||||
updateIcons(true);
|
||||
}
|
||||
} else {
|
||||
html.classList.remove('dark');
|
||||
html.setAttribute('data-theme', 'light');
|
||||
html.style.colorScheme = 'light';
|
||||
|
||||
if (darkModeToggle) {
|
||||
darkModeToggle.setAttribute('aria-pressed', 'false');
|
||||
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
|
||||
// Premium Button-Icons aktualisieren
|
||||
updateIcons(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Einstellung im Local Storage speichern
|
||||
localStorage.setItem(STORAGE_KEY, enable.toString());
|
||||
|
||||
// ThemeColor Meta-Tag aktualisieren
|
||||
const metaThemeColor = document.getElementById('metaThemeColor');
|
||||
if (metaThemeColor) {
|
||||
metaThemeColor.setAttribute('content', enable ? '#000000' : '#ffffff');
|
||||
}
|
||||
|
||||
// Event für andere Komponenten auslösen
|
||||
window.dispatchEvent(new CustomEvent('darkModeChanged', {
|
||||
detail: { isDark: enable }
|
||||
}));
|
||||
|
||||
// Premium-Feedback
|
||||
console.log(`${enable ? '🌙' : '☀️'} Premium Design umgeschaltet auf: ${enable ? 'Dark Mode' : 'Light Mode'}`);
|
||||
}
|
||||
|
||||
// Toggle Dark Mode Funktion
|
||||
function toggleDarkMode() {
|
||||
const currentMode = isDarkMode();
|
||||
setDarkMode(!currentMode);
|
||||
|
||||
// Premium-Animation beim Klick
|
||||
if (darkModeToggle) {
|
||||
const container = darkModeToggle.querySelector('div');
|
||||
if (container) {
|
||||
container.style.transform = 'scale(0.95)';
|
||||
setTimeout(() => {
|
||||
container.style.transform = '';
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event Listener für Premium Toggle Button
|
||||
if (darkModeToggle) {
|
||||
console.log('🎨 Premium Dark Mode Toggle Button gefunden - initialisiere...');
|
||||
|
||||
// Entferne vorherige Event-Listener, um Duplikate zu vermeiden
|
||||
const newDarkModeToggle = darkModeToggle.cloneNode(true);
|
||||
darkModeToggle.parentNode.replaceChild(newDarkModeToggle, darkModeToggle);
|
||||
|
||||
// Neuen Event-Listener hinzufügen
|
||||
newDarkModeToggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Verhindere Bubbling
|
||||
toggleDarkMode();
|
||||
});
|
||||
|
||||
// Aktualisiere die Variable auf das neue Element
|
||||
const updatedToggle = document.getElementById('darkModeToggle');
|
||||
|
||||
// Initialen Status setzen
|
||||
const isDark = isDarkMode();
|
||||
setDarkMode(isDark);
|
||||
|
||||
console.log('✨ Premium Dark Mode Toggle Button erfolgreich initialisiert');
|
||||
} else {
|
||||
console.error('❌ Premium Dark Mode Toggle Button konnte nicht gefunden werden!');
|
||||
}
|
||||
|
||||
// Tastaturkürzel: Strg+Shift+D für Dark Mode Toggle
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
|
||||
toggleDarkMode();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Alternative Tastaturkürzel: Alt+T für Theme Toggle
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.altKey && e.key === 't') {
|
||||
toggleDarkMode();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Direkte Verfügbarkeit der Funktionen im globalen Bereich
|
||||
window.toggleDarkMode = toggleDarkMode;
|
||||
window.isDarkMode = isDarkMode;
|
||||
window.setDarkMode = setDarkMode;
|
||||
|
||||
// Premium Features
|
||||
window.premiumDarkMode = {
|
||||
toggle: toggleDarkMode,
|
||||
isDark: isDarkMode,
|
||||
setMode: setDarkMode,
|
||||
version: '3.0.0-premium'
|
||||
};
|
||||
|
||||
console.log('🎨 Premium Dark Mode System geladen - Version 3.0.0');
|
||||
});
|
306
backend/static/js/dark-mode.js
Normal file
306
backend/static/js/dark-mode.js
Normal file
@ -0,0 +1,306 @@
|
||||
/**
|
||||
* MYP Platform Dark Mode Handler
|
||||
* Version: 6.0.0
|
||||
*/
|
||||
|
||||
// Sofort ausführen, um FOUC zu vermeiden (Flash of Unstyled Content)
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
// Speicherort für Dark Mode-Einstellung
|
||||
const STORAGE_KEY = 'myp-dark-mode';
|
||||
|
||||
// DOM-Elemente
|
||||
let darkModeToggle;
|
||||
const html = document.documentElement;
|
||||
|
||||
// Initialisierung beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', initialize);
|
||||
|
||||
// Prüft System-Präferenz und gespeicherte Einstellung
|
||||
function shouldUseDarkMode() {
|
||||
// Lokale Speichereinstellung prüfen
|
||||
const savedMode = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
// Prüfen ob es eine gespeicherte Einstellung gibt
|
||||
if (savedMode !== null) {
|
||||
return savedMode === 'true';
|
||||
}
|
||||
|
||||
// Ansonsten Systemeinstellung verwenden
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
// Setzt Dark/Light Mode
|
||||
function setDarkMode(enable) {
|
||||
// Deaktiviere Übergänge temporär, um Flackern zu vermeiden
|
||||
html.classList.add('disable-transitions');
|
||||
|
||||
// Dark Mode Klasse am HTML-Element setzen
|
||||
if (enable) {
|
||||
html.classList.add('dark');
|
||||
html.setAttribute('data-theme', 'dark');
|
||||
html.style.colorScheme = 'dark';
|
||||
} else {
|
||||
html.classList.remove('dark');
|
||||
html.setAttribute('data-theme', 'light');
|
||||
html.style.colorScheme = 'light';
|
||||
}
|
||||
|
||||
// Speichern in LocalStorage
|
||||
localStorage.setItem(STORAGE_KEY, enable);
|
||||
|
||||
// Update ThemeColor Meta-Tag
|
||||
updateMetaThemeColor(enable);
|
||||
|
||||
// Wenn Toggle existiert, aktualisiere Icon
|
||||
if (darkModeToggle) {
|
||||
updateDarkModeToggle(enable);
|
||||
}
|
||||
|
||||
// Event für andere Komponenten
|
||||
window.dispatchEvent(new CustomEvent('darkModeChanged', {
|
||||
detail: {
|
||||
isDark: enable,
|
||||
source: 'dark-mode-toggle',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}));
|
||||
|
||||
// Event auch als eigenen Event-Typ versenden (rückwärtskompatibel)
|
||||
const eventName = enable ? 'darkModeEnabled' : 'darkModeDisabled';
|
||||
window.dispatchEvent(new CustomEvent(eventName, {
|
||||
detail: { timestamp: new Date().toISOString() }
|
||||
}));
|
||||
|
||||
// Übergänge nach kurzer Verzögerung wieder aktivieren
|
||||
setTimeout(function() {
|
||||
html.classList.remove('disable-transitions');
|
||||
}, 100);
|
||||
|
||||
// Erfolgsmeldung in die Konsole
|
||||
console.log(`${enable ? '🌙' : '☀️'} ${enable ? 'Dark Mode aktiviert - Augenschonender Modus aktiv' : 'Light Mode aktiviert - Heller Modus aktiv'}`);
|
||||
}
|
||||
|
||||
// Aktualisiert das Theme-Color Meta-Tag
|
||||
function updateMetaThemeColor(isDark) {
|
||||
// Alle Theme-Color Meta-Tags aktualisieren
|
||||
const metaTags = [
|
||||
document.getElementById('metaThemeColor'),
|
||||
document.querySelector('meta[name="theme-color"]'),
|
||||
document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: light)"]'),
|
||||
document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: dark)"]')
|
||||
];
|
||||
|
||||
// CSS-Variablen für konsistente Farben verwenden
|
||||
const darkColor = getComputedStyle(document.documentElement).getPropertyValue('--color-bg') || '#0f172a';
|
||||
const lightColor = getComputedStyle(document.documentElement).getPropertyValue('--color-bg') || '#ffffff';
|
||||
|
||||
metaTags.forEach(tag => {
|
||||
if (tag) {
|
||||
// Für Media-spezifische Tags die entsprechende Farbe setzen
|
||||
if (tag.getAttribute('media') === '(prefers-color-scheme: dark)') {
|
||||
tag.setAttribute('content', darkColor);
|
||||
} else if (tag.getAttribute('media') === '(prefers-color-scheme: light)') {
|
||||
tag.setAttribute('content', lightColor);
|
||||
} else {
|
||||
// Für nicht-Media-spezifische Tags die aktuelle Farbe setzen
|
||||
tag.setAttribute('content', isDark ? darkColor : lightColor);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Aktualisiert das Aussehen des Toggle-Buttons
|
||||
function updateDarkModeToggle(isDark) {
|
||||
// Aria-Attribute für Barrierefreiheit
|
||||
darkModeToggle.setAttribute('aria-pressed', isDark.toString());
|
||||
darkModeToggle.title = isDark ? "Light Mode aktivieren" : "Dark Mode aktivieren";
|
||||
|
||||
// Icons aktualisieren
|
||||
const sunIcon = darkModeToggle.querySelector('.sun-icon');
|
||||
const moonIcon = darkModeToggle.querySelector('.moon-icon');
|
||||
|
||||
if (sunIcon && moonIcon) {
|
||||
if (isDark) {
|
||||
sunIcon.classList.add('hidden');
|
||||
moonIcon.classList.remove('hidden');
|
||||
} else {
|
||||
sunIcon.classList.remove('hidden');
|
||||
moonIcon.classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
// Fallback für ältere Implementierung mit einem Icon
|
||||
const icon = darkModeToggle.querySelector('svg');
|
||||
if (icon) {
|
||||
// Animationsklasse hinzufügen
|
||||
icon.classList.add('animate-spin-once');
|
||||
|
||||
// Nach Animation wieder entfernen
|
||||
setTimeout(() => {
|
||||
icon.classList.remove('animate-spin-once');
|
||||
}, 300);
|
||||
|
||||
const pathElement = icon.querySelector('path');
|
||||
if (pathElement) {
|
||||
// Sonnen- oder Mond-Symbol
|
||||
if (isDark) {
|
||||
pathElement.setAttribute("d", "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z");
|
||||
} else {
|
||||
pathElement.setAttribute("d", "M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisierungsfunktion
|
||||
function initialize() {
|
||||
// Toggle-Button finden
|
||||
darkModeToggle = document.getElementById('darkModeToggle');
|
||||
|
||||
// Wenn kein Toggle existiert, erstelle einen
|
||||
if (!darkModeToggle) {
|
||||
console.log('🔧 Dark Mode Toggle nicht gefunden - erstelle automatisch einen neuen Button');
|
||||
createDarkModeToggle();
|
||||
}
|
||||
|
||||
// Event-Listener für Dark Mode Toggle
|
||||
if (darkModeToggle) {
|
||||
darkModeToggle.addEventListener('click', function() {
|
||||
const isDark = !shouldUseDarkMode();
|
||||
console.log(`👆 Dark Mode Toggle: Wechsel zu ${isDark ? '🌙 dunkel' : '☀️ hell'} angefordert`);
|
||||
setDarkMode(isDark);
|
||||
});
|
||||
}
|
||||
|
||||
// Tastenkombination: Strg+Shift+D
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
|
||||
const isDark = !shouldUseDarkMode();
|
||||
console.log(`⌨️ Tastenkombination STRG+SHIFT+D erkannt: Wechsel zu ${isDark ? '🌙 dunkel' : '☀️ hell'}`);
|
||||
setDarkMode(isDark);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Auf Systemeinstellungsänderungen reagieren
|
||||
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
// Moderne Event-API verwenden
|
||||
try {
|
||||
darkModeMediaQuery.addEventListener('change', function(e) {
|
||||
// Nur anwenden, wenn keine benutzerdefinierte Einstellung gespeichert ist
|
||||
if (localStorage.getItem(STORAGE_KEY) === null) {
|
||||
console.log(`🖥️ Systemeinstellung geändert: ${e.matches ? '🌙 dunkel' : '☀️ hell'}`);
|
||||
setDarkMode(e.matches);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// Fallback für ältere Browser
|
||||
darkModeMediaQuery.addListener(function(e) {
|
||||
if (localStorage.getItem(STORAGE_KEY) === null) {
|
||||
console.log(`🖥️ Systemeinstellung geändert (Legacy-Browser): ${e.matches ? '🌙 dunkel' : '☀️ hell'}`);
|
||||
setDarkMode(e.matches);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialer Zustand
|
||||
const initialState = shouldUseDarkMode();
|
||||
console.log(`🔍 Ermittelter Ausgangszustand: ${initialState ? '🌙 Dark Mode' : '☀️ Light Mode'}`);
|
||||
setDarkMode(initialState);
|
||||
|
||||
// Animation für den korrekten Modus hinzufügen
|
||||
const animClass = initialState ? 'dark-mode-transition' : 'light-mode-transition';
|
||||
document.body.classList.add(animClass);
|
||||
|
||||
// Animation entfernen nach Abschluss
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove(animClass);
|
||||
}, 300);
|
||||
|
||||
console.log('🚀 Dark Mode Handler erfolgreich initialisiert');
|
||||
}
|
||||
|
||||
// Erstellt ein Toggle-Element, falls keines existiert
|
||||
function createDarkModeToggle() {
|
||||
// Bestehende Header-Elemente finden
|
||||
const header = document.querySelector('header');
|
||||
const nav = document.querySelector('nav');
|
||||
const container = document.querySelector('.dark-mode-container') || header || nav;
|
||||
|
||||
if (!container) {
|
||||
console.error('⚠️ Kein geeigneter Container für Dark Mode Toggle gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle-Button erstellen
|
||||
darkModeToggle = document.createElement('button');
|
||||
darkModeToggle.id = 'darkModeToggle';
|
||||
darkModeToggle.className = 'dark-mode-toggle-new';
|
||||
darkModeToggle.setAttribute('aria-label', 'Dark Mode umschalten');
|
||||
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
|
||||
darkModeToggle.setAttribute('data-action', 'toggle-dark-mode');
|
||||
|
||||
// Sonnen-Icon erstellen
|
||||
const sunIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
sunIcon.setAttribute("class", "w-5 h-5 sm:w-5 sm:h-5 sun-icon");
|
||||
sunIcon.setAttribute("fill", "none");
|
||||
sunIcon.setAttribute("stroke", "currentColor");
|
||||
sunIcon.setAttribute("viewBox", "0 0 24 24");
|
||||
sunIcon.setAttribute("aria-hidden", "true");
|
||||
|
||||
const sunPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
sunPath.setAttribute("stroke-linecap", "round");
|
||||
sunPath.setAttribute("stroke-linejoin", "round");
|
||||
sunPath.setAttribute("stroke-width", "2");
|
||||
sunPath.setAttribute("d", "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z");
|
||||
|
||||
// Mond-Icon erstellen
|
||||
const moonIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
moonIcon.setAttribute("class", "w-5 h-5 sm:w-5 sm:h-5 moon-icon hidden");
|
||||
moonIcon.setAttribute("fill", "none");
|
||||
moonIcon.setAttribute("stroke", "currentColor");
|
||||
moonIcon.setAttribute("viewBox", "0 0 24 24");
|
||||
moonIcon.setAttribute("aria-hidden", "true");
|
||||
|
||||
const moonPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
moonPath.setAttribute("stroke-linecap", "round");
|
||||
moonPath.setAttribute("stroke-linejoin", "round");
|
||||
moonPath.setAttribute("stroke-width", "2");
|
||||
moonPath.setAttribute("d", "M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z");
|
||||
|
||||
// Elemente zusammenfügen
|
||||
sunIcon.appendChild(sunPath);
|
||||
moonIcon.appendChild(moonPath);
|
||||
darkModeToggle.appendChild(sunIcon);
|
||||
darkModeToggle.appendChild(moonIcon);
|
||||
|
||||
// Zum Container hinzufügen
|
||||
container.appendChild(darkModeToggle);
|
||||
console.log('✅ Dark Mode Toggle Button erfolgreich erstellt und zur Benutzeroberfläche hinzugefügt');
|
||||
}
|
||||
|
||||
// Sofort Dark/Light Mode anwenden (vor DOMContentLoaded)
|
||||
const isDark = shouldUseDarkMode();
|
||||
console.log(`🏃♂️ Sofortige Anwendung: ${isDark ? '🌙 Dark Mode' : '☀️ Light Mode'} (vor DOM-Ladung)`);
|
||||
setDarkMode(isDark);
|
||||
})();
|
||||
|
||||
// Animationen für Spin-Effekt
|
||||
if (!document.querySelector('style#dark-mode-animations')) {
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.id = 'dark-mode-animations';
|
||||
styleTag.textContent = `
|
||||
@keyframes spin-once {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.animate-spin-once {
|
||||
animation: spin-once 0.3s ease-in-out;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleTag);
|
||||
console.log('💫 Animations-Styles für Dark Mode Toggle hinzugefügt');
|
||||
}
|
354
backend/static/js/dashboard.js
Normal file
354
backend/static/js/dashboard.js
Normal file
@ -0,0 +1,354 @@
|
||||
// Dashboard JavaScript - Externe Datei für CSP-Konformität
|
||||
|
||||
// Globale Variablen
|
||||
let dashboardData = {};
|
||||
let updateInterval;
|
||||
|
||||
// DOM-Elemente
|
||||
const elements = {
|
||||
activeJobs: null,
|
||||
scheduledJobs: null,
|
||||
availablePrinters: null,
|
||||
totalPrintTime: null,
|
||||
schedulerStatus: null,
|
||||
recentJobsList: null,
|
||||
recentActivitiesList: null,
|
||||
refreshBtn: null,
|
||||
schedulerToggleBtn: null
|
||||
};
|
||||
|
||||
// Initialisierung beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeDashboard();
|
||||
});
|
||||
|
||||
function initializeDashboard() {
|
||||
// DOM-Elemente referenzieren
|
||||
elements.activeJobs = document.getElementById('active-jobs');
|
||||
elements.scheduledJobs = document.getElementById('scheduled-jobs');
|
||||
elements.availablePrinters = document.getElementById('available-printers');
|
||||
elements.totalPrintTime = document.getElementById('total-print-time');
|
||||
elements.schedulerStatus = document.getElementById('scheduler-status');
|
||||
elements.recentJobsList = document.getElementById('recent-jobs-list');
|
||||
elements.recentActivitiesList = document.getElementById('recent-activities-list');
|
||||
elements.refreshBtn = document.getElementById('refresh-btn');
|
||||
elements.schedulerToggleBtn = document.getElementById('scheduler-toggle-btn');
|
||||
|
||||
// Event-Listener hinzufügen
|
||||
if (elements.refreshBtn) {
|
||||
elements.refreshBtn.addEventListener('click', refreshDashboard);
|
||||
}
|
||||
|
||||
if (elements.schedulerToggleBtn) {
|
||||
elements.schedulerToggleBtn.addEventListener('click', toggleScheduler);
|
||||
}
|
||||
|
||||
// Initiales Laden der Daten
|
||||
loadDashboardData();
|
||||
loadRecentJobs();
|
||||
loadRecentActivities();
|
||||
loadSchedulerStatus();
|
||||
|
||||
// Auto-Update alle 30 Sekunden
|
||||
updateInterval = setInterval(function() {
|
||||
loadDashboardData();
|
||||
loadRecentJobs();
|
||||
loadRecentActivities();
|
||||
loadSchedulerStatus();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// Dashboard-Hauptdaten laden
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
const response = await fetch('/api/dashboard');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
dashboardData = await response.json();
|
||||
updateDashboardUI();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Dashboard-Daten:', error);
|
||||
showError('Fehler beim Laden der Dashboard-Daten');
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard-UI aktualisieren
|
||||
function updateDashboardUI() {
|
||||
if (elements.activeJobs) {
|
||||
elements.activeJobs.textContent = dashboardData.active_jobs || 0;
|
||||
}
|
||||
|
||||
if (elements.scheduledJobs) {
|
||||
elements.scheduledJobs.textContent = dashboardData.scheduled_jobs || 0;
|
||||
}
|
||||
|
||||
if (elements.availablePrinters) {
|
||||
elements.availablePrinters.textContent = dashboardData.available_printers || 0;
|
||||
}
|
||||
|
||||
if (elements.totalPrintTime) {
|
||||
const hours = Math.floor((dashboardData.total_print_time || 0) / 3600);
|
||||
const minutes = Math.floor(((dashboardData.total_print_time || 0) % 3600) / 60);
|
||||
elements.totalPrintTime.textContent = `${hours}h ${minutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
// Aktuelle Jobs laden
|
||||
async function loadRecentJobs() {
|
||||
try {
|
||||
const response = await fetch('/api/jobs/recent');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
updateRecentJobsList(data.jobs);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der aktuellen Jobs:', error);
|
||||
if (elements.recentJobsList) {
|
||||
elements.recentJobsList.innerHTML = '<li class="list-group-item text-danger">Fehler beim Laden</li>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Jobs-Liste aktualisieren
|
||||
function updateRecentJobsList(jobs) {
|
||||
if (!elements.recentJobsList) return;
|
||||
|
||||
if (!jobs || jobs.length === 0) {
|
||||
elements.recentJobsList.innerHTML = '<li class="list-group-item text-muted">Keine aktuellen Jobs</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
const jobsHtml = jobs.map(job => {
|
||||
const statusClass = getStatusClass(job.status);
|
||||
const timeAgo = formatTimeAgo(job.created_at);
|
||||
|
||||
return `
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>${escapeHtml(job.name)}</strong><br>
|
||||
<small class="text-muted">${escapeHtml(job.printer_name)} • ${timeAgo}</small>
|
||||
</div>
|
||||
<span class="badge ${statusClass}">${getStatusText(job.status)}</span>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
elements.recentJobsList.innerHTML = jobsHtml;
|
||||
}
|
||||
|
||||
// Aktuelle Aktivitäten laden
|
||||
async function loadRecentActivities() {
|
||||
try {
|
||||
const response = await fetch('/api/activity/recent');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
updateRecentActivitiesList(data.activities);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Aktivitäten:', error);
|
||||
if (elements.recentActivitiesList) {
|
||||
elements.recentActivitiesList.innerHTML = '<li class="list-group-item text-danger">Fehler beim Laden</li>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aktivitäten-Liste aktualisieren
|
||||
function updateRecentActivitiesList(activities) {
|
||||
if (!elements.recentActivitiesList) return;
|
||||
|
||||
if (!activities || activities.length === 0) {
|
||||
elements.recentActivitiesList.innerHTML = '<li class="list-group-item text-muted">Keine aktuellen Aktivitäten</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
const activitiesHtml = activities.map(activity => {
|
||||
const timeAgo = formatTimeAgo(activity.timestamp);
|
||||
|
||||
return `
|
||||
<li class="list-group-item">
|
||||
<div>${escapeHtml(activity.description)}</div>
|
||||
<small class="text-muted">${timeAgo}</small>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
elements.recentActivitiesList.innerHTML = activitiesHtml;
|
||||
}
|
||||
|
||||
// Scheduler-Status laden
|
||||
async function loadSchedulerStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/scheduler/status');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
updateSchedulerStatus(data.running);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden des Scheduler-Status:', error);
|
||||
if (elements.schedulerStatus) {
|
||||
elements.schedulerStatus.innerHTML = '<span class="badge bg-secondary">Unbekannt</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scheduler-Status aktualisieren
|
||||
function updateSchedulerStatus(isRunning) {
|
||||
if (!elements.schedulerStatus) return;
|
||||
|
||||
const statusClass = isRunning ? 'bg-success' : 'bg-danger';
|
||||
const statusText = isRunning ? 'Aktiv' : 'Gestoppt';
|
||||
|
||||
elements.schedulerStatus.innerHTML = `<span class="badge ${statusClass}">${statusText}</span>`;
|
||||
|
||||
if (elements.schedulerToggleBtn) {
|
||||
elements.schedulerToggleBtn.textContent = isRunning ? 'Scheduler stoppen' : 'Scheduler starten';
|
||||
elements.schedulerToggleBtn.className = isRunning ? 'btn btn-danger btn-sm' : 'btn btn-success btn-sm';
|
||||
}
|
||||
}
|
||||
|
||||
// Scheduler umschalten
|
||||
async function toggleScheduler() {
|
||||
try {
|
||||
const isRunning = dashboardData.scheduler_running;
|
||||
const endpoint = isRunning ? '/api/scheduler/stop' : '/api/scheduler/start';
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showSuccess(result.message);
|
||||
// Status sofort neu laden
|
||||
setTimeout(loadSchedulerStatus, 1000);
|
||||
} else {
|
||||
showError(result.error || 'Unbekannter Fehler');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Umschalten des Schedulers:', error);
|
||||
showError('Fehler beim Umschalten des Schedulers');
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard manuell aktualisieren
|
||||
function refreshDashboard() {
|
||||
if (elements.refreshBtn) {
|
||||
elements.refreshBtn.disabled = true;
|
||||
elements.refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Aktualisiere...';
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
loadDashboardData(),
|
||||
loadRecentJobs(),
|
||||
loadRecentActivities(),
|
||||
loadSchedulerStatus()
|
||||
]).finally(() => {
|
||||
if (elements.refreshBtn) {
|
||||
elements.refreshBtn.disabled = false;
|
||||
elements.refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i> Aktualisieren';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hilfsfunktionen
|
||||
function getStatusClass(status) {
|
||||
const statusClasses = {
|
||||
'pending': 'bg-warning',
|
||||
'printing': 'bg-primary',
|
||||
'completed': 'bg-success',
|
||||
'failed': 'bg-danger',
|
||||
'cancelled': 'bg-secondary',
|
||||
'scheduled': 'bg-info'
|
||||
};
|
||||
return statusClasses[status] || 'bg-secondary';
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
const statusTexts = {
|
||||
'pending': 'Wartend',
|
||||
'printing': 'Druckt',
|
||||
'completed': 'Abgeschlossen',
|
||||
'failed': 'Fehlgeschlagen',
|
||||
'cancelled': 'Abgebrochen',
|
||||
'scheduled': 'Geplant'
|
||||
};
|
||||
return statusTexts[status] || status;
|
||||
}
|
||||
|
||||
function formatTimeAgo(timestamp) {
|
||||
const now = new Date();
|
||||
const time = new Date(timestamp);
|
||||
const diffMs = now - time;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffMins < 1) return 'Gerade eben';
|
||||
if (diffMins < 60) return `vor ${diffMins} Min`;
|
||||
if (diffHours < 24) return `vor ${diffHours} Std`;
|
||||
return `vor ${diffDays} Tag${diffDays > 1 ? 'en' : ''}`;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
showNotification(message, 'success');
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
showNotification(message, 'danger');
|
||||
}
|
||||
|
||||
function showNotification(message, type) {
|
||||
// Einfache Notification - kann später durch Toast-System ersetzt werden
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
||||
alertDiv.style.top = '20px';
|
||||
alertDiv.style.right = '20px';
|
||||
alertDiv.style.zIndex = '9999';
|
||||
alertDiv.innerHTML = `
|
||||
${escapeHtml(message)}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// Automatisch nach 5 Sekunden entfernen
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.parentNode.removeChild(alertDiv);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Cleanup beim Verlassen der Seite
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (updateInterval) {
|
||||
clearInterval(updateInterval);
|
||||
}
|
||||
});
|
169
backend/static/js/debug-fix.js
Normal file
169
backend/static/js/debug-fix.js
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Debug Fix Script für MYP Platform
|
||||
* Temporäre Fehlerbehebung für JavaScript-Probleme
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
console.log('🔧 Debug Fix Script wird geladen...');
|
||||
|
||||
// Namespace sicherstellen
|
||||
window.MYP = window.MYP || {};
|
||||
window.MYP.UI = window.MYP.UI || {};
|
||||
|
||||
// MVP.UI Alias erstellen falls es fehlerhaft verwendet wird
|
||||
window.MVP = window.MVP || {};
|
||||
window.MVP.UI = window.MVP.UI || {};
|
||||
|
||||
// Sofortiger Alias für DarkModeManager
|
||||
window.MVP.UI.DarkModeManager = function() {
|
||||
console.log('⚠️ MVP.UI.DarkModeManager Konstruktor aufgerufen - verwende MYP.UI.darkMode stattdessen');
|
||||
if (window.MYP && window.MYP.UI && window.MYP.UI.darkMode) {
|
||||
return window.MYP.UI.darkMode;
|
||||
}
|
||||
// Fallback: Dummy-Objekt zurückgeben
|
||||
return {
|
||||
init: function() { console.log('DarkModeManager Fallback init'); },
|
||||
setDarkMode: function() { console.log('DarkModeManager Fallback setDarkMode'); },
|
||||
isDarkMode: function() { return false; }
|
||||
};
|
||||
};
|
||||
|
||||
// DOMContentLoaded Event abwarten
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('🚀 Debug Fix: DOM Content geladen');
|
||||
|
||||
// Warten bis ui-components.js geladen ist
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// MVP.UI DarkModeManager Alias aktualisieren
|
||||
if (window.MYP && window.MYP.UI && window.MYP.UI.darkMode) {
|
||||
window.MVP.UI.DarkModeManager = function() {
|
||||
console.log('⚠️ MVP.UI.DarkModeManager Konstruktor aufgerufen - verwende MYP.UI.darkMode stattdessen');
|
||||
return window.MYP.UI.darkMode;
|
||||
};
|
||||
console.log('✅ MVP.UI.DarkModeManager Alias aktualisiert');
|
||||
}
|
||||
|
||||
// JobManager sicherstellen
|
||||
if (!window.jobManager && window.JobManager) {
|
||||
window.jobManager = new window.JobManager();
|
||||
console.log('✅ JobManager Instanz erstellt');
|
||||
}
|
||||
|
||||
// Fehlende setupFormHandlers Methode hinzufügen falls nötig
|
||||
if (window.jobManager && !window.jobManager.setupFormHandlers) {
|
||||
window.jobManager.setupFormHandlers = function() {
|
||||
console.log('✅ setupFormHandlers Fallback aufgerufen');
|
||||
};
|
||||
}
|
||||
|
||||
// Global verfügbare Wrapper-Funktionen erstellen
|
||||
window.refreshJobs = function() {
|
||||
if (window.jobManager && window.jobManager.loadJobs) {
|
||||
return window.jobManager.loadJobs();
|
||||
} else {
|
||||
console.warn('⚠️ JobManager nicht verfügbar - Seite wird neu geladen');
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// Weitere globale Funktionen für Kompatibilität
|
||||
window.startJob = function(jobId) {
|
||||
if (window.jobManager && window.jobManager.startJob) {
|
||||
return window.jobManager.startJob(jobId);
|
||||
}
|
||||
};
|
||||
|
||||
window.pauseJob = function(jobId) {
|
||||
if (window.jobManager && window.jobManager.pauseJob) {
|
||||
return window.jobManager.pauseJob(jobId);
|
||||
}
|
||||
};
|
||||
|
||||
window.resumeJob = function(jobId) {
|
||||
if (window.jobManager && window.jobManager.resumeJob) {
|
||||
return window.jobManager.resumeJob(jobId);
|
||||
}
|
||||
};
|
||||
|
||||
window.deleteJob = function(jobId) {
|
||||
if (window.jobManager && window.jobManager.deleteJob) {
|
||||
return window.jobManager.deleteJob(jobId);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('✅ Debug Fix Script erfolgreich angewendet');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Debug Fix Fehler:', error);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Error Handler für unbehandelte Fehler
|
||||
window.addEventListener('error', function(e) {
|
||||
// Bessere Fehler-Serialisierung
|
||||
const errorInfo = {
|
||||
message: e.message || 'Unbekannter Fehler',
|
||||
filename: e.filename || 'Unbekannte Datei',
|
||||
lineno: e.lineno || 0,
|
||||
colno: e.colno || 0,
|
||||
stack: e.error ? e.error.stack : 'Stack nicht verfügbar',
|
||||
type: e.error ? e.error.constructor.name : 'Unbekannter Typ'
|
||||
};
|
||||
|
||||
console.error('🐛 JavaScript Error abgefangen:', errorInfo);
|
||||
|
||||
// Spezifische Fehlerbehebungen
|
||||
if (e.message.includes('MVP.UI.DarkModeManager is not a constructor')) {
|
||||
console.log('🔧 DarkModeManager Fehler erkannt - verwende MYP.UI.darkMode');
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.message.includes('setupFormHandlers is not a function')) {
|
||||
console.log('🔧 setupFormHandlers Fehler erkannt - verwende Fallback');
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.message.includes('refreshStats is not defined')) {
|
||||
console.log('🔧 refreshStats Fehler erkannt - lade global-refresh-functions.js');
|
||||
// Versuche, global-refresh-functions.js zu laden
|
||||
const script = document.createElement('script');
|
||||
script.src = '/static/js/global-refresh-functions.js';
|
||||
script.onload = function() {
|
||||
console.log('✅ global-refresh-functions.js nachgeladen');
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.message.includes('Cannot read properties of undefined')) {
|
||||
console.log('🔧 Undefined Properties Fehler erkannt - ignoriert für Stabilität');
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.message.includes('jobManager') || e.message.includes('JobManager')) {
|
||||
console.log('🔧 JobManager Fehler erkannt - verwende Fallback');
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Promise rejection handler
|
||||
window.addEventListener('unhandledrejection', function(e) {
|
||||
console.error('🐛 Promise Rejection abgefangen:', e.reason);
|
||||
|
||||
if (e.reason && e.reason.message && e.reason.message.includes('Jobs')) {
|
||||
console.log('🔧 Jobs-bezogener Promise Fehler - ignoriert');
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ Debug Fix Script bereit');
|
||||
})();
|
482
backend/static/js/event-handlers.js
Normal file
482
backend/static/js/event-handlers.js
Normal file
@ -0,0 +1,482 @@
|
||||
/**
|
||||
* Mercedes-Benz MYP Platform - Zentrale Event Handler
|
||||
* CSP-konforme Event-Handler ohne Inline-JavaScript
|
||||
*/
|
||||
|
||||
class GlobalEventManager {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Event-Delegation für bessere Performance und CSP-Konformität
|
||||
document.addEventListener('click', this.handleClick.bind(this));
|
||||
document.addEventListener('DOMContentLoaded', this.setupEventListeners.bind(this));
|
||||
|
||||
// Falls DOM bereits geladen
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', this.setupEventListeners.bind(this));
|
||||
} else {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zentrale Click-Handler mit Event-Delegation
|
||||
*/
|
||||
handleClick(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
const action = target.getAttribute('data-action');
|
||||
const params = this.parseActionParams(target);
|
||||
|
||||
event.preventDefault();
|
||||
this.executeAction(action, params, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action-Parameter aus data-Attributen parsen
|
||||
*/
|
||||
parseActionParams(element) {
|
||||
const params = {};
|
||||
|
||||
// Alle data-action-* Attribute sammeln
|
||||
for (const attr of element.attributes) {
|
||||
if (attr.name.startsWith('data-action-')) {
|
||||
const key = attr.name.replace('data-action-', '');
|
||||
params[key] = attr.value;
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action ausführen basierend auf dem Action-Namen
|
||||
*/
|
||||
executeAction(action, params, element) {
|
||||
console.log(`🎯 Führe Action aus: ${action}`, params);
|
||||
|
||||
switch (action) {
|
||||
// Dashboard Actions
|
||||
case 'refresh-dashboard':
|
||||
if (typeof refreshDashboard === 'function') refreshDashboard();
|
||||
break;
|
||||
|
||||
// Navigation Actions
|
||||
case 'logout':
|
||||
if (typeof handleLogout === 'function') handleLogout();
|
||||
break;
|
||||
|
||||
case 'go-back':
|
||||
window.history.back();
|
||||
break;
|
||||
|
||||
case 'reload-page':
|
||||
window.location.reload();
|
||||
break;
|
||||
|
||||
case 'print-page':
|
||||
window.print();
|
||||
break;
|
||||
|
||||
// Job Management Actions
|
||||
case 'refresh-jobs':
|
||||
if (typeof refreshJobs === 'function') refreshJobs();
|
||||
break;
|
||||
|
||||
case 'toggle-batch-mode':
|
||||
if (typeof toggleBatchMode === 'function') toggleBatchMode();
|
||||
break;
|
||||
|
||||
case 'start-job':
|
||||
if (typeof jobManager !== 'undefined' && params.id) {
|
||||
jobManager.startJob(params.id);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'pause-job':
|
||||
if (typeof jobManager !== 'undefined' && params.id) {
|
||||
jobManager.pauseJob(params.id);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'resume-job':
|
||||
if (typeof jobManager !== 'undefined' && params.id) {
|
||||
jobManager.resumeJob(params.id);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete-job':
|
||||
if (typeof jobManager !== 'undefined' && params.id) {
|
||||
jobManager.deleteJob(params.id);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'open-job-details':
|
||||
if (typeof jobManager !== 'undefined' && params.id) {
|
||||
jobManager.openJobDetails(params.id);
|
||||
}
|
||||
break;
|
||||
|
||||
// Printer Management Actions
|
||||
case 'refresh-printers':
|
||||
if (typeof refreshPrinters === 'function') refreshPrinters();
|
||||
break;
|
||||
|
||||
case 'toggle-maintenance-mode':
|
||||
if (typeof toggleMaintenanceMode === 'function') toggleMaintenanceMode();
|
||||
break;
|
||||
|
||||
case 'open-add-printer-modal':
|
||||
if (typeof openAddPrinterModal === 'function') openAddPrinterModal();
|
||||
break;
|
||||
|
||||
case 'toggle-auto-refresh':
|
||||
if (typeof toggleAutoRefresh === 'function') toggleAutoRefresh();
|
||||
break;
|
||||
|
||||
case 'clear-all-filters':
|
||||
if (typeof clearAllFilters === 'function') clearAllFilters();
|
||||
break;
|
||||
|
||||
case 'test-printer-connection':
|
||||
if (typeof testPrinterConnection === 'function') testPrinterConnection();
|
||||
break;
|
||||
|
||||
case 'delete-printer':
|
||||
if (typeof deletePrinter === 'function') deletePrinter();
|
||||
break;
|
||||
|
||||
case 'edit-printer':
|
||||
if (typeof printerManager !== 'undefined' && params.id) {
|
||||
printerManager.editPrinter(params.id);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'connect-printer':
|
||||
if (typeof printerManager !== 'undefined' && params.id) {
|
||||
printerManager.connectPrinter(params.id);
|
||||
}
|
||||
break;
|
||||
|
||||
// Calendar Actions
|
||||
case 'refresh-calendar':
|
||||
if (typeof refreshCalendar === 'function') refreshCalendar();
|
||||
break;
|
||||
|
||||
case 'toggle-auto-optimization':
|
||||
if (typeof toggleAutoOptimization === 'function') toggleAutoOptimization();
|
||||
break;
|
||||
|
||||
case 'export-calendar':
|
||||
if (typeof exportCalendar === 'function') exportCalendar();
|
||||
break;
|
||||
|
||||
case 'open-create-event-modal':
|
||||
if (typeof openCreateEventModal === 'function') openCreateEventModal();
|
||||
break;
|
||||
|
||||
// Modal Actions
|
||||
case 'close-modal':
|
||||
this.closeModal(params.target || element.closest('.fixed'));
|
||||
break;
|
||||
|
||||
case 'close-printer-modal':
|
||||
if (typeof closePrinterModal === 'function') closePrinterModal();
|
||||
break;
|
||||
|
||||
case 'close-job-modal':
|
||||
if (typeof closeJobModal === 'function') closeJobModal();
|
||||
break;
|
||||
|
||||
case 'close-event-modal':
|
||||
if (typeof closeEventModal === 'function') closeEventModal();
|
||||
break;
|
||||
|
||||
// Form Actions
|
||||
case 'reset-form':
|
||||
if (typeof resetForm === 'function') resetForm();
|
||||
else this.resetNearestForm(element);
|
||||
break;
|
||||
|
||||
case 'clear-file':
|
||||
if (typeof clearFile === 'function') clearFile();
|
||||
break;
|
||||
|
||||
// Guest Request Actions
|
||||
case 'check-status':
|
||||
if (typeof checkStatus === 'function') checkStatus();
|
||||
break;
|
||||
|
||||
case 'copy-code':
|
||||
if (typeof copyCode === 'function') copyCode();
|
||||
break;
|
||||
|
||||
case 'refresh-status':
|
||||
if (typeof refreshStatus === 'function') refreshStatus();
|
||||
break;
|
||||
|
||||
case 'show-status-check':
|
||||
if (typeof showStatusCheck === 'function') showStatusCheck();
|
||||
break;
|
||||
|
||||
// Admin Actions
|
||||
case 'perform-bulk-action':
|
||||
if (typeof performBulkAction === 'function' && params.type) {
|
||||
performBulkAction(params.type);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'close-bulk-modal':
|
||||
if (typeof closeBulkModal === 'function') closeBulkModal();
|
||||
break;
|
||||
|
||||
case 'clear-cache':
|
||||
if (typeof clearCache === 'function') clearCache();
|
||||
break;
|
||||
|
||||
case 'optimize-database':
|
||||
if (typeof optimizeDatabase === 'function') optimizeDatabase();
|
||||
break;
|
||||
|
||||
case 'create-backup':
|
||||
if (typeof createBackup === 'function') createBackup();
|
||||
break;
|
||||
|
||||
case 'download-logs':
|
||||
if (typeof downloadLogs === 'function') downloadLogs();
|
||||
break;
|
||||
|
||||
case 'run-maintenance':
|
||||
if (typeof runMaintenance === 'function') runMaintenance();
|
||||
break;
|
||||
|
||||
case 'save-settings':
|
||||
if (typeof saveSettings === 'function') saveSettings();
|
||||
break;
|
||||
|
||||
// Profile Actions
|
||||
case 'toggle-edit-mode':
|
||||
if (typeof toggleEditMode === 'function') toggleEditMode();
|
||||
break;
|
||||
|
||||
case 'trigger-avatar-upload':
|
||||
if (typeof triggerAvatarUpload === 'function') triggerAvatarUpload();
|
||||
break;
|
||||
|
||||
case 'cancel-edit':
|
||||
if (typeof cancelEdit === 'function') cancelEdit();
|
||||
break;
|
||||
|
||||
case 'download-user-data':
|
||||
if (typeof downloadUserData === 'function') downloadUserData();
|
||||
break;
|
||||
|
||||
// Stats Actions
|
||||
case 'refresh-stats':
|
||||
if (typeof refreshStats === 'function') refreshStats();
|
||||
break;
|
||||
|
||||
case 'export-stats':
|
||||
if (typeof exportStats === 'function') exportStats();
|
||||
break;
|
||||
|
||||
// Generic Actions
|
||||
case 'remove-element':
|
||||
const targetElement = params.target ?
|
||||
document.querySelector(params.target) :
|
||||
element.closest(params.selector || '.removable');
|
||||
if (targetElement) {
|
||||
targetElement.remove();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'toggle-element':
|
||||
const toggleTarget = params.target ?
|
||||
document.querySelector(params.target) :
|
||||
element.nextElementSibling;
|
||||
if (toggleTarget) {
|
||||
toggleTarget.classList.toggle('hidden');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'show-element':
|
||||
const showTarget = document.querySelector(params.target);
|
||||
if (showTarget) {
|
||||
showTarget.classList.remove('hidden');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'hide-element':
|
||||
const hideTarget = document.querySelector(params.target);
|
||||
if (hideTarget) {
|
||||
hideTarget.classList.add('hidden');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`⚠️ Unbekannte Action: ${action}`);
|
||||
// Versuche globale Funktion zu finden
|
||||
if (typeof window[action] === 'function') {
|
||||
window[action](params);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generische Modal-Schließfunktion
|
||||
*/
|
||||
closeModal(modalElement) {
|
||||
if (modalElement) {
|
||||
modalElement.classList.add('hidden');
|
||||
modalElement.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nächstes Formular zurücksetzen
|
||||
*/
|
||||
resetNearestForm(element) {
|
||||
const form = element.closest('form');
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spezifische Event-Listener einrichten
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Auto-Refresh für bestimmte Seiten
|
||||
this.setupAutoRefresh();
|
||||
|
||||
// Keyboard Shortcuts
|
||||
this.setupKeyboardShortcuts();
|
||||
|
||||
// Form Validierung
|
||||
this.setupFormValidation();
|
||||
|
||||
console.log('🔧 Globale Event-Handler initialisiert');
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Refresh für Dashboard/Jobs einrichten
|
||||
*/
|
||||
setupAutoRefresh() {
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
// Auto-refresh für Dashboard alle 30 Sekunden
|
||||
if (currentPath.includes('/dashboard')) {
|
||||
setInterval(() => {
|
||||
if (typeof refreshDashboard === 'function') {
|
||||
refreshDashboard();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// Auto-refresh für Jobs alle 15 Sekunden
|
||||
if (currentPath.includes('/jobs')) {
|
||||
setInterval(() => {
|
||||
if (typeof refreshJobs === 'function') {
|
||||
refreshJobs();
|
||||
}
|
||||
}, 15000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard Shortcuts einrichten
|
||||
*/
|
||||
setupKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
// ESC zum Schließen von Modals
|
||||
if (event.key === 'Escape') {
|
||||
const openModal = document.querySelector('.fixed:not(.hidden)');
|
||||
if (openModal) {
|
||||
this.closeModal(openModal);
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl+R für Refresh (überschreiben für spezifische Funktionen)
|
||||
if (event.ctrlKey && event.key === 'r') {
|
||||
event.preventDefault();
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
if (currentPath.includes('/dashboard') && typeof refreshDashboard === 'function') {
|
||||
refreshDashboard();
|
||||
} else if (currentPath.includes('/jobs') && typeof refreshJobs === 'function') {
|
||||
refreshJobs();
|
||||
} else if (currentPath.includes('/printers') && typeof refreshPrinters === 'function') {
|
||||
refreshPrinters();
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Form-Validierung einrichten
|
||||
*/
|
||||
setupFormValidation() {
|
||||
// Alle Formulare finden und Validierung hinzufügen
|
||||
const forms = document.querySelectorAll('form[data-validate]');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', this.validateForm.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formular validieren
|
||||
*/
|
||||
validateForm(event) {
|
||||
const form = event.target;
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
let isValid = true;
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
if (!field.value.trim()) {
|
||||
isValid = false;
|
||||
field.classList.add('border-red-500');
|
||||
|
||||
// Fehlermeldung anzeigen
|
||||
const errorId = `${field.id}-error`;
|
||||
let errorElement = document.getElementById(errorId);
|
||||
|
||||
if (!errorElement) {
|
||||
errorElement = document.createElement('div');
|
||||
errorElement.id = errorId;
|
||||
errorElement.className = 'text-red-500 text-sm mt-1';
|
||||
field.parentNode.appendChild(errorElement);
|
||||
}
|
||||
|
||||
errorElement.textContent = `${field.getAttribute('data-label') || 'Dieses Feld'} ist erforderlich.`;
|
||||
} else {
|
||||
field.classList.remove('border-red-500');
|
||||
const errorElement = document.getElementById(`${field.id}-error`);
|
||||
if (errorElement) {
|
||||
errorElement.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Instanz erstellen
|
||||
const globalEventManager = new GlobalEventManager();
|
||||
|
||||
// Export für Module
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = GlobalEventManager;
|
||||
}
|
||||
|
||||
console.log('🌍 Globaler Event Manager geladen');
|
6
backend/static/js/fullcalendar/core.min.js
vendored
Normal file
6
backend/static/js/fullcalendar/core.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
backend/static/js/fullcalendar/daygrid.min.js
vendored
Normal file
6
backend/static/js/fullcalendar/daygrid.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
backend/static/js/fullcalendar/interaction.min.js
vendored
Normal file
6
backend/static/js/fullcalendar/interaction.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
backend/static/js/fullcalendar/list.min.js
vendored
Normal file
6
backend/static/js/fullcalendar/list.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
backend/static/js/fullcalendar/main.min.css
vendored
Normal file
2
backend/static/js/fullcalendar/main.min.css
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/* FullCalendar v6 CSS is embedded in the JavaScript bundle */
|
||||
/* This file is kept for template compatibility */
|
6
backend/static/js/fullcalendar/timegrid.min.js
vendored
Normal file
6
backend/static/js/fullcalendar/timegrid.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
483
backend/static/js/global-refresh-functions.js
Normal file
483
backend/static/js/global-refresh-functions.js
Normal file
@ -0,0 +1,483 @@
|
||||
/**
|
||||
* MYP Platform - Globale Refresh-Funktionen
|
||||
* Sammelt alle Refresh-Funktionen für verschiedene Seiten und Komponenten
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dashboard-Refresh-Funktion
|
||||
*/
|
||||
window.refreshDashboard = async function() {
|
||||
const refreshButton = document.getElementById('refreshDashboard');
|
||||
if (refreshButton) {
|
||||
// Button-State ändern
|
||||
refreshButton.disabled = true;
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.classList.add('animate-spin');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Dashboard-Statistiken aktualisieren
|
||||
const response = await fetch('/api/dashboard/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCSRFToken()
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Statistiken im DOM aktualisieren
|
||||
updateDashboardStats(data.stats);
|
||||
|
||||
// Benachrichtigung anzeigen
|
||||
showToast('✅ Dashboard erfolgreich aktualisiert', 'success');
|
||||
|
||||
// Seite neu laden für vollständige Aktualisierung
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
showToast('❌ Fehler beim Aktualisieren des Dashboards', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Dashboard-Refresh Fehler:', error);
|
||||
showToast('❌ Netzwerkfehler beim Dashboard-Refresh', 'error');
|
||||
} finally {
|
||||
// Button-State zurücksetzen
|
||||
if (refreshButton) {
|
||||
refreshButton.disabled = false;
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.classList.remove('animate-spin');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Statistiken-Refresh-Funktion
|
||||
*/
|
||||
window.refreshStats = async function() {
|
||||
const refreshButton = document.querySelector('button[onclick="refreshStats()"]');
|
||||
if (refreshButton) {
|
||||
refreshButton.disabled = true;
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.classList.add('animate-spin');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Basis-Statistiken laden
|
||||
if (typeof loadBasicStats === 'function') {
|
||||
await loadBasicStats();
|
||||
} else {
|
||||
// Fallback: API-Aufruf für Statistiken
|
||||
const response = await fetch('/api/stats');
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Statistiken im DOM aktualisieren
|
||||
updateStatsCounter('total-jobs-count', data.total_jobs);
|
||||
updateStatsCounter('completed-jobs-count', data.completed_jobs);
|
||||
updateStatsCounter('online-printers-count', data.online_printers);
|
||||
updateStatsCounter('success-rate-percent', data.success_rate + '%');
|
||||
updateStatsCounter('active-jobs-count', data.active_jobs);
|
||||
updateStatsCounter('failed-jobs-count', data.failed_jobs);
|
||||
updateStatsCounter('total-users-count', data.total_users);
|
||||
} else {
|
||||
throw new Error(data.error || 'Fehler beim Laden der Statistiken');
|
||||
}
|
||||
}
|
||||
|
||||
// Charts aktualisieren
|
||||
if (window.refreshAllCharts) {
|
||||
window.refreshAllCharts();
|
||||
}
|
||||
|
||||
showToast('✅ Statistiken erfolgreich aktualisiert', 'success');
|
||||
} catch (error) {
|
||||
console.error('Stats-Refresh Fehler:', error);
|
||||
showToast('❌ Fehler beim Aktualisieren der Statistiken', 'error');
|
||||
} finally {
|
||||
if (refreshButton) {
|
||||
refreshButton.disabled = false;
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.classList.remove('animate-spin');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Jobs-Refresh-Funktion
|
||||
*/
|
||||
window.refreshJobs = async function() {
|
||||
const refreshButton = document.getElementById('refresh-button');
|
||||
if (refreshButton) {
|
||||
refreshButton.disabled = true;
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.classList.add('animate-spin');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Jobs-Daten neu laden
|
||||
if (typeof jobManager !== 'undefined' && jobManager.loadJobs) {
|
||||
await jobManager.loadJobs();
|
||||
} else {
|
||||
// Fallback: Seite neu laden
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
showToast('✅ Druckaufträge erfolgreich aktualisiert', 'success');
|
||||
} catch (error) {
|
||||
console.error('Jobs-Refresh Fehler:', error);
|
||||
showToast('❌ Fehler beim Aktualisieren der Jobs', 'error');
|
||||
} finally {
|
||||
if (refreshButton) {
|
||||
refreshButton.disabled = false;
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.classList.remove('animate-spin');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calendar-Refresh-Funktion
|
||||
*/
|
||||
window.refreshCalendar = async function() {
|
||||
const refreshButton = document.getElementById('refresh-button');
|
||||
if (refreshButton) {
|
||||
refreshButton.disabled = true;
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.classList.add('animate-spin');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// FullCalendar neu laden falls verfügbar
|
||||
if (typeof calendar !== 'undefined' && calendar.refetchEvents) {
|
||||
calendar.refetchEvents();
|
||||
showToast('✅ Kalender erfolgreich aktualisiert', 'success');
|
||||
} else {
|
||||
// Fallback: Seite neu laden
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Calendar-Refresh Fehler:', error);
|
||||
showToast('❌ Fehler beim Aktualisieren des Kalenders', 'error');
|
||||
} finally {
|
||||
if (refreshButton) {
|
||||
refreshButton.disabled = false;
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.classList.remove('animate-spin');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Drucker-Refresh-Funktion
|
||||
*/
|
||||
window.refreshPrinters = async function() {
|
||||
const refreshButton = document.getElementById('refresh-button');
|
||||
if (refreshButton) {
|
||||
refreshButton.disabled = true;
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.classList.add('animate-spin');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Drucker-Manager verwenden falls verfügbar
|
||||
if (typeof printerManager !== 'undefined' && printerManager.loadPrinters) {
|
||||
await printerManager.loadPrinters();
|
||||
} else {
|
||||
// Fallback: API-Aufruf für Drucker-Update
|
||||
const response = await fetch('/api/printers/status/live', {
|
||||
headers: {
|
||||
'X-CSRFToken': getCSRFToken()
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Seite neu laden für vollständige Aktualisierung
|
||||
window.location.reload();
|
||||
} else {
|
||||
throw new Error('Drucker-Status konnte nicht abgerufen werden');
|
||||
}
|
||||
}
|
||||
|
||||
showToast('✅ Drucker erfolgreich aktualisiert', 'success');
|
||||
} catch (error) {
|
||||
console.error('Printer-Refresh Fehler:', error);
|
||||
showToast('❌ Fehler beim Aktualisieren der Drucker', 'error');
|
||||
} finally {
|
||||
if (refreshButton) {
|
||||
refreshButton.disabled = false;
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.classList.remove('animate-spin');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dashboard-Statistiken im DOM aktualisieren
|
||||
*/
|
||||
function updateDashboardStats(stats) {
|
||||
// Aktive Jobs
|
||||
const activeJobsEl = document.querySelector('[data-stat="active-jobs"]');
|
||||
if (activeJobsEl) {
|
||||
activeJobsEl.textContent = stats.active_jobs || 0;
|
||||
}
|
||||
|
||||
// Verfügbare Drucker
|
||||
const availablePrintersEl = document.querySelector('[data-stat="available-printers"]');
|
||||
if (availablePrintersEl) {
|
||||
availablePrintersEl.textContent = stats.available_printers || 0;
|
||||
}
|
||||
|
||||
// Gesamte Jobs
|
||||
const totalJobsEl = document.querySelector('[data-stat="total-jobs"]');
|
||||
if (totalJobsEl) {
|
||||
totalJobsEl.textContent = stats.total_jobs || 0;
|
||||
}
|
||||
|
||||
// Erfolgsrate
|
||||
const successRateEl = document.querySelector('[data-stat="success-rate"]');
|
||||
if (successRateEl) {
|
||||
successRateEl.textContent = (stats.success_rate || 0) + '%';
|
||||
}
|
||||
|
||||
console.log('📊 Dashboard-Statistiken aktualisiert:', stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiken-Counter im DOM aktualisieren
|
||||
*/
|
||||
function updateStatsCounter(elementId, value, animate = true) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return;
|
||||
|
||||
if (animate) {
|
||||
// Animierte Zählung
|
||||
const currentValue = parseInt(element.textContent.replace(/[^\d]/g, '')) || 0;
|
||||
const targetValue = parseInt(value.toString().replace(/[^\d]/g, '')) || 0;
|
||||
|
||||
if (currentValue !== targetValue) {
|
||||
animateCounter(element, currentValue, targetValue, value.toString());
|
||||
}
|
||||
} else {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animierte Counter-Funktion
|
||||
*/
|
||||
function animateCounter(element, start, end, finalText) {
|
||||
const duration = 1000; // 1 Sekunde
|
||||
const startTime = performance.now();
|
||||
|
||||
function updateCounter(currentTime) {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// Easing-Funktion (ease-out)
|
||||
const easeOut = 1 - Math.pow(1 - progress, 3);
|
||||
const currentValue = Math.round(start + (end - start) * easeOut);
|
||||
|
||||
if (finalText.includes('%')) {
|
||||
element.textContent = currentValue + '%';
|
||||
} else {
|
||||
element.textContent = currentValue;
|
||||
}
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCounter);
|
||||
} else {
|
||||
element.textContent = finalText;
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateCounter);
|
||||
}
|
||||
|
||||
/**
|
||||
* CSRF-Token abrufen
|
||||
*/
|
||||
function getCSRFToken() {
|
||||
const token = document.querySelector('meta[name="csrf-token"]');
|
||||
return token ? token.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast-Benachrichtigung anzeigen
|
||||
*/
|
||||
function showToast(message, type = 'info') {
|
||||
// Prüfen ob der OptimizationManager verfügbar ist und dessen Toast-Funktion verwenden
|
||||
if (typeof optimizationManager !== 'undefined' && optimizationManager.showToast) {
|
||||
optimizationManager.showToast(message, type);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: Einfache Console-Ausgabe
|
||||
const emoji = {
|
||||
success: '✅',
|
||||
error: '❌',
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️'
|
||||
};
|
||||
|
||||
console.log(`${emoji[type] || 'ℹ️'} ${message}`);
|
||||
|
||||
// Versuche eine Alert-Benachrichtigung zu erstellen
|
||||
try {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transition-all duration-300 transform translate-x-full`;
|
||||
|
||||
const colors = {
|
||||
success: 'bg-green-500 text-white',
|
||||
error: 'bg-red-500 text-white',
|
||||
warning: 'bg-yellow-500 text-black',
|
||||
info: 'bg-blue-500 text-white'
|
||||
};
|
||||
|
||||
toast.className += ` ${colors[type]}`;
|
||||
toast.textContent = message;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Animation einblenden
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// Nach 3 Sekunden automatisch entfernen
|
||||
setTimeout(() => {
|
||||
toast.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
console.warn('Toast-Erstellung fehlgeschlagen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Universelle Refresh-Funktion basierend auf aktueller Seite
|
||||
*/
|
||||
window.universalRefresh = function() {
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
if (currentPath.includes('/dashboard')) {
|
||||
refreshDashboard();
|
||||
} else if (currentPath.includes('/jobs')) {
|
||||
refreshJobs();
|
||||
} else if (currentPath.includes('/calendar') || currentPath.includes('/schichtplan')) {
|
||||
refreshCalendar();
|
||||
} else if (currentPath.includes('/printers') || currentPath.includes('/drucker')) {
|
||||
refreshPrinters();
|
||||
} else {
|
||||
// Fallback: Seite neu laden
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Auto-Refresh-Funktionalität
|
||||
*/
|
||||
class AutoRefreshManager {
|
||||
constructor() {
|
||||
this.isEnabled = false;
|
||||
this.interval = null;
|
||||
this.intervalTime = 30000; // 30 Sekunden
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.isEnabled) return;
|
||||
|
||||
this.isEnabled = true;
|
||||
this.interval = setInterval(() => {
|
||||
// Nur refresh wenn Seite sichtbar ist
|
||||
if (!document.hidden) {
|
||||
universalRefresh();
|
||||
}
|
||||
}, this.intervalTime);
|
||||
|
||||
console.log('🔄 Auto-Refresh aktiviert (alle 30 Sekunden)');
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.isEnabled) return;
|
||||
|
||||
this.isEnabled = false;
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
|
||||
console.log('⏸️ Auto-Refresh deaktiviert');
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.isEnabled) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Globaler Auto-Refresh-Manager
|
||||
window.autoRefreshManager = new AutoRefreshManager();
|
||||
|
||||
/**
|
||||
* Keyboard-Shortcuts
|
||||
*/
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// F5 oder Ctrl+R abfangen und eigene Refresh-Funktion verwenden
|
||||
if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {
|
||||
e.preventDefault();
|
||||
universalRefresh();
|
||||
}
|
||||
|
||||
// Ctrl+Shift+R für Auto-Refresh-Toggle
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'R') {
|
||||
e.preventDefault();
|
||||
autoRefreshManager.toggle();
|
||||
showToast(
|
||||
autoRefreshManager.isEnabled ?
|
||||
'🔄 Auto-Refresh aktiviert' :
|
||||
'⏸️ Auto-Refresh deaktiviert',
|
||||
'info'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Visibility API für Auto-Refresh bei Tab-Wechsel
|
||||
*/
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (!document.hidden && autoRefreshManager.isEnabled) {
|
||||
// Verzögertes Refresh wenn Tab wieder sichtbar wird
|
||||
setTimeout(universalRefresh, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🔄 Globale Refresh-Funktionen geladen');
|
736
backend/static/js/job-manager.js
Normal file
736
backend/static/js/job-manager.js
Normal file
@ -0,0 +1,736 @@
|
||||
/**
|
||||
* MYP Platform Job Manager
|
||||
* Verwaltung und Steuerung von 3D-Druckaufträgen
|
||||
* Version: 1.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Job Manager Klasse für 3D-Druckaufträge
|
||||
*/
|
||||
class JobManager {
|
||||
constructor() {
|
||||
this.jobs = [];
|
||||
this.currentPage = 1;
|
||||
this.totalPages = 1;
|
||||
this.isLoading = false;
|
||||
this.refreshInterval = null;
|
||||
this.autoRefreshEnabled = false;
|
||||
|
||||
console.log('🔧 JobManager wird initialisiert...');
|
||||
}
|
||||
|
||||
/**
|
||||
* JobManager initialisieren
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
console.log('🚀 JobManager-Initialisierung gestartet');
|
||||
|
||||
// Event-Listener einrichten
|
||||
this.setupEventListeners();
|
||||
|
||||
// Formular-Handler einrichten
|
||||
this.setupFormHandlers();
|
||||
|
||||
// Anfängliche Jobs laden
|
||||
await this.loadJobs();
|
||||
|
||||
// Auto-Refresh starten falls aktiviert
|
||||
if (this.autoRefreshEnabled) {
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
|
||||
console.log('✅ JobManager erfolgreich initialisiert');
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei JobManager-Initialisierung:', error);
|
||||
this.showToast('Fehler beim Initialisieren des Job-Managers', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event-Listener für Job-Aktionen einrichten
|
||||
*/
|
||||
setupEventListeners() {
|
||||
console.log('📡 Event-Listener werden eingerichtet...');
|
||||
|
||||
// Job-Aktionen über data-Attribute
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-job-action]');
|
||||
if (!target) return;
|
||||
|
||||
const action = target.getAttribute('data-job-action');
|
||||
const jobId = target.getAttribute('data-job-id');
|
||||
|
||||
if (!jobId) {
|
||||
console.warn('⚠️ Job-ID fehlt für Aktion:', action);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'start':
|
||||
this.startJob(jobId);
|
||||
break;
|
||||
case 'pause':
|
||||
this.pauseJob(jobId);
|
||||
break;
|
||||
case 'resume':
|
||||
this.resumeJob(jobId);
|
||||
break;
|
||||
case 'stop':
|
||||
this.stopJob(jobId);
|
||||
break;
|
||||
case 'delete':
|
||||
this.deleteJob(jobId);
|
||||
break;
|
||||
case 'details':
|
||||
this.openJobDetails(jobId);
|
||||
break;
|
||||
default:
|
||||
console.warn('⚠️ Unbekannte Job-Aktion:', action);
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh-Button
|
||||
const refreshBtn = document.getElementById('refresh-jobs');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => this.loadJobs());
|
||||
}
|
||||
|
||||
// Auto-Refresh Toggle
|
||||
const autoRefreshToggle = document.getElementById('auto-refresh-toggle');
|
||||
if (autoRefreshToggle) {
|
||||
autoRefreshToggle.addEventListener('change', (e) => {
|
||||
this.autoRefreshEnabled = e.target.checked;
|
||||
if (this.autoRefreshEnabled) {
|
||||
this.startAutoRefresh();
|
||||
} else {
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ Event-Listener erfolgreich eingerichtet');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formular-Handler für Job-Erstellung einrichten
|
||||
*/
|
||||
setupFormHandlers() {
|
||||
console.log('📝 Formular-Handler werden eingerichtet...');
|
||||
|
||||
const newJobForm = document.getElementById('new-job-form');
|
||||
if (newJobForm) {
|
||||
newJobForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
await this.createNewJob(new FormData(newJobForm));
|
||||
});
|
||||
}
|
||||
|
||||
const editJobForm = document.getElementById('edit-job-form');
|
||||
if (editJobForm) {
|
||||
editJobForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const jobId = editJobForm.getAttribute('data-job-id');
|
||||
await this.updateJob(jobId, new FormData(editJobForm));
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ Formular-Handler erfolgreich eingerichtet');
|
||||
}
|
||||
|
||||
/**
|
||||
* Jobs von Server laden
|
||||
*/
|
||||
async loadJobs(page = 1) {
|
||||
if (this.isLoading) {
|
||||
console.log('⚠️ Jobs werden bereits geladen...');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.showLoadingState(true);
|
||||
|
||||
try {
|
||||
console.log(`📥 Lade Jobs (Seite ${page})...`);
|
||||
|
||||
const response = await fetch(`/api/jobs?page=${page}`, {
|
||||
headers: {
|
||||
'X-CSRFToken': this.getCSRFToken(),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
this.jobs = data.jobs || [];
|
||||
this.currentPage = data.current_page || 1;
|
||||
this.totalPages = data.total_pages || 1;
|
||||
|
||||
this.renderJobs();
|
||||
this.updatePagination();
|
||||
|
||||
console.log(`✅ ${this.jobs.length} Jobs erfolgreich geladen`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Laden der Jobs:', error);
|
||||
this.showToast('Fehler beim Laden der Jobs', 'error');
|
||||
|
||||
// Fallback: Leere Jobs-Liste anzeigen
|
||||
this.jobs = [];
|
||||
this.renderJobs();
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.showLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job starten
|
||||
*/
|
||||
async startJob(jobId) {
|
||||
try {
|
||||
console.log(`▶️ Starte Job ${jobId}...`);
|
||||
|
||||
const response = await fetch(`/api/jobs/${jobId}/start`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': this.getCSRFToken(),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
this.showToast('Job erfolgreich gestartet', 'success');
|
||||
await this.loadJobs();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Starten des Jobs:', error);
|
||||
this.showToast('Fehler beim Starten des Jobs', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job pausieren
|
||||
*/
|
||||
async pauseJob(jobId) {
|
||||
try {
|
||||
console.log(`⏸️ Pausiere Job ${jobId}...`);
|
||||
|
||||
const response = await fetch(`/api/jobs/${jobId}/pause`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': this.getCSRFToken(),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
this.showToast('Job erfolgreich pausiert', 'success');
|
||||
await this.loadJobs();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Pausieren des Jobs:', error);
|
||||
this.showToast('Fehler beim Pausieren des Jobs', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job fortsetzen
|
||||
*/
|
||||
async resumeJob(jobId) {
|
||||
try {
|
||||
console.log(`▶️ Setze Job ${jobId} fort...`);
|
||||
|
||||
const response = await fetch(`/api/jobs/${jobId}/resume`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': this.getCSRFToken(),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
this.showToast('Job erfolgreich fortgesetzt', 'success');
|
||||
await this.loadJobs();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Fortsetzen des Jobs:', error);
|
||||
this.showToast('Fehler beim Fortsetzen des Jobs', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job stoppen
|
||||
*/
|
||||
async stopJob(jobId) {
|
||||
if (!confirm('Möchten Sie diesen Job wirklich stoppen?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`⏹️ Stoppe Job ${jobId}...`);
|
||||
|
||||
const response = await fetch(`/api/jobs/${jobId}/stop`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': this.getCSRFToken(),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
this.showToast('Job erfolgreich gestoppt', 'success');
|
||||
await this.loadJobs();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Stoppen des Jobs:', error);
|
||||
this.showToast('Fehler beim Stoppen des Jobs', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job löschen
|
||||
*/
|
||||
async deleteJob(jobId) {
|
||||
if (!confirm('Möchten Sie diesen Job wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🗑️ Lösche Job ${jobId}...`);
|
||||
|
||||
const response = await fetch(`/api/jobs/${jobId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': this.getCSRFToken(),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
this.showToast('Job erfolgreich gelöscht', 'success');
|
||||
await this.loadJobs();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Löschen des Jobs:', error);
|
||||
this.showToast('Fehler beim Löschen des Jobs', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job-Details öffnen
|
||||
*/
|
||||
openJobDetails(jobId) {
|
||||
console.log(`📄 Öffne Details für Job ${jobId}...`);
|
||||
|
||||
// Modal für Job-Details öffnen oder zu Detail-Seite navigieren
|
||||
const detailsUrl = `/jobs/${jobId}`;
|
||||
|
||||
// Prüfen ob Modal verfügbar ist
|
||||
const detailsModal = document.getElementById(`job-details-${jobId}`);
|
||||
if (detailsModal && typeof window.MYP !== 'undefined' && window.MYP.UI && window.MYP.UI.modal) {
|
||||
window.MYP.UI.modal.open(`job-details-${jobId}`);
|
||||
} else {
|
||||
// Fallback: Zur Detail-Seite navigieren
|
||||
window.location.href = detailsUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Jobs in der UI rendern
|
||||
*/
|
||||
renderJobs() {
|
||||
const jobsList = document.getElementById('jobs-list');
|
||||
if (!jobsList) {
|
||||
console.warn('⚠️ Jobs-Liste Element nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.jobs.length === 0) {
|
||||
jobsList.innerHTML = `
|
||||
<div class="text-center py-12">
|
||||
<div class="text-gray-400 dark:text-gray-600 text-6xl mb-4">📭</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Keine Jobs vorhanden</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">Es wurden noch keine Druckaufträge erstellt.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const jobsHTML = this.jobs.map(job => this.renderJobCard(job)).join('');
|
||||
jobsList.innerHTML = jobsHTML;
|
||||
|
||||
console.log(`📋 ${this.jobs.length} Jobs gerendert`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelne Job-Karte rendern
|
||||
*/
|
||||
renderJobCard(job) {
|
||||
const statusClass = this.getJobStatusClass(job.status);
|
||||
const statusText = this.getJobStatusText(job.status);
|
||||
|
||||
return `
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">${job.name || 'Unbenannter Job'}</h3>
|
||||
<div class="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
<span>ID: ${job.id}</span>
|
||||
<span>•</span>
|
||||
<span>Drucker: ${job.printer_name || 'Unbekannt'}</span>
|
||||
<span>•</span>
|
||||
<span>Erstellt: ${new Date(job.created_at).toLocaleDateString('de-DE')}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusClass}">
|
||||
${statusText}
|
||||
</span>
|
||||
${job.progress ? `<span class="text-sm text-gray-500 dark:text-gray-400">${job.progress}%</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2 ml-4">
|
||||
${this.renderJobActions(job)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Job-Aktionen rendern
|
||||
*/
|
||||
renderJobActions(job) {
|
||||
const actions = [];
|
||||
|
||||
// Details-Button immer verfügbar
|
||||
actions.push(`
|
||||
<button data-job-action="details" data-job-id="${job.id}"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Details
|
||||
</button>
|
||||
`);
|
||||
|
||||
// Status-abhängige Aktionen
|
||||
switch (job.status) {
|
||||
case 'pending':
|
||||
case 'ready':
|
||||
actions.push(`
|
||||
<button data-job-action="start" data-job-id="${job.id}"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
||||
Starten
|
||||
</button>
|
||||
`);
|
||||
break;
|
||||
|
||||
case 'running':
|
||||
case 'printing':
|
||||
actions.push(`
|
||||
<button data-job-action="pause" data-job-id="${job.id}"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500">
|
||||
Pausieren
|
||||
</button>
|
||||
`);
|
||||
actions.push(`
|
||||
<button data-job-action="stop" data-job-id="${job.id}"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
|
||||
Stoppen
|
||||
</button>
|
||||
`);
|
||||
break;
|
||||
|
||||
case 'paused':
|
||||
actions.push(`
|
||||
<button data-job-action="resume" data-job-id="${job.id}"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
||||
Fortsetzen
|
||||
</button>
|
||||
`);
|
||||
actions.push(`
|
||||
<button data-job-action="stop" data-job-id="${job.id}"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
|
||||
Stoppen
|
||||
</button>
|
||||
`);
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
case 'failed':
|
||||
case 'cancelled':
|
||||
actions.push(`
|
||||
<button data-job-action="delete" data-job-id="${job.id}"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
|
||||
Löschen
|
||||
</button>
|
||||
`);
|
||||
break;
|
||||
}
|
||||
|
||||
return actions.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS-Klasse für Job-Status
|
||||
*/
|
||||
getJobStatusClass(status) {
|
||||
const statusClasses = {
|
||||
'pending': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
||||
'ready': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
||||
'running': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||
'printing': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||
'paused': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
|
||||
'completed': 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300',
|
||||
'failed': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
||||
'cancelled': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
|
||||
};
|
||||
|
||||
return statusClasses[status] || statusClasses['pending'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Anzeigetext für Job-Status
|
||||
*/
|
||||
getJobStatusText(status) {
|
||||
const statusTexts = {
|
||||
'pending': 'Wartend',
|
||||
'ready': 'Bereit',
|
||||
'running': 'Läuft',
|
||||
'printing': 'Druckt',
|
||||
'paused': 'Pausiert',
|
||||
'completed': 'Abgeschlossen',
|
||||
'failed': 'Fehlgeschlagen',
|
||||
'cancelled': 'Abgebrochen'
|
||||
};
|
||||
|
||||
return statusTexts[status] || 'Unbekannt';
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading-Zustand anzeigen/verstecken
|
||||
*/
|
||||
showLoadingState(show) {
|
||||
const loadingEl = document.getElementById('jobs-loading');
|
||||
const jobsList = document.getElementById('jobs-list');
|
||||
|
||||
if (loadingEl) {
|
||||
loadingEl.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (jobsList) {
|
||||
jobsList.style.opacity = show ? '0.5' : '1';
|
||||
jobsList.style.pointerEvents = show ? 'none' : 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CSRF-Token abrufen
|
||||
*/
|
||||
getCSRFToken() {
|
||||
const token = document.querySelector('meta[name="csrf-token"]');
|
||||
return token ? token.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast-Nachricht anzeigen
|
||||
*/
|
||||
showToast(message, type = 'info') {
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast(message, type);
|
||||
} else {
|
||||
console.log(`${type.toUpperCase()}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Refresh starten
|
||||
*/
|
||||
startAutoRefresh() {
|
||||
this.stopAutoRefresh(); // Vorherigen Refresh stoppen
|
||||
|
||||
this.refreshInterval = setInterval(() => {
|
||||
if (!this.isLoading) {
|
||||
this.loadJobs(this.currentPage);
|
||||
}
|
||||
}, 30000); // Alle 30 Sekunden
|
||||
|
||||
console.log('🔄 Auto-Refresh gestartet (30s Intervall)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Refresh stoppen
|
||||
*/
|
||||
stopAutoRefresh() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
console.log('⏹️ Auto-Refresh gestoppt');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginierung aktualisieren
|
||||
*/
|
||||
updatePagination() {
|
||||
const paginationEl = document.getElementById('jobs-pagination');
|
||||
if (!paginationEl) return;
|
||||
|
||||
if (this.totalPages <= 1) {
|
||||
paginationEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
paginationEl.style.display = 'flex';
|
||||
|
||||
let paginationHTML = '';
|
||||
|
||||
// Vorherige Seite
|
||||
if (this.currentPage > 1) {
|
||||
paginationHTML += `
|
||||
<button onclick="jobManager.loadJobs(${this.currentPage - 1})"
|
||||
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">
|
||||
Zurück
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// Seitenzahlen
|
||||
for (let i = 1; i <= this.totalPages; i++) {
|
||||
const isActive = i === this.currentPage;
|
||||
paginationHTML += `
|
||||
<button onclick="jobManager.loadJobs(${i})"
|
||||
class="px-3 py-2 text-sm font-medium ${isActive
|
||||
? 'text-blue-600 bg-blue-50 border-blue-300 dark:bg-blue-900 dark:text-blue-300'
|
||||
: 'text-gray-500 bg-white border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700'
|
||||
} border">
|
||||
${i}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// Nächste Seite
|
||||
if (this.currentPage < this.totalPages) {
|
||||
paginationHTML += `
|
||||
<button onclick="jobManager.loadJobs(${this.currentPage + 1})"
|
||||
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">
|
||||
Weiter
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
paginationEl.innerHTML = paginationHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Neuen Job erstellen
|
||||
*/
|
||||
async createNewJob(formData) {
|
||||
try {
|
||||
console.log('📝 Erstelle neuen Job...');
|
||||
|
||||
const response = await fetch('/api/jobs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': this.getCSRFToken()
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
this.showToast('Job erfolgreich erstellt', 'success');
|
||||
|
||||
// Jobs neu laden
|
||||
await this.loadJobs();
|
||||
|
||||
// Formular zurücksetzen
|
||||
const form = document.getElementById('new-job-form');
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Erstellen des Jobs:', error);
|
||||
this.showToast('Fehler beim Erstellen des Jobs', 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Job aktualisieren
|
||||
*/
|
||||
async updateJob(jobId, formData) {
|
||||
try {
|
||||
console.log(`📝 Aktualisiere Job ${jobId}...`);
|
||||
|
||||
const response = await fetch(`/api/jobs/${jobId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRFToken': this.getCSRFToken()
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
this.showToast('Job erfolgreich aktualisiert', 'success');
|
||||
|
||||
// Jobs neu laden
|
||||
await this.loadJobs();
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Aktualisieren des Jobs:', error);
|
||||
this.showToast('Fehler beim Aktualisieren des Jobs', 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JobManager global verfügbar machen
|
||||
window.JobManager = JobManager;
|
||||
|
||||
// JobManager-Instanz erstellen wenn DOM bereit ist
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof window.jobManager === 'undefined') {
|
||||
window.jobManager = new JobManager();
|
||||
|
||||
// Nur initialisieren wenn wir uns auf einer Jobs-Seite befinden
|
||||
if (document.getElementById('jobs-list') || document.querySelector('[data-job-action]')) {
|
||||
window.jobManager.init();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ JobManager-Modul geladen');
|
||||
})();
|
315
backend/static/js/notifications.js
Normal file
315
backend/static/js/notifications.js
Normal file
@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Benachrichtigungssystem für die MYP 3D-Druck Platform
|
||||
* Verwaltet die Anzeige und Interaktion mit Benachrichtigungen
|
||||
*/
|
||||
|
||||
class NotificationManager {
|
||||
constructor() {
|
||||
this.notificationToggle = document.getElementById('notificationToggle');
|
||||
this.notificationDropdown = document.getElementById('notificationDropdown');
|
||||
this.notificationBadge = document.getElementById('notificationBadge');
|
||||
this.notificationList = document.getElementById('notificationList');
|
||||
this.markAllReadBtn = document.getElementById('markAllRead');
|
||||
|
||||
this.isOpen = false;
|
||||
this.notifications = [];
|
||||
|
||||
// CSRF-Token aus Meta-Tag holen
|
||||
this.csrfToken = this.getCSRFToken();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt das CSRF-Token aus dem Meta-Tag
|
||||
* @returns {string} Das CSRF-Token
|
||||
*/
|
||||
getCSRFToken() {
|
||||
const metaTag = document.querySelector('meta[name="csrf-token"]');
|
||||
return metaTag ? metaTag.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt die Standard-Headers für API-Anfragen mit CSRF-Token
|
||||
* @returns {Object} Headers-Objekt
|
||||
*/
|
||||
getAPIHeaders() {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (this.csrfToken) {
|
||||
headers['X-CSRFToken'] = this.csrfToken;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.notificationToggle) return;
|
||||
|
||||
// Event Listeners
|
||||
this.notificationToggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleDropdown();
|
||||
});
|
||||
|
||||
if (this.markAllReadBtn) {
|
||||
this.markAllReadBtn.addEventListener('click', () => {
|
||||
this.markAllAsRead();
|
||||
});
|
||||
}
|
||||
|
||||
// Dropdown schließen bei Klick außerhalb
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.notificationDropdown.contains(e.target) && !this.notificationToggle.contains(e.target)) {
|
||||
this.closeDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// Benachrichtigungen laden
|
||||
this.loadNotifications();
|
||||
|
||||
// Regelmäßige Updates
|
||||
setInterval(() => {
|
||||
this.loadNotifications();
|
||||
}, 30000); // Alle 30 Sekunden
|
||||
}
|
||||
|
||||
toggleDropdown() {
|
||||
if (this.isOpen) {
|
||||
this.closeDropdown();
|
||||
} else {
|
||||
this.openDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
openDropdown() {
|
||||
this.notificationDropdown.classList.remove('hidden');
|
||||
this.notificationToggle.setAttribute('aria-expanded', 'true');
|
||||
this.isOpen = true;
|
||||
|
||||
// Animation
|
||||
this.notificationDropdown.style.opacity = '0';
|
||||
this.notificationDropdown.style.transform = 'translateY(-10px)';
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.notificationDropdown.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
|
||||
this.notificationDropdown.style.opacity = '1';
|
||||
this.notificationDropdown.style.transform = 'translateY(0)';
|
||||
});
|
||||
}
|
||||
|
||||
closeDropdown() {
|
||||
this.notificationDropdown.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
|
||||
this.notificationDropdown.style.opacity = '0';
|
||||
this.notificationDropdown.style.transform = 'translateY(-10px)';
|
||||
|
||||
setTimeout(() => {
|
||||
this.notificationDropdown.classList.add('hidden');
|
||||
this.notificationToggle.setAttribute('aria-expanded', 'false');
|
||||
this.isOpen = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
async loadNotifications() {
|
||||
try {
|
||||
const response = await fetch('/api/notifications');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.notifications = data.notifications || [];
|
||||
this.updateUI();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Benachrichtigungen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
this.updateBadge();
|
||||
this.updateNotificationList();
|
||||
}
|
||||
|
||||
updateBadge() {
|
||||
const unreadCount = this.notifications.filter(n => !n.read).length;
|
||||
|
||||
if (unreadCount > 0) {
|
||||
this.notificationBadge.textContent = unreadCount > 99 ? '99+' : unreadCount.toString();
|
||||
this.notificationBadge.classList.remove('hidden');
|
||||
} else {
|
||||
this.notificationBadge.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
updateNotificationList() {
|
||||
if (this.notifications.length === 0) {
|
||||
this.notificationList.innerHTML = `
|
||||
<div class="p-4 text-center text-slate-500 dark:text-slate-400">
|
||||
Keine neuen Benachrichtigungen
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationHTML = this.notifications.map(notification => {
|
||||
const isUnread = !notification.read;
|
||||
const timeAgo = this.formatTimeAgo(new Date(notification.created_at));
|
||||
|
||||
return `
|
||||
<div class="notification-item p-4 border-b border-slate-200 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors ${isUnread ? 'bg-blue-50 dark:bg-blue-900/20' : ''}"
|
||||
data-notification-id="${notification.id}">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
${this.getNotificationIcon(notification.type)}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-medium text-slate-900 dark:text-white">
|
||||
${this.getNotificationTitle(notification.type)}
|
||||
</p>
|
||||
${isUnread ? '<div class="w-2 h-2 bg-blue-500 rounded-full"></div>' : ''}
|
||||
</div>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
|
||||
${this.getNotificationMessage(notification)}
|
||||
</p>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-500 mt-2">
|
||||
${timeAgo}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
this.notificationList.innerHTML = notificationHTML;
|
||||
|
||||
// Event Listeners für Benachrichtigungen
|
||||
this.notificationList.querySelectorAll('.notification-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
const notificationId = item.dataset.notificationId;
|
||||
this.markAsRead(notificationId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getNotificationIcon(type) {
|
||||
const icons = {
|
||||
'guest_request': `
|
||||
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
`,
|
||||
'job_completed': `
|
||||
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
`,
|
||||
'system': `
|
||||
<div class="w-8 h-8 bg-gray-100 dark:bg-gray-900 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
return icons[type] || icons['system'];
|
||||
}
|
||||
|
||||
getNotificationTitle(type) {
|
||||
const titles = {
|
||||
'guest_request': 'Neue Gastanfrage',
|
||||
'job_completed': 'Druckauftrag abgeschlossen',
|
||||
'system': 'System-Benachrichtigung'
|
||||
};
|
||||
|
||||
return titles[type] || 'Benachrichtigung';
|
||||
}
|
||||
|
||||
getNotificationMessage(notification) {
|
||||
try {
|
||||
const payload = JSON.parse(notification.payload || '{}');
|
||||
|
||||
switch (notification.type) {
|
||||
case 'guest_request':
|
||||
return `${payload.guest_name || 'Ein Gast'} hat eine neue Druckanfrage gestellt.`;
|
||||
case 'job_completed':
|
||||
return `Der Druckauftrag "${payload.job_name || 'Unbekannt'}" wurde erfolgreich abgeschlossen.`;
|
||||
default:
|
||||
return payload.message || 'Neue Benachrichtigung erhalten.';
|
||||
}
|
||||
} catch (error) {
|
||||
return 'Neue Benachrichtigung erhalten.';
|
||||
}
|
||||
}
|
||||
|
||||
formatTimeAgo(date) {
|
||||
const now = new Date();
|
||||
const diffInSeconds = Math.floor((now - date) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) {
|
||||
return 'Gerade eben';
|
||||
} else if (diffInSeconds < 3600) {
|
||||
const minutes = Math.floor(diffInSeconds / 60);
|
||||
return `vor ${minutes} Minute${minutes !== 1 ? 'n' : ''}`;
|
||||
} else if (diffInSeconds < 86400) {
|
||||
const hours = Math.floor(diffInSeconds / 3600);
|
||||
return `vor ${hours} Stunde${hours !== 1 ? 'n' : ''}`;
|
||||
} else {
|
||||
const days = Math.floor(diffInSeconds / 86400);
|
||||
return `vor ${days} Tag${days !== 1 ? 'en' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
async markAsRead(notificationId) {
|
||||
try {
|
||||
const response = await fetch(`/api/notifications/${notificationId}/read`, {
|
||||
method: 'POST',
|
||||
headers: this.getAPIHeaders()
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Benachrichtigung als gelesen markieren
|
||||
const notification = this.notifications.find(n => n.id == notificationId);
|
||||
if (notification) {
|
||||
notification.read = true;
|
||||
this.updateUI();
|
||||
}
|
||||
} else {
|
||||
console.error('Fehler beim Markieren als gelesen:', response.status, response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Markieren als gelesen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async markAllAsRead() {
|
||||
try {
|
||||
const response = await fetch('/api/notifications/mark-all-read', {
|
||||
method: 'POST',
|
||||
headers: this.getAPIHeaders()
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Alle Benachrichtigungen als gelesen markieren
|
||||
this.notifications.forEach(notification => {
|
||||
notification.read = true;
|
||||
});
|
||||
this.updateUI();
|
||||
} else {
|
||||
console.error('Fehler beim Markieren aller als gelesen:', response.status, response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Markieren aller als gelesen:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisierung nach DOM-Load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new NotificationManager();
|
||||
});
|
521
backend/static/js/offline-app.js
Normal file
521
backend/static/js/offline-app.js
Normal file
@ -0,0 +1,521 @@
|
||||
/**
|
||||
* MYP Platform Offline-First Application
|
||||
* Version: 2.0.0
|
||||
* Description: Offline-fähige PWA für die MYP Plattform
|
||||
*/
|
||||
|
||||
class MYPApp {
|
||||
constructor() {
|
||||
this.isOffline = !navigator.onLine;
|
||||
this.registerTime = new Date().toISOString();
|
||||
this.darkMode = document.documentElement.classList.contains('dark');
|
||||
this.setupOfflineDetection();
|
||||
this.setupServiceWorker();
|
||||
this.setupLocalStorage();
|
||||
this.setupUI();
|
||||
this.setupThemeListeners();
|
||||
|
||||
// Debug-Info für die Entwicklung
|
||||
console.log(`MYP App initialisiert um ${this.registerTime}`);
|
||||
console.log(`Initiale Netzwerkverbindung: ${navigator.onLine ? 'Online' : 'Offline'}`);
|
||||
console.log(`Aktueller Modus: ${this.darkMode ? 'Dark Mode' : 'Light Mode'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Überwacht die Netzwerkverbindung
|
||||
*/
|
||||
setupOfflineDetection() {
|
||||
window.addEventListener('online', () => {
|
||||
this.isOffline = false;
|
||||
document.body.classList.remove('offline-mode');
|
||||
console.log('Netzwerkverbindung wiederhergestellt!');
|
||||
|
||||
// Bei Wiederverbindung: Synchronisiere Daten
|
||||
this.syncOfflineData();
|
||||
|
||||
// Event an andere Komponenten senden
|
||||
window.dispatchEvent(new CustomEvent('networkStatusChange', {
|
||||
detail: { isOffline: false }
|
||||
}));
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
this.isOffline = true;
|
||||
document.body.classList.add('offline-mode');
|
||||
console.log('Netzwerkverbindung verloren!');
|
||||
|
||||
// Event an andere Komponenten senden
|
||||
window.dispatchEvent(new CustomEvent('networkStatusChange', {
|
||||
detail: { isOffline: true }
|
||||
}));
|
||||
});
|
||||
|
||||
// Klasse setzen, wenn initial offline
|
||||
if (this.isOffline) {
|
||||
document.body.classList.add('offline-mode');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert den Service Worker für Offline-Funktionalität
|
||||
*/
|
||||
setupServiceWorker() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
const swPath = '/sw.js';
|
||||
|
||||
navigator.serviceWorker.register(swPath, {
|
||||
scope: '/'
|
||||
}).then(registration => {
|
||||
console.log('Service Worker erfolgreich registriert mit Scope:', registration.scope);
|
||||
|
||||
// Status des Service Workers überprüfen
|
||||
if (registration.installing) {
|
||||
console.log('Service Worker wird installiert');
|
||||
} else if (registration.waiting) {
|
||||
console.log('Service Worker wartet auf Aktivierung');
|
||||
} else if (registration.active) {
|
||||
console.log('Service Worker ist aktiv');
|
||||
}
|
||||
|
||||
// Auf Updates überwachen
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing;
|
||||
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
console.log(`Service Worker Status: ${newWorker.state}`);
|
||||
|
||||
// Bei Aktivierung Cache aktualisieren
|
||||
if (newWorker.state === 'activated') {
|
||||
this.fetchAndCacheAppData();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Prüfen auf Update beim App-Start
|
||||
registration.update();
|
||||
|
||||
}).catch(error => {
|
||||
console.error('Service Worker Registrierung fehlgeschlagen:', error);
|
||||
// Fallback auf lokalen Cache wenn Service Worker fehlschlägt
|
||||
this.setupLocalCache();
|
||||
});
|
||||
|
||||
// Kontrolltransfer abfangen
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
console.log('Service Worker Controller hat gewechselt');
|
||||
});
|
||||
|
||||
} else {
|
||||
console.warn('Service Worker werden von diesem Browser nicht unterstützt');
|
||||
// Fallback auf lokalen Cache wenn Service Worker nicht unterstützt werden
|
||||
this.setupLocalCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lokaler Cache-Fallback wenn Service Worker nicht verfügbar
|
||||
*/
|
||||
setupLocalCache() {
|
||||
console.log('Verwende lokalen Cache als Fallback');
|
||||
|
||||
// Wichtige App-Daten sofort laden und cachen
|
||||
this.fetchAndCacheAppData();
|
||||
|
||||
// Timer für regelmäßige Aktualisierung des Cache
|
||||
setInterval(() => {
|
||||
if (navigator.onLine) {
|
||||
this.fetchAndCacheAppData();
|
||||
}
|
||||
}, 15 * 60 * 1000); // Alle 15 Minuten
|
||||
}
|
||||
|
||||
/**
|
||||
* Laden und Cachen wichtiger API-Endpunkte
|
||||
*/
|
||||
fetchAndCacheAppData() {
|
||||
if (!navigator.onLine) return;
|
||||
|
||||
const endpoints = [
|
||||
'/api/printers',
|
||||
'/api/jobs',
|
||||
'/api/schedule',
|
||||
'/api/status'
|
||||
];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
fetch(endpoint)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
localStorage.setItem(`cache_${endpoint}`, JSON.stringify({
|
||||
timestamp: new Date().getTime(),
|
||||
data: data
|
||||
}));
|
||||
console.log(`Daten für ${endpoint} gecached`);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Fehler beim Cachen von ${endpoint}:`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lokaler Speicher für Offline-Daten
|
||||
*/
|
||||
setupLocalStorage() {
|
||||
// Speicher für Offline-Bearbeitung einrichten
|
||||
if (!localStorage.getItem('offlineChanges')) {
|
||||
localStorage.setItem('offlineChanges', JSON.stringify([]));
|
||||
}
|
||||
|
||||
// Speicher für App-Konfiguration einrichten
|
||||
if (!localStorage.getItem('appConfig')) {
|
||||
const defaultConfig = {
|
||||
theme: 'system',
|
||||
notifications: true,
|
||||
dataSync: true,
|
||||
lastSync: null
|
||||
};
|
||||
localStorage.setItem('appConfig', JSON.stringify(defaultConfig));
|
||||
}
|
||||
|
||||
// Aufräumen alter Cache-Einträge (älter als 7 Tage)
|
||||
this.cleanupOldCache(7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt alte Cache-Einträge
|
||||
* @param {number} daysOld - Alter in Tagen
|
||||
*/
|
||||
cleanupOldCache(daysOld) {
|
||||
const now = new Date().getTime();
|
||||
const maxAge = daysOld * 24 * 60 * 60 * 1000;
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
|
||||
// Nur Cache-Einträge prüfen
|
||||
if (key.startsWith('cache_')) {
|
||||
try {
|
||||
const item = JSON.parse(localStorage.getItem(key));
|
||||
if (item && item.timestamp && (now - item.timestamp > maxAge)) {
|
||||
localStorage.removeItem(key);
|
||||
console.log(`Alter Cache-Eintrag entfernt: ${key}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Fehler beim Verarbeiten von Cache-Eintrag ${key}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronisiert offline getätigte Änderungen
|
||||
*/
|
||||
syncOfflineData() {
|
||||
if (!navigator.onLine) return;
|
||||
|
||||
const offlineChanges = JSON.parse(localStorage.getItem('offlineChanges') || '[]');
|
||||
|
||||
if (offlineChanges.length === 0) {
|
||||
console.log('Keine Offline-Änderungen zu synchronisieren');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${offlineChanges.length} Offline-Änderungen werden synchronisiert...`);
|
||||
|
||||
// Status-Indikator für Synchronisierung anzeigen
|
||||
document.body.classList.add('syncing');
|
||||
|
||||
const syncPromises = offlineChanges.map(change => {
|
||||
return fetch(change.url, {
|
||||
method: change.method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Offline-Change': 'true'
|
||||
},
|
||||
body: JSON.stringify(change.data)
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fehler ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log(`Änderung erfolgreich synchronisiert: ${change.url}`);
|
||||
return { success: true, change };
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Synchronisierung fehlgeschlagen für ${change.url}:`, error);
|
||||
return { success: false, change, error };
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(syncPromises)
|
||||
.then(results => {
|
||||
// Fehlgeschlagene Änderungen behalten
|
||||
const failedChanges = results
|
||||
.filter(result => !result.success)
|
||||
.map(result => result.change);
|
||||
|
||||
localStorage.setItem('offlineChanges', JSON.stringify(failedChanges));
|
||||
|
||||
// App-Konfiguration aktualisieren
|
||||
const appConfig = JSON.parse(localStorage.getItem('appConfig') || '{}');
|
||||
appConfig.lastSync = new Date().toISOString();
|
||||
localStorage.setItem('appConfig', JSON.stringify(appConfig));
|
||||
|
||||
// Status-Indikator entfernen
|
||||
document.body.classList.remove('syncing');
|
||||
|
||||
// Event-Benachrichtigung
|
||||
const syncEvent = new CustomEvent('offlineDataSynced', {
|
||||
detail: {
|
||||
total: offlineChanges.length,
|
||||
succeeded: offlineChanges.length - failedChanges.length,
|
||||
failed: failedChanges.length
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(syncEvent);
|
||||
|
||||
console.log(`Synchronisierung abgeschlossen: ${offlineChanges.length - failedChanges.length} erfolgreich, ${failedChanges.length} fehlgeschlagen`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt Offline-Änderung zum Synchronisierungsstapel hinzu
|
||||
* @param {string} url - API-Endpunkt
|
||||
* @param {string} method - HTTP-Methode (POST, PUT, DELETE)
|
||||
* @param {object} data - Zu sendende Daten
|
||||
*/
|
||||
addOfflineChange(url, method, data) {
|
||||
const offlineChanges = JSON.parse(localStorage.getItem('offlineChanges') || '[]');
|
||||
|
||||
offlineChanges.push({
|
||||
id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5),
|
||||
url,
|
||||
method,
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
localStorage.setItem('offlineChanges', JSON.stringify(offlineChanges));
|
||||
console.log(`Offline-Änderung gespeichert: ${method} ${url}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme-Wechsel überwachen und reagieren
|
||||
*/
|
||||
setupThemeListeners() {
|
||||
// Auf Dark Mode Änderungen reagieren
|
||||
window.addEventListener('darkModeChanged', (e) => {
|
||||
this.darkMode = e.detail.isDark;
|
||||
this.updateAppTheme();
|
||||
|
||||
// App-Konfiguration aktualisieren
|
||||
const appConfig = JSON.parse(localStorage.getItem('appConfig') || '{}');
|
||||
appConfig.theme = this.darkMode ? 'dark' : 'light';
|
||||
localStorage.setItem('appConfig', JSON.stringify(appConfig));
|
||||
|
||||
console.log(`Theme geändert: ${this.darkMode ? 'Dark Mode' : 'Light Mode'}`);
|
||||
});
|
||||
|
||||
// Bei Systemthemen-Änderung prüfen, ob wir automatisch wechseln sollen
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
const appConfig = JSON.parse(localStorage.getItem('appConfig') || '{}');
|
||||
|
||||
// Nur reagieren, wenn der Nutzer "system" als Theme gewählt hat
|
||||
if (appConfig.theme === 'system') {
|
||||
this.darkMode = e.matches;
|
||||
this.updateAppTheme();
|
||||
console.log(`Systemthema geändert: ${this.darkMode ? 'Dark Mode' : 'Light Mode'}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme im UI aktualisieren
|
||||
*/
|
||||
updateAppTheme() {
|
||||
// Theme-color Meta-Tag aktualisieren für Mobile Browser
|
||||
const metaThemeColor = document.getElementById('metaThemeColor') ||
|
||||
document.querySelector('meta[name="theme-color"]');
|
||||
if (metaThemeColor) {
|
||||
metaThemeColor.setAttribute('content', this.darkMode ? '#0f172a' : '#ffffff');
|
||||
}
|
||||
|
||||
// Dynamische UI-Elemente anpassen
|
||||
this.updateUIForTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* UI-Elemente für das aktuelle Theme anpassen
|
||||
*/
|
||||
updateUIForTheme() {
|
||||
// Offline-Banner und Sync-Indikator anpassen
|
||||
const offlineBanner = document.getElementById('offline-banner');
|
||||
const syncIndicator = document.getElementById('sync-indicator');
|
||||
|
||||
if (offlineBanner) {
|
||||
if (this.darkMode) {
|
||||
offlineBanner.classList.add('dark-theme');
|
||||
offlineBanner.classList.remove('light-theme');
|
||||
} else {
|
||||
offlineBanner.classList.add('light-theme');
|
||||
offlineBanner.classList.remove('dark-theme');
|
||||
}
|
||||
}
|
||||
|
||||
if (syncIndicator) {
|
||||
if (this.darkMode) {
|
||||
syncIndicator.classList.add('dark-theme');
|
||||
syncIndicator.classList.remove('light-theme');
|
||||
} else {
|
||||
syncIndicator.classList.add('light-theme');
|
||||
syncIndicator.classList.remove('dark-theme');
|
||||
}
|
||||
}
|
||||
|
||||
// Andere dynamische UI-Elemente hier anpassen
|
||||
}
|
||||
|
||||
/**
|
||||
* UI-Komponenten initialisieren
|
||||
*/
|
||||
setupUI() {
|
||||
// Offline-Modus Banner einfügen wenn nicht vorhanden
|
||||
if (!document.getElementById('offline-banner')) {
|
||||
const banner = document.createElement('div');
|
||||
banner.id = 'offline-banner';
|
||||
banner.className = `hidden fixed top-0 left-0 right-0 bg-amber-500 dark:bg-amber-600 text-white text-center py-2 px-4 z-50 ${this.darkMode ? 'dark-theme' : 'light-theme'}`;
|
||||
banner.textContent = 'Sie sind offline. Einige Funktionen sind eingeschränkt.';
|
||||
document.body.prepend(banner);
|
||||
|
||||
// Anzeigen wenn offline
|
||||
if (this.isOffline) {
|
||||
banner.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Event-Listener für Online/Offline-Status
|
||||
window.addEventListener('online', () => banner.classList.add('hidden'));
|
||||
window.addEventListener('offline', () => banner.classList.remove('hidden'));
|
||||
}
|
||||
|
||||
// Synchronisierungs-Indikator einfügen wenn nicht vorhanden
|
||||
if (!document.getElementById('sync-indicator')) {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.id = 'sync-indicator';
|
||||
indicator.className = `hidden fixed bottom-4 right-4 bg-indigo-600 dark:bg-indigo-700 text-white text-sm rounded-full py-1 px-3 z-50 flex items-center ${this.darkMode ? 'dark-theme' : 'light-theme'}`;
|
||||
|
||||
const spinnerSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
spinnerSvg.setAttribute('class', 'animate-spin -ml-1 mr-2 h-4 w-4 text-white');
|
||||
spinnerSvg.setAttribute('fill', 'none');
|
||||
spinnerSvg.setAttribute('viewBox', '0 0 24 24');
|
||||
|
||||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
circle.setAttribute('class', 'opacity-25');
|
||||
circle.setAttribute('cx', '12');
|
||||
circle.setAttribute('cy', '12');
|
||||
circle.setAttribute('r', '10');
|
||||
circle.setAttribute('stroke', 'currentColor');
|
||||
circle.setAttribute('stroke-width', '4');
|
||||
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
path.setAttribute('class', 'opacity-75');
|
||||
path.setAttribute('fill', 'currentColor');
|
||||
path.setAttribute('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');
|
||||
|
||||
spinnerSvg.appendChild(circle);
|
||||
spinnerSvg.appendChild(path);
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.textContent = 'Synchronisiere...';
|
||||
|
||||
indicator.appendChild(spinnerSvg);
|
||||
indicator.appendChild(text);
|
||||
document.body.appendChild(indicator);
|
||||
|
||||
// Anzeigen wenn synchronisiert wird
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||
if (document.body.classList.contains('syncing')) {
|
||||
indicator.classList.remove('hidden');
|
||||
} else {
|
||||
indicator.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, { attributes: true });
|
||||
}
|
||||
|
||||
// Mobile Optimierungen
|
||||
this.setupMobileOptimizations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile Optimierungen
|
||||
*/
|
||||
setupMobileOptimizations() {
|
||||
// Service Worker-Status auf mobilen Geräten überwachen und UI entsprechend anpassen
|
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||
// Verbessert Touch-Handhabung auf Mobilgeräten
|
||||
this.setupTouchFeedback();
|
||||
|
||||
// Viewport-Höhe für mobile Browser anpassen (Adressleisten-Problem)
|
||||
this.fixMobileViewportHeight();
|
||||
|
||||
// Scroll-Wiederherstellung deaktivieren für eine bessere UX auf Mobilgeräten
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch-Feedback für mobile Geräte
|
||||
*/
|
||||
setupTouchFeedback() {
|
||||
// Aktive Klasse für Touch-Feedback hinzufügen
|
||||
document.addEventListener('touchstart', function(e) {
|
||||
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A' ||
|
||||
e.target.closest('button') || e.target.closest('a')) {
|
||||
const element = e.target.tagName === 'BUTTON' || e.target.tagName === 'A' ?
|
||||
e.target : (e.target.closest('button') || e.target.closest('a'));
|
||||
element.classList.add('touch-active');
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchend', function() {
|
||||
const activeElements = document.querySelectorAll('.touch-active');
|
||||
activeElements.forEach(el => el.classList.remove('touch-active'));
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Viewport-Höhe für mobile Browser fixieren
|
||||
*/
|
||||
fixMobileViewportHeight() {
|
||||
// Mobile Viewport-Höhe berechnen und als CSS-Variable setzen
|
||||
const setViewportHeight = () => {
|
||||
const vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
};
|
||||
|
||||
// Initial setzen und bei Größenänderung aktualisieren
|
||||
setViewportHeight();
|
||||
window.addEventListener('resize', setViewportHeight);
|
||||
|
||||
// Auch bei Orientierungsänderung aktualisieren
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(setViewportHeight, 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// App initialisieren wenn DOM geladen ist
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.myp = new MYPApp();
|
||||
});
|
560
backend/static/js/optimization-features.js
Normal file
560
backend/static/js/optimization-features.js
Normal file
@ -0,0 +1,560 @@
|
||||
/**
|
||||
* MYP Platform - Erweiterte Optimierungs- und Batch-Funktionen
|
||||
* Implementiert Auto-Optimierung und Batch-Planung für 3D-Druckaufträge
|
||||
*/
|
||||
|
||||
class OptimizationManager {
|
||||
constructor() {
|
||||
this.isAutoOptimizationEnabled = false;
|
||||
this.isBatchModeEnabled = false;
|
||||
this.selectedJobs = new Set();
|
||||
this.optimizationSettings = {
|
||||
algorithm: 'round_robin', // 'round_robin', 'load_balance', 'priority_based'
|
||||
considerDistance: true,
|
||||
minimizeChangeover: true,
|
||||
maxBatchSize: 10,
|
||||
timeWindow: 24 // Stunden
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.loadSavedSettings();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.altKey && e.key === 'O') {
|
||||
this.toggleAutoOptimization();
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.ctrlKey && e.altKey && e.key === 'B') {
|
||||
this.toggleBatchMode();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Optimierung ein-/ausschalten
|
||||
* Diese Funktion optimiert automatisch die Druckreihenfolge basierend auf verschiedenen Faktoren
|
||||
*/
|
||||
toggleAutoOptimization() {
|
||||
this.isAutoOptimizationEnabled = !this.isAutoOptimizationEnabled;
|
||||
|
||||
const button = document.getElementById('auto-opt-toggle');
|
||||
if (button) {
|
||||
this.updateAutoOptimizationButton(button);
|
||||
}
|
||||
|
||||
// Settings speichern
|
||||
localStorage.setItem('myp-auto-optimization', this.isAutoOptimizationEnabled);
|
||||
|
||||
// Notification anzeigen
|
||||
this.showOptimizationNotification(
|
||||
this.isAutoOptimizationEnabled ? 'aktiviert' : 'deaktiviert',
|
||||
'auto-optimization'
|
||||
);
|
||||
|
||||
// Wenn aktiviert, sofortige Optimierung durchführen
|
||||
if (this.isAutoOptimizationEnabled) {
|
||||
this.performAutoOptimization();
|
||||
}
|
||||
|
||||
// UI aktualisieren
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
updateAutoOptimizationButton(button) {
|
||||
const span = button.querySelector('span');
|
||||
const icon = button.querySelector('svg');
|
||||
|
||||
if (this.isAutoOptimizationEnabled) {
|
||||
button.classList.remove('btn-secondary');
|
||||
button.classList.add('btn-primary');
|
||||
span.textContent = 'Auto-Optimierung AN';
|
||||
|
||||
// Button-Animation
|
||||
button.style.transform = 'scale(1.05)';
|
||||
setTimeout(() => {
|
||||
button.style.transform = '';
|
||||
}, 200);
|
||||
|
||||
// Icon-Animation
|
||||
icon.style.animation = 'spin 1s ease-in-out';
|
||||
setTimeout(() => {
|
||||
icon.style.animation = '';
|
||||
}, 1000);
|
||||
} else {
|
||||
button.classList.remove('btn-primary');
|
||||
button.classList.add('btn-secondary');
|
||||
span.textContent = 'Auto-Optimierung';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch-Modus ein-/ausschalten
|
||||
* Ermöglicht die Auswahl mehrerer Jobs für Batch-Operationen
|
||||
*/
|
||||
toggleBatchMode() {
|
||||
this.isBatchModeEnabled = !this.isBatchModeEnabled;
|
||||
|
||||
const button = document.getElementById('batch-toggle');
|
||||
if (button) {
|
||||
this.updateBatchModeButton(button);
|
||||
}
|
||||
|
||||
// Batch-Funktionalität aktivieren/deaktivieren
|
||||
this.toggleBatchSelection();
|
||||
|
||||
// Settings speichern
|
||||
localStorage.setItem('myp-batch-mode', this.isBatchModeEnabled);
|
||||
|
||||
// Notification anzeigen
|
||||
this.showOptimizationNotification(
|
||||
this.isBatchModeEnabled ? 'aktiviert' : 'deaktiviert',
|
||||
'batch-mode'
|
||||
);
|
||||
|
||||
// UI aktualisieren
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
updateBatchModeButton(button) {
|
||||
const span = button.querySelector('span');
|
||||
|
||||
if (this.isBatchModeEnabled) {
|
||||
button.classList.remove('btn-secondary');
|
||||
button.classList.add('btn-warning');
|
||||
span.textContent = `Batch-Modus (${this.selectedJobs.size})`;
|
||||
|
||||
// Button-Animation
|
||||
button.style.transform = 'scale(1.05)';
|
||||
setTimeout(() => {
|
||||
button.style.transform = '';
|
||||
}, 200);
|
||||
} else {
|
||||
button.classList.remove('btn-warning');
|
||||
button.classList.add('btn-secondary');
|
||||
span.textContent = 'Mehrfachauswahl';
|
||||
|
||||
// Auswahl zurücksetzen
|
||||
this.selectedJobs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
toggleBatchSelection() {
|
||||
const jobCards = document.querySelectorAll('.job-card, [data-job-id]');
|
||||
|
||||
jobCards.forEach(card => {
|
||||
if (this.isBatchModeEnabled) {
|
||||
this.enableBatchSelection(card);
|
||||
} else {
|
||||
this.disableBatchSelection(card);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableBatchSelection(card) {
|
||||
// Checkbox hinzufügen
|
||||
let checkbox = card.querySelector('.batch-checkbox');
|
||||
if (!checkbox) {
|
||||
checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.className = 'batch-checkbox absolute top-3 left-3 w-5 h-5 rounded border-2 border-gray-300 text-blue-600 focus:ring-blue-500';
|
||||
checkbox.style.zIndex = '10';
|
||||
|
||||
// Event Listener für Checkbox
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const jobId = card.dataset.jobId;
|
||||
if (e.target.checked) {
|
||||
this.selectedJobs.add(jobId);
|
||||
card.classList.add('selected-for-batch');
|
||||
} else {
|
||||
this.selectedJobs.delete(jobId);
|
||||
card.classList.remove('selected-for-batch');
|
||||
}
|
||||
this.updateBatchCounter();
|
||||
});
|
||||
|
||||
// Checkbox in die Karte einfügen
|
||||
card.style.position = 'relative';
|
||||
card.appendChild(checkbox);
|
||||
}
|
||||
|
||||
checkbox.style.display = 'block';
|
||||
card.classList.add('batch-selectable');
|
||||
}
|
||||
|
||||
disableBatchSelection(card) {
|
||||
const checkbox = card.querySelector('.batch-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.style.display = 'none';
|
||||
}
|
||||
card.classList.remove('batch-selectable', 'selected-for-batch');
|
||||
}
|
||||
|
||||
updateBatchCounter() {
|
||||
const button = document.getElementById('batch-toggle');
|
||||
if (button && this.isBatchModeEnabled) {
|
||||
const span = button.querySelector('span');
|
||||
span.textContent = `Batch-Modus (${this.selectedJobs.size})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatische Optimierung durchführen
|
||||
*/
|
||||
async performAutoOptimization() {
|
||||
try {
|
||||
const response = await fetch('/api/optimization/auto-optimize', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCSRFToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
settings: this.optimizationSettings,
|
||||
enabled: this.isAutoOptimizationEnabled
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSuccessMessage(`Optimierung erfolgreich: ${data.optimized_jobs} Jobs optimiert`);
|
||||
this.refreshCurrentView();
|
||||
} else {
|
||||
this.showErrorMessage(`Optimierung fehlgeschlagen: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auto-Optimierung Fehler:', error);
|
||||
this.showErrorMessage('Netzwerkfehler bei der Auto-Optimierung');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch-Operationen durchführen
|
||||
*/
|
||||
async performBatchOperation(operation) {
|
||||
if (this.selectedJobs.size === 0) {
|
||||
this.showWarningMessage('Keine Jobs für Batch-Operation ausgewählt');
|
||||
return;
|
||||
}
|
||||
|
||||
const jobIds = Array.from(this.selectedJobs);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/jobs/batch-operation', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCSRFToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
job_ids: jobIds,
|
||||
operation: operation
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSuccessMessage(`Batch-Operation "${operation}" erfolgreich auf ${jobIds.length} Jobs angewendet`);
|
||||
this.selectedJobs.clear();
|
||||
this.updateBatchCounter();
|
||||
this.refreshCurrentView();
|
||||
} else {
|
||||
this.showErrorMessage(`Batch-Operation fehlgeschlagen: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Batch-Operation Fehler:', error);
|
||||
this.showErrorMessage('Netzwerkfehler bei der Batch-Operation');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimierungs-Einstellungen konfigurieren
|
||||
*/
|
||||
showOptimizationSettings() {
|
||||
this.createOptimizationModal();
|
||||
}
|
||||
|
||||
createOptimizationModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'optimization-settings-modal';
|
||||
modal.className = 'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="dashboard-card max-w-2xl w-full p-8 transform transition-all duration-300">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">
|
||||
Optimierungs-Einstellungen
|
||||
</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400">
|
||||
Konfigurieren Sie die automatische Optimierung für maximale Effizienz
|
||||
</p>
|
||||
</div>
|
||||
<button onclick="this.closest('#optimization-settings-modal').remove()"
|
||||
class="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg transition-colors">
|
||||
<svg class="w-6 h-6 text-slate-500 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Optimierungs-Algorithmus
|
||||
</label>
|
||||
<select id="optimization-algorithm" class="block w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
|
||||
<option value="round_robin">Round Robin - Gleichmäßige Verteilung</option>
|
||||
<option value="load_balance">Load Balancing - Auslastungsoptimierung</option>
|
||||
<option value="priority_based">Prioritätsbasiert - Wichtige Jobs zuerst</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="consider-distance" class="mr-2">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">
|
||||
Druckerentfernung berücksichtigen
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="minimize-changeover" class="mr-2">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">
|
||||
Rüstzeiten minimieren
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Max. Batch-Größe
|
||||
</label>
|
||||
<input type="number" id="max-batch-size" min="1" max="50"
|
||||
class="block w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Planungshorizont (Stunden)
|
||||
</label>
|
||||
<input type="number" id="time-window" min="1" max="168"
|
||||
class="block w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 pt-6 border-t border-gray-200 dark:border-slate-600 mt-6">
|
||||
<button onclick="this.closest('#optimization-settings-modal').remove()"
|
||||
class="btn-secondary">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button onclick="optimizationManager.saveOptimizationSettings()"
|
||||
class="btn-primary">
|
||||
Einstellungen speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
this.loadOptimizationSettingsInModal();
|
||||
}
|
||||
|
||||
loadOptimizationSettingsInModal() {
|
||||
document.getElementById('optimization-algorithm').value = this.optimizationSettings.algorithm;
|
||||
document.getElementById('consider-distance').checked = this.optimizationSettings.considerDistance;
|
||||
document.getElementById('minimize-changeover').checked = this.optimizationSettings.minimizeChangeover;
|
||||
document.getElementById('max-batch-size').value = this.optimizationSettings.maxBatchSize;
|
||||
document.getElementById('time-window').value = this.optimizationSettings.timeWindow;
|
||||
}
|
||||
|
||||
saveOptimizationSettings() {
|
||||
this.optimizationSettings.algorithm = document.getElementById('optimization-algorithm').value;
|
||||
this.optimizationSettings.considerDistance = document.getElementById('consider-distance').checked;
|
||||
this.optimizationSettings.minimizeChangeover = document.getElementById('minimize-changeover').checked;
|
||||
this.optimizationSettings.maxBatchSize = parseInt(document.getElementById('max-batch-size').value);
|
||||
this.optimizationSettings.timeWindow = parseInt(document.getElementById('time-window').value);
|
||||
|
||||
localStorage.setItem('myp-optimization-settings', JSON.stringify(this.optimizationSettings));
|
||||
|
||||
document.getElementById('optimization-settings-modal').remove();
|
||||
this.showSuccessMessage('Optimierungs-Einstellungen gespeichert');
|
||||
|
||||
// Wenn Auto-Optimierung aktiv ist, neue Optimierung durchführen
|
||||
if (this.isAutoOptimizationEnabled) {
|
||||
this.performAutoOptimization();
|
||||
}
|
||||
}
|
||||
|
||||
loadSavedSettings() {
|
||||
// Auto-Optimierung Status laden
|
||||
const savedAutoOpt = localStorage.getItem('myp-auto-optimization');
|
||||
if (savedAutoOpt !== null) {
|
||||
this.isAutoOptimizationEnabled = savedAutoOpt === 'true';
|
||||
}
|
||||
|
||||
// Batch-Modus Status laden
|
||||
const savedBatchMode = localStorage.getItem('myp-batch-mode');
|
||||
if (savedBatchMode !== null) {
|
||||
this.isBatchModeEnabled = savedBatchMode === 'true';
|
||||
}
|
||||
|
||||
// Optimierungs-Einstellungen laden
|
||||
const savedSettings = localStorage.getItem('myp-optimization-settings');
|
||||
if (savedSettings) {
|
||||
try {
|
||||
this.optimizationSettings = { ...this.optimizationSettings, ...JSON.parse(savedSettings) };
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Optimierungs-Einstellungen:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
// Buttons aktualisieren
|
||||
const autoOptButton = document.getElementById('auto-opt-toggle');
|
||||
if (autoOptButton) {
|
||||
this.updateAutoOptimizationButton(autoOptButton);
|
||||
}
|
||||
|
||||
const batchButton = document.getElementById('batch-toggle');
|
||||
if (batchButton) {
|
||||
this.updateBatchModeButton(batchButton);
|
||||
}
|
||||
|
||||
// Batch-Auswahl aktualisieren
|
||||
if (this.isBatchModeEnabled) {
|
||||
this.toggleBatchSelection();
|
||||
}
|
||||
}
|
||||
|
||||
// Utility-Funktionen
|
||||
getCSRFToken() {
|
||||
const token = document.querySelector('meta[name="csrf-token"]');
|
||||
return token ? token.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
refreshCurrentView() {
|
||||
// Je nach aktueller Seite entsprechende Refresh-Funktion aufrufen
|
||||
if (typeof refreshJobs === 'function') {
|
||||
refreshJobs();
|
||||
} else if (typeof refreshCalendar === 'function') {
|
||||
refreshCalendar();
|
||||
} else if (typeof refreshDashboard === 'function') {
|
||||
refreshDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
showOptimizationNotification(status, type) {
|
||||
const messages = {
|
||||
'auto-optimization': {
|
||||
'aktiviert': '🚀 Auto-Optimierung aktiviert - Jobs werden automatisch optimiert',
|
||||
'deaktiviert': '⏸️ Auto-Optimierung deaktiviert'
|
||||
},
|
||||
'batch-mode': {
|
||||
'aktiviert': '📦 Batch-Modus aktiviert - Wählen Sie Jobs für Batch-Operationen aus',
|
||||
'deaktiviert': '✅ Batch-Modus deaktiviert'
|
||||
}
|
||||
};
|
||||
|
||||
const message = messages[type]?.[status] || `${type} ${status}`;
|
||||
this.showSuccessMessage(message);
|
||||
}
|
||||
|
||||
showSuccessMessage(message) {
|
||||
this.showToast(message, 'success');
|
||||
}
|
||||
|
||||
showErrorMessage(message) {
|
||||
this.showToast(message, 'error');
|
||||
}
|
||||
|
||||
showWarningMessage(message) {
|
||||
this.showToast(message, 'warning');
|
||||
}
|
||||
|
||||
showToast(message, type = 'info') {
|
||||
// Einfache Toast-Benachrichtigung
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transition-all duration-300 transform translate-x-full`;
|
||||
|
||||
const colors = {
|
||||
success: 'bg-green-500 text-white',
|
||||
error: 'bg-red-500 text-white',
|
||||
warning: 'bg-yellow-500 text-black',
|
||||
info: 'bg-blue-500 text-white'
|
||||
};
|
||||
|
||||
toast.className += ` ${colors[type]}`;
|
||||
toast.textContent = message;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Animation einblenden
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// Nach 5 Sekunden automatisch entfernen
|
||||
setTimeout(() => {
|
||||
toast.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Funktionen für Template-Integration
|
||||
let optimizationManager;
|
||||
|
||||
// Auto-Optimierung umschalten
|
||||
window.toggleAutoOptimization = function() {
|
||||
if (!optimizationManager) {
|
||||
optimizationManager = new OptimizationManager();
|
||||
}
|
||||
optimizationManager.toggleAutoOptimization();
|
||||
};
|
||||
|
||||
// Batch-Modus umschalten
|
||||
window.toggleBatchMode = function() {
|
||||
if (!optimizationManager) {
|
||||
optimizationManager = new OptimizationManager();
|
||||
}
|
||||
optimizationManager.toggleBatchMode();
|
||||
};
|
||||
|
||||
// Batch-Planung Modal öffnen
|
||||
window.openBatchPlanningModal = function() {
|
||||
if (!optimizationManager) {
|
||||
optimizationManager = new OptimizationManager();
|
||||
}
|
||||
optimizationManager.showOptimizationSettings();
|
||||
};
|
||||
|
||||
// Optimierungs-Einstellungen anzeigen
|
||||
window.showOptimizationSettings = function() {
|
||||
if (!optimizationManager) {
|
||||
optimizationManager = new OptimizationManager();
|
||||
}
|
||||
optimizationManager.showOptimizationSettings();
|
||||
};
|
||||
|
||||
// Initialisierung beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
optimizationManager = new OptimizationManager();
|
||||
console.log('🎯 Optimierungs-Manager initialisiert');
|
||||
});
|
471
backend/static/js/printer_monitor.js
Normal file
471
backend/static/js/printer_monitor.js
Normal file
@ -0,0 +1,471 @@
|
||||
/**
|
||||
* Live-Drucker-Monitor für MYP Platform
|
||||
* Verwaltet Live-Status-Updates mit Session-Caching und automatischer Aktualisierung
|
||||
*/
|
||||
|
||||
class PrinterMonitor {
|
||||
constructor() {
|
||||
this.refreshInterval = 30000; // 30 Sekunden Standard-Intervall
|
||||
this.fastRefreshInterval = 5000; // 5 Sekunden für schnelle Updates
|
||||
this.currentInterval = null;
|
||||
this.printers = new Map();
|
||||
this.callbacks = new Set();
|
||||
this.isActive = false;
|
||||
this.useCache = true;
|
||||
this.lastUpdate = null;
|
||||
this.errorCount = 0;
|
||||
this.maxErrors = 3;
|
||||
|
||||
// Status-Kategorien für bessere Übersicht
|
||||
this.statusCategories = {
|
||||
'online': { label: 'Online', color: 'success', icon: '🟢' },
|
||||
'offline': { label: 'Offline', color: 'danger', icon: '🔴' },
|
||||
'standby': { label: 'Standby', color: 'warning', icon: '🟡' },
|
||||
'unreachable': { label: 'Nicht erreichbar', color: 'secondary', icon: '⚫' },
|
||||
'unconfigured': { label: 'Nicht konfiguriert', color: 'info', icon: '🔵' }
|
||||
};
|
||||
|
||||
console.log('🖨️ PrinterMonitor initialisiert');
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet das Live-Monitoring
|
||||
*/
|
||||
start() {
|
||||
if (this.isActive) {
|
||||
console.log('⚠️ PrinterMonitor läuft bereits');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isActive = true;
|
||||
this.errorCount = 0;
|
||||
|
||||
console.log('🚀 Starte PrinterMonitor');
|
||||
|
||||
// Sofortige erste Aktualisierung
|
||||
this.updatePrinterStatus();
|
||||
|
||||
// Reguläres Intervall starten
|
||||
this.startInterval();
|
||||
|
||||
// Event-Listener für Sichtbarkeitsänderungen
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stoppt das Live-Monitoring
|
||||
*/
|
||||
stop() {
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isActive = false;
|
||||
|
||||
if (this.currentInterval) {
|
||||
clearInterval(this.currentInterval);
|
||||
this.currentInterval = null;
|
||||
}
|
||||
|
||||
document.removeEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
|
||||
|
||||
console.log('⏹️ PrinterMonitor gestoppt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet das Update-Intervall
|
||||
*/
|
||||
startInterval() {
|
||||
if (this.currentInterval) {
|
||||
clearInterval(this.currentInterval);
|
||||
}
|
||||
|
||||
this.currentInterval = setInterval(() => {
|
||||
this.updatePrinterStatus();
|
||||
}, this.refreshInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Behandelt Sichtbarkeitsänderungen der Seite
|
||||
*/
|
||||
handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
// Seite nicht sichtbar - langsamere Updates
|
||||
this.refreshInterval = 60000; // 1 Minute
|
||||
} else {
|
||||
// Seite sichtbar - normale Updates
|
||||
this.refreshInterval = 30000; // 30 Sekunden
|
||||
// Sofortige Aktualisierung wenn Seite wieder sichtbar
|
||||
this.updatePrinterStatus();
|
||||
}
|
||||
|
||||
if (this.isActive) {
|
||||
this.startInterval();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt aktuelle Drucker-Status-Daten
|
||||
*/
|
||||
async updatePrinterStatus() {
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/printers/monitor/live-status?use_cache=${this.useCache}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.processPrinterData(data);
|
||||
this.errorCount = 0; // Reset error count on success
|
||||
} else {
|
||||
throw new Error(data.error || 'Unbekannter Fehler');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.errorCount++;
|
||||
console.error('❌ Fehler beim Abrufen des Drucker-Status:', error);
|
||||
|
||||
// Bei wiederholten Fehlern weniger frequent versuchen
|
||||
if (this.errorCount >= this.maxErrors) {
|
||||
this.refreshInterval = Math.min(this.refreshInterval * 2, 300000); // Max 5 Minuten
|
||||
this.startInterval();
|
||||
|
||||
this.notifyCallbacks({
|
||||
type: 'error',
|
||||
message: `Drucker-Status nicht verfügbar (${this.errorCount} Fehler)`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet empfangene Drucker-Daten
|
||||
*/
|
||||
processPrinterData(data) {
|
||||
const previousPrinters = new Map(this.printers);
|
||||
|
||||
// Drucker-Daten aktualisieren
|
||||
this.printers.clear();
|
||||
|
||||
// Null-Check für data.printers hinzufügen
|
||||
if (data && data.printers && typeof data.printers === 'object') {
|
||||
Object.values(data.printers).forEach(printer => {
|
||||
this.printers.set(printer.id, {
|
||||
...printer,
|
||||
statusInfo: this.statusCategories[printer.status] || this.statusCategories['offline']
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ Keine gültigen Drucker-Daten erhalten:', data);
|
||||
// Benachrichtige Callbacks über Fehler
|
||||
this.notifyCallbacks({
|
||||
type: 'error',
|
||||
message: 'Ungültige Drucker-Daten erhalten',
|
||||
data: data
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastUpdate = new Date(data.timestamp || Date.now());
|
||||
|
||||
// Änderungen erkennen und benachrichtigen
|
||||
const changes = this.detectChanges(previousPrinters, this.printers);
|
||||
|
||||
// Callbacks benachrichtigen
|
||||
this.notifyCallbacks({
|
||||
type: 'update',
|
||||
printers: this.printers,
|
||||
summary: data.summary,
|
||||
changes: changes,
|
||||
timestamp: this.lastUpdate,
|
||||
cacheUsed: data.cache_used
|
||||
});
|
||||
|
||||
console.log(`🔄 Drucker-Status aktualisiert: ${this.printers.size} Drucker`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erkennt Änderungen zwischen zwei Drucker-Status-Maps
|
||||
*/
|
||||
detectChanges(oldPrinters, newPrinters) {
|
||||
const changes = [];
|
||||
|
||||
newPrinters.forEach((newPrinter, id) => {
|
||||
const oldPrinter = oldPrinters.get(id);
|
||||
|
||||
if (!oldPrinter) {
|
||||
changes.push({
|
||||
type: 'added',
|
||||
printer: newPrinter
|
||||
});
|
||||
} else if (oldPrinter.status !== newPrinter.status) {
|
||||
changes.push({
|
||||
type: 'status_change',
|
||||
printer: newPrinter,
|
||||
oldStatus: oldPrinter.status,
|
||||
newStatus: newPrinter.status
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
oldPrinters.forEach((oldPrinter, id) => {
|
||||
if (!newPrinters.has(id)) {
|
||||
changes.push({
|
||||
type: 'removed',
|
||||
printer: oldPrinter
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Benachrichtigt alle registrierten Callbacks
|
||||
*/
|
||||
notifyCallbacks(data) {
|
||||
this.callbacks.forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler in PrinterMonitor Callback:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert einen Callback für Status-Updates
|
||||
*/
|
||||
onUpdate(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.callbacks.add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt einen Callback
|
||||
*/
|
||||
offUpdate(callback) {
|
||||
this.callbacks.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzwingt eine sofortige Aktualisierung ohne Cache
|
||||
*/
|
||||
async forceUpdate() {
|
||||
const oldUseCache = this.useCache;
|
||||
this.useCache = false;
|
||||
|
||||
try {
|
||||
await this.updatePrinterStatus();
|
||||
} finally {
|
||||
this.useCache = oldUseCache;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht den Cache und erzwingt eine Aktualisierung
|
||||
*/
|
||||
async clearCache() {
|
||||
try {
|
||||
const response = await fetch('/api/printers/monitor/clear-cache', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('🧹 Drucker-Cache geleert');
|
||||
await this.forceUpdate();
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Leeren des Caches:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt eine schnelle Status-Zusammenfassung
|
||||
*/
|
||||
async getSummary() {
|
||||
try {
|
||||
const response = await fetch('/api/printers/monitor/summary', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return data.success ? data.summary : null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Abrufen der Zusammenfassung:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den aktuellen Status eines Druckers zurück
|
||||
*/
|
||||
getPrinter(id) {
|
||||
return this.printers.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Drucker zurück
|
||||
*/
|
||||
getAllPrinters() {
|
||||
return Array.from(this.printers.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Drucker nach Status gefiltert zurück
|
||||
*/
|
||||
getPrintersByStatus(status) {
|
||||
return this.getAllPrinters().filter(printer => printer.status === status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine Status-Zusammenfassung zurück
|
||||
*/
|
||||
getStatusSummary() {
|
||||
const summary = {
|
||||
total: this.printers.size,
|
||||
online: 0,
|
||||
offline: 0,
|
||||
printing: 0, // Neuer Status: Drucker druckt gerade
|
||||
standby: 0,
|
||||
unreachable: 0,
|
||||
unconfigured: 0,
|
||||
error: 0 // Status für unbekannte Fehler
|
||||
};
|
||||
|
||||
this.printers.forEach(printer => {
|
||||
const status = printer.status;
|
||||
if (summary.hasOwnProperty(status)) {
|
||||
summary[status]++;
|
||||
} else {
|
||||
// Fallback für unbekannte Status
|
||||
summary.offline++;
|
||||
}
|
||||
});
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktiviert schnelle Updates (für kritische Operationen)
|
||||
*/
|
||||
enableFastUpdates() {
|
||||
this.refreshInterval = this.fastRefreshInterval;
|
||||
if (this.isActive) {
|
||||
this.startInterval();
|
||||
}
|
||||
console.log('⚡ Schnelle Updates aktiviert');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deaktiviert schnelle Updates
|
||||
*/
|
||||
disableFastUpdates() {
|
||||
this.refreshInterval = 30000; // Zurück zu normal
|
||||
if (this.isActive) {
|
||||
this.startInterval();
|
||||
}
|
||||
console.log('🐌 Normale Updates aktiviert');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert alle Steckdosen (nur für Admins)
|
||||
*/
|
||||
async initializeAllOutlets() {
|
||||
try {
|
||||
const response = await fetch('/api/printers/monitor/initialize-outlets', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
console.log('🔌 Steckdosen-Initialisierung erfolgreich:', data.statistics);
|
||||
|
||||
// Benachrichtige über Initialisierung
|
||||
this.notifyCallbacks({
|
||||
type: 'initialization',
|
||||
results: data.results,
|
||||
statistics: data.statistics,
|
||||
message: data.message
|
||||
});
|
||||
|
||||
// Erzwinge Update nach Initialisierung
|
||||
await this.forceUpdate();
|
||||
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(data.error || 'Initialisierung fehlgeschlagen');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei Steckdosen-Initialisierung:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Instanz
|
||||
window.printerMonitor = new PrinterMonitor();
|
||||
|
||||
// Auto-Start wenn DOM bereit ist
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Nur starten wenn wir auf einer relevanten Seite sind
|
||||
const relevantPages = ['/printers', '/dashboard', '/admin', '/admin-dashboard'];
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
if (relevantPages.some(page => currentPath.includes(page))) {
|
||||
console.log('🖨️ Auto-Start PrinterMonitor für Seite:', currentPath);
|
||||
window.printerMonitor.start();
|
||||
}
|
||||
});
|
||||
|
||||
// Automatisches Cleanup bei Seitenverlassen
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (window.printerMonitor) {
|
||||
window.printerMonitor.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// Export für Module
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = PrinterMonitor;
|
||||
}
|
82
backend/static/js/service-worker.js
Normal file
82
backend/static/js/service-worker.js
Normal file
@ -0,0 +1,82 @@
|
||||
// Backup Service Worker for MYP Platform
|
||||
const CACHE_NAME = 'myp-platform-backup-v1';
|
||||
|
||||
// Assets to cache
|
||||
const ASSETS_TO_CACHE = [
|
||||
'/',
|
||||
'/dashboard',
|
||||
'/static/css/tailwind.min.css',
|
||||
'/static/css/tailwind-dark.min.css',
|
||||
'/static/js/ui-components.js',
|
||||
'/static/js/offline-app.js',
|
||||
'/static/favicon.ico'
|
||||
];
|
||||
|
||||
// Install event - cache core assets
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('Backup SW: Installing...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => {
|
||||
console.log('Backup SW: Caching assets');
|
||||
return cache.addAll(ASSETS_TO_CACHE);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Backup SW: Assets cached');
|
||||
return self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('Backup SW: Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
console.log('Backup SW: Deleting old cache', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Backup SW: Activated');
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - cache first, network fallback
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return fetch(event.request)
|
||||
.then((fetchResponse) => {
|
||||
// Don't cache non-success responses
|
||||
if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') {
|
||||
return fetchResponse;
|
||||
}
|
||||
|
||||
// Cache successful responses
|
||||
const responseToCache = fetchResponse.clone();
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => {
|
||||
cache.put(event.request, responseToCache);
|
||||
});
|
||||
|
||||
return fetchResponse;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
490
backend/static/js/session-manager.js
Normal file
490
backend/static/js/session-manager.js
Normal file
@ -0,0 +1,490 @@
|
||||
/**
|
||||
* Erweitertes Session-Management für MYP Platform
|
||||
*
|
||||
* Features:
|
||||
* - Automatische Session-Überwachung
|
||||
* - Heartbeat-System für Session-Verlängerung
|
||||
* - Benutzer-Warnungen bei bevorstehender Abmeldung
|
||||
* - Graceful Logout bei Session-Ablauf
|
||||
* - Modal-Dialoge für Session-Verlängerung
|
||||
*
|
||||
* @author Mercedes-Benz MYP Platform
|
||||
* @version 2.0
|
||||
*/
|
||||
|
||||
class SessionManager {
|
||||
constructor() {
|
||||
this.isAuthenticated = false;
|
||||
this.maxInactiveMinutes = 30; // Standard: 30 Minuten
|
||||
this.heartbeatInterval = 5 * 60 * 1000; // 5 Minuten
|
||||
this.warningTime = 5 * 60 * 1000; // 5 Minuten vor Ablauf warnen
|
||||
this.checkInterval = 30 * 1000; // Alle 30 Sekunden prüfen
|
||||
|
||||
this.heartbeatTimer = null;
|
||||
this.statusCheckTimer = null;
|
||||
this.warningShown = false;
|
||||
this.sessionWarningModal = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
// Prüfe initial ob Benutzer angemeldet ist
|
||||
await this.checkAuthenticationStatus();
|
||||
|
||||
if (this.isAuthenticated) {
|
||||
this.startSessionMonitoring();
|
||||
this.createWarningModal();
|
||||
|
||||
console.log('🔐 Session Manager gestartet');
|
||||
console.log(`📊 Max Inaktivität: ${this.maxInactiveMinutes} Minuten`);
|
||||
console.log(`💓 Heartbeat Intervall: ${this.heartbeatInterval / 1000 / 60} Minuten`);
|
||||
} else {
|
||||
console.log('👤 Benutzer nicht angemeldet - Session Manager inaktiv');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Session Manager Initialisierung fehlgeschlagen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async checkAuthenticationStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/session/status', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
this.isAuthenticated = true;
|
||||
this.maxInactiveMinutes = data.session.max_inactive_minutes;
|
||||
|
||||
console.log('✅ Session Status:', {
|
||||
user: data.user.email,
|
||||
timeLeft: Math.floor(data.session.time_left_seconds / 60) + ' Minuten',
|
||||
lastActivity: new Date(data.session.last_activity).toLocaleString('de-DE')
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
} else if (response.status === 401) {
|
||||
this.isAuthenticated = false;
|
||||
this.handleSessionExpired('Authentication check failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Prüfen des Session-Status:', error);
|
||||
this.isAuthenticated = false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
startSessionMonitoring() {
|
||||
// Heartbeat alle 5 Minuten senden
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
this.sendHeartbeat();
|
||||
}, this.heartbeatInterval);
|
||||
|
||||
// Session-Status alle 30 Sekunden prüfen
|
||||
this.statusCheckTimer = setInterval(() => {
|
||||
this.checkSessionStatus();
|
||||
}, this.checkInterval);
|
||||
|
||||
// Initial Heartbeat senden
|
||||
setTimeout(() => this.sendHeartbeat(), 1000);
|
||||
}
|
||||
|
||||
async sendHeartbeat() {
|
||||
try {
|
||||
// CSRF-Token aus dem Meta-Tag holen
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
};
|
||||
|
||||
// CSRF-Token hinzufügen wenn verfügbar - Flask-WTF erwartet X-CSRFToken oder den Token im Body
|
||||
if (csrfToken) {
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/session/heartbeat', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
page: window.location.pathname,
|
||||
csrf_token: csrfToken // Zusätzlich im Body senden
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
console.log('💓 Heartbeat gesendet - Session aktiv:',
|
||||
Math.floor(data.time_left_seconds / 60) + ' Minuten verbleibend');
|
||||
} else {
|
||||
console.warn('⚠️ Heartbeat fehlgeschlagen:', data);
|
||||
}
|
||||
} else if (response.status === 401) {
|
||||
this.handleSessionExpired('Heartbeat failed - unauthorized');
|
||||
} else if (response.status === 400) {
|
||||
console.warn('⚠️ CSRF-Token Problem beim Heartbeat - versuche Seite neu zu laden');
|
||||
// Bei CSRF-Problemen die Seite neu laden
|
||||
setTimeout(() => location.reload(), 5000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Heartbeat-Fehler:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async checkSessionStatus() {
|
||||
try {
|
||||
const sessionData = await this.checkAuthenticationStatus();
|
||||
|
||||
if (sessionData && sessionData.session) {
|
||||
const timeLeftSeconds = sessionData.session.time_left_seconds;
|
||||
const timeLeftMinutes = Math.floor(timeLeftSeconds / 60);
|
||||
|
||||
// Warnung anzeigen wenn weniger als 5 Minuten verbleiben
|
||||
if (timeLeftSeconds <= this.warningTime / 1000 && timeLeftSeconds > 0) {
|
||||
if (!this.warningShown) {
|
||||
this.showSessionWarning(timeLeftMinutes);
|
||||
this.warningShown = true;
|
||||
}
|
||||
} else if (timeLeftSeconds <= 0) {
|
||||
// Session abgelaufen
|
||||
this.handleSessionExpired('Session time expired');
|
||||
} else {
|
||||
// Session OK - Warnung zurücksetzen
|
||||
this.warningShown = false;
|
||||
this.hideSessionWarning();
|
||||
}
|
||||
|
||||
// Session-Status in der UI aktualisieren
|
||||
this.updateSessionStatusDisplay(sessionData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Session-Status-Check fehlgeschlagen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
showSessionWarning(minutesLeft) {
|
||||
// Bestehende Warnung entfernen
|
||||
this.hideSessionWarning();
|
||||
|
||||
// Toast-Notification anzeigen
|
||||
this.showToast(
|
||||
'Session läuft ab',
|
||||
`Ihre Session läuft in ${minutesLeft} Minuten ab. Möchten Sie verlängern?`,
|
||||
'warning',
|
||||
10000, // 10 Sekunden anzeigen
|
||||
[
|
||||
{
|
||||
text: 'Verlängern',
|
||||
action: () => this.extendSession()
|
||||
},
|
||||
{
|
||||
text: 'Abmelden',
|
||||
action: () => this.logout()
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
// Modal anzeigen für wichtige Warnung
|
||||
if (this.sessionWarningModal) {
|
||||
this.sessionWarningModal.show();
|
||||
this.updateWarningModal(minutesLeft);
|
||||
}
|
||||
|
||||
console.log(`⚠️ Session-Warnung: ${minutesLeft} Minuten verbleibend`);
|
||||
}
|
||||
|
||||
hideSessionWarning() {
|
||||
if (this.sessionWarningModal) {
|
||||
this.sessionWarningModal.hide();
|
||||
}
|
||||
}
|
||||
|
||||
createWarningModal() {
|
||||
// Modal HTML erstellen
|
||||
const modalHTML = `
|
||||
<div id="sessionWarningModal" class="fixed inset-0 z-50 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
|
||||
|
||||
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<svg class="h-6 w-6 text-red-600" 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-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.314 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||
Session läuft ab
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500" id="warningMessage">
|
||||
Ihre Session läuft in <span id="timeRemaining" class="font-bold text-red-600">5</span> Minuten ab.
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-2">
|
||||
Möchten Sie Ihre Session verlängern oder sich abmelden?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button type="button" id="extendSessionBtn" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Session verlängern
|
||||
</button>
|
||||
<button type="button" id="logoutBtn" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Modal in DOM einfügen
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
|
||||
// Event-Listener hinzufügen
|
||||
document.getElementById('extendSessionBtn').addEventListener('click', () => {
|
||||
this.extendSession();
|
||||
this.hideSessionWarning();
|
||||
});
|
||||
|
||||
document.getElementById('logoutBtn').addEventListener('click', () => {
|
||||
this.logout();
|
||||
});
|
||||
|
||||
// Modal-Objekt erstellen
|
||||
this.sessionWarningModal = {
|
||||
element: document.getElementById('sessionWarningModal'),
|
||||
show: () => {
|
||||
document.getElementById('sessionWarningModal').classList.remove('hidden');
|
||||
},
|
||||
hide: () => {
|
||||
document.getElementById('sessionWarningModal').classList.add('hidden');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
updateWarningModal(minutesLeft) {
|
||||
const timeElement = document.getElementById('timeRemaining');
|
||||
if (timeElement) {
|
||||
timeElement.textContent = minutesLeft;
|
||||
}
|
||||
}
|
||||
|
||||
async extendSession(extendMinutes = 30) {
|
||||
try {
|
||||
const response = await fetch('/api/session/extend', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
extend_minutes: extendMinutes
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
this.warningShown = false;
|
||||
|
||||
this.showToast(
|
||||
'Session verlängert',
|
||||
`Ihre Session wurde um ${data.extended_minutes} Minuten verlängert`,
|
||||
'success',
|
||||
5000
|
||||
);
|
||||
|
||||
console.log('✅ Session verlängert:', data);
|
||||
} else {
|
||||
this.showToast('Fehler', 'Session konnte nicht verlängert werden', 'error');
|
||||
}
|
||||
} else if (response.status === 401) {
|
||||
this.handleSessionExpired('Extend session failed - unauthorized');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Session-Verlängerung fehlgeschlagen:', error);
|
||||
this.showToast('Fehler', 'Session-Verlängerung fehlgeschlagen', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
this.stopSessionMonitoring();
|
||||
|
||||
// Logout-Request senden
|
||||
const response = await fetch('/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
// Zur Login-Seite weiterleiten
|
||||
if (response.ok) {
|
||||
window.location.href = '/auth/login';
|
||||
} else {
|
||||
// Fallback: Direkter Redirect
|
||||
window.location.href = '/auth/login';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Logout-Fehler:', error);
|
||||
// Fallback: Direkter Redirect
|
||||
window.location.href = '/auth/login';
|
||||
}
|
||||
}
|
||||
|
||||
handleSessionExpired(reason) {
|
||||
console.log('🕒 Session abgelaufen:', reason);
|
||||
|
||||
this.stopSessionMonitoring();
|
||||
this.isAuthenticated = false;
|
||||
|
||||
// Benutzer benachrichtigen
|
||||
this.showToast(
|
||||
'Session abgelaufen',
|
||||
'Sie wurden automatisch abgemeldet. Bitte melden Sie sich erneut an.',
|
||||
'warning',
|
||||
8000
|
||||
);
|
||||
|
||||
// Nach kurzer Verzögerung zur Login-Seite weiterleiten
|
||||
setTimeout(() => {
|
||||
window.location.href = '/auth/login?reason=session_expired';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
stopSessionMonitoring() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
|
||||
if (this.statusCheckTimer) {
|
||||
clearInterval(this.statusCheckTimer);
|
||||
this.statusCheckTimer = null;
|
||||
}
|
||||
|
||||
console.log('🛑 Session-Monitoring gestoppt');
|
||||
}
|
||||
|
||||
updateSessionStatusDisplay(sessionData) {
|
||||
// Session-Status in der Navigation/Header anzeigen (falls vorhanden)
|
||||
const statusElement = document.getElementById('sessionStatus');
|
||||
if (statusElement) {
|
||||
const timeLeftMinutes = Math.floor(sessionData.session.time_left_seconds / 60);
|
||||
statusElement.textContent = `Session: ${timeLeftMinutes}min`;
|
||||
|
||||
// Farbe basierend auf verbleibender Zeit
|
||||
if (timeLeftMinutes <= 5) {
|
||||
statusElement.className = 'text-red-600 font-medium';
|
||||
} else if (timeLeftMinutes <= 10) {
|
||||
statusElement.className = 'text-yellow-600 font-medium';
|
||||
} else {
|
||||
statusElement.className = 'text-green-600 font-medium';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showToast(title, message, type = 'info', duration = 5000, actions = []) {
|
||||
// Verwende das bestehende Toast-System falls verfügbar
|
||||
if (window.showToast) {
|
||||
window.showToast(message, type, duration);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: Simple Browser-Notification
|
||||
if (type === 'error' || type === 'warning') {
|
||||
alert(`${title}: ${message}`);
|
||||
} else {
|
||||
console.log(`${title}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// === ÖFFENTLICHE API ===
|
||||
|
||||
/**
|
||||
* Prüft ob Benutzer angemeldet ist
|
||||
*/
|
||||
isLoggedIn() {
|
||||
return this.isAuthenticated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet Session-Monitoring manuell
|
||||
*/
|
||||
start() {
|
||||
if (!this.heartbeatTimer && this.isAuthenticated) {
|
||||
this.startSessionMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stoppt Session-Monitoring manuell
|
||||
*/
|
||||
stop() {
|
||||
this.stopSessionMonitoring();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verlängert Session manuell
|
||||
*/
|
||||
async extend(minutes = 30) {
|
||||
return await this.extendSession(minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Meldet Benutzer manuell ab
|
||||
*/
|
||||
async logoutUser() {
|
||||
return await this.logout();
|
||||
}
|
||||
}
|
||||
|
||||
// Session Manager automatisch starten wenn DOM geladen ist
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Nur starten wenn wir nicht auf der Login-Seite sind
|
||||
if (!window.location.pathname.includes('/auth/login')) {
|
||||
window.sessionManager = new SessionManager();
|
||||
|
||||
// Globale Event-Listener für Session-Management
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (window.sessionManager) {
|
||||
window.sessionManager.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// Reaktion auf Sichtbarkeitsänderungen (Tab-Wechsel)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (window.sessionManager && window.sessionManager.isLoggedIn()) {
|
||||
if (document.hidden) {
|
||||
console.log('🙈 Tab versteckt - Session-Monitoring reduziert');
|
||||
} else {
|
||||
console.log('👁️ Tab sichtbar - Session-Check');
|
||||
// Sofortiger Session-Check wenn Tab wieder sichtbar wird
|
||||
setTimeout(() => window.sessionManager.checkSessionStatus(), 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Session Manager für andere Scripts verfügbar machen
|
||||
window.SessionManager = SessionManager;
|
432
backend/static/js/sw.js
Normal file
432
backend/static/js/sw.js
Normal file
@ -0,0 +1,432 @@
|
||||
/**
|
||||
* MYP Platform Service Worker
|
||||
* Offline-First Caching Strategy
|
||||
*/
|
||||
|
||||
// MYP Platform Service Worker
|
||||
const CACHE_NAME = 'myp-platform-cache-v1';
|
||||
const STATIC_CACHE = 'myp-static-v1';
|
||||
const DYNAMIC_CACHE = 'myp-dynamic-v1';
|
||||
|
||||
const ASSETS_TO_CACHE = [
|
||||
'/',
|
||||
'/dashboard',
|
||||
'/static/css/tailwind.min.css',
|
||||
'/static/css/tailwind-dark.min.css',
|
||||
'/static/js/ui-components.js',
|
||||
'/static/js/offline-app.js',
|
||||
'/static/icons/mercedes-logo.svg',
|
||||
'/static/icons/icon-144x144.png',
|
||||
'/static/favicon.ico'
|
||||
];
|
||||
|
||||
// Static files patterns
|
||||
const STATIC_PATTERNS = [
|
||||
/\.css$/,
|
||||
/\.js$/,
|
||||
/\.svg$/,
|
||||
/\.png$/,
|
||||
/\.ico$/,
|
||||
/\.woff2?$/
|
||||
];
|
||||
|
||||
// API request patterns to avoid caching
|
||||
const API_PATTERNS = [
|
||||
/^\/api\//,
|
||||
/^\/auth\//,
|
||||
/^\/api\/jobs/,
|
||||
/^\/api\/printers/,
|
||||
/^\/api\/stats/
|
||||
];
|
||||
|
||||
// Install event - cache core assets
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('Service Worker: Installing...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE)
|
||||
.then((cache) => {
|
||||
console.log('Service Worker: Caching static files');
|
||||
return cache.addAll(ASSETS_TO_CACHE);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Service Worker: Static files cached');
|
||||
return self.skipWaiting();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Service Worker: Error caching static files', error);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('Service Worker: Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
|
||||
console.log('Service Worker: Deleting old cache', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Service Worker: Activated');
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - handle requests
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Unterstütze sowohl HTTP als auch HTTPS
|
||||
if (request.method !== 'GET' ||
|
||||
(url.protocol !== 'http:' && url.protocol !== 'https:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple network-first approach
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
// Cache successful static responses
|
||||
if (response && response.status === 200 && isStaticFile(url.pathname) &&
|
||||
(url.protocol === 'http:' || url.protocol === 'https:')) {
|
||||
const responseClone = response.clone();
|
||||
caches.open(STATIC_CACHE).then((cache) => {
|
||||
cache.put(request, responseClone);
|
||||
}).catch(err => {
|
||||
console.warn('Failed to cache response:', err);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => {
|
||||
// Fallback to cache if network fails
|
||||
return caches.match(request);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Check if request is for a static file
|
||||
function isStaticFile(pathname) {
|
||||
return STATIC_PATTERNS.some(pattern => pattern.test(pathname));
|
||||
}
|
||||
|
||||
// Check if request is an API request
|
||||
function isAPIRequest(pathname) {
|
||||
return API_PATTERNS.some(pattern => pattern.test(pathname));
|
||||
}
|
||||
|
||||
// Check if request is for a page
|
||||
function isPageRequest(request) {
|
||||
return request.mode === 'navigate';
|
||||
}
|
||||
|
||||
// MYP Platform Service Worker
|
||||
const STATIC_FILES = [
|
||||
'/',
|
||||
'/static/css/tailwind.min.css',
|
||||
'/static/css/tailwind-dark.min.css',
|
||||
'/static/js/ui-components.js',
|
||||
'/static/js/offline-app.js',
|
||||
'/login',
|
||||
'/dashboard'
|
||||
];
|
||||
|
||||
// API endpoints to cache
|
||||
const API_CACHE_PATTERNS = [
|
||||
/^\/api\/dashboard/,
|
||||
/^\/api\/printers/,
|
||||
/^\/api\/jobs/,
|
||||
/^\/api\/stats/
|
||||
];
|
||||
|
||||
// Handle static file requests - Cache First strategy
|
||||
async function handleStaticFile(request) {
|
||||
try {
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Cache successful responses
|
||||
if (networkResponse.ok) {
|
||||
const cache = await caches.open(STATIC_CACHE);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.error('Service Worker: Error handling static file', error);
|
||||
|
||||
// Return cached version if available
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Return offline fallback
|
||||
return new Response('Offline - Datei nicht verfügbar', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle API requests - Network First with cache fallback
|
||||
async function handleAPIRequest(request) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip caching for chrome-extension URLs
|
||||
if (url.protocol === 'chrome-extension:') {
|
||||
try {
|
||||
return await fetch(request);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch from chrome-extension:', error);
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Fehler beim Zugriff auf chrome-extension',
|
||||
offline: true
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Try network first
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
if (networkResponse.ok) {
|
||||
// Cache successful GET responses for specific endpoints
|
||||
if (request.method === 'GET' && shouldCacheAPIResponse(url.pathname)) {
|
||||
const cache = await caches.open(DYNAMIC_CACHE);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
}
|
||||
|
||||
throw new Error(`HTTP ${networkResponse.status}`);
|
||||
} catch (error) {
|
||||
console.log('Service Worker: Network failed for API request, trying cache');
|
||||
|
||||
// Try cache fallback for GET requests
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
// Add offline header to indicate cached response
|
||||
const response = cachedResponse.clone();
|
||||
response.headers.set('X-Served-By', 'ServiceWorker-Cache');
|
||||
return response;
|
||||
}
|
||||
|
||||
// Return offline response
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Offline - Daten nicht verfügbar',
|
||||
offline: true,
|
||||
timestamp: new Date().toISOString()
|
||||
}), {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Served-By': 'ServiceWorker-Offline'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle page requests - Network First with offline fallback
|
||||
async function handlePageRequest(request) {
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
if (networkResponse.ok) {
|
||||
// Cache successful page responses
|
||||
const cache = await caches.open(DYNAMIC_CACHE);
|
||||
cache.put(request, networkResponse.clone());
|
||||
return networkResponse;
|
||||
}
|
||||
|
||||
throw new Error(`HTTP ${networkResponse.status}`);
|
||||
} catch (error) {
|
||||
console.log('Service Worker: Network failed for page request, trying cache');
|
||||
|
||||
// Try cache fallback
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Fallback offline response
|
||||
return caches.match('/offline.html') || new Response(
|
||||
'<html><body><h1>Offline</h1><p>Sie sind momentan offline. Bitte überprüfen Sie Ihre Internetverbindung.</p></body></html>',
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if API response should be cached
|
||||
function shouldCacheAPIResponse(pathname) {
|
||||
return API_CACHE_PATTERNS.some(pattern => pattern.test(pathname));
|
||||
}
|
||||
|
||||
// Background sync for offline actions
|
||||
self.addEventListener('sync', (event) => {
|
||||
console.log('Service Worker: Background sync triggered', event.tag);
|
||||
|
||||
if (event.tag === 'background-sync') {
|
||||
event.waitUntil(doBackgroundSync());
|
||||
}
|
||||
});
|
||||
|
||||
// Perform background sync
|
||||
async function doBackgroundSync() {
|
||||
try {
|
||||
// Get pending requests from IndexedDB or localStorage
|
||||
const pendingRequests = await getPendingRequests();
|
||||
|
||||
for (const request of pendingRequests) {
|
||||
try {
|
||||
await fetch(request.url, request.options);
|
||||
await removePendingRequest(request.id);
|
||||
console.log('Service Worker: Synced request', request.url);
|
||||
} catch (error) {
|
||||
console.error('Service Worker: Failed to sync request', request.url, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Service Worker: Background sync failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get pending requests (placeholder - implement with IndexedDB)
|
||||
async function getPendingRequests() {
|
||||
// This would typically use IndexedDB to store pending requests
|
||||
// For now, return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
// Remove pending request (placeholder - implement with IndexedDB)
|
||||
async function removePendingRequest(id) {
|
||||
// This would typically remove the request from IndexedDB
|
||||
console.log('Service Worker: Removing pending request', id);
|
||||
}
|
||||
|
||||
// Push notification handling
|
||||
self.addEventListener('push', (event) => {
|
||||
console.log('Service Worker: Push notification received');
|
||||
|
||||
const options = {
|
||||
body: 'Sie haben neue Benachrichtigungen',
|
||||
icon: '/static/icons/icon-192x192.png',
|
||||
badge: '/static/icons/badge-72x72.png',
|
||||
vibrate: [100, 50, 100],
|
||||
data: {
|
||||
dateOfArrival: Date.now(),
|
||||
primaryKey: 1
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
action: 'explore',
|
||||
title: 'Anzeigen',
|
||||
icon: '/static/icons/checkmark.png'
|
||||
},
|
||||
{
|
||||
action: 'close',
|
||||
title: 'Schließen',
|
||||
icon: '/static/icons/xmark.png'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification('MYP Platform', options)
|
||||
);
|
||||
});
|
||||
|
||||
// Notification click handling
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
console.log('Service Worker: Notification clicked');
|
||||
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'explore') {
|
||||
event.waitUntil(
|
||||
clients.openWindow('/dashboard')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Message handling from main thread
|
||||
self.addEventListener('message', (event) => {
|
||||
console.log('Service Worker: Message received', event.data);
|
||||
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'CACHE_URLS') {
|
||||
event.waitUntil(
|
||||
cacheUrls(event.data.urls)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Cache specific URLs
|
||||
async function cacheUrls(urls) {
|
||||
try {
|
||||
const cache = await caches.open(DYNAMIC_CACHE);
|
||||
await cache.addAll(urls);
|
||||
console.log('Service Worker: URLs cached', urls);
|
||||
} catch (error) {
|
||||
console.error('Service Worker: Error caching URLs', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic background sync (if supported)
|
||||
self.addEventListener('periodicsync', (event) => {
|
||||
console.log('Service Worker: Periodic sync triggered', event.tag);
|
||||
|
||||
if (event.tag === 'content-sync') {
|
||||
event.waitUntil(syncContent());
|
||||
}
|
||||
});
|
||||
|
||||
// Sync content periodically
|
||||
async function syncContent() {
|
||||
try {
|
||||
// Sync critical data in background
|
||||
const endpoints = ['/api/dashboard', '/api/jobs'];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
const response = await fetch(endpoint);
|
||||
if (response.ok) {
|
||||
const cache = await caches.open(DYNAMIC_CACHE);
|
||||
cache.put(endpoint, response.clone());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Service Worker: Error syncing', endpoint, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Service Worker: Content sync failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Service Worker: Script loaded');
|
1294
backend/static/js/ui-components.js
Normal file
1294
backend/static/js/ui-components.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user