📝 Commit Details:

This commit is contained in:
2025-05-31 22:40:29 +02:00
parent 91b1886dde
commit df8fb197c0
14061 changed files with 997277 additions and 103548 deletions

View File

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

View File

@ -0,0 +1,875 @@
/**
* Mercedes-Benz MYP Admin Guest Requests Management
* Moderne Verwaltung von Gastaufträgen mit Live-Updates
*/
// Globale Variablen
let currentRequests = [];
let filteredRequests = [];
let currentPage = 0;
let totalPages = 0;
let totalRequests = 0;
let refreshInterval = null;
let csrfToken = '';
// API Base URL Detection - Korrigierte Version für CSP-Kompatibilität
function detectApiBaseUrl() {
// Für lokale Entwicklung und CSP-Kompatibilität immer relative URLs verwenden
// Das verhindert CSP-Probleme mit connect-src
return ''; // Leerer String für relative URLs
}
const API_BASE_URL = detectApiBaseUrl();
// Initialisierung beim Laden der Seite
document.addEventListener('DOMContentLoaded', function() {
// CSRF Token abrufen
csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
// Event Listeners initialisieren
initEventListeners();
// Daten initial laden
loadGuestRequests();
// Auto-Refresh starten
startAutoRefresh();
console.log('🎯 Admin Guest Requests Management geladen');
});
/**
* Event Listeners initialisieren
*/
function initEventListeners() {
// Search Input
const searchInput = document.getElementById('search-requests');
if (searchInput) {
searchInput.addEventListener('input', debounce(handleSearch, 300));
}
// Status Filter
const statusFilter = document.getElementById('status-filter');
if (statusFilter) {
statusFilter.addEventListener('change', handleFilterChange);
}
// Sort Order
const sortOrder = document.getElementById('sort-order');
if (sortOrder) {
sortOrder.addEventListener('change', handleSortChange);
}
// Action Buttons
const refreshBtn = document.getElementById('refresh-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
loadGuestRequests();
showNotification('🔄 Gastaufträge aktualisiert', 'info');
});
}
const exportBtn = document.getElementById('export-btn');
if (exportBtn) {
exportBtn.addEventListener('click', handleExport);
}
const bulkActionsBtn = document.getElementById('bulk-actions-btn');
if (bulkActionsBtn) {
bulkActionsBtn.addEventListener('click', showBulkActionsModal);
}
// Select All Checkbox
const selectAllCheckbox = document.getElementById('select-all');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', handleSelectAll);
}
}
/**
* Gastaufträge von der API laden
*/
async function loadGuestRequests() {
try {
showLoading(true);
const url = `${API_BASE_URL}/api/admin/guest-requests`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.success) {
currentRequests = data.requests || [];
totalRequests = data.total || 0;
// Statistiken aktualisieren
updateStats(data.stats || {});
// Tabelle aktualisieren
applyFiltersAndSort();
console.log(`${currentRequests.length} Gastaufträge geladen`);
} else {
throw new Error(data.message || 'Fehler beim Laden der Gastaufträge');
}
} catch (error) {
console.error('Fehler beim Laden der Gastaufträge:', error);
showNotification('❌ Fehler beim Laden der Gastaufträge: ' + error.message, 'error');
showEmptyState();
} finally {
showLoading(false);
}
}
/**
* Statistiken aktualisieren
*/
function updateStats(stats) {
const elements = {
'pending-count': stats.pending || 0,
'approved-count': stats.approved || 0,
'rejected-count': stats.rejected || 0,
'total-count': stats.total || 0
};
Object.entries(elements).forEach(([id, value]) => {
const element = document.getElementById(id);
if (element) {
animateCounter(element, value);
}
});
}
/**
* Counter mit Animation
*/
function animateCounter(element, targetValue) {
const currentValue = parseInt(element.textContent) || 0;
const difference = targetValue - currentValue;
const steps = 20;
const stepValue = difference / steps;
let step = 0;
const interval = setInterval(() => {
step++;
const value = Math.round(currentValue + (stepValue * step));
element.textContent = value;
if (step >= steps) {
clearInterval(interval);
element.textContent = targetValue;
}
}, 50);
}
/**
* Filter und Sortierung anwenden
*/
function applyFiltersAndSort() {
let requests = [...currentRequests];
// Status Filter
const statusFilter = document.getElementById('status-filter')?.value;
if (statusFilter && statusFilter !== 'all') {
requests = requests.filter(req => req.status === statusFilter);
}
// Such-Filter
const searchTerm = document.getElementById('search-requests')?.value.toLowerCase();
if (searchTerm) {
requests = requests.filter(req =>
req.name?.toLowerCase().includes(searchTerm) ||
req.email?.toLowerCase().includes(searchTerm) ||
req.file_name?.toLowerCase().includes(searchTerm) ||
req.reason?.toLowerCase().includes(searchTerm)
);
}
// Sortierung
const sortOrder = document.getElementById('sort-order')?.value;
requests.sort((a, b) => {
switch (sortOrder) {
case 'oldest':
return new Date(a.created_at) - new Date(b.created_at);
case 'priority':
return getPriorityValue(b) - getPriorityValue(a);
case 'newest':
default:
return new Date(b.created_at) - new Date(a.created_at);
}
});
filteredRequests = requests;
renderRequestsTable();
}
/**
* Prioritätswert für Sortierung berechnen
*/
function getPriorityValue(request) {
const now = new Date();
const created = new Date(request.created_at);
const hoursOld = (now - created) / (1000 * 60 * 60);
let priority = 0;
// Status-basierte Priorität
if (request.status === 'pending') priority += 10;
else if (request.status === 'approved') priority += 5;
// Alter-basierte Priorität
if (hoursOld > 24) priority += 5;
else if (hoursOld > 8) priority += 3;
else if (hoursOld > 2) priority += 1;
return priority;
}
/**
* Requests-Tabelle rendern
*/
function renderRequestsTable() {
const tableBody = document.getElementById('requests-table-body');
const emptyState = document.getElementById('empty-state');
if (!tableBody) return;
if (filteredRequests.length === 0) {
tableBody.innerHTML = '';
showEmptyState();
return;
}
hideEmptyState();
const requestsHtml = filteredRequests.map(request => createRequestRow(request)).join('');
tableBody.innerHTML = requestsHtml;
// Event Listeners für neue Rows hinzufügen
addRowEventListeners();
}
/**
* Request Row HTML erstellen
*/
function createRequestRow(request) {
const statusColor = getStatusColor(request.status);
const priorityLevel = getPriorityLevel(request);
const timeAgo = getTimeAgo(request.created_at);
return `
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors duration-200" data-request-id="${request.id}">
<td class="px-6 py-4">
<input type="checkbox" class="request-checkbox rounded border-slate-300 text-blue-600 focus:ring-blue-500"
value="${request.id}">
</td>
<td class="px-6 py-4">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<div class="h-10 w-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">
${request.name ? request.name[0].toUpperCase() : 'G'}
</div>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-slate-900 dark:text-white">${escapeHtml(request.name || 'Unbekannt')}</div>
<div class="text-sm text-slate-500 dark:text-slate-400">${escapeHtml(request.email || 'Keine E-Mail')}</div>
</div>
</div>
</td>
<td class="px-6 py-4">
<div class="text-sm text-slate-900 dark:text-white font-medium">${escapeHtml(request.file_name || 'Keine Datei')}</div>
<div class="text-sm text-slate-500 dark:text-slate-400">
${request.duration_minutes ? `${request.duration_minutes} Min.` : 'Unbekannte Dauer'}
${request.copies ? `${request.copies} Kopien` : ''}
</div>
${request.reason ? `<div class="text-xs text-slate-400 dark:text-slate-500 mt-1 truncate max-w-xs">${escapeHtml(request.reason)}</div>` : ''}
</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full ${statusColor}">
<span class="w-2 h-2 mr-1 rounded-full ${getStatusDot(request.status)}"></span>
${getStatusText(request.status)}
</span>
</td>
<td class="px-6 py-4">
<div class="text-sm text-slate-900 dark:text-white">${timeAgo}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">${formatDateTime(request.created_at)}</div>
</td>
<td class="px-6 py-4">
<div class="flex items-center">
${getPriorityBadge(priorityLevel)}
${request.is_urgent ? '<span class="ml-2 text-red-500 text-xs">🔥 Dringend</span>' : ''}
</div>
</td>
<td class="px-6 py-4">
<div class="flex items-center space-x-2">
<button onclick="showRequestDetail(${request.id})"
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
title="Details anzeigen">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
</button>
${request.status === 'pending' ? `
<button onclick="approveRequest(${request.id})"
class="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300 transition-colors"
title="Genehmigen">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</button>
<button onclick="rejectRequest(${request.id})"
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 transition-colors"
title="Ablehnen">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</button>
` : ''}
<button onclick="deleteRequest(${request.id})"
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 transition-colors"
title="Löschen">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
</td>
</tr>
`;
}
/**
* Status-Helper-Funktionen
*/
function getStatusColor(status) {
const colors = {
'pending': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300',
'approved': 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
'rejected': 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
'expired': 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300'
};
return colors[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300';
}
function getStatusDot(status) {
const dots = {
'pending': 'bg-yellow-400 dark:bg-yellow-300',
'approved': 'bg-green-400 dark:bg-green-300',
'rejected': 'bg-red-400 dark:bg-red-300',
'expired': 'bg-gray-400 dark:bg-gray-300'
};
return dots[status] || 'bg-gray-400 dark:bg-gray-300';
}
function getStatusText(status) {
const texts = {
'pending': 'Wartend',
'approved': 'Genehmigt',
'rejected': 'Abgelehnt',
'expired': 'Abgelaufen'
};
return texts[status] || status;
}
function getPriorityLevel(request) {
const priority = getPriorityValue(request);
if (priority >= 15) return 'high';
if (priority >= 8) return 'medium';
return 'low';
}
function getPriorityBadge(level) {
const badges = {
'high': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300">🔴 Hoch</span>',
'medium': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300">🟡 Mittel</span>',
'low': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">🟢 Niedrig</span>'
};
return badges[level] || badges['low'];
}
/**
* CRUD-Operationen
*/
async function approveRequest(requestId) {
if (!confirm('Möchten Sie diesen Gastauftrag wirklich genehmigen?')) return;
try {
showLoading(true);
const url = `${API_BASE_URL}/api/guest-requests/${requestId}/approve`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({}) // Leeres JSON-Objekt senden
});
const data = await response.json();
if (data.success) {
showNotification('✅ Gastauftrag erfolgreich genehmigt', 'success');
loadGuestRequests();
} else {
throw new Error(data.message || 'Fehler beim Genehmigen');
}
} catch (error) {
console.error('Fehler beim Genehmigen:', error);
showNotification('❌ Fehler beim Genehmigen: ' + error.message, 'error');
} finally {
showLoading(false);
}
}
async function rejectRequest(requestId) {
const reason = prompt('Grund für die Ablehnung:');
if (!reason) return;
try {
showLoading(true);
const url = `${API_BASE_URL}/api/guest-requests/${requestId}/reject`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ reason })
});
const data = await response.json();
if (data.success) {
showNotification('✅ Gastauftrag erfolgreich abgelehnt', 'success');
loadGuestRequests();
} else {
throw new Error(data.message || 'Fehler beim Ablehnen');
}
} catch (error) {
console.error('Fehler beim Ablehnen:', error);
showNotification('❌ Fehler beim Ablehnen: ' + error.message, 'error');
} finally {
showLoading(false);
}
}
async function deleteRequest(requestId) {
if (!confirm('Möchten Sie diesen Gastauftrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) return;
try {
showLoading(true);
const url = `${API_BASE_URL}/api/guest-requests/${requestId}`;
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
showNotification('✅ Gastauftrag erfolgreich gelöscht', 'success');
loadGuestRequests();
} else {
throw new Error(data.message || 'Fehler beim Löschen');
}
} catch (error) {
console.error('Fehler beim Löschen:', error);
showNotification('❌ Fehler beim Löschen: ' + error.message, 'error');
} finally {
showLoading(false);
}
}
/**
* Detail-Modal Funktionen
*/
function showRequestDetail(requestId) {
const request = currentRequests.find(req => req.id === requestId);
if (!request) return;
const modal = document.getElementById('detail-modal');
const content = document.getElementById('modal-content');
content.innerHTML = `
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center">
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Gastauftrag Details</h3>
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Antragsteller</h4>
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
<p><strong>Name:</strong> ${escapeHtml(request.name || 'Unbekannt')}</p>
<p><strong>E-Mail:</strong> ${escapeHtml(request.email || 'Keine E-Mail')}</p>
<p><strong>Erstellt am:</strong> ${formatDateTime(request.created_at)}</p>
</div>
</div>
<div class="space-y-4">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Auftrag Details</h4>
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
<p><strong>Datei:</strong> ${escapeHtml(request.file_name || 'Keine Datei')}</p>
<p><strong>Dauer:</strong> ${request.duration_minutes || 'Unbekannt'} Minuten</p>
<p><strong>Kopien:</strong> ${request.copies || 1}</p>
<p><strong>Status:</strong> ${getStatusText(request.status)}</p>
</div>
</div>
</div>
${request.reason ? `
<div class="mt-6">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Begründung</h4>
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
<p class="text-gray-700 dark:text-gray-300">${escapeHtml(request.reason)}</p>
</div>
</div>
` : ''}
<div class="mt-8 flex justify-end space-x-3">
${request.status === 'pending' ? `
<button onclick="approveRequest(${request.id}); closeDetailModal();"
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
Genehmigen
</button>
<button onclick="rejectRequest(${request.id}); closeDetailModal();"
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
Ablehnen
</button>
` : ''}
<button onclick="closeDetailModal()"
class="px-4 py-2 bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors">
Schließen
</button>
</div>
</div>
`;
modal.classList.remove('hidden');
}
function closeDetailModal() {
const modal = document.getElementById('detail-modal');
modal.classList.add('hidden');
}
/**
* Bulk Actions
*/
function showBulkActionsModal() {
const selectedIds = getSelectedRequestIds();
if (selectedIds.length === 0) {
showNotification('⚠️ Bitte wählen Sie mindestens einen Gastauftrag aus', 'warning');
return;
}
const modal = document.getElementById('bulk-modal');
modal.classList.remove('hidden');
}
function closeBulkModal() {
const modal = document.getElementById('bulk-modal');
modal.classList.add('hidden');
}
async function performBulkAction(action) {
const selectedIds = getSelectedRequestIds();
if (selectedIds.length === 0) return;
const confirmMessages = {
'approve': `Möchten Sie ${selectedIds.length} Gastaufträge genehmigen?`,
'reject': `Möchten Sie ${selectedIds.length} Gastaufträge ablehnen?`,
'delete': `Möchten Sie ${selectedIds.length} Gastaufträge löschen?`
};
if (!confirm(confirmMessages[action])) return;
try {
showLoading(true);
closeBulkModal();
const promises = selectedIds.map(async (id) => {
const url = `${API_BASE_URL}/api/guest-requests/${id}/${action}`;
const method = action === 'delete' ? 'DELETE' : 'POST';
return fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
});
const results = await Promise.allSettled(promises);
const successCount = results.filter(r => r.status === 'fulfilled' && r.value.ok).length;
showNotification(`${successCount} von ${selectedIds.length} Aktionen erfolgreich`, 'success');
loadGuestRequests();
// Alle Checkboxen zurücksetzen
document.getElementById('select-all').checked = false;
document.querySelectorAll('.request-checkbox').forEach(cb => cb.checked = false);
} catch (error) {
console.error('Fehler bei Bulk-Aktion:', error);
showNotification('❌ Fehler bei der Bulk-Aktion: ' + error.message, 'error');
} finally {
showLoading(false);
}
}
function getSelectedRequestIds() {
const checkboxes = document.querySelectorAll('.request-checkbox:checked');
return Array.from(checkboxes).map(cb => parseInt(cb.value));
}
/**
* Event Handlers
*/
function handleSearch() {
applyFiltersAndSort();
}
function handleFilterChange() {
applyFiltersAndSort();
}
function handleSortChange() {
applyFiltersAndSort();
}
function handleSelectAll(event) {
const checkboxes = document.querySelectorAll('.request-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = event.target.checked;
});
}
function handleExport() {
const selectedIds = getSelectedRequestIds();
const exportData = selectedIds.length > 0 ?
filteredRequests.filter(req => selectedIds.includes(req.id)) :
filteredRequests;
if (exportData.length === 0) {
showNotification('⚠️ Keine Daten zum Exportieren verfügbar', 'warning');
return;
}
exportToCSV(exportData);
}
/**
* Export-Funktionen
*/
function exportToCSV(data) {
const headers = ['ID', 'Name', 'E-Mail', 'Datei', 'Status', 'Erstellt', 'Dauer (Min)', 'Kopien', 'Begründung'];
const rows = data.map(req => [
req.id,
req.name || '',
req.email || '',
req.file_name || '',
getStatusText(req.status),
formatDateTime(req.created_at),
req.duration_minutes || '',
req.copies || '',
req.reason || ''
]);
const csvContent = [headers, ...rows]
.map(row => row.map(field => `"${String(field).replace(/"/g, '""')}"`).join(','))
.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `gastauftraege_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
showNotification('📄 CSV-Export erfolgreich erstellt', 'success');
}
/**
* Auto-Refresh
*/
function startAutoRefresh() {
// Refresh alle 30 Sekunden
refreshInterval = setInterval(() => {
loadGuestRequests();
}, 30000);
}
function stopAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
}
/**
* Utility-Funktionen
*/
function addRowEventListeners() {
// Falls notwendig, können hier zusätzliche Event Listener hinzugefügt werden
}
function showLoading(show) {
const loadingElement = document.getElementById('table-loading');
const tableBody = document.getElementById('requests-table-body');
if (loadingElement) {
loadingElement.classList.toggle('hidden', !show);
}
if (show && tableBody) {
tableBody.innerHTML = '';
}
}
function showEmptyState() {
const emptyState = document.getElementById('empty-state');
if (emptyState) {
emptyState.classList.remove('hidden');
}
}
function hideEmptyState() {
const emptyState = document.getElementById('empty-state');
if (emptyState) {
emptyState.classList.add('hidden');
}
}
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 px-6 py-4 rounded-xl shadow-2xl z-50 transform transition-all duration-500 translate-x-full ${
type === 'success' ? 'bg-green-500 text-white' :
type === 'error' ? 'bg-red-500 text-white' :
type === 'warning' ? 'bg-yellow-500 text-black' :
'bg-blue-500 text-white'
}`;
notification.innerHTML = `
<div class="flex items-center space-x-3">
<span class="text-lg">
${type === 'success' ? '✅' :
type === 'error' ? '❌' :
type === 'warning' ? '⚠️' : ''}
</span>
<span class="font-medium">${message}</span>
</div>
`;
document.body.appendChild(notification);
// Animation einblenden
setTimeout(() => {
notification.classList.remove('translate-x-full');
}, 100);
// Nach 5 Sekunden entfernen
setTimeout(() => {
notification.classList.add('translate-x-full');
setTimeout(() => notification.remove(), 5000);
}, 5000);
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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');

View File

@ -0,0 +1,585 @@
/**
* Mercedes-Benz MYP Admin Live Dashboard
* Echtzeit-Updates für das Admin Panel mit echten Daten
*/
class AdminLiveDashboard {
constructor() {
this.isLive = false;
this.updateInterval = null;
this.retryCount = 0;
this.maxRetries = 3;
// Dynamische API-Base-URL-Erkennung
this.apiBaseUrl = this.detectApiBaseUrl();
console.log('🔗 API Base URL erkannt:', this.apiBaseUrl);
this.init();
}
detectApiBaseUrl() {
const currentHost = window.location.hostname;
const currentProtocol = window.location.protocol;
const currentPort = window.location.port;
console.log('🔍 Live Dashboard API URL Detection:', { currentHost, currentProtocol, currentPort });
// Wenn wir bereits auf dem richtigen Port sind, verwende relative URLs
if (currentPort === '443' || !currentPort) {
console.log('✅ Verwende relative URLs (HTTPS Port 443)');
return '';
}
// Für alle anderen Fälle, verwende HTTPS auf Port 443
const fallbackUrl = `https://${currentHost}`;
console.log('🔄 Fallback zu HTTPS:443:', fallbackUrl);
return fallbackUrl;
}
init() {
console.log('🚀 Mercedes-Benz MYP Admin Live Dashboard gestartet');
// Live-Status anzeigen
this.updateLiveTime();
this.startLiveUpdates();
// Event Listeners
this.bindEvents();
// Initial Load
this.loadLiveStats();
// Error Monitoring System
this.initErrorMonitoring();
}
bindEvents() {
// Quick Action Buttons
const systemStatusBtn = document.getElementById('system-status-btn');
const analyticsBtn = document.getElementById('analytics-btn');
const maintenanceBtn = document.getElementById('maintenance-btn');
if (systemStatusBtn) {
systemStatusBtn.addEventListener('click', () => this.showSystemStatus());
}
if (analyticsBtn) {
analyticsBtn.addEventListener('click', () => this.showAnalytics());
}
if (maintenanceBtn) {
maintenanceBtn.addEventListener('click', () => this.showMaintenance());
}
// Page Visibility API für optimierte Updates
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.pauseLiveUpdates();
} else {
this.resumeLiveUpdates();
}
});
}
startLiveUpdates() {
this.isLive = true;
this.updateLiveIndicator(true);
// Live Stats alle 30 Sekunden aktualisieren
this.updateInterval = setInterval(() => {
this.loadLiveStats();
}, 30000);
// Zeit jede Sekunde aktualisieren
setInterval(() => {
this.updateLiveTime();
}, 1000);
}
pauseLiveUpdates() {
this.isLive = false;
this.updateLiveIndicator(false);
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
}
resumeLiveUpdates() {
if (!this.isLive) {
this.startLiveUpdates();
this.loadLiveStats(); // Sofortiges Update beim Fortsetzen
}
}
updateLiveIndicator(isLive) {
const indicator = document.getElementById('live-indicator');
if (indicator) {
if (isLive) {
indicator.className = 'w-2 h-2 bg-green-400 rounded-full animate-pulse';
} else {
indicator.className = 'w-2 h-2 bg-gray-400 rounded-full';
}
}
}
updateLiveTime() {
const timeElement = document.getElementById('live-time');
if (timeElement) {
const now = new Date();
timeElement.textContent = now.toLocaleTimeString('de-DE');
}
}
async loadLiveStats() {
try {
const url = `${this.apiBaseUrl}/api/admin/stats/live`;
console.log('🔄 Lade Live-Statistiken von:', url);
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.success) {
this.updateStatsDisplay(data);
this.retryCount = 0; // Reset retry count on success
// Success notification (optional)
this.showQuietNotification('Live-Daten aktualisiert', 'success');
} else {
throw new Error(data.error || 'Unbekannter Fehler beim Laden der Live-Statistiken');
}
} catch (error) {
console.error('Fehler beim Laden der Live-Statistiken:', error);
this.retryCount++;
if (this.retryCount <= this.maxRetries) {
console.log(`Versuche erneut... (${this.retryCount}/${this.maxRetries})`);
setTimeout(() => this.loadLiveStats(), 5000); // Retry nach 5 Sekunden
} else {
this.handleConnectionError();
}
}
}
updateStatsDisplay(data) {
// Benutzer Stats
this.updateCounter('live-users-count', data.total_users);
this.updateProgress('users-progress', Math.min((data.total_users / 20) * 100, 100)); // Max 20 users = 100%
// Drucker Stats
this.updateCounter('live-printers-count', data.total_printers);
this.updateElement('live-printers-online', `${data.online_printers} online`);
if (data.total_printers > 0) {
this.updateProgress('printers-progress', (data.online_printers / data.total_printers) * 100);
}
// Jobs Stats
this.updateCounter('live-jobs-active', data.active_jobs);
this.updateElement('live-jobs-queued', `${data.queued_jobs} in Warteschlange`);
this.updateProgress('jobs-progress', Math.min(data.active_jobs * 20, 100)); // Max 5 jobs = 100%
// Erfolgsrate Stats
this.updateCounter('live-success-rate', `${data.success_rate}%`);
this.updateProgress('success-progress', data.success_rate);
// Trend Analysis
this.updateSuccessTrend(data.success_rate);
console.log('📊 Live-Statistiken aktualisiert:', data);
}
updateCounter(elementId, newValue) {
const element = document.getElementById(elementId);
if (element) {
const currentValue = parseInt(element.textContent) || 0;
if (currentValue !== newValue) {
this.animateCounter(element, currentValue, newValue);
}
}
}
animateCounter(element, from, to) {
const duration = 1000; // 1 Sekunde
const increment = (to - from) / (duration / 16); // 60 FPS
let current = from;
const timer = setInterval(() => {
current += increment;
if ((increment > 0 && current >= to) || (increment < 0 && current <= to)) {
current = to;
clearInterval(timer);
}
element.textContent = Math.round(current);
}, 16);
}
updateElement(elementId, newValue) {
const element = document.getElementById(elementId);
if (element && element.textContent !== newValue) {
element.textContent = newValue;
}
}
updateProgress(elementId, percentage) {
const element = document.getElementById(elementId);
if (element) {
element.style.width = `${Math.max(0, Math.min(100, percentage))}%`;
}
}
updateSuccessTrend(successRate) {
const trendElement = document.getElementById('success-trend');
if (trendElement) {
let trendText = 'Stabil';
let trendClass = 'text-green-500';
let trendIcon = 'M5 10l7-7m0 0l7 7m-7-7v18'; // Up arrow
if (successRate >= 95) {
trendText = 'Excellent';
trendClass = 'text-green-600';
} else if (successRate >= 80) {
trendText = 'Gut';
trendClass = 'text-green-500';
} else if (successRate >= 60) {
trendText = 'Mittel';
trendClass = 'text-yellow-500';
trendIcon = 'M5 12h14'; // Horizontal line
} else {
trendText = 'Niedrig';
trendClass = 'text-red-500';
trendIcon = 'M19 14l-7 7m0 0l-7-7m7 7V3'; // Down arrow
}
trendElement.className = `text-sm ${trendClass}`;
trendElement.innerHTML = `
<span class="inline-flex items-center">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${trendIcon}"/>
</svg>
${trendText}
</span>
`;
}
}
showSystemStatus() {
// System Status Modal oder Navigation
console.log('🔧 System Status angezeigt');
this.showNotification('System Status wird geladen...', 'info');
// Hier könnten weitere System-Details geladen werden
const url = `${this.apiBaseUrl}/api/admin/system/status`;
fetch(url)
.then(response => response.json())
.then(data => {
// System Status anzeigen
console.log('System Status:', data);
})
.catch(error => {
console.error('Fehler beim Laden des System Status:', error);
});
}
showAnalytics() {
console.log('📈 Live Analytics angezeigt');
this.showNotification('Analytics werden geladen...', 'info');
// Analytics Tab aktivieren oder Modal öffnen
const analyticsTab = document.querySelector('a[href*="tab=system"]');
if (analyticsTab) {
analyticsTab.click();
}
}
showMaintenance() {
console.log('🛠️ Wartung angezeigt');
this.showNotification('Wartungsoptionen werden geladen...', 'info');
// Wartungs-Tab aktivieren oder Modal öffnen
const systemTab = document.querySelector('a[href*="tab=system"]');
if (systemTab) {
systemTab.click();
}
}
handleConnectionError() {
console.error('🔴 Verbindung zu Live-Updates verloren');
this.updateLiveIndicator(false);
this.showNotification('Verbindung zu Live-Updates verloren. Versuche erneut...', 'error');
// Auto-Recovery nach 30 Sekunden
setTimeout(() => {
this.retryCount = 0;
this.loadLiveStats();
}, 30000);
}
showNotification(message, type = 'info') {
// Erstelle oder aktualisiere Notification
let notification = document.getElementById('live-notification');
if (!notification) {
notification = document.createElement('div');
notification.id = 'live-notification';
notification.className = 'fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm';
document.body.appendChild(notification);
}
const colors = {
success: 'bg-green-500 text-white',
error: 'bg-red-500 text-white',
info: 'bg-blue-500 text-white',
warning: 'bg-yellow-500 text-white'
};
notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm ${colors[type]} transform transition-all duration-300 translate-x-0`;
notification.textContent = message;
// Auto-Hide nach 3 Sekunden
setTimeout(() => {
if (notification) {
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
if (notification && notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}
}, 3000);
}
showQuietNotification(message, type) {
// Nur in der Konsole loggen für nicht-störende Updates
const emoji = type === 'success' ? '✅' : type === 'error' ? '❌' : '';
console.log(`${emoji} ${message}`);
}
getCSRFToken() {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
}
// Error Monitoring System
initErrorMonitoring() {
// Check system health every 30 seconds
this.checkSystemHealth();
setInterval(() => this.checkSystemHealth(), 30000);
// Setup error alert event handlers
this.setupErrorAlertHandlers();
}
async checkSystemHealth() {
try {
const response = await fetch('/api/admin/system-health');
const data = await response.json();
if (data.success) {
this.updateHealthDisplay(data);
this.updateErrorAlerts(data);
} else {
console.error('System health check failed:', data.error);
}
} catch (error) {
console.error('Error checking system health:', error);
}
}
updateHealthDisplay(data) {
// Update database health status
const statusIndicator = document.getElementById('db-status-indicator');
const statusText = document.getElementById('db-status-text');
const lastMigration = document.getElementById('last-migration');
const schemaIntegrity = document.getElementById('schema-integrity');
const recentErrorsCount = document.getElementById('recent-errors-count');
if (statusIndicator && statusText) {
if (data.health_status === 'critical') {
statusIndicator.className = 'w-3 h-3 bg-red-500 rounded-full animate-pulse';
statusText.textContent = 'Kritisch';
statusText.className = 'text-sm font-medium text-red-600 dark:text-red-400';
} else if (data.health_status === 'warning') {
statusIndicator.className = 'w-3 h-3 bg-yellow-500 rounded-full animate-pulse';
statusText.textContent = 'Warnung';
statusText.className = 'text-sm font-medium text-yellow-600 dark:text-yellow-400';
} else {
statusIndicator.className = 'w-3 h-3 bg-green-400 rounded-full animate-pulse';
statusText.textContent = 'Gesund';
statusText.className = 'text-sm font-medium text-green-600 dark:text-green-400';
}
}
if (lastMigration) {
lastMigration.textContent = data.last_migration || 'Unbekannt';
}
if (schemaIntegrity) {
schemaIntegrity.textContent = data.schema_integrity || 'Prüfung';
if (data.schema_integrity === 'FEHLER') {
schemaIntegrity.className = 'text-lg font-semibold text-red-600 dark:text-red-400';
} else {
schemaIntegrity.className = 'text-lg font-semibold text-green-600 dark:text-green-400';
}
}
if (recentErrorsCount) {
const errorCount = data.recent_errors_count || 0;
recentErrorsCount.textContent = errorCount;
if (errorCount > 0) {
recentErrorsCount.className = 'text-lg font-semibold text-red-600 dark:text-red-400';
} else {
recentErrorsCount.className = 'text-lg font-semibold text-green-600 dark:text-green-400';
}
}
}
updateErrorAlerts(data) {
const alertContainer = document.getElementById('critical-errors-alert');
const errorList = document.getElementById('error-list');
if (!alertContainer || !errorList) return;
const allErrors = [...(data.critical_errors || []), ...(data.warnings || [])];
if (allErrors.length > 0) {
// Show alert container
alertContainer.classList.remove('hidden');
// Clear previous errors
errorList.innerHTML = '';
// Add each error
allErrors.forEach(error => {
const errorElement = document.createElement('div');
errorElement.className = `p-3 rounded-lg border-l-4 ${
error.severity === 'critical' ? 'bg-red-50 dark:bg-red-900/30 border-red-500' :
error.severity === 'high' ? 'bg-orange-50 dark:bg-orange-900/30 border-orange-500' :
'bg-yellow-50 dark:bg-yellow-900/30 border-yellow-500'
}`;
errorElement.innerHTML = `
<div class="flex items-start justify-between">
<div class="flex-1">
<h4 class="font-medium ${
error.severity === 'critical' ? 'text-red-800 dark:text-red-200' :
error.severity === 'high' ? 'text-orange-800 dark:text-orange-200' :
'text-yellow-800 dark:text-yellow-200'
}">${error.message}</h4>
<p class="text-sm mt-1 ${
error.severity === 'critical' ? 'text-red-600 dark:text-red-300' :
error.severity === 'high' ? 'text-orange-600 dark:text-orange-300' :
'text-yellow-600 dark:text-yellow-300'
}">💡 ${error.suggested_fix}</p>
<p class="text-xs mt-1 text-gray-500 dark:text-gray-400">
📅 ${new Date(error.timestamp).toLocaleString('de-DE')}
</p>
</div>
<span class="ml-2 px-2 py-1 text-xs font-medium rounded-full ${
error.severity === 'critical' ? 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100' :
error.severity === 'high' ? 'bg-orange-100 text-orange-800 dark:bg-orange-800 dark:text-orange-100' :
'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100'
}">
${error.severity.toUpperCase()}
</span>
</div>
`;
errorList.appendChild(errorElement);
});
} else {
// Hide alert container
alertContainer.classList.add('hidden');
}
}
setupErrorAlertHandlers() {
// Fix errors button
const fixErrorsBtn = document.getElementById('fix-errors-btn');
if (fixErrorsBtn) {
fixErrorsBtn.addEventListener('click', async () => {
await this.fixErrors();
});
}
// Dismiss errors button
const dismissErrorsBtn = document.getElementById('dismiss-errors-btn');
if (dismissErrorsBtn) {
dismissErrorsBtn.addEventListener('click', () => {
const alertContainer = document.getElementById('critical-errors-alert');
if (alertContainer) {
alertContainer.classList.add('hidden');
}
});
}
// View details button
const viewDetailsBtn = document.getElementById('view-error-details-btn');
if (viewDetailsBtn) {
viewDetailsBtn.addEventListener('click', () => {
// Redirect to logs tab
window.location.href = '/admin-dashboard?tab=logs';
});
}
}
async fixErrors() {
const fixBtn = document.getElementById('fix-errors-btn');
if (!fixBtn) return;
// Show loading state
const originalText = fixBtn.innerHTML;
fixBtn.innerHTML = '🔄 Repariere...';
fixBtn.disabled = true;
try {
const response = await fetch('/api/admin/fix-errors', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
// Show success message
this.showNotification('✅ Automatische Reparatur erfolgreich durchgeführt!', 'success');
// Refresh health check
setTimeout(() => {
this.checkSystemHealth();
}, 2000);
} else {
// Show error message
this.showNotification(`❌ Reparatur fehlgeschlagen: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error fixing errors:', error);
this.showNotification('❌ Fehler bei der automatischen Reparatur', 'error');
} finally {
// Restore button
fixBtn.innerHTML = originalText;
fixBtn.disabled = false;
}
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
new AdminLiveDashboard();
});
// Export for global access
window.AdminLiveDashboard = AdminLiveDashboard;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,350 @@
/**
* Admin System Management JavaScript
* Funktionen für System-Wartung und -Konfiguration
*/
// CSRF Token für AJAX-Anfragen
function getCsrfToken() {
const token = document.querySelector('meta[name="csrf-token"]');
return token ? token.getAttribute('content') : '';
}
// Hilfsfunktion für API-Aufrufe
async function makeApiCall(url, method = 'GET', data = null) {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
};
if (data) {
options.body = JSON.stringify(data);
}
try {
const response = await fetch(url, options);
const result = await response.json();
if (response.ok) {
showNotification(result.message || 'Aktion erfolgreich ausgeführt', 'success');
return result;
} else {
showNotification(result.error || 'Ein Fehler ist aufgetreten', 'error');
return null;
}
} catch (error) {
showNotification('Netzwerkfehler: ' + error.message, 'error');
return null;
}
}
// Logs laden und anzeigen
async function loadLogs() {
const logsContainer = document.getElementById('logs-container');
if (!logsContainer) return;
// Lade-Animation anzeigen
logsContainer.innerHTML = `
<div class="flex justify-center items-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 dark:border-indigo-400"></div>
</div>
`;
try {
const response = await fetch('/api/logs');
if (!response.ok) throw new Error('Fehler beim Laden der Logs');
const data = await response.json();
window.logsData = data.logs || [];
window.filteredLogs = [...window.logsData];
renderLogs();
updateLogStatistics();
scrollLogsToBottom();
} catch (error) {
console.error('Fehler beim Laden der Logs:', error);
logsContainer.innerHTML = `
<div class="text-center py-8">
<div class="text-red-600 dark:text-red-400 text-xl mb-2">Fehler beim Laden der Logs</div>
<p class="text-gray-600 dark:text-gray-400">${error.message}</p>
<button onclick="loadLogs()" class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
Erneut versuchen
</button>
</div>
`;
}
}
// Logs rendern
function renderLogs() {
const logsContainer = document.getElementById('logs-container');
if (!logsContainer || !window.filteredLogs) return;
if (window.filteredLogs.length === 0) {
logsContainer.innerHTML = `
<div class="text-center py-8">
<p class="text-gray-600 dark:text-gray-400">Keine Logs gefunden</p>
</div>
`;
return;
}
const logsHtml = window.filteredLogs.map(log => {
const levelColor = getLogLevelColor(log.level);
return `
<div class="bg-white/40 dark:bg-slate-700/40 rounded-lg p-4 border ${levelColor.border}">
<div class="flex items-start space-x-3">
<span class="inline-block px-2 py-1 text-xs font-semibold rounded-full ${levelColor.bg} ${levelColor.text}">
${log.level}
</span>
<div class="flex-1">
<div class="flex items-center justify-between mb-1">
<span class="text-sm font-medium text-slate-600 dark:text-slate-400">${log.category}</span>
<span class="text-xs text-slate-500 dark:text-slate-500">${log.timestamp}</span>
</div>
<p class="text-sm text-slate-900 dark:text-white break-all">${log.message}</p>
</div>
</div>
</div>
`;
}).join('');
logsContainer.innerHTML = logsHtml;
}
// Log-Level-Farben bestimmen
function getLogLevelColor(level) {
const colors = {
'ERROR': {
bg: 'bg-red-100 dark:bg-red-900/30',
text: 'text-red-800 dark:text-red-200',
border: 'border-red-200 dark:border-red-700'
},
'WARNING': {
bg: 'bg-yellow-100 dark:bg-yellow-900/30',
text: 'text-yellow-800 dark:text-yellow-200',
border: 'border-yellow-200 dark:border-yellow-700'
},
'INFO': {
bg: 'bg-blue-100 dark:bg-blue-900/30',
text: 'text-blue-800 dark:text-blue-200',
border: 'border-blue-200 dark:border-blue-700'
},
'DEBUG': {
bg: 'bg-gray-100 dark:bg-gray-900/30',
text: 'text-gray-800 dark:text-gray-200',
border: 'border-gray-200 dark:border-gray-700'
}
};
return colors[level.toUpperCase()] || colors['INFO'];
}
// Log-Statistiken aktualisieren
function updateLogStatistics() {
if (!window.logsData) return;
const stats = {
total: window.logsData.length,
errors: window.logsData.filter(log => log.level.toUpperCase() === 'ERROR').length,
warnings: window.logsData.filter(log => log.level.toUpperCase() === 'WARNING').length,
info: window.logsData.filter(log => log.level.toUpperCase() === 'INFO').length
};
// Aktualisiere Statistik-Anzeigen falls vorhanden
const totalElement = document.getElementById('log-stats-total');
const errorsElement = document.getElementById('log-stats-errors');
const warningsElement = document.getElementById('log-stats-warnings');
const infoElement = document.getElementById('log-stats-info');
if (totalElement) totalElement.textContent = stats.total;
if (errorsElement) errorsElement.textContent = stats.errors;
if (warningsElement) warningsElement.textContent = stats.warnings;
if (infoElement) infoElement.textContent = stats.info;
}
// Zum Ende der Logs scrollen
function scrollLogsToBottom() {
const logsContainer = document.getElementById('logs-container');
if (logsContainer) {
logsContainer.scrollTop = logsContainer.scrollHeight;
}
}
// Notification anzeigen
function showNotification(message, type = 'info') {
// Erstelle Notification-Element falls nicht vorhanden
let notification = document.getElementById('admin-notification');
if (!notification) {
notification = document.createElement('div');
notification.id = 'admin-notification';
notification.className = 'fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm transition-all duration-300 transform translate-x-full';
document.body.appendChild(notification);
}
// Setze Farbe basierend auf Typ
const colors = {
success: 'bg-green-500 text-white',
error: 'bg-red-500 text-white',
warning: 'bg-yellow-500 text-white',
info: 'bg-blue-500 text-white'
};
notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm transition-all duration-300 ${colors[type] || colors.info}`;
notification.textContent = message;
// Zeige Notification
notification.style.transform = 'translateX(0)';
// Verstecke nach 5 Sekunden
setTimeout(() => {
notification.style.transform = 'translateX(100%)';
}, 5000);
}
// Cache leeren
async function clearCache() {
if (confirm('Möchten Sie wirklich den Cache leeren?')) {
showNotification('Cache wird geleert...', 'info');
const result = await makeApiCall('/api/admin/cache/clear', 'POST');
if (result) {
setTimeout(() => location.reload(), 2000);
}
}
}
// Datenbank optimieren
async function optimizeDatabase() {
if (confirm('Möchten Sie wirklich die Datenbank optimieren? Dies kann einige Minuten dauern.')) {
showNotification('Datenbank wird optimiert...', 'info');
const result = await makeApiCall('/api/admin/database/optimize', 'POST');
if (result) {
setTimeout(() => location.reload(), 2000);
}
}
}
// Backup erstellen
async function createBackup() {
if (confirm('Möchten Sie wirklich ein Backup erstellen?')) {
showNotification('Backup wird erstellt...', 'info');
const result = await makeApiCall('/api/admin/backup/create', 'POST');
}
}
// Drucker aktualisieren
async function updatePrinters() {
if (confirm('Möchten Sie alle Drucker-Verbindungen aktualisieren?')) {
showNotification('Drucker werden aktualisiert...', 'info');
const result = await makeApiCall('/api/admin/printers/update', 'POST');
if (result) {
setTimeout(() => location.reload(), 2000);
}
}
}
// System neustarten
async function restartSystem() {
if (confirm('WARNUNG: Möchten Sie wirklich das System neustarten? Alle aktiven Verbindungen werden getrennt.')) {
const result = await makeApiCall('/api/admin/system/restart', 'POST');
if (result) {
showNotification('System wird neugestartet...', 'warning');
setTimeout(() => {
window.location.href = '/';
}, 3000);
}
}
}
// Einstellungen bearbeiten
function editSettings() {
window.location.href = '/settings';
}
// Systemstatus automatisch aktualisieren
async function updateSystemStatus() {
if (window.location.search.includes('tab=system')) {
const result = await makeApiCall('/api/admin/system/status');
if (result) {
// Aktualisiere die Anzeige
updateStatusDisplay('cpu_usage', result.cpu_usage + '%');
updateStatusDisplay('memory_usage', result.memory_usage + '%');
updateStatusDisplay('disk_usage', result.disk_usage + '%');
updateStatusDisplay('uptime', result.uptime);
updateStatusDisplay('db_size', result.db_size);
updateStatusDisplay('scheduler_jobs', result.scheduler_jobs);
updateStatusDisplay('next_job', result.next_job);
// Scheduler-Status aktualisieren
const schedulerStatus = document.querySelector('.scheduler-status');
if (schedulerStatus) {
if (result.scheduler_running) {
schedulerStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-blue-400 animate-pulse"></span>Läuft';
schedulerStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
} else {
schedulerStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-red-400"></span>Gestoppt';
schedulerStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
}
}
}
}
}
// Hilfsfunktion zum Aktualisieren der Status-Anzeige
function updateStatusDisplay(key, value) {
const element = document.querySelector(`[data-status="${key}"]`);
if (element) {
element.textContent = value;
}
}
// Datenbankstatus aktualisieren
async function updateDatabaseStatus() {
if (window.location.search.includes('tab=system')) {
const result = await makeApiCall('/api/admin/database/status');
if (result) {
const dbStatus = document.querySelector('.database-status');
if (dbStatus) {
if (result.connected) {
dbStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-green-400 animate-pulse"></span>Verbunden';
dbStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
} else {
dbStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-red-400"></span>Getrennt';
dbStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
}
}
updateStatusDisplay('db_size', result.size);
updateStatusDisplay('db_connections', result.connected ? 'Aktiv' : 'Getrennt');
}
}
}
// Auto-Update alle 30 Sekunden
setInterval(() => {
updateSystemStatus();
updateDatabaseStatus();
}, 30000);
// Initial load
document.addEventListener('DOMContentLoaded', function() {
updateSystemStatus();
updateDatabaseStatus();
});
// Export für globale Verwendung
window.adminSystem = {
clearCache,
optimizeDatabase,
createBackup,
updatePrinters,
restartSystem,
editSettings,
updateSystemStatus,
updateDatabaseStatus,
loadLogs,
renderLogs,
updateLogStatistics,
scrollLogsToBottom
};

1580
backend/static/js/admin.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,752 @@
/**
* MYP Platform Advanced UI Components
* Erweiterte Komponenten: Progress-Bars, File-Upload, Datepicker
* Version: 2.0.0
*/
(function() {
'use strict';
// Namespace erweitern
window.MYP = window.MYP || {};
window.MYP.Advanced = window.MYP.Advanced || {};
/**
* Progress Bar Component
*/
class ProgressBar {
constructor(container, options = {}) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
this.options = {
value: 0,
max: 100,
showLabel: true,
showPercentage: true,
animated: true,
color: 'blue',
size: 'md',
striped: false,
...options
};
this.currentValue = this.options.value;
this.init();
}
init() {
if (!this.container) {
console.error('ProgressBar: Container nicht gefunden');
return;
}
this.render();
}
render() {
const percentage = Math.round((this.currentValue / this.options.max) * 100);
const sizeClass = this.getSizeClass();
const colorClass = this.getColorClass();
this.container.innerHTML = `
<div class="progress-bar-container ${sizeClass}">
${this.options.showLabel ? `
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
${this.options.label || 'Fortschritt'}
</span>
${this.options.showPercentage ? `
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
${percentage}%
</span>
` : ''}
</div>
` : ''}
<div class="progress-bar-track ${sizeClass}">
<div class="progress-bar-fill ${colorClass} ${this.options.animated ? 'animated' : ''} ${this.options.striped ? 'striped' : ''}"
style="width: ${percentage}%"
role="progressbar"
aria-valuenow="${this.currentValue}"
aria-valuemin="0"
aria-valuemax="${this.options.max}">
</div>
</div>
</div>
`;
}
getSizeClass() {
const sizes = {
'sm': 'h-2',
'md': 'h-3',
'lg': 'h-4',
'xl': 'h-6'
};
return sizes[this.options.size] || sizes.md;
}
getColorClass() {
const colors = {
'blue': 'bg-blue-500',
'green': 'bg-green-500',
'red': 'bg-red-500',
'yellow': 'bg-yellow-500',
'purple': 'bg-purple-500',
'indigo': 'bg-indigo-500'
};
return colors[this.options.color] || colors.blue;
}
setValue(value, animate = true) {
const oldValue = this.currentValue;
this.currentValue = Math.max(0, Math.min(this.options.max, value));
if (animate) {
this.animateToValue(oldValue, this.currentValue);
} else {
this.render();
}
}
animateToValue(from, to) {
const duration = 500; // ms
const steps = 60;
const stepValue = (to - from) / steps;
let currentStep = 0;
const animate = () => {
if (currentStep < steps) {
this.currentValue = from + (stepValue * currentStep);
this.render();
currentStep++;
requestAnimationFrame(animate);
} else {
this.currentValue = to;
this.render();
}
};
animate();
}
increment(amount = 1) {
this.setValue(this.currentValue + amount);
}
decrement(amount = 1) {
this.setValue(this.currentValue - amount);
}
reset() {
this.setValue(0);
}
complete() {
this.setValue(this.options.max);
}
}
/**
* Advanced File Upload Component
*/
class FileUpload {
constructor(container, options = {}) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
this.options = {
multiple: false,
accept: '*/*',
maxSize: 50 * 1024 * 1024, // 50MB
maxFiles: 10,
dragDrop: true,
showProgress: true,
showPreview: true,
uploadUrl: '/api/upload',
chunkSize: 1024 * 1024, // 1MB chunks
...options
};
this.files = [];
this.uploads = new Map();
this.init();
}
init() {
if (!this.container) {
console.error('FileUpload: Container nicht gefunden');
return;
}
this.render();
this.setupEventListeners();
}
render() {
this.container.innerHTML = `
<div class="file-upload-area" id="fileUploadArea">
<div class="file-upload-dropzone ${this.options.dragDrop ? 'drag-enabled' : ''}" id="dropzone">
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-slate-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="mt-4">
<label for="fileInput" class="cursor-pointer">
<span class="mt-2 block text-sm font-medium text-slate-900 dark:text-white">
${this.options.dragDrop ? 'Dateien hierher ziehen oder' : ''}
<span class="text-blue-600 dark:text-blue-400 hover:text-blue-500">durchsuchen</span>
</span>
</label>
<input type="file"
id="fileInput"
name="files"
${this.options.multiple ? 'multiple' : ''}
accept="${this.options.accept}"
class="sr-only">
</div>
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
${this.getFileTypeText()} • Max. ${this.formatFileSize(this.options.maxSize)}
</p>
</div>
</div>
<div class="file-list mt-4" id="fileList"></div>
</div>
`;
}
setupEventListeners() {
const fileInput = this.container.querySelector('#fileInput');
const dropzone = this.container.querySelector('#dropzone');
// File Input Change
fileInput.addEventListener('change', (e) => {
this.handleFiles(Array.from(e.target.files));
});
if (this.options.dragDrop) {
// Drag and Drop Events
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('drag-over');
});
dropzone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropzone.classList.remove('drag-over');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('drag-over');
this.handleFiles(Array.from(e.dataTransfer.files));
});
}
}
handleFiles(fileList) {
for (const file of fileList) {
if (this.validateFile(file)) {
this.addFile(file);
}
}
this.renderFileList();
}
validateFile(file) {
// Dateigröße prüfen
if (file.size > this.options.maxSize) {
this.showError(`Datei "${file.name}" ist zu groß. Maximum: ${this.formatFileSize(this.options.maxSize)}`);
return false;
}
// Anzahl Dateien prüfen
if (!this.options.multiple && this.files.length > 0) {
this.files = []; // Ersetze einzelne Datei
} else if (this.files.length >= this.options.maxFiles) {
this.showError(`Maximal ${this.options.maxFiles} Dateien erlaubt`);
return false;
}
return true;
}
addFile(file) {
const fileData = {
id: this.generateId(),
file: file,
name: file.name,
size: file.size,
type: file.type,
status: 'pending',
progress: 0,
error: null
};
this.files.push(fileData);
// Preview generieren
if (this.options.showPreview && file.type.startsWith('image/')) {
this.generatePreview(fileData);
}
}
generatePreview(fileData) {
const reader = new FileReader();
reader.onload = (e) => {
fileData.preview = e.target.result;
this.renderFileList();
};
reader.readAsDataURL(fileData.file);
}
renderFileList() {
const fileListContainer = this.container.querySelector('#fileList');
if (this.files.length === 0) {
fileListContainer.innerHTML = '';
return;
}
fileListContainer.innerHTML = this.files.map(fileData => `
<div class="file-item" data-file-id="${fileData.id}">
<div class="flex items-center space-x-4 p-4 bg-slate-50 dark:bg-slate-800 rounded-lg">
${fileData.preview ? `
<img src="${fileData.preview}" class="w-12 h-12 object-cover rounded" alt="Preview">
` : `
<div class="w-12 h-12 bg-slate-200 dark:bg-slate-700 rounded flex items-center justify-center">
<svg class="w-6 h-6 text-slate-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/>
</svg>
</div>
`}
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-slate-900 dark:text-white truncate">
${fileData.name}
</p>
<button class="remove-file text-slate-400 hover:text-red-500" data-file-id="${fileData.id}">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400">
${this.formatFileSize(fileData.size)}${this.getStatusText(fileData.status)}
</p>
${this.options.showProgress && fileData.status === 'uploading' ? `
<div class="mt-2">
<div class="w-full bg-slate-200 dark:bg-slate-600 rounded-full h-2">
<div class="bg-blue-500 h-2 rounded-full transition-all duration-300"
style="width: ${fileData.progress}%"></div>
</div>
</div>
` : ''}
${fileData.error ? `
<p class="text-xs text-red-500 mt-1">${fileData.error}</p>
` : ''}
</div>
</div>
</div>
`).join('');
// Event Listeners für Remove-Buttons
fileListContainer.querySelectorAll('.remove-file').forEach(button => {
button.addEventListener('click', (e) => {
const fileId = e.target.closest('.remove-file').dataset.fileId;
this.removeFile(fileId);
});
});
}
removeFile(fileId) {
this.files = this.files.filter(f => f.id !== fileId);
this.renderFileList();
}
async uploadFiles() {
const pendingFiles = this.files.filter(f => f.status === 'pending');
for (const fileData of pendingFiles) {
await this.uploadFile(fileData);
}
}
async uploadFile(fileData) {
fileData.status = 'uploading';
fileData.progress = 0;
this.renderFileList();
try {
const formData = new FormData();
formData.append('file', fileData.file);
const response = await fetch(this.options.uploadUrl, {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
});
if (response.ok) {
fileData.status = 'completed';
fileData.progress = 100;
const result = await response.json();
fileData.url = result.url;
} else {
throw new Error(`Upload fehlgeschlagen: ${response.status}`);
}
} catch (error) {
fileData.status = 'error';
fileData.error = error.message;
}
this.renderFileList();
}
getFileTypeText() {
if (this.options.accept === '*/*') return 'Alle Dateitypen';
if (this.options.accept.includes('image/')) return 'Bilder';
if (this.options.accept.includes('.pdf')) return 'PDF-Dateien';
return 'Spezifische Dateitypen';
}
getStatusText(status) {
const statusTexts = {
'pending': 'Wartend',
'uploading': 'Wird hochgeladen...',
'completed': 'Abgeschlossen',
'error': 'Fehler'
};
return statusTexts[status] || status;
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
generateId() {
return 'file_' + Math.random().toString(36).substr(2, 9);
}
showError(message) {
if (window.showToast) {
window.showToast(message, 'error');
} else {
alert(message);
}
}
getFiles() {
return this.files;
}
getCompletedFiles() {
return this.files.filter(f => f.status === 'completed');
}
clear() {
this.files = [];
this.renderFileList();
}
}
/**
* Simple Datepicker Component
*/
class DatePicker {
constructor(input, options = {}) {
this.input = typeof input === 'string' ? document.querySelector(input) : input;
this.options = {
format: 'dd.mm.yyyy',
minDate: null,
maxDate: null,
disabledDates: [],
language: 'de',
closeOnSelect: true,
showWeekNumbers: false,
...options
};
this.isOpen = false;
this.currentDate = new Date();
this.selectedDate = null;
this.init();
}
init() {
if (!this.input) {
console.error('DatePicker: Input-Element nicht gefunden');
return;
}
this.setupInput();
this.createCalendar();
this.setupEventListeners();
}
setupInput() {
this.input.setAttribute('readonly', 'true');
this.input.classList.add('datepicker-input');
// Container für Input und Calendar
this.container = document.createElement('div');
this.container.className = 'datepicker-container relative';
this.input.parentNode.insertBefore(this.container, this.input);
this.container.appendChild(this.input);
}
createCalendar() {
this.calendar = document.createElement('div');
this.calendar.className = 'datepicker-calendar absolute top-full left-0 mt-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-lg shadow-lg z-50 hidden';
this.calendar.innerHTML = this.renderCalendar();
this.container.appendChild(this.calendar);
}
renderCalendar() {
const year = this.currentDate.getFullYear();
const month = this.currentDate.getMonth();
const monthName = this.getMonthName(month);
return `
<div class="datepicker-header p-4 border-b border-slate-200 dark:border-slate-600">
<div class="flex items-center justify-between">
<button type="button" class="prev-month p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
</button>
<div class="text-sm font-medium text-slate-900 dark:text-white">
${monthName} ${year}
</div>
<button type="button" class="next-month p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>
</button>
</div>
</div>
<div class="datepicker-body p-4">
<div class="grid grid-cols-7 gap-1 mb-2">
${this.getWeekdayHeaders()}
</div>
<div class="grid grid-cols-7 gap-1">
${this.getDaysOfMonth(year, month)}
</div>
</div>
`;
}
getWeekdayHeaders() {
const weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
return weekdays.map(day =>
`<div class="text-xs font-medium text-slate-500 dark:text-slate-400 text-center p-1">${day}</div>`
).join('');
}
getDaysOfMonth(year, month) {
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - ((firstDay.getDay() + 6) % 7));
const days = [];
const current = new Date(startDate);
while (current <= lastDay || current.getMonth() === month) {
const isCurrentMonth = current.getMonth() === month;
const isToday = this.isToday(current);
const isSelected = this.isSelectedDate(current);
const isDisabled = this.isDisabledDate(current);
const classes = [
'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center transition-colors',
isCurrentMonth ? 'text-slate-900 dark:text-white' : 'text-slate-400 dark:text-slate-600',
isToday ? 'bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100' : '',
isSelected ? 'bg-blue-500 text-white' : '',
!isDisabled && isCurrentMonth ? 'hover:bg-slate-100 dark:hover:bg-slate-700' : '',
isDisabled ? 'cursor-not-allowed opacity-50' : ''
].filter(Boolean);
days.push(`
<div class="${classes.join(' ')}"
data-date="${this.formatDateForData(current)}"
${isDisabled ? '' : 'data-selectable="true"'}>
${current.getDate()}
</div>
`);
current.setDate(current.getDate() + 1);
if (days.length >= 42) break; // Max 6 Wochen
}
return days.join('');
}
setupEventListeners() {
// Input click
this.input.addEventListener('click', () => {
this.toggle();
});
// Calendar clicks
this.calendar.addEventListener('click', (e) => {
if (e.target.classList.contains('prev-month')) {
this.previousMonth();
} else if (e.target.classList.contains('next-month')) {
this.nextMonth();
} else if (e.target.dataset.selectable) {
this.selectDate(new Date(e.target.dataset.date));
}
});
// Click outside
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target)) {
this.close();
}
});
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
open() {
this.calendar.classList.remove('hidden');
this.isOpen = true;
this.updateCalendar();
}
close() {
this.calendar.classList.add('hidden');
this.isOpen = false;
}
selectDate(date) {
this.selectedDate = new Date(date);
this.input.value = this.formatDate(date);
// Custom Event
this.input.dispatchEvent(new CustomEvent('dateselected', {
detail: { date: new Date(date) }
}));
if (this.options.closeOnSelect) {
this.close();
}
}
previousMonth() {
this.currentDate.setMonth(this.currentDate.getMonth() - 1);
this.updateCalendar();
}
nextMonth() {
this.currentDate.setMonth(this.currentDate.getMonth() + 1);
this.updateCalendar();
}
updateCalendar() {
this.calendar.innerHTML = this.renderCalendar();
}
isToday(date) {
const today = new Date();
return date.toDateString() === today.toDateString();
}
isSelectedDate(date) {
return this.selectedDate && date.toDateString() === this.selectedDate.toDateString();
}
isDisabledDate(date) {
if (this.options.minDate && date < this.options.minDate) return true;
if (this.options.maxDate && date > this.options.maxDate) return true;
return this.options.disabledDates.some(disabled =>
date.toDateString() === disabled.toDateString()
);
}
formatDate(date) {
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return this.options.format
.replace('dd', day)
.replace('mm', month)
.replace('yyyy', year);
}
formatDateForData(date) {
return date.toISOString().split('T')[0];
}
getMonthName(monthIndex) {
const months = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
];
return months[monthIndex];
}
setValue(date) {
if (date) {
this.selectDate(new Date(date));
}
}
getValue() {
return this.selectedDate;
}
clear() {
this.selectedDate = null;
this.input.value = '';
}
}
// Globale API
window.MYP.Advanced = {
ProgressBar,
FileUpload,
DatePicker,
// Convenience Functions
createProgressBar: (container, options) => new ProgressBar(container, options),
createFileUpload: (container, options) => new FileUpload(container, options),
createDatePicker: (input, options) => new DatePicker(input, options)
};
// Auto-Initialize
document.addEventListener('DOMContentLoaded', function() {
// Auto-initialize datepickers
document.querySelectorAll('[data-datepicker]').forEach(input => {
const options = JSON.parse(input.dataset.datepicker || '{}');
new DatePicker(input, options);
});
// Auto-initialize file uploads
document.querySelectorAll('[data-file-upload]').forEach(container => {
const options = JSON.parse(container.dataset.fileUpload || '{}');
new FileUpload(container, options);
});
console.log('🚀 MYP Advanced Components geladen');
});
})();

View File

@ -0,0 +1,143 @@
class AutoLogoutManager {
constructor() {
this.timer = null;
this.warningTimer = null;
this.timeout = 60; // Standard: 60 Minuten
this.warningTime = 5; // Warnung 5 Minuten vor Logout
this.isWarningShown = false;
this.init();
}
async init() {
await this.loadSettings();
this.setupActivityListeners();
this.startTimer();
}
async loadSettings() {
try {
const response = await fetch('/api/user/settings');
if (response.ok) {
const data = await response.json();
if (data.success && data.settings.privacy?.auto_logout) {
const timeout = parseInt(data.settings.privacy.auto_logout);
if (timeout > 0 && timeout !== 'never') {
this.timeout = timeout;
} else {
this.timeout = 0; // Deaktiviert
}
}
}
} catch (error) {
console.warn('Auto-Logout-Einstellungen konnten nicht geladen werden:', error);
}
}
setupActivityListeners() {
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
events.forEach(event => {
document.addEventListener(event, () => this.resetTimer(), { passive: true });
});
}
startTimer() {
if (this.timeout <= 0) return;
this.clearTimers();
const timeoutMs = this.timeout * 60 * 1000;
const warningMs = this.warningTime * 60 * 1000;
this.warningTimer = setTimeout(() => this.showWarning(), timeoutMs - warningMs);
this.timer = setTimeout(() => this.performLogout(), timeoutMs);
}
resetTimer() {
if (this.isWarningShown) {
this.closeWarning();
}
this.startTimer();
}
clearTimers() {
if (this.timer) clearTimeout(this.timer);
if (this.warningTimer) clearTimeout(this.warningTimer);
}
showWarning() {
if (this.isWarningShown) return;
this.isWarningShown = true;
const modal = document.createElement('div');
modal.id = 'auto-logout-warning';
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
modal.innerHTML = `
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 max-w-md mx-4 shadow-xl">
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-4">Automatische Abmeldung</h3>
<p class="text-sm text-slate-600 dark:text-slate-300 mb-4">
Sie werden in ${this.warningTime} Minuten aufgrund von Inaktivität abgemeldet.
</p>
<div class="flex space-x-3">
<button id="stay-logged-in" class="bg-blue-600 text-white px-4 py-2 rounded-lg">
Angemeldet bleiben
</button>
<button id="logout-now" class="bg-gray-300 text-slate-700 px-4 py-2 rounded-lg">
Jetzt abmelden
</button>
</div>
</div>
`;
document.body.appendChild(modal);
document.getElementById('stay-logged-in').onclick = () => {
this.closeWarning();
this.sendKeepAlive();
this.resetTimer();
};
document.getElementById('logout-now').onclick = () => {
this.performLogout();
};
}
closeWarning() {
const modal = document.getElementById('auto-logout-warning');
if (modal) modal.remove();
this.isWarningShown = false;
}
async sendKeepAlive() {
try {
await fetch('/api/auth/keep-alive', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
}
});
} catch (error) {
console.warn('Keep-Alive fehlgeschlagen:', error);
}
}
getCSRFToken() {
const metaTag = document.querySelector('meta[name="csrf-token"]');
return metaTag ? metaTag.getAttribute('content') : '';
}
async performLogout() {
this.closeWarning();
this.clearTimers();
window.location.href = '/auth/logout';
}
}
// Initialisierung
document.addEventListener('DOMContentLoaded', function() {
if (!window.location.pathname.includes('/login')) {
window.autoLogoutManager = new AutoLogoutManager();
}
});

413
backend/static/js/charts.js Normal file
View File

@ -0,0 +1,413 @@
/**
* Charts.js - Diagramm-Management mit Chart.js für MYP Platform
*
* Verwaltet alle Diagramme auf der Statistiken-Seite.
* Unterstützt Dark Mode und Live-Updates.
*/
// Chart.js Instanzen Global verfügbar machen
window.statsCharts = {};
// Chart.js Konfiguration für Dark/Light Theme
function getChartTheme() {
const isDark = document.documentElement.classList.contains('dark');
return {
isDark: isDark,
backgroundColor: isDark ? 'rgba(30, 41, 59, 0.8)' : 'rgba(255, 255, 255, 0.8)',
textColor: isDark ? '#e2e8f0' : '#374151',
gridColor: isDark ? 'rgba(148, 163, 184, 0.1)' : 'rgba(156, 163, 175, 0.2)',
borderColor: isDark ? 'rgba(148, 163, 184, 0.3)' : 'rgba(156, 163, 175, 0.5)'
};
}
// Standard Chart.js Optionen
function getDefaultChartOptions() {
const theme = getChartTheme();
return {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: theme.textColor,
font: {
family: 'Inter, sans-serif'
}
}
},
tooltip: {
backgroundColor: theme.backgroundColor,
titleColor: theme.textColor,
bodyColor: theme.textColor,
borderColor: theme.borderColor,
borderWidth: 1
}
},
scales: {
x: {
ticks: {
color: theme.textColor,
font: {
family: 'Inter, sans-serif'
}
},
grid: {
color: theme.gridColor
}
},
y: {
ticks: {
color: theme.textColor,
font: {
family: 'Inter, sans-serif'
}
},
grid: {
color: theme.gridColor
}
}
}
};
}
// Job Status Doughnut Chart
async function createJobStatusChart() {
try {
const response = await fetch('/api/stats/charts/job-status');
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Fehler beim Laden der Job-Status-Daten');
}
const ctx = document.getElementById('job-status-chart');
if (!ctx) return;
// Vorhandenes Chart zerstören falls vorhanden
if (window.statsCharts.jobStatus) {
window.statsCharts.jobStatus.destroy();
}
const theme = getChartTheme();
window.statsCharts.jobStatus = new Chart(ctx, {
type: 'doughnut',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: theme.textColor,
font: {
family: 'Inter, sans-serif',
size: 12
},
padding: 15
}
},
tooltip: {
backgroundColor: theme.backgroundColor,
titleColor: theme.textColor,
bodyColor: theme.textColor,
borderColor: theme.borderColor,
borderWidth: 1,
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
return `${label}: ${value} (${percentage}%)`;
}
}
}
},
cutout: '60%'
}
});
} catch (error) {
console.error('Fehler beim Erstellen des Job-Status-Charts:', error);
showChartError('job-status-chart', 'Fehler beim Laden der Job-Status-Daten');
}
}
// Drucker-Nutzung Bar Chart
async function createPrinterUsageChart() {
try {
const response = await fetch('/api/stats/charts/printer-usage');
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Fehler beim Laden der Drucker-Nutzung-Daten');
}
const ctx = document.getElementById('printer-usage-chart');
if (!ctx) return;
// Vorhandenes Chart zerstören falls vorhanden
if (window.statsCharts.printerUsage) {
window.statsCharts.printerUsage.destroy();
}
const options = getDefaultChartOptions();
options.scales.y.title = {
display: true,
text: 'Anzahl Jobs',
color: getChartTheme().textColor,
font: {
family: 'Inter, sans-serif'
}
};
window.statsCharts.printerUsage = new Chart(ctx, {
type: 'bar',
data: data,
options: options
});
} catch (error) {
console.error('Fehler beim Erstellen des Drucker-Nutzung-Charts:', error);
showChartError('printer-usage-chart', 'Fehler beim Laden der Drucker-Nutzung-Daten');
}
}
// Jobs Timeline Line Chart
async function createJobsTimelineChart() {
try {
const response = await fetch('/api/stats/charts/jobs-timeline');
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Fehler beim Laden der Jobs-Timeline-Daten');
}
const ctx = document.getElementById('jobs-timeline-chart');
if (!ctx) return;
// Vorhandenes Chart zerstören falls vorhanden
if (window.statsCharts.jobsTimeline) {
window.statsCharts.jobsTimeline.destroy();
}
const options = getDefaultChartOptions();
options.scales.y.title = {
display: true,
text: 'Jobs pro Tag',
color: getChartTheme().textColor,
font: {
family: 'Inter, sans-serif'
}
};
options.scales.x.title = {
display: true,
text: 'Datum (letzte 30 Tage)',
color: getChartTheme().textColor,
font: {
family: 'Inter, sans-serif'
}
};
window.statsCharts.jobsTimeline = new Chart(ctx, {
type: 'line',
data: data,
options: options
});
} catch (error) {
console.error('Fehler beim Erstellen des Jobs-Timeline-Charts:', error);
showChartError('jobs-timeline-chart', 'Fehler beim Laden der Jobs-Timeline-Daten');
}
}
// Benutzer-Aktivität Bar Chart
async function createUserActivityChart() {
try {
const response = await fetch('/api/stats/charts/user-activity');
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Fehler beim Laden der Benutzer-Aktivität-Daten');
}
const ctx = document.getElementById('user-activity-chart');
if (!ctx) return;
// Vorhandenes Chart zerstören falls vorhanden
if (window.statsCharts.userActivity) {
window.statsCharts.userActivity.destroy();
}
const options = getDefaultChartOptions();
options.indexAxis = 'y'; // Horizontales Balkendiagramm
options.scales.x.title = {
display: true,
text: 'Anzahl Jobs',
color: getChartTheme().textColor,
font: {
family: 'Inter, sans-serif'
}
};
options.scales.y.title = {
display: true,
text: 'Benutzer',
color: getChartTheme().textColor,
font: {
family: 'Inter, sans-serif'
}
};
window.statsCharts.userActivity = new Chart(ctx, {
type: 'bar',
data: data,
options: options
});
} catch (error) {
console.error('Fehler beim Erstellen des Benutzer-Aktivität-Charts:', error);
showChartError('user-activity-chart', 'Fehler beim Laden der Benutzer-Aktivität-Daten');
}
}
// Fehleranzeige in Chart-Container
function showChartError(chartId, message) {
const container = document.getElementById(chartId);
if (container) {
container.innerHTML = `
<div class="flex items-center justify-center h-full">
<div class="text-center">
<svg class="h-12 w-12 mx-auto text-red-500 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<p class="text-red-500 font-medium">${message}</p>
<button onclick="refreshAllCharts()" class="mt-2 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
Erneut versuchen
</button>
</div>
</div>
`;
}
}
// Alle Charts erstellen
async function initializeAllCharts() {
// Loading-Indikatoren anzeigen
showChartLoading();
// Charts parallel erstellen
await Promise.allSettled([
createJobStatusChart(),
createPrinterUsageChart(),
createJobsTimelineChart(),
createUserActivityChart()
]);
}
// Loading-Indikatoren anzeigen
function showChartLoading() {
const chartIds = ['job-status-chart', 'printer-usage-chart', 'jobs-timeline-chart', 'user-activity-chart'];
chartIds.forEach(chartId => {
const container = document.getElementById(chartId);
if (container) {
container.innerHTML = `
<div class="flex items-center justify-center h-full">
<div class="text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
<p class="text-slate-500 dark:text-slate-400 text-sm">Diagramm wird geladen...</p>
</div>
</div>
`;
}
});
}
// Alle Charts aktualisieren
async function refreshAllCharts() {
console.log('Aktualisiere alle Diagramme...');
// Bestehende Charts zerstören
Object.values(window.statsCharts).forEach(chart => {
if (chart && typeof chart.destroy === 'function') {
chart.destroy();
}
});
// Charts neu erstellen
await initializeAllCharts();
console.log('Alle Diagramme aktualisiert');
}
// Theme-Wechsel handhaben
function updateChartsTheme() {
// Alle Charts mit neuem Theme aktualisieren
refreshAllCharts();
}
// Auto-refresh (alle 5 Minuten)
let chartRefreshInterval;
function startChartAutoRefresh() {
// Bestehenden Interval stoppen
if (chartRefreshInterval) {
clearInterval(chartRefreshInterval);
}
// Neuen Interval starten (5 Minuten)
chartRefreshInterval = setInterval(() => {
refreshAllCharts();
}, 5 * 60 * 1000);
}
function stopChartAutoRefresh() {
if (chartRefreshInterval) {
clearInterval(chartRefreshInterval);
chartRefreshInterval = null;
}
}
// Cleanup beim Verlassen der Seite
function cleanup() {
stopChartAutoRefresh();
// Alle Charts zerstören
Object.values(window.statsCharts).forEach(chart => {
if (chart && typeof chart.destroy === 'function') {
chart.destroy();
}
});
window.statsCharts = {};
}
// Globale Funktionen verfügbar machen
window.refreshAllCharts = refreshAllCharts;
window.updateChartsTheme = updateChartsTheme;
window.startChartAutoRefresh = startChartAutoRefresh;
window.stopChartAutoRefresh = stopChartAutoRefresh;
window.cleanup = cleanup;
// Event Listeners
document.addEventListener('DOMContentLoaded', function() {
// Charts initialisieren wenn auf Stats-Seite
if (document.getElementById('job-status-chart')) {
initializeAllCharts();
startChartAutoRefresh();
}
});
// Dark Mode Event Listener
if (typeof window.addEventListener !== 'undefined') {
window.addEventListener('darkModeChanged', function(e) {
updateChartsTheme();
});
}
// Page unload cleanup
window.addEventListener('beforeunload', cleanup);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,291 @@
/**
* MYP Platform Chart-Adapter
* Verbindet bestehende API/Logik mit ApexCharts
* Version: 1.0.0
*/
/**
* Überbrückt die bestehende renderChart Funktion mit der neuen ApexCharts Implementation
* @param {HTMLElement} container - Der Diagramm-Container
* @param {Object} data - Die Diagrammdaten vom API-Endpunkt
*/
function renderChart(container, data) {
// Überprüfen, ob die Daten vorhanden sind
if (!data || (Array.isArray(data) && data.length === 0) || Object.keys(data).length === 0) {
showEmptyState(container, 'Keine Daten', 'Es sind keine Daten für dieses Diagramm verfügbar.');
return;
}
// Container-ID überprüfen und ggf. generieren
if (!container.id) {
container.id = 'chart-' + Math.random().toString(36).substr(2, 9);
}
// Chart-Typ basierend auf Container-ID oder Attributen bestimmen
let chartType = container.getAttribute('data-chart-type');
if (!chartType) {
if (container.id === 'job-status-chart') {
chartType = 'pie';
container.setAttribute('data-chart-type', 'pie');
} else if (container.id === 'printer-usage-chart') {
chartType = 'bar';
container.setAttribute('data-chart-type', 'bar');
} else if (container.id.includes('line')) {
chartType = 'line';
container.setAttribute('data-chart-type', 'line');
} else if (container.id.includes('area')) {
chartType = 'area';
container.setAttribute('data-chart-type', 'area');
} else if (container.id.includes('bar')) {
chartType = 'bar';
container.setAttribute('data-chart-type', 'bar');
} else if (container.id.includes('pie')) {
chartType = 'pie';
container.setAttribute('data-chart-type', 'pie');
} else if (container.id.includes('donut')) {
chartType = 'donut';
container.setAttribute('data-chart-type', 'donut');
} else {
// Standard-Typ
chartType = 'line';
container.setAttribute('data-chart-type', 'line');
}
}
// Daten für das Diagramm vorbereiten
let chartData = {};
// Daten-Transformation basierend auf Container-ID
if (container.id === 'job-status-chart') {
chartData = transformJobStatusData(data);
} else if (container.id === 'printer-usage-chart') {
chartData = transformPrinterUsageData(data);
} else {
// Generischer Ansatz für andere Diagrammtypen
chartData = transformGenericData(data, chartType);
}
// Existierendes Chart zerstören, falls vorhanden
if (activeCharts[container.id]) {
destroyChart(container.id);
}
// Daten als Attribut am Container speichern
container.setAttribute('data-chart-data', JSON.stringify(chartData));
// Neues Chart erstellen
createChart(container, chartType, chartData);
}
/**
* Transformiert Job-Status-Daten für Pie-Chart
* @param {Object} data - Rohdaten vom API-Endpunkt
* @returns {Object} - Formatierte Daten für ApexCharts
*/
function transformJobStatusData(data) {
// Werte für Pie-Chart extrahieren
const series = [
data.scheduled || 0,
data.active || 0,
data.completed || 0,
data.cancelled || 0
];
// Labels für die Diagramm-Segmente
const labels = ['Geplant', 'Aktiv', 'Abgeschlossen', 'Abgebrochen'];
// Benutzerdefinierte Farben
const colors = [
MYP_CHART_COLORS.info, // Blau für geplant
MYP_CHART_COLORS.primary, // Primär für aktiv
MYP_CHART_COLORS.success, // Grün für abgeschlossen
MYP_CHART_COLORS.warning // Gelb für abgebrochen
];
// Zusätzliche Optionen
const options = {
colors: colors,
chart: {
height: 320
},
plotOptions: {
pie: {
donut: {
size: '0%' // Vollständiger Kreis (kein Donut)
}
}
},
legend: {
position: 'bottom'
}
};
return {
series: series,
labels: labels,
options: options
};
}
/**
* Transformiert Drucker-Nutzungsdaten für Bar-Chart
* @param {Object} data - Rohdaten vom API-Endpunkt
* @returns {Object} - Formatierte Daten für ApexCharts
*/
function transformPrinterUsageData(data) {
// Prüfen, ob Daten ein Array sind
if (!Array.isArray(data)) {
console.error('Drucker-Nutzungsdaten müssen ein Array sein:', data);
return {
series: [],
categories: [],
options: {}
};
}
// Druckernamen für X-Achse extrahieren
const categories = data.map(item => item.printer_name || 'Unbekannt');
// Datenreihen für Jobs und Stunden erstellen
const jobsSeries = {
name: 'Jobs',
data: data.map(item => item.job_count || 0)
};
const hoursSeries = {
name: 'Stunden',
data: data.map(item => Math.round((item.print_hours || 0) * 10) / 10)
};
// Zusätzliche Optionen
const options = {
colors: [MYP_CHART_COLORS.primary, MYP_CHART_COLORS.success],
chart: {
height: 320
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: '60%',
borderRadius: 4
}
},
dataLabels: {
enabled: false
},
xaxis: {
categories: categories
}
};
return {
series: [jobsSeries, hoursSeries],
categories: categories,
options: options
};
}
/**
* Transformiert generische Daten für verschiedene Diagrammtypen
* @param {Object|Array} data - Rohdaten vom API-Endpunkt
* @param {string} chartType - Art des Diagramms
* @returns {Object} - Formatierte Daten für ApexCharts
*/
function transformGenericData(data, chartType) {
// Standard-Ergebnisobjekt
const result = {
series: [],
categories: [],
labels: [],
options: {}
};
// Arrays verarbeiten
if (Array.isArray(data)) {
if (chartType === 'pie' || chartType === 'donut' || chartType === 'radial') {
// Für Kreisdiagramme
result.series = data.map(item => item.value || 0);
result.labels = data.map(item => item.name || item.label || 'Unbekannt');
} else {
// Für Linien-, Flächen- und Balkendiagramme
// Annahme: Erste Datenreihe
result.series = [{
name: 'Werte',
data: data.map(item => item.value || 0)
}];
result.categories = data.map(item => item.name || item.label || item.date || 'Unbekannt');
}
}
// Objekte mit Datenreihen verarbeiten
else if (data.series && Array.isArray(data.series)) {
result.series = data.series;
if (data.categories) {
result.categories = data.categories;
}
if (data.labels) {
result.labels = data.labels;
}
if (data.options) {
result.options = data.options;
}
}
// Einfache Objekte verarbeiten
else {
// Für Kreisdiagramme: Alle Eigenschaften als Segmente verwenden
if (chartType === 'pie' || chartType === 'donut' || chartType === 'radial') {
const seriesData = [];
const labelData = [];
Object.keys(data).forEach(key => {
if (typeof data[key] === 'number') {
seriesData.push(data[key]);
labelData.push(key);
}
});
result.series = seriesData;
result.labels = labelData;
}
// Für andere Diagrammtypen: Versuchen, Zeit-/Wertepaare zu finden
else {
const timeKeys = [];
const values = [];
Object.keys(data).forEach(key => {
if (typeof data[key] === 'number') {
timeKeys.push(key);
values.push(data[key]);
}
});
result.series = [{
name: 'Werte',
data: values
}];
result.categories = timeKeys;
}
}
return result;
}
/**
* Zeigt einen leeren Status-Container an
* @param {HTMLElement} container - Der Diagramm-Container
* @param {string} title - Titel der Meldung
* @param {string} message - Meldungstext
*/
function showEmptyState(container, title, message) {
container.innerHTML = `
<div class="text-center py-8">
<svg class="w-12 h-12 mx-auto text-slate-500 dark:text-slate-400 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
</svg>
<h3 class="text-lg font-medium text-slate-700 dark:text-slate-300">${title}</h3>
<p class="text-sm text-slate-500 dark:text-slate-400 italic mt-1">${message}</p>
</div>
`;
}

View File

@ -0,0 +1,431 @@
/**
* MYP Platform Chart-Konfigurationen
* Basierend auf ApexCharts Bibliothek
* Version: 1.0.0
*/
// Standard Farben für Diagramme
const MYP_CHART_COLORS = {
primary: '#3b82f6', // Blau
secondary: '#8b5cf6', // Lila
success: '#10b981', // Grün
warning: '#f59e0b', // Orange
danger: '#ef4444', // Rot
info: '#06b6d4', // Türkis
gray: '#6b7280', // Grau
gradient: {
blue: ['#3b82f6', '#93c5fd'],
purple: ['#8b5cf6', '#c4b5fd'],
green: ['#10b981', '#6ee7b7'],
red: ['#ef4444', '#fca5a5'],
orange: ['#f59e0b', '#fcd34d'],
}
};
// Gemeinsame Grundeinstellungen für alle Diagramme
const getBaseChartOptions = () => {
return {
chart: {
fontFamily: 'Inter, sans-serif',
toolbar: {
show: false
},
zoom: {
enabled: false
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
animateGradually: {
enabled: true,
delay: 150
},
dynamicAnimation: {
enabled: true,
speed: 350
}
}
},
tooltip: {
enabled: true,
theme: 'dark',
style: {
fontSize: '12px',
fontFamily: 'Inter, sans-serif'
}
},
grid: {
show: true,
borderColor: '#334155',
strokeDashArray: 4,
position: 'back',
xaxis: {
lines: {
show: false
}
},
yaxis: {
lines: {
show: true
}
}
},
legend: {
position: 'bottom',
horizontalAlign: 'center',
offsetY: 8,
fontSize: '14px',
fontFamily: 'Inter, sans-serif',
markers: {
width: 10,
height: 10,
strokeWidth: 0,
radius: 4
},
itemMargin: {
horizontal: 10,
vertical: 0
}
},
stroke: {
curve: 'smooth',
width: 3
},
xaxis: {
labels: {
style: {
fontSize: '12px',
fontFamily: 'Inter, sans-serif',
colors: '#94a3b8'
}
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
labels: {
style: {
fontSize: '12px',
fontFamily: 'Inter, sans-serif',
colors: '#94a3b8'
}
}
},
dataLabels: {
enabled: false
},
responsive: [
{
breakpoint: 768,
options: {
chart: {
height: '300px'
},
legend: {
position: 'bottom',
offsetY: 0
}
}
}
]
};
};
/**
* Liniendiagramm Konfiguration
* @param {Array} series - Datenreihen
* @param {Array} categories - X-Achsen Kategorien
* @param {Object} customOptions - Benutzerdefinierte Optionen
* @returns {Object} Konfigurationsobjekt für ApexCharts
*/
function getLineChartConfig(series, categories, customOptions = {}) {
const baseOptions = getBaseChartOptions();
const defaultOptions = {
chart: {
...baseOptions.chart,
type: 'line',
height: 350
},
colors: [
MYP_CHART_COLORS.primary,
MYP_CHART_COLORS.secondary,
MYP_CHART_COLORS.success
],
series: series || [],
xaxis: {
...baseOptions.xaxis,
categories: categories || []
}
};
// Optionen zusammenführen
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
}
/**
* Flächendiagramm Konfiguration
* @param {Array} series - Datenreihen
* @param {Array} categories - X-Achsen Kategorien
* @param {Object} customOptions - Benutzerdefinierte Optionen
* @returns {Object} Konfigurationsobjekt für ApexCharts
*/
function getAreaChartConfig(series, categories, customOptions = {}) {
const baseOptions = getBaseChartOptions();
const defaultOptions = {
chart: {
...baseOptions.chart,
type: 'area',
height: 350
},
colors: [
MYP_CHART_COLORS.primary,
MYP_CHART_COLORS.success
],
series: series || [],
xaxis: {
...baseOptions.xaxis,
categories: categories || []
},
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.7,
opacityTo: 0.3,
stops: [0, 90, 100]
}
}
};
// Optionen zusammenführen
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
}
/**
* Balkendiagramm Konfiguration
* @param {Array} series - Datenreihen
* @param {Array} categories - X-Achsen Kategorien
* @param {Object} customOptions - Benutzerdefinierte Optionen
* @returns {Object} Konfigurationsobjekt für ApexCharts
*/
function getBarChartConfig(series, categories, customOptions = {}) {
const baseOptions = getBaseChartOptions();
const defaultOptions = {
chart: {
...baseOptions.chart,
type: 'bar',
height: 350
},
colors: [
MYP_CHART_COLORS.primary,
MYP_CHART_COLORS.secondary
],
series: series || [],
xaxis: {
...baseOptions.xaxis,
categories: categories || []
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: '70%',
borderRadius: 6,
dataLabels: {
position: 'top'
}
}
}
};
// Optionen zusammenführen
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
}
/**
* Kreisdiagramm Konfiguration
* @param {Array} series - Datenreihen (Werte)
* @param {Array} labels - Beschriftungen
* @param {Object} customOptions - Benutzerdefinierte Optionen
* @returns {Object} Konfigurationsobjekt für ApexCharts
*/
function getPieChartConfig(series, labels, customOptions = {}) {
const baseOptions = getBaseChartOptions();
const defaultOptions = {
chart: {
...baseOptions.chart,
type: 'pie',
height: 350
},
colors: [
MYP_CHART_COLORS.primary,
MYP_CHART_COLORS.success,
MYP_CHART_COLORS.warning,
MYP_CHART_COLORS.danger,
MYP_CHART_COLORS.info
],
series: series || [],
labels: labels || [],
legend: {
position: 'bottom'
},
responsive: [
{
breakpoint: 480,
options: {
chart: {
width: 300
},
legend: {
position: 'bottom'
}
}
}
]
};
// Optionen zusammenführen
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
}
/**
* Donut-Diagramm Konfiguration
* @param {Array} series - Datenreihen (Werte)
* @param {Array} labels - Beschriftungen
* @param {Object} customOptions - Benutzerdefinierte Optionen
* @returns {Object} Konfigurationsobjekt für ApexCharts
*/
function getDonutChartConfig(series, labels, customOptions = {}) {
const baseOptions = getBaseChartOptions();
const defaultOptions = {
chart: {
...baseOptions.chart,
type: 'donut',
height: 350
},
colors: [
MYP_CHART_COLORS.primary,
MYP_CHART_COLORS.success,
MYP_CHART_COLORS.warning,
MYP_CHART_COLORS.danger,
MYP_CHART_COLORS.info
],
series: series || [],
labels: labels || [],
legend: {
position: 'bottom'
},
plotOptions: {
pie: {
donut: {
size: '70%',
labels: {
show: true,
name: {
show: true
},
value: {
show: true,
formatter: function(val) {
return val;
}
},
total: {
show: true,
formatter: function(w) {
return w.globals.seriesTotals.reduce((a, b) => a + b, 0);
}
}
}
}
}
}
};
// Optionen zusammenführen
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
}
/**
* Radial-Diagramm Konfiguration
* @param {Array} series - Datenreihen (Werte)
* @param {Array} labels - Beschriftungen
* @param {Object} customOptions - Benutzerdefinierte Optionen
* @returns {Object} Konfigurationsobjekt für ApexCharts
*/
function getRadialChartConfig(series, labels, customOptions = {}) {
const baseOptions = getBaseChartOptions();
const defaultOptions = {
chart: {
...baseOptions.chart,
type: 'radialBar',
height: 350
},
colors: [
MYP_CHART_COLORS.primary,
MYP_CHART_COLORS.success,
MYP_CHART_COLORS.warning
],
series: series || [],
labels: labels || [],
plotOptions: {
radialBar: {
dataLabels: {
name: {
fontSize: '22px',
},
value: {
fontSize: '16px',
},
total: {
show: true,
label: 'Gesamt',
formatter: function(w) {
return w.globals.seriesTotals.reduce((a, b) => a + b, 0) + '%';
}
}
}
}
}
};
// Optionen zusammenführen
return mergeDeep(mergeDeep({}, baseOptions), mergeDeep(defaultOptions, customOptions));
}
/**
* Helper-Funktion zum tiefen Zusammenführen von Objekten
*/
function mergeDeep(target, source) {
const isObject = obj => obj && typeof obj === 'object';
if (!isObject(target) || !isObject(source)) {
return source;
}
const output = { ...target };
Object.keys(source).forEach(key => {
if (isObject(source[key])) {
if (!(key in target)) {
output[key] = source[key];
} else {
output[key] = mergeDeep(target[key], source[key]);
}
} else {
output[key] = source[key];
}
});
return output;
}

View File

@ -0,0 +1,400 @@
/**
* MYP Platform Chart-Renderer
* Erstellt und verwaltet Diagramme mit ApexCharts
* Version: 1.0.0
*/
// Speicher für aktive Chart-Instanzen
const activeCharts = {};
/**
* Initialisiert alle Diagramme auf der Seite
*/
function initCharts() {
// Prüfen, ob ApexCharts verfügbar ist
if (typeof ApexCharts === 'undefined') {
console.error('ApexCharts ist nicht geladen. Bitte ApexCharts vor chart-renderer.js einbinden.');
return;
}
// Alle Diagramm-Container mit data-chart-type Attribut finden
const chartContainers = document.querySelectorAll('[data-chart-type]');
// Für jeden Container ein Diagramm erstellen
chartContainers.forEach(container => {
const chartId = container.id;
const chartType = container.getAttribute('data-chart-type');
// Prüfen, ob Container eine ID hat
if (!chartId) {
console.error('Chart-Container benötigt eine ID:', container);
return;
}
// Bereits erstellte Charts nicht neu initialisieren
if (activeCharts[chartId]) {
return;
}
// Daten aus data-chart-data Attribut laden (als JSON)
let chartData = {};
try {
const dataAttr = container.getAttribute('data-chart-data');
if (dataAttr) {
chartData = JSON.parse(dataAttr);
}
} catch (error) {
console.error(`Fehler beim Parsen der Chart-Daten für ${chartId}:`, error);
return;
}
// Chart basierend auf Typ erstellen
createChart(container, chartType, chartData);
});
}
/**
* Erstellt ein einzelnes Diagramm
* @param {HTMLElement} container - Der Container für das Diagramm
* @param {string} chartType - Typ des Diagramms (line, area, bar, pie, donut, radial)
* @param {Object} chartData - Daten und Optionen für das Diagramm
*/
function createChart(container, chartType, chartData = {}) {
const chartId = container.id;
let chartOptions = {};
// Diagramm-Typ-spezifische Konfiguration laden
switch(chartType.toLowerCase()) {
case 'line':
chartOptions = getLineChartConfig(
chartData.series || [],
chartData.categories || [],
chartData.options || {}
);
break;
case 'area':
chartOptions = getAreaChartConfig(
chartData.series || [],
chartData.categories || [],
chartData.options || {}
);
break;
case 'bar':
chartOptions = getBarChartConfig(
chartData.series || [],
chartData.categories || [],
chartData.options || {}
);
break;
case 'pie':
chartOptions = getPieChartConfig(
chartData.series || [],
chartData.labels || [],
chartData.options || {}
);
break;
case 'donut':
chartOptions = getDonutChartConfig(
chartData.series || [],
chartData.labels || [],
chartData.options || {}
);
break;
case 'radial':
chartOptions = getRadialChartConfig(
chartData.series || [],
chartData.labels || [],
chartData.options || {}
);
break;
default:
console.error(`Unbekannter Chart-Typ: ${chartType}`);
return;
}
// Dark Mode Anpassungen
updateChartTheme(chartOptions);
// Chart erstellen und speichern
try {
const chart = new ApexCharts(container, chartOptions);
chart.render();
// Referenz speichern
activeCharts[chartId] = {
instance: chart,
type: chartType,
lastData: chartData
};
return chart;
} catch (error) {
console.error(`Fehler beim Erstellen des Charts ${chartId}:`, error);
return null;
}
}
/**
* Aktualisiert ein bestehendes Diagramm mit neuen Daten
* @param {string} chartId - ID des Diagramm-Containers
* @param {Object} newData - Neue Daten für das Diagramm
*/
function updateChart(chartId, newData) {
const chartInfo = activeCharts[chartId];
if (!chartInfo) {
console.error(`Chart mit ID ${chartId} nicht gefunden.`);
return;
}
const chart = chartInfo.instance;
// Aktualisieren basierend auf Chart-Typ
if (chartInfo.type === 'pie' || chartInfo.type === 'donut' || chartInfo.type === 'radial') {
// Für Pie/Donut/Radial-Charts
chart.updateSeries(newData.series || []);
if (newData.labels) {
chart.updateOptions({
labels: newData.labels
});
}
} else {
// Für Line/Area/Bar-Charts
chart.updateSeries(newData.series || []);
if (newData.categories) {
chart.updateOptions({
xaxis: {
categories: newData.categories
}
});
}
}
// Zusätzliche Optionen aktualisieren
if (newData.options) {
chart.updateOptions(newData.options);
}
// Gespeicherte Daten aktualisieren
chartInfo.lastData = {
...chartInfo.lastData,
...newData
};
}
/**
* Lädt Diagrammdaten über AJAX und aktualisiert das Diagramm
* @param {string} chartId - ID des Diagramm-Containers
* @param {string} url - URL zur Datenbeschaffung
* @param {Function} successCallback - Callback nach erfolgreicher Aktualisierung
*/
function loadChartData(chartId, url, successCallback) {
const container = document.getElementById(chartId);
if (!container) {
console.error(`Container mit ID ${chartId} nicht gefunden.`);
return;
}
// Lade-Animation anzeigen
container.classList.add('chart-loading');
// Daten vom Server laden
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Netzwerkantwort war nicht ok');
}
return response.json();
})
.then(data => {
// Lade-Animation entfernen
container.classList.remove('chart-loading');
const chartInfo = activeCharts[chartId];
// Chart erstellen, falls nicht vorhanden
if (!chartInfo) {
const chartType = container.getAttribute('data-chart-type');
if (chartType) {
createChart(container, chartType, data);
} else {
console.error(`Kein Chart-Typ für ${chartId} definiert.`);
}
} else {
// Bestehendes Chart aktualisieren
updateChart(chartId, data);
}
// Callback aufrufen, falls vorhanden
if (typeof successCallback === 'function') {
successCallback(data);
}
})
.catch(error => {
console.error('Fehler beim Laden der Chart-Daten:', error);
container.classList.remove('chart-loading');
// Fehlermeldung im Container anzeigen
container.innerHTML = `
<div class="chart-error">
<svg class="w-10 h-10 text-red-500 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 class="text-lg font-medium">Fehler beim Laden</h3>
<p class="text-sm text-gray-500">Die Diagrammdaten konnten nicht geladen werden.</p>
</div>
`;
});
}
/**
* Aktualisiert die Farbthemen basierend auf dem Dark Mode
* @param {Object} chartOptions - Chart-Optionen Objekt
*/
function updateChartTheme(chartOptions) {
const isDarkMode = document.documentElement.classList.contains('dark');
// Theme anpassen
if (isDarkMode) {
// Dark Mode Einstellungen
chartOptions.theme = {
mode: 'dark',
palette: 'palette1'
};
chartOptions.grid = {
...chartOptions.grid,
borderColor: '#334155'
};
// Text Farben anpassen
chartOptions.xaxis = {
...chartOptions.xaxis,
labels: {
...chartOptions.xaxis?.labels,
style: {
...chartOptions.xaxis?.labels?.style,
colors: '#94a3b8'
}
}
};
chartOptions.yaxis = {
...chartOptions.yaxis,
labels: {
...chartOptions.yaxis?.labels,
style: {
...chartOptions.yaxis?.labels?.style,
colors: '#94a3b8'
}
}
};
} else {
// Light Mode Einstellungen
chartOptions.theme = {
mode: 'light',
palette: 'palette1'
};
chartOptions.grid = {
...chartOptions.grid,
borderColor: '#e2e8f0'
};
// Text Farben anpassen
chartOptions.xaxis = {
...chartOptions.xaxis,
labels: {
...chartOptions.xaxis?.labels,
style: {
...chartOptions.xaxis?.labels?.style,
colors: '#64748b'
}
}
};
chartOptions.yaxis = {
...chartOptions.yaxis,
labels: {
...chartOptions.yaxis?.labels,
style: {
...chartOptions.yaxis?.labels?.style,
colors: '#64748b'
}
}
};
}
return chartOptions;
}
/**
* Event-Listener für Dark Mode Änderungen
*/
function setupDarkModeListener() {
window.addEventListener('darkModeChanged', function(event) {
const isDark = event.detail?.isDark;
// Alle aktiven Charts aktualisieren
Object.keys(activeCharts).forEach(chartId => {
const chartInfo = activeCharts[chartId];
const chart = chartInfo.instance;
// Theme aktualisieren
const updatedOptions = updateChartTheme({
grid: chart.opts.grid,
xaxis: chart.opts.xaxis,
yaxis: chart.opts.yaxis
});
// Chart aktualisieren
chart.updateOptions({
theme: updatedOptions.theme,
grid: updatedOptions.grid,
xaxis: updatedOptions.xaxis,
yaxis: updatedOptions.yaxis
});
});
});
}
/**
* Entfernt alle Chart-Instanzen
*/
function destroyAllCharts() {
Object.keys(activeCharts).forEach(chartId => {
const chartInfo = activeCharts[chartId];
if (chartInfo && chartInfo.instance) {
chartInfo.instance.destroy();
}
});
// Aktive Charts zurücksetzen
Object.keys(activeCharts).forEach(key => delete activeCharts[key]);
}
/**
* Entfernt eine spezifische Chart-Instanz
* @param {string} chartId - ID des Diagramm-Containers
*/
function destroyChart(chartId) {
const chartInfo = activeCharts[chartId];
if (chartInfo && chartInfo.instance) {
chartInfo.instance.destroy();
delete activeCharts[chartId];
}
}
// DOM bereit Event-Listener
document.addEventListener('DOMContentLoaded', function() {
initCharts();
setupDarkModeListener();
});

View File

@ -0,0 +1,283 @@
/**
* Mercedes-Benz MYP Platform - CSP Violation Handler
* Protokolliert und behandelt Content Security Policy Verletzungen
*/
class CSPViolationHandler {
constructor() {
this.violations = [];
this.init();
}
init() {
// CSP Violation Event Listener
document.addEventListener('securitypolicyviolation', this.handleViolation.bind(this));
// Report-To API fallback
if ('ReportingObserver' in window) {
const observer = new ReportingObserver((reports, observer) => {
for (const report of reports) {
if (report.type === 'csp-violation') {
this.handleViolation(report.body);
}
}
});
observer.observe();
}
console.log('🛡️ CSP Violation Handler initialisiert');
}
/**
* CSP-Verletzung behandeln
*/
handleViolation(violationEvent) {
const violation = {
timestamp: new Date().toISOString(),
blockedURI: violationEvent.blockedURI || 'unknown',
violatedDirective: violationEvent.violatedDirective || 'unknown',
originalPolicy: violationEvent.originalPolicy || 'unknown',
documentURI: violationEvent.documentURI || window.location.href,
sourceFile: violationEvent.sourceFile || 'unknown',
lineNumber: violationEvent.lineNumber || 0,
columnNumber: violationEvent.columnNumber || 0,
sample: violationEvent.sample || '',
disposition: violationEvent.disposition || 'enforce'
};
this.violations.push(violation);
this.logViolation(violation);
this.suggestFix(violation);
// Violation an Server senden (falls API verfügbar)
this.reportViolation(violation);
}
/**
* Verletzung protokollieren
*/
logViolation(violation) {
console.group('🚨 CSP Violation detected');
console.error('Blocked URI:', violation.blockedURI);
console.error('Violated Directive:', violation.violatedDirective);
console.error('Source:', `${violation.sourceFile}:${violation.lineNumber}:${violation.columnNumber}`);
console.error('Sample:', violation.sample);
console.error('Full Policy:', violation.originalPolicy);
console.groupEnd();
}
/**
* Lösungsvorschlag basierend auf Verletzungstyp
*/
suggestFix(violation) {
const directive = violation.violatedDirective;
const blockedURI = violation.blockedURI;
console.group('💡 Lösungsvorschlag');
if (directive.includes('script-src')) {
if (blockedURI === 'inline') {
console.log('Problem: Inline-Script blockiert');
console.log('Lösung 1: Script in externe .js-Datei auslagern');
console.log('Lösung 2: data-action Attribute für Event-Handler verwenden');
console.log('Lösung 3: Nonce verwenden (nicht empfohlen für Entwicklung)');
console.log('Beispiel: <button data-action="refresh-dashboard">Aktualisieren</button>');
} else if (blockedURI.includes('eval')) {
console.log('Problem: eval() oder ähnliche Funktionen blockiert');
console.log('Lösung: Verwende sichere Alternativen zu eval()');
} else {
console.log(`Problem: Externes Script von ${blockedURI} blockiert`);
console.log('Lösung: URL zur CSP script-src Richtlinie hinzufügen');
}
} else if (directive.includes('style-src')) {
console.log('Problem: Style blockiert');
console.log('Lösung: CSS in externe .css-Datei auslagern oder CSP erweitern');
} else if (directive.includes('connect-src')) {
console.log(`Problem: Verbindung zu ${blockedURI} blockiert`);
console.log('Lösung: URL zur CSP connect-src Richtlinie hinzufügen');
console.log('Tipp: Für API-Calls relative URLs verwenden');
} else if (directive.includes('img-src')) {
console.log(`Problem: Bild von ${blockedURI} blockiert`);
console.log('Lösung: URL zur CSP img-src Richtlinie hinzufügen');
}
console.groupEnd();
}
/**
* Verletzung an Server senden
*/
async reportViolation(violation) {
try {
// Nur in Produktion an Server senden
if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
await fetch('/api/security/csp-violation', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(violation)
});
}
} catch (error) {
console.warn('Fehler beim Senden der CSP-Verletzung:', error);
}
}
/**
* Alle Verletzungen abrufen
*/
getViolations() {
return this.violations;
}
/**
* Verletzungsstatistiken
*/
getStats() {
const stats = {
total: this.violations.length,
byDirective: {},
byURI: {},
recent: this.violations.slice(-10)
};
this.violations.forEach(violation => {
// Nach Direktive gruppieren
const directive = violation.violatedDirective;
stats.byDirective[directive] = (stats.byDirective[directive] || 0) + 1;
// Nach URI gruppieren
const uri = violation.blockedURI;
stats.byURI[uri] = (stats.byURI[uri] || 0) + 1;
});
return stats;
}
/**
* Entwickler-Debugging-Tools
*/
enableDebugMode() {
// Debug-Panel erstellen
this.createDebugPanel();
// Konsolen-Hilfe ausgeben
console.log('🔧 CSP Debug Mode aktiviert');
console.log('Verfügbare Befehle:');
console.log('- cspHandler.getViolations() - Alle Verletzungen anzeigen');
console.log('- cspHandler.getStats() - Statistiken anzeigen');
console.log('- cspHandler.clearViolations() - Verletzungen löschen');
console.log('- cspHandler.exportViolations() - Als JSON exportieren');
}
/**
* Debug-Panel erstellen
*/
createDebugPanel() {
const panel = document.createElement('div');
panel.id = 'csp-debug-panel';
panel.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
width: 300px;
max-height: 400px;
background: rgba(0, 0, 0, 0.9);
color: white;
font-family: monospace;
font-size: 12px;
padding: 10px;
border-radius: 5px;
z-index: 10000;
overflow-y: auto;
display: none;
`;
panel.innerHTML = `
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<strong>CSP Violations</strong>
<button onclick="this.parentElement.parentElement.style.display='none'"
style="background: none; border: none; color: white; cursor: pointer;">&times;</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');

View File

@ -0,0 +1,194 @@
/**
* Dark Mode Toggle Fix - Premium Edition
* Diese Datei stellt sicher, dass der neue Premium Dark Mode Toggle Button korrekt funktioniert
*/
document.addEventListener('DOMContentLoaded', function() {
// Dark Mode Toggle Button (Premium Design)
const darkModeToggle = document.getElementById('darkModeToggle');
const html = document.documentElement;
// Local Storage Key
const STORAGE_KEY = 'myp-dark-mode';
/**
* Aktuellen Dark Mode Status aus Local Storage oder Systemeinstellung abrufen
*/
function isDarkMode() {
const savedMode = localStorage.getItem(STORAGE_KEY);
if (savedMode !== null) {
return savedMode === 'true';
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
/**
* Icons im Premium Toggle-Button aktualisieren
*/
function updateIcons(isDark) {
if (!darkModeToggle) return;
// Finde die Premium-Icons
const sunIcon = darkModeToggle.querySelector('.sun-icon');
const moonIcon = darkModeToggle.querySelector('.moon-icon');
if (!sunIcon || !moonIcon) {
console.warn('Premium Dark Mode Icons nicht gefunden');
return;
}
// Animation für Übergänge
if (isDark) {
// Dark Mode aktiviert - zeige Mond
sunIcon.style.opacity = '0';
sunIcon.style.transform = 'scale(0.75) rotate(90deg)';
moonIcon.style.opacity = '1';
moonIcon.style.transform = 'scale(1) rotate(0deg)';
// CSS-Klassen für Dark Mode
sunIcon.classList.add('opacity-0', 'dark:opacity-0', 'scale-75', 'dark:scale-75', 'rotate-90', 'dark:rotate-90');
sunIcon.classList.remove('opacity-100', 'scale-100', 'rotate-0');
moonIcon.classList.add('opacity-100', 'dark:opacity-100', 'scale-100', 'dark:scale-100', 'rotate-0', 'dark:rotate-0');
moonIcon.classList.remove('opacity-0', 'scale-75', 'rotate-90');
} else {
// Light Mode aktiviert - zeige Sonne
sunIcon.style.opacity = '1';
sunIcon.style.transform = 'scale(1) rotate(0deg)';
moonIcon.style.opacity = '0';
moonIcon.style.transform = 'scale(0.75) rotate(-90deg)';
// CSS-Klassen für Light Mode
sunIcon.classList.add('opacity-100', 'scale-100', 'rotate-0');
sunIcon.classList.remove('opacity-0', 'dark:opacity-0', 'scale-75', 'dark:scale-75', 'rotate-90', 'dark:rotate-90');
moonIcon.classList.add('opacity-0', 'dark:opacity-100', 'scale-75', 'dark:scale-100', 'rotate-90', 'dark:rotate-0');
moonIcon.classList.remove('opacity-100', 'scale-100', 'rotate-0');
}
// Icon-Animationen hinzufügen
sunIcon.classList.toggle('icon-enter', !isDark);
moonIcon.classList.toggle('icon-enter', isDark);
}
/**
* Premium Dark Mode aktivieren/deaktivieren
*/
function setDarkMode(enable) {
console.log(`🎨 Setze Premium Dark Mode auf: ${enable ? 'Aktiviert' : 'Deaktiviert'}`);
if (enable) {
html.classList.add('dark');
html.setAttribute('data-theme', 'dark');
html.style.colorScheme = 'dark';
if (darkModeToggle) {
darkModeToggle.setAttribute('aria-pressed', 'true');
darkModeToggle.setAttribute('title', 'Light Mode aktivieren');
// Premium Button-Icons aktualisieren
updateIcons(true);
}
} else {
html.classList.remove('dark');
html.setAttribute('data-theme', 'light');
html.style.colorScheme = 'light';
if (darkModeToggle) {
darkModeToggle.setAttribute('aria-pressed', 'false');
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
// Premium Button-Icons aktualisieren
updateIcons(false);
}
}
// Einstellung im Local Storage speichern
localStorage.setItem(STORAGE_KEY, enable.toString());
// ThemeColor Meta-Tag aktualisieren
const metaThemeColor = document.getElementById('metaThemeColor');
if (metaThemeColor) {
metaThemeColor.setAttribute('content', enable ? '#000000' : '#ffffff');
}
// Event für andere Komponenten auslösen
window.dispatchEvent(new CustomEvent('darkModeChanged', {
detail: { isDark: enable }
}));
// Premium-Feedback
console.log(`${enable ? '🌙' : '☀️'} Premium Design umgeschaltet auf: ${enable ? 'Dark Mode' : 'Light Mode'}`);
}
// Toggle Dark Mode Funktion
function toggleDarkMode() {
const currentMode = isDarkMode();
setDarkMode(!currentMode);
// Premium-Animation beim Klick
if (darkModeToggle) {
const container = darkModeToggle.querySelector('div');
if (container) {
container.style.transform = 'scale(0.95)';
setTimeout(() => {
container.style.transform = '';
}, 150);
}
}
}
// Event Listener für Premium Toggle Button
if (darkModeToggle) {
console.log('🎨 Premium Dark Mode Toggle Button gefunden - initialisiere...');
// Entferne vorherige Event-Listener, um Duplikate zu vermeiden
const newDarkModeToggle = darkModeToggle.cloneNode(true);
darkModeToggle.parentNode.replaceChild(newDarkModeToggle, darkModeToggle);
// Neuen Event-Listener hinzufügen
newDarkModeToggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation(); // Verhindere Bubbling
toggleDarkMode();
});
// Aktualisiere die Variable auf das neue Element
const updatedToggle = document.getElementById('darkModeToggle');
// Initialen Status setzen
const isDark = isDarkMode();
setDarkMode(isDark);
console.log('✨ Premium Dark Mode Toggle Button erfolgreich initialisiert');
} else {
console.error('❌ Premium Dark Mode Toggle Button konnte nicht gefunden werden!');
}
// Tastaturkürzel: Strg+Shift+D für Dark Mode Toggle
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
toggleDarkMode();
e.preventDefault();
}
});
// Alternative Tastaturkürzel: Alt+T für Theme Toggle
document.addEventListener('keydown', function(e) {
if (e.altKey && e.key === 't') {
toggleDarkMode();
e.preventDefault();
}
});
// Direkte Verfügbarkeit der Funktionen im globalen Bereich
window.toggleDarkMode = toggleDarkMode;
window.isDarkMode = isDarkMode;
window.setDarkMode = setDarkMode;
// Premium Features
window.premiumDarkMode = {
toggle: toggleDarkMode,
isDark: isDarkMode,
setMode: setDarkMode,
version: '3.0.0-premium'
};
console.log('🎨 Premium Dark Mode System geladen - Version 3.0.0');
});

View File

@ -0,0 +1,306 @@
/**
* MYP Platform Dark Mode Handler
* Version: 6.0.0
*/
// Sofort ausführen, um FOUC zu vermeiden (Flash of Unstyled Content)
(function() {
"use strict";
// Speicherort für Dark Mode-Einstellung
const STORAGE_KEY = 'myp-dark-mode';
// DOM-Elemente
let darkModeToggle;
const html = document.documentElement;
// Initialisierung beim Laden der Seite
document.addEventListener('DOMContentLoaded', initialize);
// Prüft System-Präferenz und gespeicherte Einstellung
function shouldUseDarkMode() {
// Lokale Speichereinstellung prüfen
const savedMode = localStorage.getItem(STORAGE_KEY);
// Prüfen ob es eine gespeicherte Einstellung gibt
if (savedMode !== null) {
return savedMode === 'true';
}
// Ansonsten Systemeinstellung verwenden
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
// Setzt Dark/Light Mode
function setDarkMode(enable) {
// Deaktiviere Übergänge temporär, um Flackern zu vermeiden
html.classList.add('disable-transitions');
// Dark Mode Klasse am HTML-Element setzen
if (enable) {
html.classList.add('dark');
html.setAttribute('data-theme', 'dark');
html.style.colorScheme = 'dark';
} else {
html.classList.remove('dark');
html.setAttribute('data-theme', 'light');
html.style.colorScheme = 'light';
}
// Speichern in LocalStorage
localStorage.setItem(STORAGE_KEY, enable);
// Update ThemeColor Meta-Tag
updateMetaThemeColor(enable);
// Wenn Toggle existiert, aktualisiere Icon
if (darkModeToggle) {
updateDarkModeToggle(enable);
}
// Event für andere Komponenten
window.dispatchEvent(new CustomEvent('darkModeChanged', {
detail: {
isDark: enable,
source: 'dark-mode-toggle',
timestamp: new Date().toISOString()
}
}));
// Event auch als eigenen Event-Typ versenden (rückwärtskompatibel)
const eventName = enable ? 'darkModeEnabled' : 'darkModeDisabled';
window.dispatchEvent(new CustomEvent(eventName, {
detail: { timestamp: new Date().toISOString() }
}));
// Übergänge nach kurzer Verzögerung wieder aktivieren
setTimeout(function() {
html.classList.remove('disable-transitions');
}, 100);
// Erfolgsmeldung in die Konsole
console.log(`${enable ? '🌙' : '☀️'} ${enable ? 'Dark Mode aktiviert - Augenschonender Modus aktiv' : 'Light Mode aktiviert - Heller Modus aktiv'}`);
}
// Aktualisiert das Theme-Color Meta-Tag
function updateMetaThemeColor(isDark) {
// Alle Theme-Color Meta-Tags aktualisieren
const metaTags = [
document.getElementById('metaThemeColor'),
document.querySelector('meta[name="theme-color"]'),
document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: light)"]'),
document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: dark)"]')
];
// CSS-Variablen für konsistente Farben verwenden
const darkColor = getComputedStyle(document.documentElement).getPropertyValue('--color-bg') || '#0f172a';
const lightColor = getComputedStyle(document.documentElement).getPropertyValue('--color-bg') || '#ffffff';
metaTags.forEach(tag => {
if (tag) {
// Für Media-spezifische Tags die entsprechende Farbe setzen
if (tag.getAttribute('media') === '(prefers-color-scheme: dark)') {
tag.setAttribute('content', darkColor);
} else if (tag.getAttribute('media') === '(prefers-color-scheme: light)') {
tag.setAttribute('content', lightColor);
} else {
// Für nicht-Media-spezifische Tags die aktuelle Farbe setzen
tag.setAttribute('content', isDark ? darkColor : lightColor);
}
}
});
}
// Aktualisiert das Aussehen des Toggle-Buttons
function updateDarkModeToggle(isDark) {
// Aria-Attribute für Barrierefreiheit
darkModeToggle.setAttribute('aria-pressed', isDark.toString());
darkModeToggle.title = isDark ? "Light Mode aktivieren" : "Dark Mode aktivieren";
// Icons aktualisieren
const sunIcon = darkModeToggle.querySelector('.sun-icon');
const moonIcon = darkModeToggle.querySelector('.moon-icon');
if (sunIcon && moonIcon) {
if (isDark) {
sunIcon.classList.add('hidden');
moonIcon.classList.remove('hidden');
} else {
sunIcon.classList.remove('hidden');
moonIcon.classList.add('hidden');
}
} else {
// Fallback für ältere Implementierung mit einem Icon
const icon = darkModeToggle.querySelector('svg');
if (icon) {
// Animationsklasse hinzufügen
icon.classList.add('animate-spin-once');
// Nach Animation wieder entfernen
setTimeout(() => {
icon.classList.remove('animate-spin-once');
}, 300);
const pathElement = icon.querySelector('path');
if (pathElement) {
// Sonnen- oder Mond-Symbol
if (isDark) {
pathElement.setAttribute("d", "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z");
} else {
pathElement.setAttribute("d", "M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z");
}
}
}
}
}
// Initialisierungsfunktion
function initialize() {
// Toggle-Button finden
darkModeToggle = document.getElementById('darkModeToggle');
// Wenn kein Toggle existiert, erstelle einen
if (!darkModeToggle) {
console.log('🔧 Dark Mode Toggle nicht gefunden - erstelle automatisch einen neuen Button');
createDarkModeToggle();
}
// Event-Listener für Dark Mode Toggle
if (darkModeToggle) {
darkModeToggle.addEventListener('click', function() {
const isDark = !shouldUseDarkMode();
console.log(`👆 Dark Mode Toggle: Wechsel zu ${isDark ? '🌙 dunkel' : '☀️ hell'} angefordert`);
setDarkMode(isDark);
});
}
// Tastenkombination: Strg+Shift+D
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
const isDark = !shouldUseDarkMode();
console.log(`⌨️ Tastenkombination STRG+SHIFT+D erkannt: Wechsel zu ${isDark ? '🌙 dunkel' : '☀️ hell'}`);
setDarkMode(isDark);
e.preventDefault();
}
});
// Auf Systemeinstellungsänderungen reagieren
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
// Moderne Event-API verwenden
try {
darkModeMediaQuery.addEventListener('change', function(e) {
// Nur anwenden, wenn keine benutzerdefinierte Einstellung gespeichert ist
if (localStorage.getItem(STORAGE_KEY) === null) {
console.log(`🖥️ Systemeinstellung geändert: ${e.matches ? '🌙 dunkel' : '☀️ hell'}`);
setDarkMode(e.matches);
}
});
} catch (error) {
// Fallback für ältere Browser
darkModeMediaQuery.addListener(function(e) {
if (localStorage.getItem(STORAGE_KEY) === null) {
console.log(`🖥️ Systemeinstellung geändert (Legacy-Browser): ${e.matches ? '🌙 dunkel' : '☀️ hell'}`);
setDarkMode(e.matches);
}
});
}
// Initialer Zustand
const initialState = shouldUseDarkMode();
console.log(`🔍 Ermittelter Ausgangszustand: ${initialState ? '🌙 Dark Mode' : '☀️ Light Mode'}`);
setDarkMode(initialState);
// Animation für den korrekten Modus hinzufügen
const animClass = initialState ? 'dark-mode-transition' : 'light-mode-transition';
document.body.classList.add(animClass);
// Animation entfernen nach Abschluss
setTimeout(() => {
document.body.classList.remove(animClass);
}, 300);
console.log('🚀 Dark Mode Handler erfolgreich initialisiert');
}
// Erstellt ein Toggle-Element, falls keines existiert
function createDarkModeToggle() {
// Bestehende Header-Elemente finden
const header = document.querySelector('header');
const nav = document.querySelector('nav');
const container = document.querySelector('.dark-mode-container') || header || nav;
if (!container) {
console.error('⚠️ Kein geeigneter Container für Dark Mode Toggle gefunden');
return;
}
// Toggle-Button erstellen
darkModeToggle = document.createElement('button');
darkModeToggle.id = 'darkModeToggle';
darkModeToggle.className = 'dark-mode-toggle-new';
darkModeToggle.setAttribute('aria-label', 'Dark Mode umschalten');
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
darkModeToggle.setAttribute('data-action', 'toggle-dark-mode');
// Sonnen-Icon erstellen
const sunIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
sunIcon.setAttribute("class", "w-5 h-5 sm:w-5 sm:h-5 sun-icon");
sunIcon.setAttribute("fill", "none");
sunIcon.setAttribute("stroke", "currentColor");
sunIcon.setAttribute("viewBox", "0 0 24 24");
sunIcon.setAttribute("aria-hidden", "true");
const sunPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
sunPath.setAttribute("stroke-linecap", "round");
sunPath.setAttribute("stroke-linejoin", "round");
sunPath.setAttribute("stroke-width", "2");
sunPath.setAttribute("d", "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z");
// Mond-Icon erstellen
const moonIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
moonIcon.setAttribute("class", "w-5 h-5 sm:w-5 sm:h-5 moon-icon hidden");
moonIcon.setAttribute("fill", "none");
moonIcon.setAttribute("stroke", "currentColor");
moonIcon.setAttribute("viewBox", "0 0 24 24");
moonIcon.setAttribute("aria-hidden", "true");
const moonPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
moonPath.setAttribute("stroke-linecap", "round");
moonPath.setAttribute("stroke-linejoin", "round");
moonPath.setAttribute("stroke-width", "2");
moonPath.setAttribute("d", "M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z");
// Elemente zusammenfügen
sunIcon.appendChild(sunPath);
moonIcon.appendChild(moonPath);
darkModeToggle.appendChild(sunIcon);
darkModeToggle.appendChild(moonIcon);
// Zum Container hinzufügen
container.appendChild(darkModeToggle);
console.log('✅ Dark Mode Toggle Button erfolgreich erstellt und zur Benutzeroberfläche hinzugefügt');
}
// Sofort Dark/Light Mode anwenden (vor DOMContentLoaded)
const isDark = shouldUseDarkMode();
console.log(`🏃‍♂️ Sofortige Anwendung: ${isDark ? '🌙 Dark Mode' : '☀️ Light Mode'} (vor DOM-Ladung)`);
setDarkMode(isDark);
})();
// Animationen für Spin-Effekt
if (!document.querySelector('style#dark-mode-animations')) {
const styleTag = document.createElement('style');
styleTag.id = 'dark-mode-animations';
styleTag.textContent = `
@keyframes spin-once {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.animate-spin-once {
animation: spin-once 0.3s ease-in-out;
}
`;
document.head.appendChild(styleTag);
console.log('💫 Animations-Styles für Dark Mode Toggle hinzugefügt');
}

View File

@ -0,0 +1,354 @@
// Dashboard JavaScript - Externe Datei für CSP-Konformität
// Globale Variablen
let dashboardData = {};
let updateInterval;
// DOM-Elemente
const elements = {
activeJobs: null,
scheduledJobs: null,
availablePrinters: null,
totalPrintTime: null,
schedulerStatus: null,
recentJobsList: null,
recentActivitiesList: null,
refreshBtn: null,
schedulerToggleBtn: null
};
// Initialisierung beim Laden der Seite
document.addEventListener('DOMContentLoaded', function() {
initializeDashboard();
});
function initializeDashboard() {
// DOM-Elemente referenzieren
elements.activeJobs = document.getElementById('active-jobs');
elements.scheduledJobs = document.getElementById('scheduled-jobs');
elements.availablePrinters = document.getElementById('available-printers');
elements.totalPrintTime = document.getElementById('total-print-time');
elements.schedulerStatus = document.getElementById('scheduler-status');
elements.recentJobsList = document.getElementById('recent-jobs-list');
elements.recentActivitiesList = document.getElementById('recent-activities-list');
elements.refreshBtn = document.getElementById('refresh-btn');
elements.schedulerToggleBtn = document.getElementById('scheduler-toggle-btn');
// Event-Listener hinzufügen
if (elements.refreshBtn) {
elements.refreshBtn.addEventListener('click', refreshDashboard);
}
if (elements.schedulerToggleBtn) {
elements.schedulerToggleBtn.addEventListener('click', toggleScheduler);
}
// Initiales Laden der Daten
loadDashboardData();
loadRecentJobs();
loadRecentActivities();
loadSchedulerStatus();
// Auto-Update alle 30 Sekunden
updateInterval = setInterval(function() {
loadDashboardData();
loadRecentJobs();
loadRecentActivities();
loadSchedulerStatus();
}, 30000);
}
// Dashboard-Hauptdaten laden
async function loadDashboardData() {
try {
const response = await fetch('/api/dashboard');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
dashboardData = await response.json();
updateDashboardUI();
} catch (error) {
console.error('Fehler beim Laden der Dashboard-Daten:', error);
showError('Fehler beim Laden der Dashboard-Daten');
}
}
// Dashboard-UI aktualisieren
function updateDashboardUI() {
if (elements.activeJobs) {
elements.activeJobs.textContent = dashboardData.active_jobs || 0;
}
if (elements.scheduledJobs) {
elements.scheduledJobs.textContent = dashboardData.scheduled_jobs || 0;
}
if (elements.availablePrinters) {
elements.availablePrinters.textContent = dashboardData.available_printers || 0;
}
if (elements.totalPrintTime) {
const hours = Math.floor((dashboardData.total_print_time || 0) / 3600);
const minutes = Math.floor(((dashboardData.total_print_time || 0) % 3600) / 60);
elements.totalPrintTime.textContent = `${hours}h ${minutes}m`;
}
}
// Aktuelle Jobs laden
async function loadRecentJobs() {
try {
const response = await fetch('/api/jobs/recent');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
updateRecentJobsList(data.jobs);
} catch (error) {
console.error('Fehler beim Laden der aktuellen Jobs:', error);
if (elements.recentJobsList) {
elements.recentJobsList.innerHTML = '<li class="list-group-item text-danger">Fehler beim Laden</li>';
}
}
}
// Jobs-Liste aktualisieren
function updateRecentJobsList(jobs) {
if (!elements.recentJobsList) return;
if (!jobs || jobs.length === 0) {
elements.recentJobsList.innerHTML = '<li class="list-group-item text-muted">Keine aktuellen Jobs</li>';
return;
}
const jobsHtml = jobs.map(job => {
const statusClass = getStatusClass(job.status);
const timeAgo = formatTimeAgo(job.created_at);
return `
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>${escapeHtml(job.name)}</strong><br>
<small class="text-muted">${escapeHtml(job.printer_name)}${timeAgo}</small>
</div>
<span class="badge ${statusClass}">${getStatusText(job.status)}</span>
</li>
`;
}).join('');
elements.recentJobsList.innerHTML = jobsHtml;
}
// Aktuelle Aktivitäten laden
async function loadRecentActivities() {
try {
const response = await fetch('/api/activity/recent');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
updateRecentActivitiesList(data.activities);
} catch (error) {
console.error('Fehler beim Laden der Aktivitäten:', error);
if (elements.recentActivitiesList) {
elements.recentActivitiesList.innerHTML = '<li class="list-group-item text-danger">Fehler beim Laden</li>';
}
}
}
// Aktivitäten-Liste aktualisieren
function updateRecentActivitiesList(activities) {
if (!elements.recentActivitiesList) return;
if (!activities || activities.length === 0) {
elements.recentActivitiesList.innerHTML = '<li class="list-group-item text-muted">Keine aktuellen Aktivitäten</li>';
return;
}
const activitiesHtml = activities.map(activity => {
const timeAgo = formatTimeAgo(activity.timestamp);
return `
<li class="list-group-item">
<div>${escapeHtml(activity.description)}</div>
<small class="text-muted">${timeAgo}</small>
</li>
`;
}).join('');
elements.recentActivitiesList.innerHTML = activitiesHtml;
}
// Scheduler-Status laden
async function loadSchedulerStatus() {
try {
const response = await fetch('/api/scheduler/status');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
updateSchedulerStatus(data.running);
} catch (error) {
console.error('Fehler beim Laden des Scheduler-Status:', error);
if (elements.schedulerStatus) {
elements.schedulerStatus.innerHTML = '<span class="badge bg-secondary">Unbekannt</span>';
}
}
}
// Scheduler-Status aktualisieren
function updateSchedulerStatus(isRunning) {
if (!elements.schedulerStatus) return;
const statusClass = isRunning ? 'bg-success' : 'bg-danger';
const statusText = isRunning ? 'Aktiv' : 'Gestoppt';
elements.schedulerStatus.innerHTML = `<span class="badge ${statusClass}">${statusText}</span>`;
if (elements.schedulerToggleBtn) {
elements.schedulerToggleBtn.textContent = isRunning ? 'Scheduler stoppen' : 'Scheduler starten';
elements.schedulerToggleBtn.className = isRunning ? 'btn btn-danger btn-sm' : 'btn btn-success btn-sm';
}
}
// Scheduler umschalten
async function toggleScheduler() {
try {
const isRunning = dashboardData.scheduler_running;
const endpoint = isRunning ? '/api/scheduler/stop' : '/api/scheduler/start';
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.success) {
showSuccess(result.message);
// Status sofort neu laden
setTimeout(loadSchedulerStatus, 1000);
} else {
showError(result.error || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Fehler beim Umschalten des Schedulers:', error);
showError('Fehler beim Umschalten des Schedulers');
}
}
// Dashboard manuell aktualisieren
function refreshDashboard() {
if (elements.refreshBtn) {
elements.refreshBtn.disabled = true;
elements.refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Aktualisiere...';
}
Promise.all([
loadDashboardData(),
loadRecentJobs(),
loadRecentActivities(),
loadSchedulerStatus()
]).finally(() => {
if (elements.refreshBtn) {
elements.refreshBtn.disabled = false;
elements.refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i> Aktualisieren';
}
});
}
// Hilfsfunktionen
function getStatusClass(status) {
const statusClasses = {
'pending': 'bg-warning',
'printing': 'bg-primary',
'completed': 'bg-success',
'failed': 'bg-danger',
'cancelled': 'bg-secondary',
'scheduled': 'bg-info'
};
return statusClasses[status] || 'bg-secondary';
}
function getStatusText(status) {
const statusTexts = {
'pending': 'Wartend',
'printing': 'Druckt',
'completed': 'Abgeschlossen',
'failed': 'Fehlgeschlagen',
'cancelled': 'Abgebrochen',
'scheduled': 'Geplant'
};
return statusTexts[status] || status;
}
function formatTimeAgo(timestamp) {
const now = new Date();
const time = new Date(timestamp);
const diffMs = now - time;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return 'Gerade eben';
if (diffMins < 60) return `vor ${diffMins} Min`;
if (diffHours < 24) return `vor ${diffHours} Std`;
return `vor ${diffDays} Tag${diffDays > 1 ? 'en' : ''}`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showSuccess(message) {
showNotification(message, 'success');
}
function showError(message) {
showNotification(message, 'danger');
}
function showNotification(message, type) {
// Einfache Notification - kann später durch Toast-System ersetzt werden
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.top = '20px';
alertDiv.style.right = '20px';
alertDiv.style.zIndex = '9999';
alertDiv.innerHTML = `
${escapeHtml(message)}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// Automatisch nach 5 Sekunden entfernen
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.parentNode.removeChild(alertDiv);
}
}, 5000);
}
// Cleanup beim Verlassen der Seite
window.addEventListener('beforeunload', function() {
if (updateInterval) {
clearInterval(updateInterval);
}
});

