📝 Commit Details:
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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');
|
||||
})();
|
||||
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+2
@@ -0,0 +1,2 @@
|
||||
/* FullCalendar v6 CSS is embedded in the JavaScript bundle */
|
||||
/* This file is kept for template compatibility */
|
||||
File diff suppressed because one or more lines are too long
@@ -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');
|
||||
@@ -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');
|
||||
})();
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user