View File

@ -0,0 +1,169 @@
/**
* Debug Fix Script für MYP Platform
* Temporäre Fehlerbehebung für JavaScript-Probleme
*/
(function() {
'use strict';
console.log('🔧 Debug Fix Script wird geladen...');
// Namespace sicherstellen
window.MYP = window.MYP || {};
window.MYP.UI = window.MYP.UI || {};
// MVP.UI Alias erstellen falls es fehlerhaft verwendet wird
window.MVP = window.MVP || {};
window.MVP.UI = window.MVP.UI || {};
// Sofortiger Alias für DarkModeManager
window.MVP.UI.DarkModeManager = function() {
console.log('⚠️ MVP.UI.DarkModeManager Konstruktor aufgerufen - verwende MYP.UI.darkMode stattdessen');
if (window.MYP && window.MYP.UI && window.MYP.UI.darkMode) {
return window.MYP.UI.darkMode;
}
// Fallback: Dummy-Objekt zurückgeben
return {
init: function() { console.log('DarkModeManager Fallback init'); },
setDarkMode: function() { console.log('DarkModeManager Fallback setDarkMode'); },
isDarkMode: function() { return false; }
};
};
// DOMContentLoaded Event abwarten
document.addEventListener('DOMContentLoaded', function() {
console.log('🚀 Debug Fix: DOM Content geladen');
// Warten bis ui-components.js geladen ist
setTimeout(() => {
try {
// MVP.UI DarkModeManager Alias aktualisieren
if (window.MYP && window.MYP.UI && window.MYP.UI.darkMode) {
window.MVP.UI.DarkModeManager = function() {
console.log('⚠️ MVP.UI.DarkModeManager Konstruktor aufgerufen - verwende MYP.UI.darkMode stattdessen');
return window.MYP.UI.darkMode;
};
console.log('✅ MVP.UI.DarkModeManager Alias aktualisiert');
}
// JobManager sicherstellen
if (!window.jobManager && window.JobManager) {
window.jobManager = new window.JobManager();
console.log('✅ JobManager Instanz erstellt');
}
// Fehlende setupFormHandlers Methode hinzufügen falls nötig
if (window.jobManager && !window.jobManager.setupFormHandlers) {
window.jobManager.setupFormHandlers = function() {
console.log('✅ setupFormHandlers Fallback aufgerufen');
};
}
// Global verfügbare Wrapper-Funktionen erstellen
window.refreshJobs = function() {
if (window.jobManager && window.jobManager.loadJobs) {
return window.jobManager.loadJobs();
} else {
console.warn('⚠️ JobManager nicht verfügbar - Seite wird neu geladen');
window.location.reload();
}
};
// Weitere globale Funktionen für Kompatibilität
window.startJob = function(jobId) {
if (window.jobManager && window.jobManager.startJob) {
return window.jobManager.startJob(jobId);
}
};
window.pauseJob = function(jobId) {
if (window.jobManager && window.jobManager.pauseJob) {
return window.jobManager.pauseJob(jobId);
}
};
window.resumeJob = function(jobId) {
if (window.jobManager && window.jobManager.resumeJob) {
return window.jobManager.resumeJob(jobId);
}
};
window.deleteJob = function(jobId) {
if (window.jobManager && window.jobManager.deleteJob) {
return window.jobManager.deleteJob(jobId);
}
};
console.log('✅ Debug Fix Script erfolgreich angewendet');
} catch (error) {
console.error('❌ Debug Fix Fehler:', error);
}
}, 100);
});
// Error Handler für unbehandelte Fehler
window.addEventListener('error', function(e) {
// Bessere Fehler-Serialisierung
const errorInfo = {
message: e.message || 'Unbekannter Fehler',
filename: e.filename || 'Unbekannte Datei',
lineno: e.lineno || 0,
colno: e.colno || 0,
stack: e.error ? e.error.stack : 'Stack nicht verfügbar',
type: e.error ? e.error.constructor.name : 'Unbekannter Typ'
};
console.error('🐛 JavaScript Error abgefangen:', errorInfo);
// Spezifische Fehlerbehebungen
if (e.message.includes('MVP.UI.DarkModeManager is not a constructor')) {
console.log('🔧 DarkModeManager Fehler erkannt - verwende MYP.UI.darkMode');
e.preventDefault();
return false;
}
if (e.message.includes('setupFormHandlers is not a function')) {
console.log('🔧 setupFormHandlers Fehler erkannt - verwende Fallback');
e.preventDefault();
return false;
}
if (e.message.includes('refreshStats is not defined')) {
console.log('🔧 refreshStats Fehler erkannt - lade global-refresh-functions.js');
// Versuche, global-refresh-functions.js zu laden
const script = document.createElement('script');
script.src = '/static/js/global-refresh-functions.js';
script.onload = function() {
console.log('✅ global-refresh-functions.js nachgeladen');
};
document.head.appendChild(script);
e.preventDefault();
return false;
}
if (e.message.includes('Cannot read properties of undefined')) {
console.log('🔧 Undefined Properties Fehler erkannt - ignoriert für Stabilität');
e.preventDefault();
return false;
}
if (e.message.includes('jobManager') || e.message.includes('JobManager')) {
console.log('🔧 JobManager Fehler erkannt - verwende Fallback');
e.preventDefault();
return false;
}
});
// Promise rejection handler
window.addEventListener('unhandledrejection', function(e) {
console.error('🐛 Promise Rejection abgefangen:', e.reason);
if (e.reason && e.reason.message && e.reason.message.includes('Jobs')) {
console.log('🔧 Jobs-bezogener Promise Fehler - ignoriert');
e.preventDefault();
}
});
console.log('✅ Debug Fix Script bereit');
})();

View File

@ -0,0 +1,482 @@
/**
* Mercedes-Benz MYP Platform - Zentrale Event Handler
* CSP-konforme Event-Handler ohne Inline-JavaScript
*/
class GlobalEventManager {
constructor() {
this.init();
}
init() {
// Event-Delegation für bessere Performance und CSP-Konformität
document.addEventListener('click', this.handleClick.bind(this));
document.addEventListener('DOMContentLoaded', this.setupEventListeners.bind(this));
// Falls DOM bereits geladen
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', this.setupEventListeners.bind(this));
} else {
this.setupEventListeners();
}
}
/**
* Zentrale Click-Handler mit Event-Delegation
*/
handleClick(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
const action = target.getAttribute('data-action');
const params = this.parseActionParams(target);
event.preventDefault();
this.executeAction(action, params, target);
}
/**
* Action-Parameter aus data-Attributen parsen
*/
parseActionParams(element) {
const params = {};
// Alle data-action-* Attribute sammeln
for (const attr of element.attributes) {
if (attr.name.startsWith('data-action-')) {
const key = attr.name.replace('data-action-', '');
params[key] = attr.value;
}
}
return params;
}
/**
* Action ausführen basierend auf dem Action-Namen
*/
executeAction(action, params, element) {
console.log(`🎯 Führe Action aus: ${action}`, params);
switch (action) {
// Dashboard Actions
case 'refresh-dashboard':
if (typeof refreshDashboard === 'function') refreshDashboard();
break;
// Navigation Actions
case 'logout':
if (typeof handleLogout === 'function') handleLogout();
break;
case 'go-back':
window.history.back();
break;
case 'reload-page':
window.location.reload();
break;
case 'print-page':
window.print();
break;
// Job Management Actions
case 'refresh-jobs':
if (typeof refreshJobs === 'function') refreshJobs();
break;
case 'toggle-batch-mode':
if (typeof toggleBatchMode === 'function') toggleBatchMode();
break;
case 'start-job':
if (typeof jobManager !== 'undefined' && params.id) {
jobManager.startJob(params.id);
}
break;
case 'pause-job':
if (typeof jobManager !== 'undefined' && params.id) {
jobManager.pauseJob(params.id);
}
break;
case 'resume-job':
if (typeof jobManager !== 'undefined' && params.id) {
jobManager.resumeJob(params.id);
}
break;
case 'delete-job':
if (typeof jobManager !== 'undefined' && params.id) {
jobManager.deleteJob(params.id);
}
break;
case 'open-job-details':
if (typeof jobManager !== 'undefined' && params.id) {
jobManager.openJobDetails(params.id);
}
break;
// Printer Management Actions
case 'refresh-printers':
if (typeof refreshPrinters === 'function') refreshPrinters();
break;
case 'toggle-maintenance-mode':
if (typeof toggleMaintenanceMode === 'function') toggleMaintenanceMode();
break;
case 'open-add-printer-modal':
if (typeof openAddPrinterModal === 'function') openAddPrinterModal();
break;
case 'toggle-auto-refresh':
if (typeof toggleAutoRefresh === 'function') toggleAutoRefresh();
break;
case 'clear-all-filters':
if (typeof clearAllFilters === 'function') clearAllFilters();
break;
case 'test-printer-connection':
if (typeof testPrinterConnection === 'function') testPrinterConnection();
break;
case 'delete-printer':
if (typeof deletePrinter === 'function') deletePrinter();
break;
case 'edit-printer':
if (typeof printerManager !== 'undefined' && params.id) {
printerManager.editPrinter(params.id);
}
break;
case 'connect-printer':
if (typeof printerManager !== 'undefined' && params.id) {
printerManager.connectPrinter(params.id);
}
break;
// Calendar Actions
case 'refresh-calendar':
if (typeof refreshCalendar === 'function') refreshCalendar();
break;
case 'toggle-auto-optimization':
if (typeof toggleAutoOptimization === 'function') toggleAutoOptimization();
break;
case 'export-calendar':
if (typeof exportCalendar === 'function') exportCalendar();
break;
case 'open-create-event-modal':
if (typeof openCreateEventModal === 'function') openCreateEventModal();
break;
// Modal Actions
case 'close-modal':
this.closeModal(params.target || element.closest('.fixed'));
break;
case 'close-printer-modal':
if (typeof closePrinterModal === 'function') closePrinterModal();
break;
case 'close-job-modal':
if (typeof closeJobModal === 'function') closeJobModal();
break;
case 'close-event-modal':
if (typeof closeEventModal === 'function') closeEventModal();
break;
// Form Actions
case 'reset-form':
if (typeof resetForm === 'function') resetForm();
else this.resetNearestForm(element);
break;
case 'clear-file':
if (typeof clearFile === 'function') clearFile();
break;
// Guest Request Actions
case 'check-status':
if (typeof checkStatus === 'function') checkStatus();
break;
case 'copy-code':
if (typeof copyCode === 'function') copyCode();
break;
case 'refresh-status':
if (typeof refreshStatus === 'function') refreshStatus();
break;
case 'show-status-check':
if (typeof showStatusCheck === 'function') showStatusCheck();
break;
// Admin Actions
case 'perform-bulk-action':
if (typeof performBulkAction === 'function' && params.type) {
performBulkAction(params.type);
}
break;
case 'close-bulk-modal':
if (typeof closeBulkModal === 'function') closeBulkModal();
break;
case 'clear-cache':
if (typeof clearCache === 'function') clearCache();
break;
case 'optimize-database':
if (typeof optimizeDatabase === 'function') optimizeDatabase();
break;
case 'create-backup':
if (typeof createBackup === 'function') createBackup();
break;
case 'download-logs':
if (typeof downloadLogs === 'function') downloadLogs();
break;
case 'run-maintenance':
if (typeof runMaintenance === 'function') runMaintenance();
break;
case 'save-settings':
if (typeof saveSettings === 'function') saveSettings();
break;
// Profile Actions
case 'toggle-edit-mode':
if (typeof toggleEditMode === 'function') toggleEditMode();
break;
case 'trigger-avatar-upload':
if (typeof triggerAvatarUpload === 'function') triggerAvatarUpload();
break;
case 'cancel-edit':
if (typeof cancelEdit === 'function') cancelEdit();
break;
case 'download-user-data':
if (typeof downloadUserData === 'function') downloadUserData();
break;
// Stats Actions
case 'refresh-stats':
if (typeof refreshStats === 'function') refreshStats();
break;
case 'export-stats':
if (typeof exportStats === 'function') exportStats();
break;
// Generic Actions
case 'remove-element':
const targetElement = params.target ?
document.querySelector(params.target) :
element.closest(params.selector || '.removable');
if (targetElement) {
targetElement.remove();
}
break;
case 'toggle-element':
const toggleTarget = params.target ?
document.querySelector(params.target) :
element.nextElementSibling;
if (toggleTarget) {
toggleTarget.classList.toggle('hidden');
}
break;
case 'show-element':
const showTarget = document.querySelector(params.target);
if (showTarget) {
showTarget.classList.remove('hidden');
}
break;
case 'hide-element':
const hideTarget = document.querySelector(params.target);
if (hideTarget) {
hideTarget.classList.add('hidden');
}
break;
default:
console.warn(`⚠️ Unbekannte Action: ${action}`);
// Versuche globale Funktion zu finden
if (typeof window[action] === 'function') {
window[action](params);
}
break;
}
}
/**
* Generische Modal-Schließfunktion
*/
closeModal(modalElement) {
if (modalElement) {
modalElement.classList.add('hidden');
modalElement.remove();
}
}
/**
* Nächstes Formular zurücksetzen
*/
resetNearestForm(element) {
const form = element.closest('form');
if (form) {
form.reset();
}
}
/**
* Spezifische Event-Listener einrichten
*/
setupEventListeners() {
// Auto-Refresh für bestimmte Seiten
this.setupAutoRefresh();
// Keyboard Shortcuts
this.setupKeyboardShortcuts();
// Form Validierung
this.setupFormValidation();
console.log('🔧 Globale Event-Handler initialisiert');
}
/**
* Auto-Refresh für Dashboard/Jobs einrichten
*/
setupAutoRefresh() {
const currentPath = window.location.pathname;
// Auto-refresh für Dashboard alle 30 Sekunden
if (currentPath.includes('/dashboard')) {
setInterval(() => {
if (typeof refreshDashboard === 'function') {
refreshDashboard();
}
}, 30000);
}
// Auto-refresh für Jobs alle 15 Sekunden
if (currentPath.includes('/jobs')) {
setInterval(() => {
if (typeof refreshJobs === 'function') {
refreshJobs();
}
}, 15000);
}
}
/**
* Keyboard Shortcuts einrichten
*/
setupKeyboardShortcuts() {
document.addEventListener('keydown', (event) => {
// ESC zum Schließen von Modals
if (event.key === 'Escape') {
const openModal = document.querySelector('.fixed:not(.hidden)');
if (openModal) {
this.closeModal(openModal);
}
}
// Ctrl+R für Refresh (überschreiben für spezifische Funktionen)
if (event.ctrlKey && event.key === 'r') {
event.preventDefault();
const currentPath = window.location.pathname;
if (currentPath.includes('/dashboard') && typeof refreshDashboard === 'function') {
refreshDashboard();
} else if (currentPath.includes('/jobs') && typeof refreshJobs === 'function') {
refreshJobs();
} else if (currentPath.includes('/printers') && typeof refreshPrinters === 'function') {
refreshPrinters();
} else {
window.location.reload();
}
}
});
}
/**
* Form-Validierung einrichten
*/
setupFormValidation() {
// Alle Formulare finden und Validierung hinzufügen
const forms = document.querySelectorAll('form[data-validate]');
forms.forEach(form => {
form.addEventListener('submit', this.validateForm.bind(this));
});
}
/**
* Formular validieren
*/
validateForm(event) {
const form = event.target;
const requiredFields = form.querySelectorAll('[required]');
let isValid = true;
requiredFields.forEach(field => {
if (!field.value.trim()) {
isValid = false;
field.classList.add('border-red-500');
// Fehlermeldung anzeigen
const errorId = `${field.id}-error`;
let errorElement = document.getElementById(errorId);
if (!errorElement) {
errorElement = document.createElement('div');
errorElement.id = errorId;
errorElement.className = 'text-red-500 text-sm mt-1';
field.parentNode.appendChild(errorElement);
}
errorElement.textContent = `${field.getAttribute('data-label') || 'Dieses Feld'} ist erforderlich.`;
} else {
field.classList.remove('border-red-500');
const errorElement = document.getElementById(`${field.id}-error`);
if (errorElement) {
errorElement.remove();
}
}
});
if (!isValid) {
event.preventDefault();
}
return isValid;
}
}
// Globale Instanz erstellen
const globalEventManager = new GlobalEventManager();
// Export für Module
if (typeof module !== 'undefined' && module.exports) {
module.exports = GlobalEventManager;
}
console.log('🌍 Globaler Event Manager geladen');

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

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -0,0 +1,483 @@
/**
* MYP Platform - Globale Refresh-Funktionen
* Sammelt alle Refresh-Funktionen für verschiedene Seiten und Komponenten
*/
/**
* Dashboard-Refresh-Funktion
*/
window.refreshDashboard = async function() {
const refreshButton = document.getElementById('refreshDashboard');
if (refreshButton) {
// Button-State ändern
refreshButton.disabled = true;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.add('animate-spin');
}
}
try {
// Dashboard-Statistiken aktualisieren
const response = await fetch('/api/dashboard/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
}
});
const data = await response.json();
if (data.success) {
// Statistiken im DOM aktualisieren
updateDashboardStats(data.stats);
// Benachrichtigung anzeigen
showToast('✅ Dashboard erfolgreich aktualisiert', 'success');
// Seite neu laden für vollständige Aktualisierung
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showToast('❌ Fehler beim Aktualisieren des Dashboards', 'error');
}
} catch (error) {
console.error('Dashboard-Refresh Fehler:', error);
showToast('❌ Netzwerkfehler beim Dashboard-Refresh', 'error');
} finally {
// Button-State zurücksetzen
if (refreshButton) {
refreshButton.disabled = false;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.remove('animate-spin');
}
}
}
};
/**
* Statistiken-Refresh-Funktion
*/
window.refreshStats = async function() {
const refreshButton = document.querySelector('button[onclick="refreshStats()"]');
if (refreshButton) {
refreshButton.disabled = true;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.add('animate-spin');
}
}
try {
// Basis-Statistiken laden
if (typeof loadBasicStats === 'function') {
await loadBasicStats();
} else {
// Fallback: API-Aufruf für Statistiken
const response = await fetch('/api/stats');
const data = await response.json();
if (response.ok) {
// Statistiken im DOM aktualisieren
updateStatsCounter('total-jobs-count', data.total_jobs);
updateStatsCounter('completed-jobs-count', data.completed_jobs);
updateStatsCounter('online-printers-count', data.online_printers);
updateStatsCounter('success-rate-percent', data.success_rate + '%');
updateStatsCounter('active-jobs-count', data.active_jobs);
updateStatsCounter('failed-jobs-count', data.failed_jobs);
updateStatsCounter('total-users-count', data.total_users);
} else {
throw new Error(data.error || 'Fehler beim Laden der Statistiken');
}
}
// Charts aktualisieren
if (window.refreshAllCharts) {
window.refreshAllCharts();
}
showToast('✅ Statistiken erfolgreich aktualisiert', 'success');
} catch (error) {
console.error('Stats-Refresh Fehler:', error);
showToast('❌ Fehler beim Aktualisieren der Statistiken', 'error');
} finally {
if (refreshButton) {
refreshButton.disabled = false;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.remove('animate-spin');
}
}
}
};
/**
* Jobs-Refresh-Funktion
*/
window.refreshJobs = async function() {
const refreshButton = document.getElementById('refresh-button');
if (refreshButton) {
refreshButton.disabled = true;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.add('animate-spin');
}
}
try {
// Jobs-Daten neu laden
if (typeof jobManager !== 'undefined' && jobManager.loadJobs) {
await jobManager.loadJobs();
} else {
// Fallback: Seite neu laden
window.location.reload();
}
showToast('✅ Druckaufträge erfolgreich aktualisiert', 'success');
} catch (error) {
console.error('Jobs-Refresh Fehler:', error);
showToast('❌ Fehler beim Aktualisieren der Jobs', 'error');
} finally {
if (refreshButton) {
refreshButton.disabled = false;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.remove('animate-spin');
}
}
}
};
/**
* Calendar-Refresh-Funktion
*/
window.refreshCalendar = async function() {
const refreshButton = document.getElementById('refresh-button');
if (refreshButton) {
refreshButton.disabled = true;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.add('animate-spin');
}
}
try {
// FullCalendar neu laden falls verfügbar
if (typeof calendar !== 'undefined' && calendar.refetchEvents) {
calendar.refetchEvents();
showToast('✅ Kalender erfolgreich aktualisiert', 'success');
} else {
// Fallback: Seite neu laden
window.location.reload();
}
} catch (error) {
console.error('Calendar-Refresh Fehler:', error);
showToast('❌ Fehler beim Aktualisieren des Kalenders', 'error');
} finally {
if (refreshButton) {
refreshButton.disabled = false;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.remove('animate-spin');
}
}
}
};
/**
* Drucker-Refresh-Funktion
*/
window.refreshPrinters = async function() {
const refreshButton = document.getElementById('refresh-button');
if (refreshButton) {
refreshButton.disabled = true;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.add('animate-spin');
}
}
try {
// Drucker-Manager verwenden falls verfügbar
if (typeof printerManager !== 'undefined' && printerManager.loadPrinters) {
await printerManager.loadPrinters();
} else {
// Fallback: API-Aufruf für Drucker-Update
const response = await fetch('/api/printers/status/live', {
headers: {
'X-CSRFToken': getCSRFToken()
}
});
if (response.ok) {
// Seite neu laden für vollständige Aktualisierung
window.location.reload();
} else {
throw new Error('Drucker-Status konnte nicht abgerufen werden');
}
}
showToast('✅ Drucker erfolgreich aktualisiert', 'success');
} catch (error) {
console.error('Printer-Refresh Fehler:', error);
showToast('❌ Fehler beim Aktualisieren der Drucker', 'error');
} finally {
if (refreshButton) {
refreshButton.disabled = false;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.remove('animate-spin');
}
}
}
};
/**
* Dashboard-Statistiken im DOM aktualisieren
*/
function updateDashboardStats(stats) {
// Aktive Jobs
const activeJobsEl = document.querySelector('[data-stat="active-jobs"]');
if (activeJobsEl) {
activeJobsEl.textContent = stats.active_jobs || 0;
}
// Verfügbare Drucker
const availablePrintersEl = document.querySelector('[data-stat="available-printers"]');
if (availablePrintersEl) {
availablePrintersEl.textContent = stats.available_printers || 0;
}
// Gesamte Jobs
const totalJobsEl = document.querySelector('[data-stat="total-jobs"]');
if (totalJobsEl) {
totalJobsEl.textContent = stats.total_jobs || 0;
}
// Erfolgsrate
const successRateEl = document.querySelector('[data-stat="success-rate"]');
if (successRateEl) {
successRateEl.textContent = (stats.success_rate || 0) + '%';
}
console.log('📊 Dashboard-Statistiken aktualisiert:', stats);
}
/**
* Statistiken-Counter im DOM aktualisieren
*/
function updateStatsCounter(elementId, value, animate = true) {
const element = document.getElementById(elementId);
if (!element) return;
if (animate) {
// Animierte Zählung
const currentValue = parseInt(element.textContent.replace(/[^\d]/g, '')) || 0;
const targetValue = parseInt(value.toString().replace(/[^\d]/g, '')) || 0;
if (currentValue !== targetValue) {
animateCounter(element, currentValue, targetValue, value.toString());
}
} else {
element.textContent = value;
}
}
/**
* Animierte Counter-Funktion
*/
function animateCounter(element, start, end, finalText) {
const duration = 1000; // 1 Sekunde
const startTime = performance.now();
function updateCounter(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing-Funktion (ease-out)
const easeOut = 1 - Math.pow(1 - progress, 3);
const currentValue = Math.round(start + (end - start) * easeOut);
if (finalText.includes('%')) {
element.textContent = currentValue + '%';
} else {
element.textContent = currentValue;
}
if (progress < 1) {
requestAnimationFrame(updateCounter);
} else {
element.textContent = finalText;
}
}
requestAnimationFrame(updateCounter);
}
/**
* CSRF-Token abrufen
*/
function getCSRFToken() {
const token = document.querySelector('meta[name="csrf-token"]');
return token ? token.getAttribute('content') : '';
}
/**
* Toast-Benachrichtigung anzeigen
*/
function showToast(message, type = 'info') {
// Prüfen ob der OptimizationManager verfügbar ist und dessen Toast-Funktion verwenden
if (typeof optimizationManager !== 'undefined' && optimizationManager.showToast) {
optimizationManager.showToast(message, type);
return;
}
// Fallback: Einfache Console-Ausgabe
const emoji = {
success: '✅',
error: '❌',
warning: '⚠️',
info: ''
};
console.log(`${emoji[type] || ''} ${message}`);
// Versuche eine Alert-Benachrichtigung zu erstellen
try {
const toast = document.createElement('div');
toast.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transition-all duration-300 transform translate-x-full`;
const colors = {
success: 'bg-green-500 text-white',
error: 'bg-red-500 text-white',
warning: 'bg-yellow-500 text-black',
info: 'bg-blue-500 text-white'
};
toast.className += ` ${colors[type]}`;
toast.textContent = message;
document.body.appendChild(toast);
// Animation einblenden
setTimeout(() => {
toast.classList.remove('translate-x-full');
}, 100);
// Nach 3 Sekunden automatisch entfernen
setTimeout(() => {
toast.classList.add('translate-x-full');
setTimeout(() => {
toast.remove();
}, 300);
}, 3000);
} catch (error) {
console.warn('Toast-Erstellung fehlgeschlagen:', error);
}
}
/**
* Universelle Refresh-Funktion basierend auf aktueller Seite
*/
window.universalRefresh = function() {
const currentPath = window.location.pathname;
if (currentPath.includes('/dashboard')) {
refreshDashboard();
} else if (currentPath.includes('/jobs')) {
refreshJobs();
} else if (currentPath.includes('/calendar') || currentPath.includes('/schichtplan')) {
refreshCalendar();
} else if (currentPath.includes('/printers') || currentPath.includes('/drucker')) {
refreshPrinters();
} else {
// Fallback: Seite neu laden
window.location.reload();
}
};
/**
* Auto-Refresh-Funktionalität
*/
class AutoRefreshManager {
constructor() {
this.isEnabled = false;
this.interval = null;
this.intervalTime = 30000; // 30 Sekunden
}
start() {
if (this.isEnabled) return;
this.isEnabled = true;
this.interval = setInterval(() => {
// Nur refresh wenn Seite sichtbar ist
if (!document.hidden) {
universalRefresh();
}
}, this.intervalTime);
console.log('🔄 Auto-Refresh aktiviert (alle 30 Sekunden)');
}
stop() {
if (!this.isEnabled) return;
this.isEnabled = false;
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
console.log('⏸️ Auto-Refresh deaktiviert');
}
toggle() {
if (this.isEnabled) {
this.stop();
} else {
this.start();
}
}
}
// Globaler Auto-Refresh-Manager
window.autoRefreshManager = new AutoRefreshManager();
/**
* Keyboard-Shortcuts
*/
document.addEventListener('keydown', function(e) {
// F5 oder Ctrl+R abfangen und eigene Refresh-Funktion verwenden
if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {
e.preventDefault();
universalRefresh();
}
// Ctrl+Shift+R für Auto-Refresh-Toggle
if (e.ctrlKey && e.shiftKey && e.key === 'R') {
e.preventDefault();
autoRefreshManager.toggle();
showToast(
autoRefreshManager.isEnabled ?
'🔄 Auto-Refresh aktiviert' :
'⏸️ Auto-Refresh deaktiviert',
'info'
);
}
});
/**
* Visibility API für Auto-Refresh bei Tab-Wechsel
*/
document.addEventListener('visibilitychange', function() {
if (!document.hidden && autoRefreshManager.isEnabled) {
// Verzögertes Refresh wenn Tab wieder sichtbar wird
setTimeout(universalRefresh, 1000);
}
});
console.log('🔄 Globale Refresh-Funktionen geladen');

View File

@ -0,0 +1,736 @@
/**
* MYP Platform Job Manager
* Verwaltung und Steuerung von 3D-Druckaufträgen
* Version: 1.0.0
*/
(function() {
'use strict';
/**
* Job Manager Klasse für 3D-Druckaufträge
*/
class JobManager {
constructor() {
this.jobs = [];
this.currentPage = 1;
this.totalPages = 1;
this.isLoading = false;
this.refreshInterval = null;
this.autoRefreshEnabled = false;
console.log('🔧 JobManager wird initialisiert...');
}
/**
* JobManager initialisieren
*/
async init() {
try {
console.log('🚀 JobManager-Initialisierung gestartet');
// Event-Listener einrichten
this.setupEventListeners();
// Formular-Handler einrichten
this.setupFormHandlers();
// Anfängliche Jobs laden
await this.loadJobs();
// Auto-Refresh starten falls aktiviert
if (this.autoRefreshEnabled) {
this.startAutoRefresh();
}
console.log('✅ JobManager erfolgreich initialisiert');
} catch (error) {
console.error('❌ Fehler bei JobManager-Initialisierung:', error);
this.showToast('Fehler beim Initialisieren des Job-Managers', 'error');
}
}
/**
* Event-Listener für Job-Aktionen einrichten
*/
setupEventListeners() {
console.log('📡 Event-Listener werden eingerichtet...');
// Job-Aktionen über data-Attribute
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-job-action]');
if (!target) return;
const action = target.getAttribute('data-job-action');
const jobId = target.getAttribute('data-job-id');
if (!jobId) {
console.warn('⚠️ Job-ID fehlt für Aktion:', action);
return;
}
switch (action) {
case 'start':
this.startJob(jobId);
break;
case 'pause':
this.pauseJob(jobId);
break;
case 'resume':
this.resumeJob(jobId);
break;
case 'stop':
this.stopJob(jobId);
break;
case 'delete':
this.deleteJob(jobId);
break;
case 'details':
this.openJobDetails(jobId);
break;
default:
console.warn('⚠️ Unbekannte Job-Aktion:', action);
}
});
// Refresh-Button
const refreshBtn = document.getElementById('refresh-jobs');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.loadJobs());
}
// Auto-Refresh Toggle
const autoRefreshToggle = document.getElementById('auto-refresh-toggle');
if (autoRefreshToggle) {
autoRefreshToggle.addEventListener('change', (e) => {
this.autoRefreshEnabled = e.target.checked;
if (this.autoRefreshEnabled) {
this.startAutoRefresh();
} else {
this.stopAutoRefresh();
}
});
}
console.log('✅ Event-Listener erfolgreich eingerichtet');
}
/**
* Formular-Handler für Job-Erstellung einrichten
*/
setupFormHandlers() {
console.log('📝 Formular-Handler werden eingerichtet...');
const newJobForm = document.getElementById('new-job-form');
if (newJobForm) {
newJobForm.addEventListener('submit', async (e) => {
e.preventDefault();
await this.createNewJob(new FormData(newJobForm));
});
}
const editJobForm = document.getElementById('edit-job-form');
if (editJobForm) {
editJobForm.addEventListener('submit', async (e) => {
e.preventDefault();
const jobId = editJobForm.getAttribute('data-job-id');
await this.updateJob(jobId, new FormData(editJobForm));
});
}
console.log('✅ Formular-Handler erfolgreich eingerichtet');
}
/**
* Jobs von Server laden
*/
async loadJobs(page = 1) {
if (this.isLoading) {
console.log('⚠️ Jobs werden bereits geladen...');
return;
}
this.isLoading = true;
this.showLoadingState(true);
try {
console.log(`📥 Lade Jobs (Seite ${page})...`);
const response = await fetch(`/api/jobs?page=${page}`, {
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
this.jobs = data.jobs || [];
this.currentPage = data.current_page || 1;
this.totalPages = data.total_pages || 1;
this.renderJobs();
this.updatePagination();
console.log(`${this.jobs.length} Jobs erfolgreich geladen`);
} catch (error) {
console.error('❌ Fehler beim Laden der Jobs:', error);
this.showToast('Fehler beim Laden der Jobs', 'error');
// Fallback: Leere Jobs-Liste anzeigen
this.jobs = [];
this.renderJobs();
} finally {
this.isLoading = false;
this.showLoadingState(false);
}
}
/**
* Job starten
*/
async startJob(jobId) {
try {
console.log(`▶️ Starte Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}/start`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich gestartet', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Starten des Jobs:', error);
this.showToast('Fehler beim Starten des Jobs', 'error');
}
}
/**
* Job pausieren
*/
async pauseJob(jobId) {
try {
console.log(`⏸️ Pausiere Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}/pause`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich pausiert', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Pausieren des Jobs:', error);
this.showToast('Fehler beim Pausieren des Jobs', 'error');
}
}
/**
* Job fortsetzen
*/
async resumeJob(jobId) {
try {
console.log(`▶️ Setze Job ${jobId} fort...`);
const response = await fetch(`/api/jobs/${jobId}/resume`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich fortgesetzt', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Fortsetzen des Jobs:', error);
this.showToast('Fehler beim Fortsetzen des Jobs', 'error');
}
}
/**
* Job stoppen
*/
async stopJob(jobId) {
if (!confirm('Möchten Sie diesen Job wirklich stoppen?')) {
return;
}
try {
console.log(`⏹️ Stoppe Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}/stop`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich gestoppt', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Stoppen des Jobs:', error);
this.showToast('Fehler beim Stoppen des Jobs', 'error');
}
}
/**
* Job löschen
*/
async deleteJob(jobId) {
if (!confirm('Möchten Sie diesen Job wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) {
return;
}
try {
console.log(`🗑️ Lösche Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich gelöscht', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Löschen des Jobs:', error);
this.showToast('Fehler beim Löschen des Jobs', 'error');
}
}
/**
* Job-Details öffnen
*/
openJobDetails(jobId) {
console.log(`📄 Öffne Details für Job ${jobId}...`);
// Modal für Job-Details öffnen oder zu Detail-Seite navigieren
const detailsUrl = `/jobs/${jobId}`;
// Prüfen ob Modal verfügbar ist
const detailsModal = document.getElementById(`job-details-${jobId}`);
if (detailsModal && typeof window.MYP !== 'undefined' && window.MYP.UI && window.MYP.UI.modal) {
window.MYP.UI.modal.open(`job-details-${jobId}`);
} else {
// Fallback: Zur Detail-Seite navigieren
window.location.href = detailsUrl;
}
}
/**
* Jobs in der UI rendern
*/
renderJobs() {
const jobsList = document.getElementById('jobs-list');
if (!jobsList) {
console.warn('⚠️ Jobs-Liste Element nicht gefunden');
return;
}
if (this.jobs.length === 0) {
jobsList.innerHTML = `
<div class="text-center py-12">
<div class="text-gray-400 dark:text-gray-600 text-6xl mb-4">📭</div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Keine Jobs vorhanden</h3>
<p class="text-gray-500 dark:text-gray-400">Es wurden noch keine Druckaufträge erstellt.</p>
</div>
`;
return;
}
const jobsHTML = this.jobs.map(job => this.renderJobCard(job)).join('');
jobsList.innerHTML = jobsHTML;
console.log(`📋 ${this.jobs.length} Jobs gerendert`);
}
/**
* Einzelne Job-Karte rendern
*/
renderJobCard(job) {
const statusClass = this.getJobStatusClass(job.status);
const statusText = this.getJobStatusText(job.status);
return `
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-start justify-between">
<div class="flex-1">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">${job.name || 'Unbenannter Job'}</h3>
<div class="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400 mb-4">
<span>ID: ${job.id}</span>
<span>•</span>
<span>Drucker: ${job.printer_name || 'Unbekannt'}</span>
<span>•</span>
<span>Erstellt: ${new Date(job.created_at).toLocaleDateString('de-DE')}</span>
</div>
<div class="flex items-center space-x-2 mb-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusClass}">
${statusText}
</span>
${job.progress ? `<span class="text-sm text-gray-500 dark:text-gray-400">${job.progress}%</span>` : ''}
</div>
</div>
<div class="flex flex-col space-y-2 ml-4">
${this.renderJobActions(job)}
</div>
</div>
</div>
`;
}
/**
* Job-Aktionen rendern
*/
renderJobActions(job) {
const actions = [];
// Details-Button immer verfügbar
actions.push(`
<button data-job-action="details" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Details
</button>
`);
// Status-abhängige Aktionen
switch (job.status) {
case 'pending':
case 'ready':
actions.push(`
<button data-job-action="start" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
Starten
</button>
`);
break;
case 'running':
case 'printing':
actions.push(`
<button data-job-action="pause" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500">
Pausieren
</button>
`);
actions.push(`
<button data-job-action="stop" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Stoppen
</button>
`);
break;
case 'paused':
actions.push(`
<button data-job-action="resume" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
Fortsetzen
</button>
`);
actions.push(`
<button data-job-action="stop" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Stoppen
</button>
`);
break;
case 'completed':
case 'failed':
case 'cancelled':
actions.push(`
<button data-job-action="delete" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Löschen
</button>
`);
break;
}
return actions.join('');
}
/**
* CSS-Klasse für Job-Status
*/
getJobStatusClass(status) {
const statusClasses = {
'pending': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
'ready': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
'running': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
'printing': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
'paused': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
'completed': 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300',
'failed': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
'cancelled': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
};
return statusClasses[status] || statusClasses['pending'];
}
/**
* Anzeigetext für Job-Status
*/
getJobStatusText(status) {
const statusTexts = {
'pending': 'Wartend',
'ready': 'Bereit',
'running': 'Läuft',
'printing': 'Druckt',
'paused': 'Pausiert',
'completed': 'Abgeschlossen',
'failed': 'Fehlgeschlagen',
'cancelled': 'Abgebrochen'
};
return statusTexts[status] || 'Unbekannt';
}
/**
* Loading-Zustand anzeigen/verstecken
*/
showLoadingState(show) {
const loadingEl = document.getElementById('jobs-loading');
const jobsList = document.getElementById('jobs-list');
if (loadingEl) {
loadingEl.style.display = show ? 'block' : 'none';
}
if (jobsList) {
jobsList.style.opacity = show ? '0.5' : '1';
jobsList.style.pointerEvents = show ? 'none' : 'auto';
}
}
/**
* CSRF-Token abrufen
*/
getCSRFToken() {
const token = document.querySelector('meta[name="csrf-token"]');
return token ? token.getAttribute('content') : '';
}
/**
* Toast-Nachricht anzeigen
*/
showToast(message, type = 'info') {
if (typeof window.showToast === 'function') {
window.showToast(message, type);
} else {
console.log(`${type.toUpperCase()}: ${message}`);
}
}
/**
* Auto-Refresh starten
*/
startAutoRefresh() {
this.stopAutoRefresh(); // Vorherigen Refresh stoppen
this.refreshInterval = setInterval(() => {
if (!this.isLoading) {
this.loadJobs(this.currentPage);
}
}, 30000); // Alle 30 Sekunden
console.log('🔄 Auto-Refresh gestartet (30s Intervall)');
}
/**
* Auto-Refresh stoppen
*/
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
console.log('⏹️ Auto-Refresh gestoppt');
}
}
/**
* Paginierung aktualisieren
*/
updatePagination() {
const paginationEl = document.getElementById('jobs-pagination');
if (!paginationEl) return;
if (this.totalPages <= 1) {
paginationEl.style.display = 'none';
return;
}
paginationEl.style.display = 'flex';
let paginationHTML = '';
// Vorherige Seite
if (this.currentPage > 1) {
paginationHTML += `
<button onclick="jobManager.loadJobs(${this.currentPage - 1})"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">
Zurück
</button>
`;
}
// Seitenzahlen
for (let i = 1; i <= this.totalPages; i++) {
const isActive = i === this.currentPage;
paginationHTML += `
<button onclick="jobManager.loadJobs(${i})"
class="px-3 py-2 text-sm font-medium ${isActive
? 'text-blue-600 bg-blue-50 border-blue-300 dark:bg-blue-900 dark:text-blue-300'
: 'text-gray-500 bg-white border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700'
} border">
${i}
</button>
`;
}
// Nächste Seite
if (this.currentPage < this.totalPages) {
paginationHTML += `
<button onclick="jobManager.loadJobs(${this.currentPage + 1})"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">
Weiter
</button>
`;
}
paginationEl.innerHTML = paginationHTML;
}
/**
* Neuen Job erstellen
*/
async createNewJob(formData) {
try {
console.log('📝 Erstelle neuen Job...');
const response = await fetch('/api/jobs', {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken()
},
body: formData
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
this.showToast('Job erfolgreich erstellt', 'success');
// Jobs neu laden
await this.loadJobs();
// Formular zurücksetzen
const form = document.getElementById('new-job-form');
if (form) {
form.reset();
}
return result;
} catch (error) {
console.error('❌ Fehler beim Erstellen des Jobs:', error);
this.showToast('Fehler beim Erstellen des Jobs', 'error');
throw error;
}
}
/**
* Job aktualisieren
*/
async updateJob(jobId, formData) {
try {
console.log(`📝 Aktualisiere Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}`, {
method: 'PUT',
headers: {
'X-CSRFToken': this.getCSRFToken()
},
body: formData
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
this.showToast('Job erfolgreich aktualisiert', 'success');
// Jobs neu laden
await this.loadJobs();
return result;
} catch (error) {
console.error('❌ Fehler beim Aktualisieren des Jobs:', error);
this.showToast('Fehler beim Aktualisieren des Jobs', 'error');
throw error;
}
}
}
// JobManager global verfügbar machen
window.JobManager = JobManager;
// JobManager-Instanz erstellen wenn DOM bereit ist
document.addEventListener('DOMContentLoaded', function() {
if (typeof window.jobManager === 'undefined') {
window.jobManager = new JobManager();
// Nur initialisieren wenn wir uns auf einer Jobs-Seite befinden
if (document.getElementById('jobs-list') || document.querySelector('[data-job-action]')) {
window.jobManager.init();
}
}
});
console.log('✅ JobManager-Modul geladen');
})();

View File

@ -0,0 +1,315 @@
/**
* Benachrichtigungssystem für die MYP 3D-Druck Platform
* Verwaltet die Anzeige und Interaktion mit Benachrichtigungen
*/
class NotificationManager {
constructor() {
this.notificationToggle = document.getElementById('notificationToggle');
this.notificationDropdown = document.getElementById('notificationDropdown');
this.notificationBadge = document.getElementById('notificationBadge');
this.notificationList = document.getElementById('notificationList');
this.markAllReadBtn = document.getElementById('markAllRead');
this.isOpen = false;
this.notifications = [];
// CSRF-Token aus Meta-Tag holen
this.csrfToken = this.getCSRFToken();
this.init();
}
/**
* Holt das CSRF-Token aus dem Meta-Tag
* @returns {string} Das CSRF-Token
*/
getCSRFToken() {
const metaTag = document.querySelector('meta[name="csrf-token"]');
return metaTag ? metaTag.getAttribute('content') : '';
}
/**
* Erstellt die Standard-Headers für API-Anfragen mit CSRF-Token
* @returns {Object} Headers-Objekt
*/
getAPIHeaders() {
const headers = {
'Content-Type': 'application/json',
};
if (this.csrfToken) {
headers['X-CSRFToken'] = this.csrfToken;
}
return headers;
}
init() {
if (!this.notificationToggle) return;
// Event Listeners
this.notificationToggle.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleDropdown();
});
if (this.markAllReadBtn) {
this.markAllReadBtn.addEventListener('click', () => {
this.markAllAsRead();
});
}
// Dropdown schließen bei Klick außerhalb
document.addEventListener('click', (e) => {
if (!this.notificationDropdown.contains(e.target) && !this.notificationToggle.contains(e.target)) {
this.closeDropdown();
}
});
// Benachrichtigungen laden
this.loadNotifications();
// Regelmäßige Updates
setInterval(() => {
this.loadNotifications();
}, 30000); // Alle 30 Sekunden
}
toggleDropdown() {
if (this.isOpen) {
this.closeDropdown();
} else {
this.openDropdown();
}
}
openDropdown() {
this.notificationDropdown.classList.remove('hidden');
this.notificationToggle.setAttribute('aria-expanded', 'true');
this.isOpen = true;
// Animation
this.notificationDropdown.style.opacity = '0';
this.notificationDropdown.style.transform = 'translateY(-10px)';
requestAnimationFrame(() => {
this.notificationDropdown.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
this.notificationDropdown.style.opacity = '1';
this.notificationDropdown.style.transform = 'translateY(0)';
});
}
closeDropdown() {
this.notificationDropdown.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
this.notificationDropdown.style.opacity = '0';
this.notificationDropdown.style.transform = 'translateY(-10px)';
setTimeout(() => {
this.notificationDropdown.classList.add('hidden');
this.notificationToggle.setAttribute('aria-expanded', 'false');
this.isOpen = false;
}, 200);
}
async loadNotifications() {
try {
const response = await fetch('/api/notifications');
if (response.ok) {
const data = await response.json();
this.notifications = data.notifications || [];
this.updateUI();
}
} catch (error) {
console.error('Fehler beim Laden der Benachrichtigungen:', error);
}
}
updateUI() {
this.updateBadge();
this.updateNotificationList();
}
updateBadge() {
const unreadCount = this.notifications.filter(n => !n.read).length;
if (unreadCount > 0) {
this.notificationBadge.textContent = unreadCount > 99 ? '99+' : unreadCount.toString();
this.notificationBadge.classList.remove('hidden');
} else {
this.notificationBadge.classList.add('hidden');
}
}
updateNotificationList() {
if (this.notifications.length === 0) {
this.notificationList.innerHTML = `
<div class="p-4 text-center text-slate-500 dark:text-slate-400">
Keine neuen Benachrichtigungen
</div>
`;
return;
}
const notificationHTML = this.notifications.map(notification => {
const isUnread = !notification.read;
const timeAgo = this.formatTimeAgo(new Date(notification.created_at));
return `
<div class="notification-item p-4 border-b border-slate-200 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors ${isUnread ? 'bg-blue-50 dark:bg-blue-900/20' : ''}"
data-notification-id="${notification.id}">
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
${this.getNotificationIcon(notification.type)}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-slate-900 dark:text-white">
${this.getNotificationTitle(notification.type)}
</p>
${isUnread ? '<div class="w-2 h-2 bg-blue-500 rounded-full"></div>' : ''}
</div>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
${this.getNotificationMessage(notification)}
</p>
<p class="text-xs text-slate-500 dark:text-slate-500 mt-2">
${timeAgo}
</p>
</div>
</div>
</div>
`;
}).join('');
this.notificationList.innerHTML = notificationHTML;
// Event Listeners für Benachrichtigungen
this.notificationList.querySelectorAll('.notification-item').forEach(item => {
item.addEventListener('click', (e) => {
const notificationId = item.dataset.notificationId;
this.markAsRead(notificationId);
});
});
}
getNotificationIcon(type) {
const icons = {
'guest_request': `
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
`,
'job_completed': `
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
`,
'system': `
<div class="w-8 h-8 bg-gray-100 dark:bg-gray-900 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
`
};
return icons[type] || icons['system'];
}
getNotificationTitle(type) {
const titles = {
'guest_request': 'Neue Gastanfrage',
'job_completed': 'Druckauftrag abgeschlossen',
'system': 'System-Benachrichtigung'
};
return titles[type] || 'Benachrichtigung';
}
getNotificationMessage(notification) {
try {
const payload = JSON.parse(notification.payload || '{}');
switch (notification.type) {
case 'guest_request':
return `${payload.guest_name || 'Ein Gast'} hat eine neue Druckanfrage gestellt.`;
case 'job_completed':
return `Der Druckauftrag "${payload.job_name || 'Unbekannt'}" wurde erfolgreich abgeschlossen.`;
default:
return payload.message || 'Neue Benachrichtigung erhalten.';
}
} catch (error) {
return 'Neue Benachrichtigung erhalten.';
}
}
formatTimeAgo(date) {
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) {
return 'Gerade eben';
} else if (diffInSeconds < 3600) {
const minutes = Math.floor(diffInSeconds / 60);
return `vor ${minutes} Minute${minutes !== 1 ? 'n' : ''}`;
} else if (diffInSeconds < 86400) {
const hours = Math.floor(diffInSeconds / 3600);
return `vor ${hours} Stunde${hours !== 1 ? 'n' : ''}`;
} else {
const days = Math.floor(diffInSeconds / 86400);
return `vor ${days} Tag${days !== 1 ? 'en' : ''}`;
}
}
async markAsRead(notificationId) {
try {
const response = await fetch(`/api/notifications/${notificationId}/read`, {
method: 'POST',
headers: this.getAPIHeaders()
});
if (response.ok) {
// Benachrichtigung als gelesen markieren
const notification = this.notifications.find(n => n.id == notificationId);
if (notification) {
notification.read = true;
this.updateUI();
}
} else {
console.error('Fehler beim Markieren als gelesen:', response.status, response.statusText);
}
} catch (error) {
console.error('Fehler beim Markieren als gelesen:', error);
}
}
async markAllAsRead() {
try {
const response = await fetch('/api/notifications/mark-all-read', {
method: 'POST',
headers: this.getAPIHeaders()
});
if (response.ok) {
// Alle Benachrichtigungen als gelesen markieren
this.notifications.forEach(notification => {
notification.read = true;
});
this.updateUI();
} else {
console.error('Fehler beim Markieren aller als gelesen:', response.status, response.statusText);
}
} catch (error) {
console.error('Fehler beim Markieren aller als gelesen:', error);
}
}
}
// Initialisierung nach DOM-Load
document.addEventListener('DOMContentLoaded', () => {
new NotificationManager();
});

View File

@ -0,0 +1,521 @@
/**
* MYP Platform Offline-First Application
* Version: 2.0.0
* Description: Offline-fähige PWA für die MYP Plattform
*/
class MYPApp {
constructor() {
this.isOffline = !navigator.onLine;
this.registerTime = new Date().toISOString();
this.darkMode = document.documentElement.classList.contains('dark');
this.setupOfflineDetection();
this.setupServiceWorker();
this.setupLocalStorage();
this.setupUI();
this.setupThemeListeners();
// Debug-Info für die Entwicklung
console.log(`MYP App initialisiert um ${this.registerTime}`);
console.log(`Initiale Netzwerkverbindung: ${navigator.onLine ? 'Online' : 'Offline'}`);
console.log(`Aktueller Modus: ${this.darkMode ? 'Dark Mode' : 'Light Mode'}`);
}
/**
* Überwacht die Netzwerkverbindung
*/
setupOfflineDetection() {
window.addEventListener('online', () => {
this.isOffline = false;
document.body.classList.remove('offline-mode');
console.log('Netzwerkverbindung wiederhergestellt!');
// Bei Wiederverbindung: Synchronisiere Daten
this.syncOfflineData();
// Event an andere Komponenten senden
window.dispatchEvent(new CustomEvent('networkStatusChange', {
detail: { isOffline: false }
}));
});
window.addEventListener('offline', () => {
this.isOffline = true;
document.body.classList.add('offline-mode');
console.log('Netzwerkverbindung verloren!');
// Event an andere Komponenten senden
window.dispatchEvent(new CustomEvent('networkStatusChange', {
detail: { isOffline: true }
}));
});
// Klasse setzen, wenn initial offline
if (this.isOffline) {
document.body.classList.add('offline-mode');
}
}
/**
* Registriert den Service Worker für Offline-Funktionalität
*/
setupServiceWorker() {
if ('serviceWorker' in navigator) {
const swPath = '/sw.js';
navigator.serviceWorker.register(swPath, {
scope: '/'
}).then(registration => {
console.log('Service Worker erfolgreich registriert mit Scope:', registration.scope);
// Status des Service Workers überprüfen
if (registration.installing) {
console.log('Service Worker wird installiert');
} else if (registration.waiting) {
console.log('Service Worker wartet auf Aktivierung');
} else if (registration.active) {
console.log('Service Worker ist aktiv');
}
// Auf Updates überwachen
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
console.log(`Service Worker Status: ${newWorker.state}`);
// Bei Aktivierung Cache aktualisieren
if (newWorker.state === 'activated') {
this.fetchAndCacheAppData();
}
});
});
// Prüfen auf Update beim App-Start
registration.update();
}).catch(error => {
console.error('Service Worker Registrierung fehlgeschlagen:', error);
// Fallback auf lokalen Cache wenn Service Worker fehlschlägt
this.setupLocalCache();
});
// Kontrolltransfer abfangen
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('Service Worker Controller hat gewechselt');
});
} else {
console.warn('Service Worker werden von diesem Browser nicht unterstützt');
// Fallback auf lokalen Cache wenn Service Worker nicht unterstützt werden
this.setupLocalCache();
}
}
/**
* Lokaler Cache-Fallback wenn Service Worker nicht verfügbar
*/
setupLocalCache() {
console.log('Verwende lokalen Cache als Fallback');
// Wichtige App-Daten sofort laden und cachen
this.fetchAndCacheAppData();
// Timer für regelmäßige Aktualisierung des Cache
setInterval(() => {
if (navigator.onLine) {
this.fetchAndCacheAppData();
}
}, 15 * 60 * 1000); // Alle 15 Minuten
}
/**
* Laden und Cachen wichtiger API-Endpunkte
*/
fetchAndCacheAppData() {
if (!navigator.onLine) return;
const endpoints = [
'/api/printers',
'/api/jobs',
'/api/schedule',
'/api/status'
];
for (const endpoint of endpoints) {
fetch(endpoint)
.then(response => response.json())
.then(data => {
localStorage.setItem(`cache_${endpoint}`, JSON.stringify({
timestamp: new Date().getTime(),
data: data
}));
console.log(`Daten für ${endpoint} gecached`);
})
.catch(error => {
console.error(`Fehler beim Cachen von ${endpoint}:`, error);
});
}
}
/**
* Lokaler Speicher für Offline-Daten
*/
setupLocalStorage() {
// Speicher für Offline-Bearbeitung einrichten
if (!localStorage.getItem('offlineChanges')) {
localStorage.setItem('offlineChanges', JSON.stringify([]));
}
// Speicher für App-Konfiguration einrichten
if (!localStorage.getItem('appConfig')) {
const defaultConfig = {
theme: 'system',
notifications: true,
dataSync: true,
lastSync: null
};
localStorage.setItem('appConfig', JSON.stringify(defaultConfig));
}
// Aufräumen alter Cache-Einträge (älter als 7 Tage)
this.cleanupOldCache(7);
}
/**
* Entfernt alte Cache-Einträge
* @param {number} daysOld - Alter in Tagen
*/
cleanupOldCache(daysOld) {
const now = new Date().getTime();
const maxAge = daysOld * 24 * 60 * 60 * 1000;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
// Nur Cache-Einträge prüfen
if (key.startsWith('cache_')) {
try {
const item = JSON.parse(localStorage.getItem(key));
if (item && item.timestamp && (now - item.timestamp > maxAge)) {
localStorage.removeItem(key);
console.log(`Alter Cache-Eintrag entfernt: ${key}`);
}
} catch (e) {
console.error(`Fehler beim Verarbeiten von Cache-Eintrag ${key}:`, e);
}
}
}
}
/**
* Synchronisiert offline getätigte Änderungen
*/
syncOfflineData() {
if (!navigator.onLine) return;
const offlineChanges = JSON.parse(localStorage.getItem('offlineChanges') || '[]');
if (offlineChanges.length === 0) {
console.log('Keine Offline-Änderungen zu synchronisieren');
return;
}
console.log(`${offlineChanges.length} Offline-Änderungen werden synchronisiert...`);
// Status-Indikator für Synchronisierung anzeigen
document.body.classList.add('syncing');
const syncPromises = offlineChanges.map(change => {
return fetch(change.url, {
method: change.method,
headers: {
'Content-Type': 'application/json',
'X-Offline-Change': 'true'
},
body: JSON.stringify(change.data)
})
.then(response => {
if (!response.ok) {
throw new Error(`Fehler ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log(`Änderung erfolgreich synchronisiert: ${change.url}`);
return { success: true, change };
})
.catch(error => {
console.error(`Synchronisierung fehlgeschlagen für ${change.url}:`, error);
return { success: false, change, error };
});
});
Promise.all(syncPromises)
.then(results => {
// Fehlgeschlagene Änderungen behalten
const failedChanges = results
.filter(result => !result.success)
.map(result => result.change);
localStorage.setItem('offlineChanges', JSON.stringify(failedChanges));
// App-Konfiguration aktualisieren
const appConfig = JSON.parse(localStorage.getItem('appConfig') || '{}');
appConfig.lastSync = new Date().toISOString();
localStorage.setItem('appConfig', JSON.stringify(appConfig));
// Status-Indikator entfernen
document.body.classList.remove('syncing');
// Event-Benachrichtigung
const syncEvent = new CustomEvent('offlineDataSynced', {
detail: {
total: offlineChanges.length,
succeeded: offlineChanges.length - failedChanges.length,
failed: failedChanges.length
}
});
window.dispatchEvent(syncEvent);
console.log(`Synchronisierung abgeschlossen: ${offlineChanges.length - failedChanges.length} erfolgreich, ${failedChanges.length} fehlgeschlagen`);
});
}
/**
* Fügt Offline-Änderung zum Synchronisierungsstapel hinzu
* @param {string} url - API-Endpunkt
* @param {string} method - HTTP-Methode (POST, PUT, DELETE)
* @param {object} data - Zu sendende Daten
*/
addOfflineChange(url, method, data) {
const offlineChanges = JSON.parse(localStorage.getItem('offlineChanges') || '[]');
offlineChanges.push({
id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5),
url,
method,
data,
timestamp: new Date().toISOString()
});
localStorage.setItem('offlineChanges', JSON.stringify(offlineChanges));
console.log(`Offline-Änderung gespeichert: ${method} ${url}`);
}
/**
* Theme-Wechsel überwachen und reagieren
*/
setupThemeListeners() {
// Auf Dark Mode Änderungen reagieren
window.addEventListener('darkModeChanged', (e) => {
this.darkMode = e.detail.isDark;
this.updateAppTheme();
// App-Konfiguration aktualisieren
const appConfig = JSON.parse(localStorage.getItem('appConfig') || '{}');
appConfig.theme = this.darkMode ? 'dark' : 'light';
localStorage.setItem('appConfig', JSON.stringify(appConfig));
console.log(`Theme geändert: ${this.darkMode ? 'Dark Mode' : 'Light Mode'}`);
});
// Bei Systemthemen-Änderung prüfen, ob wir automatisch wechseln sollen
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
const appConfig = JSON.parse(localStorage.getItem('appConfig') || '{}');
// Nur reagieren, wenn der Nutzer "system" als Theme gewählt hat
if (appConfig.theme === 'system') {
this.darkMode = e.matches;
this.updateAppTheme();
console.log(`Systemthema geändert: ${this.darkMode ? 'Dark Mode' : 'Light Mode'}`);
}
});
}
/**
* Theme im UI aktualisieren
*/
updateAppTheme() {
// Theme-color Meta-Tag aktualisieren für Mobile Browser
const metaThemeColor = document.getElementById('metaThemeColor') ||
document.querySelector('meta[name="theme-color"]');
if (metaThemeColor) {
metaThemeColor.setAttribute('content', this.darkMode ? '#0f172a' : '#ffffff');
}
// Dynamische UI-Elemente anpassen
this.updateUIForTheme();
}
/**
* UI-Elemente für das aktuelle Theme anpassen
*/
updateUIForTheme() {
// Offline-Banner und Sync-Indikator anpassen
const offlineBanner = document.getElementById('offline-banner');
const syncIndicator = document.getElementById('sync-indicator');
if (offlineBanner) {
if (this.darkMode) {
offlineBanner.classList.add('dark-theme');
offlineBanner.classList.remove('light-theme');
} else {
offlineBanner.classList.add('light-theme');
offlineBanner.classList.remove('dark-theme');
}
}
if (syncIndicator) {
if (this.darkMode) {
syncIndicator.classList.add('dark-theme');
syncIndicator.classList.remove('light-theme');
} else {
syncIndicator.classList.add('light-theme');
syncIndicator.classList.remove('dark-theme');
}
}
// Andere dynamische UI-Elemente hier anpassen
}
/**
* UI-Komponenten initialisieren
*/
setupUI() {
// Offline-Modus Banner einfügen wenn nicht vorhanden
if (!document.getElementById('offline-banner')) {
const banner = document.createElement('div');
banner.id = 'offline-banner';
banner.className = `hidden fixed top-0 left-0 right-0 bg-amber-500 dark:bg-amber-600 text-white text-center py-2 px-4 z-50 ${this.darkMode ? 'dark-theme' : 'light-theme'}`;
banner.textContent = 'Sie sind offline. Einige Funktionen sind eingeschränkt.';
document.body.prepend(banner);
// Anzeigen wenn offline
if (this.isOffline) {
banner.classList.remove('hidden');
}
// Event-Listener für Online/Offline-Status
window.addEventListener('online', () => banner.classList.add('hidden'));
window.addEventListener('offline', () => banner.classList.remove('hidden'));
}
// Synchronisierungs-Indikator einfügen wenn nicht vorhanden
if (!document.getElementById('sync-indicator')) {
const indicator = document.createElement('div');
indicator.id = 'sync-indicator';
indicator.className = `hidden fixed bottom-4 right-4 bg-indigo-600 dark:bg-indigo-700 text-white text-sm rounded-full py-1 px-3 z-50 flex items-center ${this.darkMode ? 'dark-theme' : 'light-theme'}`;
const spinnerSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
spinnerSvg.setAttribute('class', 'animate-spin -ml-1 mr-2 h-4 w-4 text-white');
spinnerSvg.setAttribute('fill', 'none');
spinnerSvg.setAttribute('viewBox', '0 0 24 24');
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('class', 'opacity-25');
circle.setAttribute('cx', '12');
circle.setAttribute('cy', '12');
circle.setAttribute('r', '10');
circle.setAttribute('stroke', 'currentColor');
circle.setAttribute('stroke-width', '4');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('class', 'opacity-75');
path.setAttribute('fill', 'currentColor');
path.setAttribute('d', 'M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z');
spinnerSvg.appendChild(circle);
spinnerSvg.appendChild(path);
const text = document.createElement('span');
text.textContent = 'Synchronisiere...';
indicator.appendChild(spinnerSvg);
indicator.appendChild(text);
document.body.appendChild(indicator);
// Anzeigen wenn synchronisiert wird
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
if (document.body.classList.contains('syncing')) {
indicator.classList.remove('hidden');
} else {
indicator.classList.add('hidden');
}
}
});
});
observer.observe(document.body, { attributes: true });
}
// Mobile Optimierungen
this.setupMobileOptimizations();
}
/**
* Mobile Optimierungen
*/
setupMobileOptimizations() {
// Service Worker-Status auf mobilen Geräten überwachen und UI entsprechend anpassen
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
// Verbessert Touch-Handhabung auf Mobilgeräten
this.setupTouchFeedback();
// Viewport-Höhe für mobile Browser anpassen (Adressleisten-Problem)
this.fixMobileViewportHeight();
// Scroll-Wiederherstellung deaktivieren für eine bessere UX auf Mobilgeräten
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
}
}
/**
* Touch-Feedback für mobile Geräte
*/
setupTouchFeedback() {
// Aktive Klasse für Touch-Feedback hinzufügen
document.addEventListener('touchstart', function(e) {
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A' ||
e.target.closest('button') || e.target.closest('a')) {
const element = e.target.tagName === 'BUTTON' || e.target.tagName === 'A' ?
e.target : (e.target.closest('button') || e.target.closest('a'));
element.classList.add('touch-active');
}
}, { passive: true });
document.addEventListener('touchend', function() {
const activeElements = document.querySelectorAll('.touch-active');
activeElements.forEach(el => el.classList.remove('touch-active'));
}, { passive: true });
}
/**
* Viewport-Höhe für mobile Browser fixieren
*/
fixMobileViewportHeight() {
// Mobile Viewport-Höhe berechnen und als CSS-Variable setzen
const setViewportHeight = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
};
// Initial setzen und bei Größenänderung aktualisieren
setViewportHeight();
window.addEventListener('resize', setViewportHeight);
// Auch bei Orientierungsänderung aktualisieren
window.addEventListener('orientationchange', () => {
setTimeout(setViewportHeight, 100);
});
}
}
// App initialisieren wenn DOM geladen ist
document.addEventListener('DOMContentLoaded', () => {
window.myp = new MYPApp();
});

View File

@ -0,0 +1,560 @@
/**
* MYP Platform - Erweiterte Optimierungs- und Batch-Funktionen
* Implementiert Auto-Optimierung und Batch-Planung für 3D-Druckaufträge
*/
class OptimizationManager {
constructor() {
this.isAutoOptimizationEnabled = false;
this.isBatchModeEnabled = false;
this.selectedJobs = new Set();
this.optimizationSettings = {
algorithm: 'round_robin', // 'round_robin', 'load_balance', 'priority_based'
considerDistance: true,
minimizeChangeover: true,
maxBatchSize: 10,
timeWindow: 24 // Stunden
};
this.init();
}
init() {
this.setupEventListeners();
this.loadSavedSettings();
this.updateUI();
}
setupEventListeners() {
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.altKey && e.key === 'O') {
this.toggleAutoOptimization();
e.preventDefault();
}
if (e.ctrlKey && e.altKey && e.key === 'B') {
this.toggleBatchMode();
e.preventDefault();
}
});
}
/**
* Auto-Optimierung ein-/ausschalten
* Diese Funktion optimiert automatisch die Druckreihenfolge basierend auf verschiedenen Faktoren
*/
toggleAutoOptimization() {
this.isAutoOptimizationEnabled = !this.isAutoOptimizationEnabled;
const button = document.getElementById('auto-opt-toggle');
if (button) {
this.updateAutoOptimizationButton(button);
}
// Settings speichern
localStorage.setItem('myp-auto-optimization', this.isAutoOptimizationEnabled);
// Notification anzeigen
this.showOptimizationNotification(
this.isAutoOptimizationEnabled ? 'aktiviert' : 'deaktiviert',
'auto-optimization'
);
// Wenn aktiviert, sofortige Optimierung durchführen
if (this.isAutoOptimizationEnabled) {
this.performAutoOptimization();
}
// UI aktualisieren
this.updateUI();
}
updateAutoOptimizationButton(button) {
const span = button.querySelector('span');
const icon = button.querySelector('svg');
if (this.isAutoOptimizationEnabled) {
button.classList.remove('btn-secondary');
button.classList.add('btn-primary');
span.textContent = 'Auto-Optimierung AN';
// Button-Animation
button.style.transform = 'scale(1.05)';
setTimeout(() => {
button.style.transform = '';
}, 200);
// Icon-Animation
icon.style.animation = 'spin 1s ease-in-out';
setTimeout(() => {
icon.style.animation = '';
}, 1000);
} else {
button.classList.remove('btn-primary');
button.classList.add('btn-secondary');
span.textContent = 'Auto-Optimierung';
}
}
/**
* Batch-Modus ein-/ausschalten
* Ermöglicht die Auswahl mehrerer Jobs für Batch-Operationen
*/
toggleBatchMode() {
this.isBatchModeEnabled = !this.isBatchModeEnabled;
const button = document.getElementById('batch-toggle');
if (button) {
this.updateBatchModeButton(button);
}
// Batch-Funktionalität aktivieren/deaktivieren
this.toggleBatchSelection();
// Settings speichern
localStorage.setItem('myp-batch-mode', this.isBatchModeEnabled);
// Notification anzeigen
this.showOptimizationNotification(
this.isBatchModeEnabled ? 'aktiviert' : 'deaktiviert',
'batch-mode'
);
// UI aktualisieren
this.updateUI();
}
updateBatchModeButton(button) {
const span = button.querySelector('span');
if (this.isBatchModeEnabled) {
button.classList.remove('btn-secondary');
button.classList.add('btn-warning');
span.textContent = `Batch-Modus (${this.selectedJobs.size})`;
// Button-Animation
button.style.transform = 'scale(1.05)';
setTimeout(() => {
button.style.transform = '';
}, 200);
} else {
button.classList.remove('btn-warning');
button.classList.add('btn-secondary');
span.textContent = 'Mehrfachauswahl';
// Auswahl zurücksetzen
this.selectedJobs.clear();
}
}
toggleBatchSelection() {
const jobCards = document.querySelectorAll('.job-card, [data-job-id]');
jobCards.forEach(card => {
if (this.isBatchModeEnabled) {
this.enableBatchSelection(card);
} else {
this.disableBatchSelection(card);
}
});
}
enableBatchSelection(card) {
// Checkbox hinzufügen
let checkbox = card.querySelector('.batch-checkbox');
if (!checkbox) {
checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'batch-checkbox absolute top-3 left-3 w-5 h-5 rounded border-2 border-gray-300 text-blue-600 focus:ring-blue-500';
checkbox.style.zIndex = '10';
// Event Listener für Checkbox
checkbox.addEventListener('change', (e) => {
const jobId = card.dataset.jobId;
if (e.target.checked) {
this.selectedJobs.add(jobId);
card.classList.add('selected-for-batch');
} else {
this.selectedJobs.delete(jobId);
card.classList.remove('selected-for-batch');
}
this.updateBatchCounter();
});
// Checkbox in die Karte einfügen
card.style.position = 'relative';
card.appendChild(checkbox);
}
checkbox.style.display = 'block';
card.classList.add('batch-selectable');
}
disableBatchSelection(card) {
const checkbox = card.querySelector('.batch-checkbox');
if (checkbox) {
checkbox.style.display = 'none';
}
card.classList.remove('batch-selectable', 'selected-for-batch');
}
updateBatchCounter() {
const button = document.getElementById('batch-toggle');
if (button && this.isBatchModeEnabled) {
const span = button.querySelector('span');
span.textContent = `Batch-Modus (${this.selectedJobs.size})`;
}
}
/**
* Automatische Optimierung durchführen
*/
async performAutoOptimization() {
try {
const response = await fetch('/api/optimization/auto-optimize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
},
body: JSON.stringify({
settings: this.optimizationSettings,
enabled: this.isAutoOptimizationEnabled
})
});
const data = await response.json();
if (data.success) {
this.showSuccessMessage(`Optimierung erfolgreich: ${data.optimized_jobs} Jobs optimiert`);
this.refreshCurrentView();
} else {
this.showErrorMessage(`Optimierung fehlgeschlagen: ${data.error}`);
}
} catch (error) {
console.error('Auto-Optimierung Fehler:', error);
this.showErrorMessage('Netzwerkfehler bei der Auto-Optimierung');
}
}
/**
* Batch-Operationen durchführen
*/
async performBatchOperation(operation) {
if (this.selectedJobs.size === 0) {
this.showWarningMessage('Keine Jobs für Batch-Operation ausgewählt');
return;
}
const jobIds = Array.from(this.selectedJobs);
try {
const response = await fetch('/api/jobs/batch-operation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
},
body: JSON.stringify({
job_ids: jobIds,
operation: operation
})
});
const data = await response.json();
if (data.success) {
this.showSuccessMessage(`Batch-Operation "${operation}" erfolgreich auf ${jobIds.length} Jobs angewendet`);
this.selectedJobs.clear();
this.updateBatchCounter();
this.refreshCurrentView();
} else {
this.showErrorMessage(`Batch-Operation fehlgeschlagen: ${data.error}`);
}
} catch (error) {
console.error('Batch-Operation Fehler:', error);
this.showErrorMessage('Netzwerkfehler bei der Batch-Operation');
}
}
/**
* Optimierungs-Einstellungen konfigurieren
*/
showOptimizationSettings() {
this.createOptimizationModal();
}
createOptimizationModal() {
const modal = document.createElement('div');
modal.id = 'optimization-settings-modal';
modal.className = 'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4';
modal.innerHTML = `
<div class="dashboard-card max-w-2xl w-full p-8 transform transition-all duration-300">
<div class="flex justify-between items-center mb-6">
<div>
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">
Optimierungs-Einstellungen
</h3>
<p class="text-slate-500 dark:text-slate-400">
Konfigurieren Sie die automatische Optimierung für maximale Effizienz
</p>
</div>
<button onclick="this.closest('#optimization-settings-modal').remove()"
class="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg transition-colors">
<svg class="w-6 h-6 text-slate-500 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="space-y-6">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Optimierungs-Algorithmus
</label>
<select id="optimization-algorithm" class="block w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
<option value="round_robin">Round Robin - Gleichmäßige Verteilung</option>
<option value="load_balance">Load Balancing - Auslastungsoptimierung</option>
<option value="priority_based">Prioritätsbasiert - Wichtige Jobs zuerst</option>
</select>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="flex items-center">
<input type="checkbox" id="consider-distance" class="mr-2">
<span class="text-sm text-slate-700 dark:text-slate-300">
Druckerentfernung berücksichtigen
</span>
</label>
</div>
<div>
<label class="flex items-center">
<input type="checkbox" id="minimize-changeover" class="mr-2">
<span class="text-sm text-slate-700 dark:text-slate-300">
Rüstzeiten minimieren
</span>
</label>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Max. Batch-Größe
</label>
<input type="number" id="max-batch-size" min="1" max="50"
class="block w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Planungshorizont (Stunden)
</label>
<input type="number" id="time-window" min="1" max="168"
class="block w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
</div>
</div>
</div>
<div class="flex items-center justify-end gap-3 pt-6 border-t border-gray-200 dark:border-slate-600 mt-6">
<button onclick="this.closest('#optimization-settings-modal').remove()"
class="btn-secondary">
Abbrechen
</button>
<button onclick="optimizationManager.saveOptimizationSettings()"
class="btn-primary">
Einstellungen speichern
</button>
</div>
</div>
`;
document.body.appendChild(modal);
this.loadOptimizationSettingsInModal();
}
loadOptimizationSettingsInModal() {
document.getElementById('optimization-algorithm').value = this.optimizationSettings.algorithm;
document.getElementById('consider-distance').checked = this.optimizationSettings.considerDistance;
document.getElementById('minimize-changeover').checked = this.optimizationSettings.minimizeChangeover;
document.getElementById('max-batch-size').value = this.optimizationSettings.maxBatchSize;
document.getElementById('time-window').value = this.optimizationSettings.timeWindow;
}
saveOptimizationSettings() {
this.optimizationSettings.algorithm = document.getElementById('optimization-algorithm').value;
this.optimizationSettings.considerDistance = document.getElementById('consider-distance').checked;
this.optimizationSettings.minimizeChangeover = document.getElementById('minimize-changeover').checked;
this.optimizationSettings.maxBatchSize = parseInt(document.getElementById('max-batch-size').value);
this.optimizationSettings.timeWindow = parseInt(document.getElementById('time-window').value);
localStorage.setItem('myp-optimization-settings', JSON.stringify(this.optimizationSettings));
document.getElementById('optimization-settings-modal').remove();
this.showSuccessMessage('Optimierungs-Einstellungen gespeichert');
// Wenn Auto-Optimierung aktiv ist, neue Optimierung durchführen
if (this.isAutoOptimizationEnabled) {
this.performAutoOptimization();
}
}
loadSavedSettings() {
// Auto-Optimierung Status laden
const savedAutoOpt = localStorage.getItem('myp-auto-optimization');
if (savedAutoOpt !== null) {
this.isAutoOptimizationEnabled = savedAutoOpt === 'true';
}
// Batch-Modus Status laden
const savedBatchMode = localStorage.getItem('myp-batch-mode');
if (savedBatchMode !== null) {
this.isBatchModeEnabled = savedBatchMode === 'true';
}
// Optimierungs-Einstellungen laden
const savedSettings = localStorage.getItem('myp-optimization-settings');
if (savedSettings) {
try {
this.optimizationSettings = { ...this.optimizationSettings, ...JSON.parse(savedSettings) };
} catch (error) {
console.error('Fehler beim Laden der Optimierungs-Einstellungen:', error);
}
}
}
updateUI() {
// Buttons aktualisieren
const autoOptButton = document.getElementById('auto-opt-toggle');
if (autoOptButton) {
this.updateAutoOptimizationButton(autoOptButton);
}
const batchButton = document.getElementById('batch-toggle');
if (batchButton) {
this.updateBatchModeButton(batchButton);
}
// Batch-Auswahl aktualisieren
if (this.isBatchModeEnabled) {
this.toggleBatchSelection();
}
}
// Utility-Funktionen
getCSRFToken() {
const token = document.querySelector('meta[name="csrf-token"]');
return token ? token.getAttribute('content') : '';
}
refreshCurrentView() {
// Je nach aktueller Seite entsprechende Refresh-Funktion aufrufen
if (typeof refreshJobs === 'function') {
refreshJobs();
} else if (typeof refreshCalendar === 'function') {
refreshCalendar();
} else if (typeof refreshDashboard === 'function') {
refreshDashboard();
}
}
showOptimizationNotification(status, type) {
const messages = {
'auto-optimization': {
'aktiviert': '🚀 Auto-Optimierung aktiviert - Jobs werden automatisch optimiert',
'deaktiviert': '⏸️ Auto-Optimierung deaktiviert'
},
'batch-mode': {
'aktiviert': '📦 Batch-Modus aktiviert - Wählen Sie Jobs für Batch-Operationen aus',
'deaktiviert': '✅ Batch-Modus deaktiviert'
}
};
const message = messages[type]?.[status] || `${type} ${status}`;
this.showSuccessMessage(message);
}
showSuccessMessage(message) {
this.showToast(message, 'success');
}
showErrorMessage(message) {
this.showToast(message, 'error');
}
showWarningMessage(message) {
this.showToast(message, 'warning');
}
showToast(message, type = 'info') {
// Einfache Toast-Benachrichtigung
const toast = document.createElement('div');
toast.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transition-all duration-300 transform translate-x-full`;
const colors = {
success: 'bg-green-500 text-white',
error: 'bg-red-500 text-white',
warning: 'bg-yellow-500 text-black',
info: 'bg-blue-500 text-white'
};
toast.className += ` ${colors[type]}`;
toast.textContent = message;
document.body.appendChild(toast);
// Animation einblenden
setTimeout(() => {
toast.classList.remove('translate-x-full');
}, 100);
// Nach 5 Sekunden automatisch entfernen
setTimeout(() => {
toast.classList.add('translate-x-full');
setTimeout(() => {
toast.remove();
}, 300);
}, 5000);
}
}
// Globale Funktionen für Template-Integration
let optimizationManager;
// Auto-Optimierung umschalten
window.toggleAutoOptimization = function() {
if (!optimizationManager) {
optimizationManager = new OptimizationManager();
}
optimizationManager.toggleAutoOptimization();
};
// Batch-Modus umschalten
window.toggleBatchMode = function() {
if (!optimizationManager) {
optimizationManager = new OptimizationManager();
}
optimizationManager.toggleBatchMode();
};
// Batch-Planung Modal öffnen
window.openBatchPlanningModal = function() {
if (!optimizationManager) {
optimizationManager = new OptimizationManager();
}
optimizationManager.showOptimizationSettings();
};
// Optimierungs-Einstellungen anzeigen
window.showOptimizationSettings = function() {
if (!optimizationManager) {
optimizationManager = new OptimizationManager();
}
optimizationManager.showOptimizationSettings();
};
// Initialisierung beim Laden der Seite
document.addEventListener('DOMContentLoaded', function() {
optimizationManager = new OptimizationManager();
console.log('🎯 Optimierungs-Manager initialisiert');
});

View File

@ -0,0 +1,471 @@
/**
* Live-Drucker-Monitor für MYP Platform
* Verwaltet Live-Status-Updates mit Session-Caching und automatischer Aktualisierung
*/
class PrinterMonitor {
constructor() {
this.refreshInterval = 30000; // 30 Sekunden Standard-Intervall
this.fastRefreshInterval = 5000; // 5 Sekunden für schnelle Updates
this.currentInterval = null;
this.printers = new Map();
this.callbacks = new Set();
this.isActive = false;
this.useCache = true;
this.lastUpdate = null;
this.errorCount = 0;
this.maxErrors = 3;
// Status-Kategorien für bessere Übersicht
this.statusCategories = {
'online': { label: 'Online', color: 'success', icon: '🟢' },
'offline': { label: 'Offline', color: 'danger', icon: '🔴' },
'standby': { label: 'Standby', color: 'warning', icon: '🟡' },
'unreachable': { label: 'Nicht erreichbar', color: 'secondary', icon: '⚫' },
'unconfigured': { label: 'Nicht konfiguriert', color: 'info', icon: '🔵' }
};
console.log('🖨️ PrinterMonitor initialisiert');
}
/**
* Startet das Live-Monitoring
*/
start() {
if (this.isActive) {
console.log('⚠️ PrinterMonitor läuft bereits');
return;
}
this.isActive = true;
this.errorCount = 0;
console.log('🚀 Starte PrinterMonitor');
// Sofortige erste Aktualisierung
this.updatePrinterStatus();
// Reguläres Intervall starten
this.startInterval();
// Event-Listener für Sichtbarkeitsänderungen
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
}
/**
* Stoppt das Live-Monitoring
*/
stop() {
if (!this.isActive) {
return;
}
this.isActive = false;
if (this.currentInterval) {
clearInterval(this.currentInterval);
this.currentInterval = null;
}
document.removeEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
console.log('⏹️ PrinterMonitor gestoppt');
}
/**
* Startet das Update-Intervall
*/
startInterval() {
if (this.currentInterval) {
clearInterval(this.currentInterval);
}
this.currentInterval = setInterval(() => {
this.updatePrinterStatus();
}, this.refreshInterval);
}
/**
* Behandelt Sichtbarkeitsänderungen der Seite
*/
handleVisibilityChange() {
if (document.hidden) {
// Seite nicht sichtbar - langsamere Updates
this.refreshInterval = 60000; // 1 Minute
} else {
// Seite sichtbar - normale Updates
this.refreshInterval = 30000; // 30 Sekunden
// Sofortige Aktualisierung wenn Seite wieder sichtbar
this.updatePrinterStatus();
}
if (this.isActive) {
this.startInterval();
}
}
/**
* Holt aktuelle Drucker-Status-Daten
*/
async updatePrinterStatus() {
if (!this.isActive) {
return;
}
try {
const response = await fetch(`/api/printers/monitor/live-status?use_cache=${this.useCache}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.success) {
this.processPrinterData(data);
this.errorCount = 0; // Reset error count on success
} else {
throw new Error(data.error || 'Unbekannter Fehler');
}
} catch (error) {
this.errorCount++;
console.error('❌ Fehler beim Abrufen des Drucker-Status:', error);
// Bei wiederholten Fehlern weniger frequent versuchen
if (this.errorCount >= this.maxErrors) {
this.refreshInterval = Math.min(this.refreshInterval * 2, 300000); // Max 5 Minuten
this.startInterval();
this.notifyCallbacks({
type: 'error',
message: `Drucker-Status nicht verfügbar (${this.errorCount} Fehler)`,
timestamp: new Date().toISOString()
});
}
}
}
/**
* Verarbeitet empfangene Drucker-Daten
*/
processPrinterData(data) {
const previousPrinters = new Map(this.printers);
// Drucker-Daten aktualisieren
this.printers.clear();
// Null-Check für data.printers hinzufügen
if (data && data.printers && typeof data.printers === 'object') {
Object.values(data.printers).forEach(printer => {
this.printers.set(printer.id, {
...printer,
statusInfo: this.statusCategories[printer.status] || this.statusCategories['offline']
});
});
} else {
console.warn('⚠️ Keine gültigen Drucker-Daten erhalten:', data);
// Benachrichtige Callbacks über Fehler
this.notifyCallbacks({
type: 'error',
message: 'Ungültige Drucker-Daten erhalten',
data: data
});
return;
}
this.lastUpdate = new Date(data.timestamp || Date.now());
// Änderungen erkennen und benachrichtigen
const changes = this.detectChanges(previousPrinters, this.printers);
// Callbacks benachrichtigen
this.notifyCallbacks({
type: 'update',
printers: this.printers,
summary: data.summary,
changes: changes,
timestamp: this.lastUpdate,
cacheUsed: data.cache_used
});
console.log(`🔄 Drucker-Status aktualisiert: ${this.printers.size} Drucker`);
}
/**
* Erkennt Änderungen zwischen zwei Drucker-Status-Maps
*/
detectChanges(oldPrinters, newPrinters) {
const changes = [];
newPrinters.forEach((newPrinter, id) => {
const oldPrinter = oldPrinters.get(id);
if (!oldPrinter) {
changes.push({
type: 'added',
printer: newPrinter
});
} else if (oldPrinter.status !== newPrinter.status) {
changes.push({
type: 'status_change',
printer: newPrinter,
oldStatus: oldPrinter.status,
newStatus: newPrinter.status
});
}
});
oldPrinters.forEach((oldPrinter, id) => {
if (!newPrinters.has(id)) {
changes.push({
type: 'removed',
printer: oldPrinter
});
}
});
return changes;
}
/**
* Benachrichtigt alle registrierten Callbacks
*/
notifyCallbacks(data) {
this.callbacks.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error('❌ Fehler in PrinterMonitor Callback:', error);
}
});
}
/**
* Registriert einen Callback für Status-Updates
*/
onUpdate(callback) {
if (typeof callback === 'function') {
this.callbacks.add(callback);
}
}
/**
* Entfernt einen Callback
*/
offUpdate(callback) {
this.callbacks.delete(callback);
}
/**
* Erzwingt eine sofortige Aktualisierung ohne Cache
*/
async forceUpdate() {
const oldUseCache = this.useCache;
this.useCache = false;
try {
await this.updatePrinterStatus();
} finally {
this.useCache = oldUseCache;
}
}
/**
* Löscht den Cache und erzwingt eine Aktualisierung
*/
async clearCache() {
try {
const response = await fetch('/api/printers/monitor/clear-cache', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
console.log('🧹 Drucker-Cache geleert');
await this.forceUpdate();
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
console.error('❌ Fehler beim Leeren des Caches:', error);
}
}
/**
* Holt eine schnelle Status-Zusammenfassung
*/
async getSummary() {
try {
const response = await fetch('/api/printers/monitor/summary', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const data = await response.json();
return data.success ? data.summary : null;
}
} catch (error) {
console.error('❌ Fehler beim Abrufen der Zusammenfassung:', error);
}
return null;
}
/**
* Gibt den aktuellen Status eines Druckers zurück
*/
getPrinter(id) {
return this.printers.get(id);
}
/**
* Gibt alle Drucker zurück
*/
getAllPrinters() {
return Array.from(this.printers.values());
}
/**
* Gibt Drucker nach Status gefiltert zurück
*/
getPrintersByStatus(status) {
return this.getAllPrinters().filter(printer => printer.status === status);
}
/**
* Gibt eine Status-Zusammenfassung zurück
*/
getStatusSummary() {
const summary = {
total: this.printers.size,
online: 0,
offline: 0,
printing: 0, // Neuer Status: Drucker druckt gerade
standby: 0,
unreachable: 0,
unconfigured: 0,
error: 0 // Status für unbekannte Fehler
};
this.printers.forEach(printer => {
const status = printer.status;
if (summary.hasOwnProperty(status)) {
summary[status]++;
} else {
// Fallback für unbekannte Status
summary.offline++;
}
});
return summary;
}
/**
* Aktiviert schnelle Updates (für kritische Operationen)
*/
enableFastUpdates() {
this.refreshInterval = this.fastRefreshInterval;
if (this.isActive) {
this.startInterval();
}
console.log('⚡ Schnelle Updates aktiviert');
}
/**
* Deaktiviert schnelle Updates
*/
disableFastUpdates() {
this.refreshInterval = 30000; // Zurück zu normal
if (this.isActive) {
this.startInterval();
}
console.log('🐌 Normale Updates aktiviert');
}
/**
* Initialisiert alle Steckdosen (nur für Admins)
*/
async initializeAllOutlets() {
try {
const response = await fetch('/api/printers/monitor/initialize-outlets', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
console.log('🔌 Steckdosen-Initialisierung erfolgreich:', data.statistics);
// Benachrichtige über Initialisierung
this.notifyCallbacks({
type: 'initialization',
results: data.results,
statistics: data.statistics,
message: data.message
});
// Erzwinge Update nach Initialisierung
await this.forceUpdate();
return data;
} else {
throw new Error(data.error || 'Initialisierung fehlgeschlagen');
}
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
console.error('❌ Fehler bei Steckdosen-Initialisierung:', error);
throw error;
}
}
}
// Globale Instanz
window.printerMonitor = new PrinterMonitor();
// Auto-Start wenn DOM bereit ist
document.addEventListener('DOMContentLoaded', () => {
// Nur starten wenn wir auf einer relevanten Seite sind
const relevantPages = ['/printers', '/dashboard', '/admin', '/admin-dashboard'];
const currentPath = window.location.pathname;
if (relevantPages.some(page => currentPath.includes(page))) {
console.log('🖨️ Auto-Start PrinterMonitor für Seite:', currentPath);
window.printerMonitor.start();
}
});
// Automatisches Cleanup bei Seitenverlassen
window.addEventListener('beforeunload', () => {
if (window.printerMonitor) {
window.printerMonitor.stop();
}
});
// Export für Module
if (typeof module !== 'undefined' && module.exports) {
module.exports = PrinterMonitor;
}

View File

@ -0,0 +1,82 @@
// Backup Service Worker for MYP Platform
const CACHE_NAME = 'myp-platform-backup-v1';
// Assets to cache
const ASSETS_TO_CACHE = [
'/',
'/dashboard',
'/static/css/tailwind.min.css',
'/static/css/tailwind-dark.min.css',
'/static/js/ui-components.js',
'/static/js/offline-app.js',
'/static/favicon.ico'
];
// Install event - cache core assets
self.addEventListener('install', (event) => {
console.log('Backup SW: Installing...');
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Backup SW: Caching assets');
return cache.addAll(ASSETS_TO_CACHE);
})
.then(() => {
console.log('Backup SW: Assets cached');
return self.skipWaiting();
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('Backup SW: Activating...');
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('Backup SW: Deleting old cache', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log('Backup SW: Activated');
return self.clients.claim();
})
);
});
// Fetch event - cache first, network fallback
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
if (response) {
return response;
}
return fetch(event.request)
.then((fetchResponse) => {
// Don't cache non-success responses
if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') {
return fetchResponse;
}
// Cache successful responses
const responseToCache = fetchResponse.clone();
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
return fetchResponse;
});
})
);
});

View File

@ -0,0 +1,490 @@
/**
* Erweitertes Session-Management für MYP Platform
*
* Features:
* - Automatische Session-Überwachung
* - Heartbeat-System für Session-Verlängerung
* - Benutzer-Warnungen bei bevorstehender Abmeldung
* - Graceful Logout bei Session-Ablauf
* - Modal-Dialoge für Session-Verlängerung
*
* @author Mercedes-Benz MYP Platform
* @version 2.0
*/
class SessionManager {
constructor() {
this.isAuthenticated = false;
this.maxInactiveMinutes = 30; // Standard: 30 Minuten
this.heartbeatInterval = 5 * 60 * 1000; // 5 Minuten
this.warningTime = 5 * 60 * 1000; // 5 Minuten vor Ablauf warnen
this.checkInterval = 30 * 1000; // Alle 30 Sekunden prüfen
this.heartbeatTimer = null;
this.statusCheckTimer = null;
this.warningShown = false;
this.sessionWarningModal = null;
this.init();
}
async init() {
try {
// Prüfe initial ob Benutzer angemeldet ist
await this.checkAuthenticationStatus();
if (this.isAuthenticated) {
this.startSessionMonitoring();
this.createWarningModal();
console.log('🔐 Session Manager gestartet');
console.log(`📊 Max Inaktivität: ${this.maxInactiveMinutes} Minuten`);
console.log(`💓 Heartbeat Intervall: ${this.heartbeatInterval / 1000 / 60} Minuten`);
} else {
console.log('👤 Benutzer nicht angemeldet - Session Manager inaktiv');
}
} catch (error) {
console.error('❌ Session Manager Initialisierung fehlgeschlagen:', error);
}
}
async checkAuthenticationStatus() {
try {
const response = await fetch('/api/session/status', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.isAuthenticated = true;
this.maxInactiveMinutes = data.session.max_inactive_minutes;
console.log('✅ Session Status:', {
user: data.user.email,
timeLeft: Math.floor(data.session.time_left_seconds / 60) + ' Minuten',
lastActivity: new Date(data.session.last_activity).toLocaleString('de-DE')
});
return data;
}
} else if (response.status === 401) {
this.isAuthenticated = false;
this.handleSessionExpired('Authentication check failed');
}
} catch (error) {
console.error('❌ Fehler beim Prüfen des Session-Status:', error);
this.isAuthenticated = false;
}
return null;
}
startSessionMonitoring() {
// Heartbeat alle 5 Minuten senden
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.heartbeatInterval);
// Session-Status alle 30 Sekunden prüfen
this.statusCheckTimer = setInterval(() => {
this.checkSessionStatus();
}, this.checkInterval);
// Initial Heartbeat senden
setTimeout(() => this.sendHeartbeat(), 1000);
}
async sendHeartbeat() {
try {
// CSRF-Token aus dem Meta-Tag holen
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const headers = {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
};
// CSRF-Token hinzufügen wenn verfügbar - Flask-WTF erwartet X-CSRFToken oder den Token im Body
if (csrfToken) {
headers['X-CSRFToken'] = csrfToken;
}
const response = await fetch('/api/session/heartbeat', {
method: 'POST',
headers: headers,
body: JSON.stringify({
timestamp: new Date().toISOString(),
page: window.location.pathname,
csrf_token: csrfToken // Zusätzlich im Body senden
})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
console.log('💓 Heartbeat gesendet - Session aktiv:',
Math.floor(data.time_left_seconds / 60) + ' Minuten verbleibend');
} else {
console.warn('⚠️ Heartbeat fehlgeschlagen:', data);
}
} else if (response.status === 401) {
this.handleSessionExpired('Heartbeat failed - unauthorized');
} else if (response.status === 400) {
console.warn('⚠️ CSRF-Token Problem beim Heartbeat - versuche Seite neu zu laden');
// Bei CSRF-Problemen die Seite neu laden
setTimeout(() => location.reload(), 5000);
}
} catch (error) {
console.error('❌ Heartbeat-Fehler:', error);
}
}
async checkSessionStatus() {
try {
const sessionData = await this.checkAuthenticationStatus();
if (sessionData && sessionData.session) {
const timeLeftSeconds = sessionData.session.time_left_seconds;
const timeLeftMinutes = Math.floor(timeLeftSeconds / 60);
// Warnung anzeigen wenn weniger als 5 Minuten verbleiben
if (timeLeftSeconds <= this.warningTime / 1000 && timeLeftSeconds > 0) {
if (!this.warningShown) {
this.showSessionWarning(timeLeftMinutes);
this.warningShown = true;
}
} else if (timeLeftSeconds <= 0) {
// Session abgelaufen
this.handleSessionExpired('Session time expired');
} else {
// Session OK - Warnung zurücksetzen
this.warningShown = false;
this.hideSessionWarning();
}
// Session-Status in der UI aktualisieren
this.updateSessionStatusDisplay(sessionData);
}
} catch (error) {
console.error('❌ Session-Status-Check fehlgeschlagen:', error);
}
}
showSessionWarning(minutesLeft) {
// Bestehende Warnung entfernen
this.hideSessionWarning();
// Toast-Notification anzeigen
this.showToast(
'Session läuft ab',
`Ihre Session läuft in ${minutesLeft} Minuten ab. Möchten Sie verlängern?`,
'warning',
10000, // 10 Sekunden anzeigen
[
{
text: 'Verlängern',
action: () => this.extendSession()
},
{
text: 'Abmelden',
action: () => this.logout()
}
]
);
// Modal anzeigen für wichtige Warnung
if (this.sessionWarningModal) {
this.sessionWarningModal.show();
this.updateWarningModal(minutesLeft);
}
console.log(`⚠️ Session-Warnung: ${minutesLeft} Minuten verbleibend`);
}
hideSessionWarning() {
if (this.sessionWarningModal) {
this.sessionWarningModal.hide();
}
}
createWarningModal() {
// Modal HTML erstellen
const modalHTML = `
<div id="sessionWarningModal" class="fixed inset-0 z-50 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.314 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Session läuft ab
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500" id="warningMessage">
Ihre Session läuft in <span id="timeRemaining" class="font-bold text-red-600">5</span> Minuten ab.
</p>
<p class="text-sm text-gray-500 mt-2">
Möchten Sie Ihre Session verlängern oder sich abmelden?
</p>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" id="extendSessionBtn" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
Session verlängern
</button>
<button type="button" id="logoutBtn" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Abmelden
</button>
</div>
</div>
</div>
</div>`;
// Modal in DOM einfügen
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Event-Listener hinzufügen
document.getElementById('extendSessionBtn').addEventListener('click', () => {
this.extendSession();
this.hideSessionWarning();
});
document.getElementById('logoutBtn').addEventListener('click', () => {
this.logout();
});
// Modal-Objekt erstellen
this.sessionWarningModal = {
element: document.getElementById('sessionWarningModal'),
show: () => {
document.getElementById('sessionWarningModal').classList.remove('hidden');
},
hide: () => {
document.getElementById('sessionWarningModal').classList.add('hidden');
}
};
}
updateWarningModal(minutesLeft) {
const timeElement = document.getElementById('timeRemaining');
if (timeElement) {
timeElement.textContent = minutesLeft;
}
}
async extendSession(extendMinutes = 30) {
try {
const response = await fetch('/api/session/extend', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
extend_minutes: extendMinutes
})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.warningShown = false;
this.showToast(
'Session verlängert',
`Ihre Session wurde um ${data.extended_minutes} Minuten verlängert`,
'success',
5000
);
console.log('✅ Session verlängert:', data);
} else {
this.showToast('Fehler', 'Session konnte nicht verlängert werden', 'error');
}
} else if (response.status === 401) {
this.handleSessionExpired('Extend session failed - unauthorized');
}
} catch (error) {
console.error('❌ Session-Verlängerung fehlgeschlagen:', error);
this.showToast('Fehler', 'Session-Verlängerung fehlgeschlagen', 'error');
}
}
async logout() {
try {
this.stopSessionMonitoring();
// Logout-Request senden
const response = await fetch('/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
// Zur Login-Seite weiterleiten
if (response.ok) {
window.location.href = '/auth/login';
} else {
// Fallback: Direkter Redirect
window.location.href = '/auth/login';
}
} catch (error) {
console.error('❌ Logout-Fehler:', error);
// Fallback: Direkter Redirect
window.location.href = '/auth/login';
}
}
handleSessionExpired(reason) {
console.log('🕒 Session abgelaufen:', reason);
this.stopSessionMonitoring();
this.isAuthenticated = false;
// Benutzer benachrichtigen
this.showToast(
'Session abgelaufen',
'Sie wurden automatisch abgemeldet. Bitte melden Sie sich erneut an.',
'warning',
8000
);
// Nach kurzer Verzögerung zur Login-Seite weiterleiten
setTimeout(() => {
window.location.href = '/auth/login?reason=session_expired';
}, 2000);
}
stopSessionMonitoring() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.statusCheckTimer) {
clearInterval(this.statusCheckTimer);
this.statusCheckTimer = null;
}
console.log('🛑 Session-Monitoring gestoppt');
}
updateSessionStatusDisplay(sessionData) {
// Session-Status in der Navigation/Header anzeigen (falls vorhanden)
const statusElement = document.getElementById('sessionStatus');
if (statusElement) {
const timeLeftMinutes = Math.floor(sessionData.session.time_left_seconds / 60);
statusElement.textContent = `Session: ${timeLeftMinutes}min`;
// Farbe basierend auf verbleibender Zeit
if (timeLeftMinutes <= 5) {
statusElement.className = 'text-red-600 font-medium';
} else if (timeLeftMinutes <= 10) {
statusElement.className = 'text-yellow-600 font-medium';
} else {
statusElement.className = 'text-green-600 font-medium';
}
}
}
showToast(title, message, type = 'info', duration = 5000, actions = []) {
// Verwende das bestehende Toast-System falls verfügbar
if (window.showToast) {
window.showToast(message, type, duration);
return;
}
// Fallback: Simple Browser-Notification
if (type === 'error' || type === 'warning') {
alert(`${title}: ${message}`);
} else {
console.log(`${title}: ${message}`);
}
}
// === ÖFFENTLICHE API ===
/**
* Prüft ob Benutzer angemeldet ist
*/
isLoggedIn() {
return this.isAuthenticated;
}
/**
* Startet Session-Monitoring manuell
*/
start() {
if (!this.heartbeatTimer && this.isAuthenticated) {
this.startSessionMonitoring();
}
}
/**
* Stoppt Session-Monitoring manuell
*/
stop() {
this.stopSessionMonitoring();
}
/**
* Verlängert Session manuell
*/
async extend(minutes = 30) {
return await this.extendSession(minutes);
}
/**
* Meldet Benutzer manuell ab
*/
async logoutUser() {
return await this.logout();
}
}
// Session Manager automatisch starten wenn DOM geladen ist
document.addEventListener('DOMContentLoaded', () => {
// Nur starten wenn wir nicht auf der Login-Seite sind
if (!window.location.pathname.includes('/auth/login')) {
window.sessionManager = new SessionManager();
// Globale Event-Listener für Session-Management
window.addEventListener('beforeunload', () => {
if (window.sessionManager) {
window.sessionManager.stop();
}
});
// Reaktion auf Sichtbarkeitsänderungen (Tab-Wechsel)
document.addEventListener('visibilitychange', () => {
if (window.sessionManager && window.sessionManager.isLoggedIn()) {
if (document.hidden) {
console.log('🙈 Tab versteckt - Session-Monitoring reduziert');
} else {
console.log('👁️ Tab sichtbar - Session-Check');
// Sofortiger Session-Check wenn Tab wieder sichtbar wird
setTimeout(() => window.sessionManager.checkSessionStatus(), 1000);
}
}
});
}
});
// Session Manager für andere Scripts verfügbar machen
window.SessionManager = SessionManager;

432
backend/static/js/sw.js Normal file
View File

@ -0,0 +1,432 @@
/**
* MYP Platform Service Worker
* Offline-First Caching Strategy
*/
// MYP Platform Service Worker
const CACHE_NAME = 'myp-platform-cache-v1';
const STATIC_CACHE = 'myp-static-v1';
const DYNAMIC_CACHE = 'myp-dynamic-v1';
const ASSETS_TO_CACHE = [
'/',
'/dashboard',
'/static/css/tailwind.min.css',
'/static/css/tailwind-dark.min.css',
'/static/js/ui-components.js',
'/static/js/offline-app.js',
'/static/icons/mercedes-logo.svg',
'/static/icons/icon-144x144.png',
'/static/favicon.ico'
];
// Static files patterns
const STATIC_PATTERNS = [
/\.css$/,
/\.js$/,
/\.svg$/,
/\.png$/,
/\.ico$/,
/\.woff2?$/
];
// API request patterns to avoid caching
const API_PATTERNS = [
/^\/api\//,
/^\/auth\//,
/^\/api\/jobs/,
/^\/api\/printers/,
/^\/api\/stats/
];
// Install event - cache core assets
self.addEventListener('install', (event) => {
console.log('Service Worker: Installing...');
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => {
console.log('Service Worker: Caching static files');
return cache.addAll(ASSETS_TO_CACHE);
})
.then(() => {
console.log('Service Worker: Static files cached');
return self.skipWaiting();
})
.catch((error) => {
console.error('Service Worker: Error caching static files', error);
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('Service Worker: Activating...');
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
console.log('Service Worker: Deleting old cache', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log('Service Worker: Activated');
return self.clients.claim();
})
);
});
// Fetch event - handle requests
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Unterstütze sowohl HTTP als auch HTTPS
if (request.method !== 'GET' ||
(url.protocol !== 'http:' && url.protocol !== 'https:')) {
return;
}
// Simple network-first approach
event.respondWith(
fetch(request)
.then((response) => {
// Cache successful static responses
if (response && response.status === 200 && isStaticFile(url.pathname) &&
(url.protocol === 'http:' || url.protocol === 'https:')) {
const responseClone = response.clone();
caches.open(STATIC_CACHE).then((cache) => {
cache.put(request, responseClone);
}).catch(err => {
console.warn('Failed to cache response:', err);
});
}
return response;
})
.catch(() => {
// Fallback to cache if network fails
return caches.match(request);
})
);
});
// Check if request is for a static file
function isStaticFile(pathname) {
return STATIC_PATTERNS.some(pattern => pattern.test(pathname));
}
// Check if request is an API request
function isAPIRequest(pathname) {
return API_PATTERNS.some(pattern => pattern.test(pathname));
}
// Check if request is for a page
function isPageRequest(request) {
return request.mode === 'navigate';
}
// MYP Platform Service Worker
const STATIC_FILES = [
'/',
'/static/css/tailwind.min.css',
'/static/css/tailwind-dark.min.css',
'/static/js/ui-components.js',
'/static/js/offline-app.js',
'/login',
'/dashboard'
];
// API endpoints to cache
const API_CACHE_PATTERNS = [
/^\/api\/dashboard/,
/^\/api\/printers/,
/^\/api\/jobs/,
/^\/api\/stats/
];
// Handle static file requests - Cache First strategy
async function handleStaticFile(request) {
try {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse.ok) {
const cache = await caches.open(STATIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('Service Worker: Error handling static file', error);
// Return cached version if available
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Return offline fallback
return new Response('Offline - Datei nicht verfügbar', {
status: 503,
statusText: 'Service Unavailable'
});
}
}
// Handle API requests - Network First with cache fallback
async function handleAPIRequest(request) {
const url = new URL(request.url);
// Skip caching for chrome-extension URLs
if (url.protocol === 'chrome-extension:') {
try {
return await fetch(request);
} catch (error) {
console.error('Failed to fetch from chrome-extension:', error);
return new Response(JSON.stringify({
error: 'Fehler beim Zugriff auf chrome-extension',
offline: true
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
}
try {
// Try network first
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// Cache successful GET responses for specific endpoints
if (request.method === 'GET' && shouldCacheAPIResponse(url.pathname)) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
}
throw new Error(`HTTP ${networkResponse.status}`);
} catch (error) {
console.log('Service Worker: Network failed for API request, trying cache');
// Try cache fallback for GET requests
const cachedResponse = await caches.match(request);
if (cachedResponse) {
// Add offline header to indicate cached response
const response = cachedResponse.clone();
response.headers.set('X-Served-By', 'ServiceWorker-Cache');
return response;
}
// Return offline response
return new Response(JSON.stringify({
error: 'Offline - Daten nicht verfügbar',
offline: true,
timestamp: new Date().toISOString()
}), {
status: 503,
statusText: 'Service Unavailable',
headers: {
'Content-Type': 'application/json',
'X-Served-By': 'ServiceWorker-Offline'
}
});
}
}
// Handle page requests - Network First with offline fallback
async function handlePageRequest(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// Cache successful page responses
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
return networkResponse;
}
throw new Error(`HTTP ${networkResponse.status}`);
} catch (error) {
console.log('Service Worker: Network failed for page request, trying cache');
// Try cache fallback
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Fallback offline response
return caches.match('/offline.html') || new Response(
'<html><body><h1>Offline</h1><p>Sie sind momentan offline. Bitte überprüfen Sie Ihre Internetverbindung.</p></body></html>',
{
status: 200,
headers: { 'Content-Type': 'text/html' }
}
);
}
}
// Check if API response should be cached
function shouldCacheAPIResponse(pathname) {
return API_CACHE_PATTERNS.some(pattern => pattern.test(pathname));
}
// Background sync for offline actions
self.addEventListener('sync', (event) => {
console.log('Service Worker: Background sync triggered', event.tag);
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync());
}
});
// Perform background sync
async function doBackgroundSync() {
try {
// Get pending requests from IndexedDB or localStorage
const pendingRequests = await getPendingRequests();
for (const request of pendingRequests) {
try {
await fetch(request.url, request.options);
await removePendingRequest(request.id);
console.log('Service Worker: Synced request', request.url);
} catch (error) {
console.error('Service Worker: Failed to sync request', request.url, error);
}
}
} catch (error) {
console.error('Service Worker: Background sync failed', error);
}
}
// Get pending requests (placeholder - implement with IndexedDB)
async function getPendingRequests() {
// This would typically use IndexedDB to store pending requests
// For now, return empty array
return [];
}
// Remove pending request (placeholder - implement with IndexedDB)
async function removePendingRequest(id) {
// This would typically remove the request from IndexedDB
console.log('Service Worker: Removing pending request', id);
}
// Push notification handling
self.addEventListener('push', (event) => {
console.log('Service Worker: Push notification received');
const options = {
body: 'Sie haben neue Benachrichtigungen',
icon: '/static/icons/icon-192x192.png',
badge: '/static/icons/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: 'Anzeigen',
icon: '/static/icons/checkmark.png'
},
{
action: 'close',
title: 'Schließen',
icon: '/static/icons/xmark.png'
}
]
};
event.waitUntil(
self.registration.showNotification('MYP Platform', options)
);
});
// Notification click handling
self.addEventListener('notificationclick', (event) => {
console.log('Service Worker: Notification clicked');
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/dashboard')
);
}
});
// Message handling from main thread
self.addEventListener('message', (event) => {
console.log('Service Worker: Message received', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CACHE_URLS') {
event.waitUntil(
cacheUrls(event.data.urls)
);
}
});
// Cache specific URLs
async function cacheUrls(urls) {
try {
const cache = await caches.open(DYNAMIC_CACHE);
await cache.addAll(urls);
console.log('Service Worker: URLs cached', urls);
} catch (error) {
console.error('Service Worker: Error caching URLs', error);
}
}
// Periodic background sync (if supported)
self.addEventListener('periodicsync', (event) => {
console.log('Service Worker: Periodic sync triggered', event.tag);
if (event.tag === 'content-sync') {
event.waitUntil(syncContent());
}
});
// Sync content periodically
async function syncContent() {
try {
// Sync critical data in background
const endpoints = ['/api/dashboard', '/api/jobs'];
for (const endpoint of endpoints) {
try {
const response = await fetch(endpoint);
if (response.ok) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(endpoint, response.clone());
}
} catch (error) {
console.error('Service Worker: Error syncing', endpoint, error);
}
}
} catch (error) {
console.error('Service Worker: Content sync failed', error);
}
}
console.log('Service Worker: Script loaded');

File diff suppressed because it is too large Load Diff