🌟 🎉 Major Update:

This commit is contained in:
2025-06-01 23:41:02 +02:00
parent 62efe03887
commit 4042f07c00
228 changed files with 8598 additions and 1893 deletions

Binary file not shown.

View File

@@ -0,0 +1,223 @@
let currentRequests=[];let filteredRequests=[];let currentPage=0;let totalPages=0;let totalRequests=0;let refreshInterval=null;let csrfToken='';function detectApiBaseUrl(){return'';}
const API_BASE_URL=detectApiBaseUrl();document.addEventListener('DOMContentLoaded',function(){csrfToken=document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')||'';initEventListeners();loadGuestRequests();startAutoRefresh();console.log('🎯 Admin Guest Requests Management geladen');});function initEventListeners(){const searchInput=document.getElementById('search-requests');if(searchInput){searchInput.addEventListener('input',debounce(handleSearch,300));}
const statusFilter=document.getElementById('status-filter');if(statusFilter){statusFilter.addEventListener('change',handleFilterChange);}
const sortOrder=document.getElementById('sort-order');if(sortOrder){sortOrder.addEventListener('change',handleSortChange);}
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);}
const selectAllCheckbox=document.getElementById('select-all');if(selectAllCheckbox){selectAllCheckbox.addEventListener('change',handleSelectAll);}}
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;updateStats(data.stats||{});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);}}
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);}});}
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);}
function applyFiltersAndSort(){let requests=[...currentRequests];const statusFilter=document.getElementById('status-filter')?.value;if(statusFilter&&statusFilter!=='all'){requests=requests.filter(req=>req.status===statusFilter);}
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));}
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();}
function getPriorityValue(request){const now=new Date();const created=new Date(request.created_at);const hoursOld=(now-created)/(1000*60*60);let priority=0;if(request.status==='pending')priority+=10;else if(request.status==='approved')priority+=5;if(hoursOld>24)priority+=5;else if(hoursOld>8)priority+=3;else if(hoursOld>2)priority+=1;return priority;}
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;addRowEventListeners();}
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>`;}
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'];}
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({})});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);}}
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');}
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();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));}
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);}
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'

Binary file not shown.

Binary file not shown.

77
backend/static/js/admin-panel.min.js vendored Normal file
View File

@@ -0,0 +1,77 @@
document.addEventListener('DOMContentLoaded',function(){initializeAdminPanel();loadAdminStats();initializeActiveTab();initializeEventHandlers();setInterval(function(){if(document.visibilityState==='visible'){refreshActiveTabData();}},30000);});function initializeActiveTab(){const hash=window.location.hash.substring(1);if(hash){activateTab(hash);}else{const defaultTab=document.querySelector('.nav-tab');if(defaultTab){const tabName=defaultTab.getAttribute('data-tab');activateTab(tabName);}}}
function activateTab(tabName){const tabs=document.querySelectorAll('.nav-tab');tabs.forEach(tab=>{if(tab.getAttribute('data-tab')===tabName){tab.classList.add('active');}else{tab.classList.remove('active');}});const tabPanes=document.querySelectorAll('.tab-pane');tabPanes.forEach(pane=>{if(pane.id===`${tabName}-tab`){pane.classList.add('active');pane.classList.remove('hidden');}else{pane.classList.remove('active');pane.classList.add('hidden');}});switch(tabName){case'users':loadUsers();break;case'printers':loadPrinters();break;case'scheduler':loadSchedulerStatus();break;case'system':loadSystemStats();break;case'logs':loadLogs();break;}
window.location.hash=tabName;}
function refreshActiveTabData(){const activeTab=document.querySelector('.nav-tab.active');if(!activeTab)return;const tabName=activeTab.getAttribute('data-tab');switch(tabName){case'users':loadUsers();break;case'printers':loadPrinters();break;case'scheduler':loadSchedulerStatus();break;case'system':loadSystemStats();break;case'logs':loadLogs();break;}
loadAdminStats();}
function initializeEventHandlers(){const addUserBtn=document.getElementById('add-user-btn');if(addUserBtn){addUserBtn.addEventListener('click',showAddUserModal);}
const addPrinterBtn=document.getElementById('add-printer-btn');if(addPrinterBtn){addPrinterBtn.addEventListener('click',showAddPrinterModal);}
const refreshLogsBtn=document.getElementById('refresh-logs-btn');if(refreshLogsBtn){refreshLogsBtn.addEventListener('click',refreshLogs);}
const closeDeleteModalBtn=document.getElementById('close-delete-modal');if(closeDeleteModalBtn){closeDeleteModalBtn.addEventListener('click',closeDeleteModal);}
const cancelDeleteBtn=document.getElementById('cancel-delete');if(cancelDeleteBtn){cancelDeleteBtn.addEventListener('click',closeDeleteModal);}
const closeAddUserBtn=document.getElementById('close-add-user-modal');if(closeAddUserBtn){closeAddUserBtn.addEventListener('click',closeAddUserModal);}
const cancelAddUserBtn=document.getElementById('cancel-add-user');if(cancelAddUserBtn){cancelAddUserBtn.addEventListener('click',closeAddUserModal);}
const confirmAddUserBtn=document.getElementById('confirm-add-user');if(confirmAddUserBtn){confirmAddUserBtn.addEventListener('click',addUser);}
const closeAddPrinterBtn=document.getElementById('close-add-printer-modal');if(closeAddPrinterBtn){closeAddPrinterBtn.addEventListener('click',closeAddPrinterModal);}
const cancelAddPrinterBtn=document.getElementById('cancel-add-printer');if(cancelAddPrinterBtn){cancelAddPrinterBtn.addEventListener('click',closeAddPrinterModal);}
const confirmAddPrinterBtn=document.getElementById('confirm-add-printer');if(confirmAddPrinterBtn){confirmAddPrinterBtn.addEventListener('click',addPrinter);}
const startSchedulerBtn=document.getElementById('start-scheduler');if(startSchedulerBtn){startSchedulerBtn.addEventListener('click',startScheduler);}
const stopSchedulerBtn=document.getElementById('stop-scheduler');if(stopSchedulerBtn){stopSchedulerBtn.addEventListener('click',stopScheduler);}
const refreshButton=document.createElement('button');refreshButton.id='refresh-admin-btn';refreshButton.className='fixed bottom-8 right-8 p-3 bg-primary text-white rounded-full shadow-lg z-40';refreshButton.innerHTML=`<svg class="w-6 h-6"fill="none"viewBox="0 0 24 24"stroke="currentColor"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>`;refreshButton.title='Alle Daten aktualisieren';refreshButton.addEventListener('click',refreshAllData);if(!document.getElementById('refresh-admin-btn')){document.body.appendChild(refreshButton);}}
function showAddUserModal(){const modal=document.getElementById('add-user-modal');if(modal){const form=document.getElementById('add-user-form');if(form){form.reset();}
modal.classList.remove('hidden');modal.classList.add('show');modal.style.display='flex';}}
function closeAddUserModal(){const modal=document.getElementById('add-user-modal');if(modal){modal.classList.remove('show');modal.classList.add('hidden');modal.style.display='none';}}
async function addUser(){const form=document.getElementById('add-user-form');if(!form)return;const userData={email:document.getElementById('user-email').value,name:document.getElementById('user-name').value,password:document.getElementById('user-password').value,role:document.getElementById('user-role').value,status:document.getElementById('user-status').value};try{const response=await fetch('/api/users',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':getCSRFToken()},body:JSON.stringify(userData)});if(!response.ok){throw new Error('Fehler beim Hinzufügen des Benutzers');}
showNotification('Benutzer erfolgreich hinzugefügt','success');closeAddUserModal();loadUsers();}catch(error){console.error('Error adding user:',error);showNotification(error.message,'error');}}
function showAddPrinterModal(){const modal=document.getElementById('add-printer-modal');if(modal){const form=document.getElementById('add-printer-form');if(form){form.reset();}
modal.classList.remove('hidden');modal.classList.add('show');modal.style.display='flex';}}
function closeAddPrinterModal(){const modal=document.getElementById('add-printer-modal');if(modal){modal.classList.remove('show');modal.classList.add('hidden');modal.style.display='none';}}
async function addPrinter(){const form=document.getElementById('add-printer-form');if(!form)return;const printerData={name:document.getElementById('printer-name').value,model:document.getElementById('printer-model').value,ip_address:document.getElementById('printer-ip').value,location:document.getElementById('printer-location').value,status:document.getElementById('printer-status').value};try{const response=await fetch('/api/printers',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':getCSRFToken()},body:JSON.stringify(printerData)});if(!response.ok){throw new Error('Fehler beim Hinzufügen des Druckers');}
showNotification('Drucker erfolgreich hinzugefügt','success');closeAddPrinterModal();loadPrinters();}catch(error){console.error('Error adding printer:',error);showNotification(error.message,'error');}}
function refreshLogs(){loadLogs();showNotification('Logs aktualisiert','info');}
function closeDeleteModal(){const modal=document.getElementById('delete-confirm-modal');if(modal){modal.classList.remove('show');modal.style.display='none';}}
function showDeleteConfirmation(id,type,name,callback){const modal=document.getElementById('delete-confirm-modal');const messageEl=document.getElementById('delete-message');const idField=document.getElementById('delete-id');const typeField=document.getElementById('delete-type');const confirmBtn=document.getElementById('confirm-delete');if(modal&&messageEl&&idField&&typeField&&confirmBtn){messageEl.textContent=`Möchten Sie ${type==='user'?'den Benutzer':'den Drucker'}"${name}"wirklich löschen?`;idField.value=id;typeField.value=type;confirmBtn.onclick=function(){if(typeof callback==='function'){callback(id,name);}
closeDeleteModal();};modal.classList.add('show');modal.style.display='flex';}}
function initializeAdminPanel(){const tabs=document.querySelectorAll('.nav-tab');tabs.forEach(tab=>{tab.addEventListener('click',function(){const tabName=this.getAttribute('data-tab');if(!tabName)return;activateTab(tabName);});});const logLevelFilter=document.getElementById('log-level-filter');if(logLevelFilter){logLevelFilter.addEventListener('change',function(){if(window.logsData){const level=this.value;if(level==='all'){window.filteredLogs=[...window.logsData];}else{window.filteredLogs=window.logsData.filter(log=>log.level.toLowerCase()===level);}
renderLogs();}});}
const confirmDeleteBtn=document.getElementById('confirm-delete');if(confirmDeleteBtn){confirmDeleteBtn.addEventListener('click',function(){const id=document.getElementById('delete-id').value;const type=document.getElementById('delete-type').value;if(type==='user'){deleteUser(id);}else if(type==='printer'){deletePrinter(id);}
closeDeleteModal();});}}
async function loadAdminStats(){try{const response=await fetch('/api/admin/stats');if(!response.ok){throw new Error('Fehler beim Laden der Admin-Statistiken');}
const data=await response.json();const statsContainer=document.getElementById('admin-stats');if(!statsContainer){console.warn('Stats-Container nicht gefunden');return;}
statsContainer.innerHTML=`<div class="stat-card"><div class="stat-icon"><svg class="w-10 h-10"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg></div><div class="stat-title">Benutzer</div><div class="stat-value"id="admin-total-users-count">${data.total_users||0}</div><div class="stat-desc">Registrierte Benutzer</div></div><div class="stat-card"><div class="stat-icon"><svg class="w-10 h-10"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/></svg></div><div class="stat-title">Drucker</div><div class="stat-value"id="admin-total-printers-count">${data.total_printers||0}</div><div class="stat-desc">Verbundene Drucker</div></div><div class="stat-card"><div class="stat-icon"><svg class="w-10 h-10"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg></div><div class="stat-title">Aktive Jobs</div><div class="stat-value"id="admin-active-jobs-count">${data.active_jobs||0}</div><div class="stat-desc">Laufende Druckaufträge</div></div><div class="stat-card"><div class="stat-icon"><svg class="w-10 h-10"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></div><div class="stat-title">Erfolgsrate</div><div class="stat-value"id="admin-success-rate">${data.success_rate||'0%'}</div><div class="stat-desc">Erfolgreiche Druckaufträge</div></div>`;console.log('✅ Admin-Statistiken erfolgreich aktualisiert:',data);}catch(error){console.error('Error loading admin stats:',error);showNotification('Fehler beim Laden der Admin-Statistiken','error');}}
async function loadUsers(){try{const response=await fetch('/api/users');if(!response.ok){throw new Error('Fehler beim Laden der Benutzer');}
const data=await response.json();const userTableBody=document.querySelector('#users-tab table tbody');if(!userTableBody)return;let html='';if(data.users&&data.users.length>0){data.users.forEach(user=>{html+=`<tr class="border-b border-gray-200 dark:border-gray-700"><td class="px-4 py-3">${user.id}</td><td class="px-4 py-3">${user.name||'-'}</td><td class="px-4 py-3">${user.email}</td><td class="px-4 py-3"><span class="px-2 py-1 text-xs rounded-full ${user.active ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}">${user.active?'Aktiv':'Inaktiv'}</span></td><td class="px-4 py-3">${user.role||'Benutzer'}</td><td class="px-4 py-3"><div class="flex justify-end space-x-2"><button onclick="editUser(${user.id})"class="p-1 text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-200"><svg class="w-5 h-5"fill="none"viewBox="0 0 24 24"stroke="currentColor"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg></button><button onclick="showDeleteConfirmation(${user.id}, 'user', '${user.name || user.email}', deleteUser)"class="p-1 text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-200"><svg class="w-5 h-5"fill="none"viewBox="0 0 24 24"stroke="currentColor"><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>`;});}else{html=`<tr><td colspan="6"class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">Keine Benutzer gefunden</td></tr>`;}
userTableBody.innerHTML=html;}catch(error){console.error('Error loading users:',error);showNotification('Fehler beim Laden der Benutzer','error');}}
async function loadPrinters(){try{const response=await fetch('/api/printers');if(!response.ok){throw new Error('Fehler beim Laden der Drucker');}
const data=await response.json();const printerTableBody=document.querySelector('#printers-tab table tbody');if(!printerTableBody)return;let html='';if(data.printers&&data.printers.length>0){data.printers.forEach(printer=>{html+=`<tr class="border-b border-gray-200 dark:border-gray-700"><td class="px-4 py-3">${printer.id}</td><td class="px-4 py-3">${printer.name}</td><td class="px-4 py-3">${printer.model||'-'}</td><td class="px-4 py-3">${printer.ip_address||'-'}</td><td class="px-4 py-3">${printer.location||'-'}</td><td class="px-4 py-3"><span class="px-2 py-1 text-xs rounded-full ${printer.status === 'online' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}">${printer.status==='online'?'Online':'Offline'}</span></td><td class="px-4 py-3"><div class="flex justify-end space-x-2"><button onclick="editPrinter(${printer.id})"class="p-1 text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-200"><svg class="w-5 h-5"fill="none"viewBox="0 0 24 24"stroke="currentColor"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg></button><button onclick="showDeleteConfirmation(${printer.id}, 'printer', '${printer.name}', deletePrinter)"class="p-1 text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-200"><svg class="w-5 h-5"fill="none"viewBox="0 0 24 24"stroke="currentColor"><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>`;});}else{html=`<tr><td colspan="7"class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">Keine Drucker gefunden</td></tr>`;}
printerTableBody.innerHTML=html;}catch(error){console.error('Error loading printers:',error);showNotification('Fehler beim Laden der Drucker','error');}}
async function loadSchedulerStatus(){try{const response=await fetch('/api/scheduler/status');if(!response.ok){throw new Error('Fehler beim Laden des Scheduler-Status');}
const data=await response.json();const statusIndicator=document.getElementById('scheduler-status-indicator');const statusText=document.getElementById('scheduler-status-text');const startSchedulerBtn=document.getElementById('start-scheduler');const stopSchedulerBtn=document.getElementById('stop-scheduler');if(statusIndicator&&statusText){if(data.active){statusIndicator.classList.remove('bg-red-500');statusIndicator.classList.add('bg-green-500');statusText.textContent='Aktiv';if(startSchedulerBtn&&stopSchedulerBtn){startSchedulerBtn.disabled=true;stopSchedulerBtn.disabled=false;}}else{statusIndicator.classList.remove('bg-green-500');statusIndicator.classList.add('bg-red-500');statusText.textContent='Inaktiv';if(startSchedulerBtn&&stopSchedulerBtn){startSchedulerBtn.disabled=false;stopSchedulerBtn.disabled=true;}}}
const schedulerDetails=document.getElementById('scheduler-details');if(schedulerDetails){schedulerDetails.innerHTML=`<div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Letzte Ausführung</h4><p class="text-gray-800 dark:text-gray-100">${data.last_run?new Date(data.last_run).toLocaleString('de-DE'):'Nie'}</p></div><div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Nächste Ausführung</h4><p class="text-gray-800 dark:text-gray-100">${data.next_run?new Date(data.next_run).toLocaleString('de-DE'):'Nicht geplant'}</p></div><div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Ausführungsintervall</h4><p class="text-gray-800 dark:text-gray-100">${data.interval||'60'}Sekunden</p></div><div><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Verarbeitete Jobs</h4><p class="text-gray-800 dark:text-gray-100">${data.processed_jobs||0}Jobs seit dem letzten Neustart</p></div>`;}
const jobQueueContainer=document.getElementById('job-queue');if(jobQueueContainer&&data.pending_jobs){let queueHtml='';if(data.pending_jobs.length>0){queueHtml=`<div class="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden"><table class="min-w-full"><thead class="bg-gray-50 dark:bg-gray-700"><tr><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Job ID</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Name</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Geplant für</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Status</th></tr></thead><tbody class="divide-y divide-gray-200 dark:divide-gray-700">`;data.pending_jobs.forEach(job=>{queueHtml+=`<tr><td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">${job.id}</td><td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">${job.name}</td><td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">${new Date(job.start_time).toLocaleString('de-DE')}</td><td class="px-4 py-3 text-sm"><span class="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">Warten</span></td></tr>`;});queueHtml+=`</tbody></table></div>`;}else{queueHtml=`<div class="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6 text-center"><p class="text-gray-500 dark:text-gray-400">Keine ausstehenden Jobs in der Warteschlange</p></div>`;}
jobQueueContainer.innerHTML=queueHtml;}}catch(error){console.error('Error loading scheduler status:',error);showNotification('Fehler beim Laden des Scheduler-Status','error');}}
async function loadSystemStats(){try{const response=await fetch('/api/system/stats');if(!response.ok){throw new Error('Fehler beim Laden der Systemstatistiken');}
const data=await response.json();const cpuUsageElement=document.getElementById('cpu-usage');if(cpuUsageElement){cpuUsageElement.style.width=`${data.cpu_usage||0}%`;cpuUsageElement.textContent=`${data.cpu_usage||0}%`;}
const ramUsageElement=document.getElementById('ram-usage');if(ramUsageElement){ramUsageElement.style.width=`${data.ram_usage_percent||0}%`;ramUsageElement.textContent=`${data.ram_usage_percent||0}%`;}
const diskUsageElement=document.getElementById('disk-usage');if(diskUsageElement){diskUsageElement.style.width=`${data.disk_usage_percent||0}%`;diskUsageElement.textContent=`${data.disk_usage_percent||0}%`;}
const systemDetailsElement=document.getElementById('system-details');if(systemDetailsElement){systemDetailsElement.innerHTML=`<div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">System</h4><p class="text-gray-800 dark:text-gray-100">${data.os_name||'Unbekannt'}${data.os_version||''}</p></div><div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Laufzeit</h4><p class="text-gray-800 dark:text-gray-100">${data.uptime||'Unbekannt'}</p></div><div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Python-Version</h4><p class="text-gray-800 dark:text-gray-100">${data.python_version||'Unbekannt'}</p></div><div><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Server-Zeit</h4><p class="text-gray-800 dark:text-gray-100">${data.server_time?new Date(data.server_time).toLocaleString('de-DE'):'Unbekannt'}</p></div>`;}
const systemEventsElement=document.getElementById('system-events');if(systemEventsElement&&data.recent_events){let eventsHtml='';if(data.recent_events.length>0){eventsHtml='<ul class="divide-y divide-gray-200 dark:divide-gray-700">';data.recent_events.forEach(event=>{let eventTypeClass='text-blue-600 dark:text-blue-400';switch(event.type.toLowerCase()){case'error':eventTypeClass='text-red-600 dark:text-red-400';break;case'warning':eventTypeClass='text-yellow-600 dark:text-yellow-400';break;case'success':eventTypeClass='text-green-600 dark:text-green-400';break;}
eventsHtml+=`<li class="py-3"><div class="flex items-start"><span class="${eventTypeClass} mr-2"><svg class="h-5 w-5"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></span><div><p class="text-sm text-gray-800 dark:text-gray-200">${event.message}</p><p class="text-xs text-gray-500 dark:text-gray-400">${new Date(event.timestamp).toLocaleString('de-DE')}</p></div></div></li>`;});eventsHtml+='</ul>';}else{eventsHtml='<p class="text-center text-gray-500 dark:text-gray-400 py-4">Keine Ereignisse vorhanden</p>';}
systemEventsElement.innerHTML=eventsHtml;}}catch(error){console.error('Error loading system stats:',error);showNotification('Fehler beim Laden der Systemstatistiken','error');}}
async function loadLogs(){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();}catch(error){console.error('Error loading logs:',error);showNotification('Fehler beim Laden der Logs','error');}}
function renderLogs(){const logsContainer=document.getElementById('logs-container');if(!logsContainer)return;if(!window.filteredLogs||window.filteredLogs.length===0){logsContainer.innerHTML='<div class="p-8 text-center text-gray-500 dark:text-gray-400">Keine Logs gefunden</div>';return;}
let html='';window.filteredLogs.forEach(log=>{let levelClass='';switch(log.level.toLowerCase()){case'error':levelClass='bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';break;case'warning':levelClass='bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';break;case'info':levelClass='bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';break;case'debug':levelClass='bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';break;default:levelClass='bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';}
html+=`<div class="log-entry border-l-4 border-gray-300 dark:border-gray-600 pl-4 py-3 mb-3"><div class="flex items-center justify-between mb-1"><div><span class="px-2 py-1 text-xs rounded-full ${levelClass} mr-2">${log.level}</span><span class="text-sm text-gray-500 dark:text-gray-400">${log.timestamp?new Date(log.timestamp).toLocaleString('de-DE'):''}</span></div><div class="text-sm text-gray-500 dark:text-gray-400">${log.source||'System'}</div></div><div class="text-sm text-gray-800 dark:text-gray-200">${log.message}</div>${log.details?`<div class="mt-1 text-xs text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 p-2 rounded overflow-auto"><pre>${log.details}</pre></div>`:''}</div>`;});logsContainer.innerHTML=html;}
async function deleteUser(userId){try{const response=await fetch(`/api/users/${userId}`,{method:'DELETE',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Löschen des Benutzers');}
showNotification('Benutzer erfolgreich gelöscht','success');loadUsers();}catch(error){console.error('Error deleting user:',error);showNotification(error.message,'error');}}
async function deletePrinter(printerId){try{const response=await fetch(`/api/printers/${printerId}`,{method:'DELETE',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Löschen des Druckers');}
showNotification('Drucker erfolgreich gelöscht','success');loadPrinters();}catch(error){console.error('Error deleting printer:',error);showNotification(error.message,'error');}}
async function startScheduler(){try{const response=await fetch('/api/scheduler/start',{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Starten des Schedulers');}
showNotification('Scheduler erfolgreich gestartet','success');loadSchedulerStatus();}catch(error){console.error('Error starting scheduler:',error);showNotification(error.message,'error');}}
async function stopScheduler(){try{const response=await fetch('/api/scheduler/stop',{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Stoppen des Schedulers');}
showNotification('Scheduler erfolgreich gestoppt','success');loadSchedulerStatus();}catch(error){console.error('Error stopping scheduler:',error);showNotification(error.message,'error');}}
function getCSRFToken(){return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')||'';}
function refreshAllData(){loadAdminStats();const activeTab=document.querySelector('.nav-tab.active');if(activeTab){const tabName=activeTab.getAttribute('data-tab');activateTab(tabName);}
showNotification('Daten wurden aktualisiert','success');}
function showNotification(message,type='info'){const notification=document.getElementById('notification');const messageEl=document.getElementById('notification-message');const iconEl=document.getElementById('notification-icon');if(!notification||!messageEl||!iconEl)return;messageEl.textContent=message;notification.classList.remove('notification-success','notification-error','notification-warning','notification-info');notification.classList.add(`notification-${type}`);let icon='';switch(type){case'success':icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />';break;case'error':icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />';break;case'warning':icon='<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" />';break;case'info':default:icon='<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" />';break;}
iconEl.innerHTML=icon;notification.classList.remove('hidden');notification.classList.add('show');setTimeout(()=>{notification.classList.remove('show');setTimeout(()=>{notification.classList.add('hidden');},300);},5000);}

Binary file not shown.

Binary file not shown.

101
backend/static/js/admin-unified.min.js vendored Normal file
View File

@@ -0,0 +1,101 @@
class AdminDashboard{constructor(){this.csrfToken=null;this.updateInterval=null;this.eventListenersAttached=false;this.apiBaseUrl=this.detectApiBaseUrl();this.retryCount=0;this.maxRetries=3;this.isInitialized=false;this.init();}
detectApiBaseUrl(){const currentHost=window.location.hostname;const currentPort=window.location.port;if(currentPort==='5000'){return'';}
return`http:}
init(){if(this.isInitialized){console.log('🔄 Admin Dashboard bereits initialisiert, überspringe...');return;}
console.log('🚀 Initialisiere Mercedes-Benz MYP Admin Dashboard');this.csrfToken=this.extractCSRFToken();console.log('🔒 CSRF Token:',this.csrfToken?'verfügbar':'FEHLT!');this.attachEventListeners();this.startLiveUpdates();this.loadInitialData();this.isInitialized=true;console.log('✅ Admin Dashboard erfolgreich initialisiert');}
extractCSRFToken(){const metaToken=document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');if(metaToken){console.log('🔒 CSRF Token aus meta Tag geladen');return metaToken;}
const hiddenInput=document.querySelector('input[name="csrf_token"]')?.value;if(hiddenInput){console.log('🔒 CSRF Token aus hidden input geladen');return hiddenInput;}
const cookieToken=document.cookie.split('; ').find(row=>row.startsWith('csrf_token='))?.split('=')[1];if(cookieToken){console.log('🔒 CSRF Token aus Cookie geladen');return cookieToken;}
const flaskToken=document.querySelector('meta[name="csrf-token"]')?.content;if(flaskToken){console.log('🔒 CSRF Token aus Flask-WTF Meta geladen');return flaskToken;}
console.error('❌ CSRF Token konnte nicht gefunden werden!');return null;}
attachEventListeners(){if(this.eventListenersAttached){console.log('⚠️ Event-Listener bereits registriert, überspringe...');return;}
this.attachSystemButtons();this.attachUserManagement();this.attachPrinterManagement();this.attachJobManagement();this.attachModalEvents();this.eventListenersAttached=true;console.log('📌 Event-Listener erfolgreich registriert');}
attachSystemButtons(){this.addEventListenerSafe('#system-status-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showSystemStatus();});this.addEventListenerSafe('#analytics-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showAnalytics();});this.addEventListenerSafe('#clear-cache-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.clearSystemCache();});this.addEventListenerSafe('#optimize-db-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.optimizeDatabase();});this.addEventListenerSafe('#create-backup-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.createSystemBackup();});this.addEventListenerSafe('#force-init-printers-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.forceInitializePrinters();});}
attachUserManagement(){this.addEventListenerSafe('#add-user-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showUserModal();});document.addEventListener('click',(e)=>{if(e.target.closest('.edit-user-btn')){e.preventDefault();e.stopPropagation();const userId=e.target.closest('button').dataset.userId;this.editUser(userId);}
if(e.target.closest('.delete-user-btn')){e.preventDefault();e.stopPropagation();const userId=e.target.closest('button').dataset.userId;const userName=e.target.closest('button').dataset.userName;this.deleteUser(userId,userName);}});}
attachPrinterManagement(){this.addEventListenerSafe('#add-printer-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showPrinterModal();});document.addEventListener('click',(e)=>{if(e.target.closest('.manage-printer-btn')){e.preventDefault();e.stopPropagation();const printerId=e.target.closest('button').dataset.printerId;this.managePrinter(printerId);}
if(e.target.closest('.settings-printer-btn')){e.preventDefault();e.stopPropagation();const printerId=e.target.closest('button').dataset.printerId;this.showPrinterSettings(printerId);}
if(e.target.closest('.toggle-printer-power-btn')){e.preventDefault();e.stopPropagation();const button=e.target.closest('button');const printerId=button.dataset.printerId;const printerName=button.dataset.printerName;this.togglePrinterPower(printerId,printerName,button);}});}
attachJobManagement(){document.addEventListener('click',(e)=>{if(e.target.closest('.job-action-btn')){e.preventDefault();e.stopPropagation();const action=e.target.closest('button').dataset.action;const jobId=e.target.closest('button').dataset.jobId;this.handleJobAction(action,jobId);}});}
attachModalEvents(){this.addEventListenerSafe('#fix-errors-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.fixErrors();});this.addEventListenerSafe('#dismiss-errors-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.dismissErrors();});this.addEventListenerSafe('#view-error-details-btn','click',(e)=>{e.preventDefault();e.stopPropagation();window.location.href='/admin-dashboard?tab=logs';});this.addEventListenerSafe('#refresh-logs-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.loadLogs();});this.addEventListenerSafe('#export-logs-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.exportLogs();});this.addEventListenerSafe('#log-level-filter','change',(e)=>{this.loadLogs();});}
addEventListenerSafe(selector,event,handler){const element=document.querySelector(selector);if(element&&!element.dataset.listenerAttached){element.addEventListener(event,handler);element.dataset.listenerAttached='true';}}
startLiveUpdates(){if(this.updateInterval){clearInterval(this.updateInterval);}
this.updateInterval=setInterval(()=>{this.loadLiveStats();},30000);setInterval(()=>{this.updateLiveTime();},1000);setInterval(()=>{this.checkSystemHealth();},30000);console.log('🔄 Live-Updates gestartet');}
async loadInitialData(){await this.loadLiveStats();await this.checkSystemHealth();setTimeout(()=>{this.testButtons();},1000);if(window.location.search.includes('tab=logs')||document.querySelector('.tabs [href*="logs"]')?.classList.contains('active')){await this.loadLogs();}
const urlParams=new URLSearchParams(window.location.search);const activeTab=urlParams.get('tab');if(activeTab==='logs'){await this.loadLogs();}
const logsContainer=document.getElementById('logs-container');if(logsContainer&&logsContainer.offsetParent!==null){await this.loadLogs();}}
async loadLiveStats(){try{const url=`${this.apiBaseUrl}/api/stats`;const response=await fetch(url);if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
const data=await response.json();this.updateStatsDisplay(data);this.retryCount=0;}catch(error){console.error('Fehler beim Laden der Live-Statistiken:',error);this.retryCount++;if(this.retryCount<=this.maxRetries){setTimeout(()=>this.loadLiveStats(),5000);}}}
updateStatsDisplay(data){this.updateElement('live-users-count',data.total_users||0);this.updateElement('live-printers-count',data.total_printers||0);this.updateElement('live-printers-online',`${data.online_printers||0}online`);this.updateElement('live-jobs-active',data.active_jobs||0);this.updateElement('live-jobs-queued',`${data.queued_jobs||0}in Warteschlange`);this.updateElement('live-success-rate',`${data.success_rate||0}%`);this.updateProgressBar('users-progress',data.total_users,20);this.updateProgressBar('printers-progress',data.online_printers,data.total_printers);this.updateProgressBar('jobs-progress',data.active_jobs,10);this.updateProgressBar('success-progress',data.success_rate,100);console.log('📊 Live-Statistiken aktualisiert');}
updateElement(elementId,value){const element=document.getElementById(elementId);if(element){element.textContent=value;}}
updateProgressBar(progressId,currentValue,maxValue){const progressEl=document.getElementById(progressId);if(progressEl&&currentValue!==undefined&&maxValue>0){const percentage=Math.min(100,Math.max(0,(currentValue/maxValue)*100));progressEl.style.width=`${percentage}%`;}}
updateLiveTime(){const timeElement=document.getElementById('live-time');if(timeElement){const now=new Date();timeElement.textContent=now.toLocaleTimeString('de-DE');}}
async showSystemStatus(){console.log('🔧 System Status wird angezeigt');this.showNotification('System Status wird geladen...','info');}
async showAnalytics(){console.log('📈 Analytics wird angezeigt');this.showNotification('Analytics werden geladen...','info');}
async showMaintenance(){console.log('🛠️ Wartung wird angezeigt');const systemTab=document.querySelector('a[href*="tab=system"]');if(systemTab){systemTab.click();}}
async clearSystemCache(){if(!confirm('🗑️ Möchten Sie wirklich den System-Cache leeren?'))return;try{const response=await fetch(`${this.apiBaseUrl}/api/admin/cache/clear`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Cache erfolgreich geleert!','success');setTimeout(()=>window.location.reload(),2000);}else{this.showNotification('❌ Fehler beim Leeren des Cache','error');}}catch(error){this.showNotification('❌ Fehler beim Leeren des Cache','error');}}
async optimizeDatabase(){if(!confirm('🔧 Möchten Sie die Datenbank optimieren?'))return;this.showNotification('🔄 Datenbank wird optimiert...','info');try{const response=await fetch(`${this.apiBaseUrl}/api/admin/database/optimize`,{method:'POST',headers:{'X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Datenbank erfolgreich optimiert!','success');}else{this.showNotification('❌ Fehler bei der Datenbank-Optimierung','error');}}catch(error){this.showNotification('❌ Fehler bei der Datenbank-Optimierung','error');}}
async createSystemBackup(){if(!confirm('💾 Möchten Sie ein System-Backup erstellen?'))return;this.showNotification('🔄 Backup wird erstellt...','info');try{const response=await fetch(`${this.apiBaseUrl}/api/admin/backup/create`,{method:'POST',headers:{'X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Backup erfolgreich erstellt!','success');}else{this.showNotification('❌ Fehler beim Erstellen des Backups','error');}}catch(error){this.showNotification('❌ Fehler beim Erstellen des Backups','error');}}
async forceInitializePrinters(){if(!confirm('🔄 Möchten Sie die Drucker-Initialisierung erzwingen?'))return;this.showNotification('🔄 Drucker werden initialisiert...','info');try{const response=await fetch(`${this.apiBaseUrl}/api/admin/printers/force-init`,{method:'POST',headers:{'X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Drucker erfolgreich initialisiert!','success');setTimeout(()=>window.location.reload(),2000);}else{this.showNotification('❌ Fehler bei der Drucker-Initialisierung','error');}}catch(error){this.showNotification('❌ Fehler bei der Drucker-Initialisierung','error');}}
showUserModal(userId=null){const isEdit=userId!==null;const title=isEdit?'Benutzer bearbeiten':'Neuer Benutzer';const modalHtml=`<div id="user-modal"class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"><div class="bg-white dark:bg-slate-800 rounded-2xl p-8 max-w-md w-full shadow-2xl transform scale-100 transition-all duration-300"><div class="flex justify-between items-center mb-6"><h3 class="text-2xl font-bold text-slate-900 dark:text-white">${title}</h3><button onclick="this.closest('#user-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"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><form id="user-form"class="space-y-4"><div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">E-Mail-Adresse*</label><input type="email"name="email"id="user-email"required
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all
dark:bg-slate-700 dark:text-white bg-slate-50"></div><div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Benutzername</label><input type="text"name="username"id="user-username"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all
dark:bg-slate-700 dark:text-white bg-slate-50"></div><div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Name</label><input type="text"name="name"id="user-name"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all
dark:bg-slate-700 dark:text-white bg-slate-50"></div><div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Passwort ${isEdit?'(leer lassen für keine Änderung)':'*'}</label><input type="password"name="password"id="user-password"${!isEdit?'required':''}
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all
dark:bg-slate-700 dark:text-white bg-slate-50"></div><div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Rolle</label><select name="role"id="user-role"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all
dark:bg-slate-700 dark:text-white bg-slate-50"><option value="user">Benutzer</option><option value="admin">Administrator</option></select></div>${isEdit?`<div class="flex items-center space-x-2"><input type="checkbox"name="is_active"id="user-active"
class="w-4 h-4 text-blue-600 bg-slate-100 border-slate-300 rounded focus:ring-blue-500"><label for="user-active"class="text-sm font-medium text-slate-700 dark:text-slate-300">Aktiv</label></div>`:''}<div class="flex justify-end space-x-3 mt-8 pt-6 border-t border-slate-200 dark:border-slate-600"><button type="button"onclick="this.closest('#user-modal').remove()"
class="px-6 py-3 bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-300
rounded-xl hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors font-medium">Abbrechen</button><button type="submit"id="user-submit-btn"
class="px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white
rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300
shadow-lg hover:shadow-xl font-medium">${isEdit?'Aktualisieren':'Erstellen'}</button></div></form></div></div>`;document.body.insertAdjacentHTML('beforeend',modalHtml);const form=document.getElementById('user-form');form.addEventListener('submit',(e)=>{e.preventDefault();e.stopPropagation();if(isEdit){this.updateUser(userId,new FormData(form));}else{this.createUser(new FormData(form));}});if(isEdit){this.loadUserData(userId);}
setTimeout(()=>{document.getElementById('user-email').focus();},100);}
async loadUserData(userId){try{const response=await fetch(`${this.apiBaseUrl}/api/admin/users/${userId}`);if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
const data=await response.json();if(data.success){const user=data.user;document.getElementById('user-email').value=user.email||'';document.getElementById('user-username').value=user.username||'';document.getElementById('user-name').value=user.name||'';document.getElementById('user-role').value=user.is_admin?'admin':'user';const activeCheckbox=document.getElementById('user-active');if(activeCheckbox){activeCheckbox.checked=user.is_active!==false;}}else{this.showNotification('❌ Fehler beim Laden der Benutzerdaten','error');}}catch(error){console.error('Fehler beim Laden der Benutzerdaten:',error);this.showNotification('❌ Fehler beim Laden der Benutzerdaten','error');}}
async createUser(formData){const submitBtn=document.getElementById('user-submit-btn');const originalText=submitBtn.innerHTML;try{submitBtn.innerHTML='<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-white mx-auto"></div>';submitBtn.disabled=true;const userData={email:formData.get('email'),username:formData.get('username')||formData.get('email').split('@')[0],name:formData.get('name'),password:formData.get('password'),is_admin:formData.get('role')==='admin'};const response=await fetch(`${this.apiBaseUrl}/api/admin/users`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken},body:JSON.stringify(userData)});const data=await response.json();if(data.success){this.showNotification('✅ Benutzer erfolgreich erstellt!','success');document.getElementById('user-modal').remove();setTimeout(()=>{window.location.reload();},1000);}else{this.showNotification(`❌ Fehler:${data.error}`,'error');}}catch(error){console.error('Fehler beim Erstellen des Benutzers:',error);this.showNotification('❌ Fehler beim Erstellen des Benutzers','error');}finally{submitBtn.innerHTML=originalText;submitBtn.disabled=false;}}
async updateUser(userId,formData){const submitBtn=document.getElementById('user-submit-btn');const originalText=submitBtn.innerHTML;try{submitBtn.innerHTML='<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-white mx-auto"></div>';submitBtn.disabled=true;const userData={email:formData.get('email'),username:formData.get('username'),name:formData.get('name'),is_admin:formData.get('role')==='admin',is_active:formData.get('is_active')==='on'};const password=formData.get('password');if(password&&password.trim()){userData.password=password;}
const response=await fetch(`${this.apiBaseUrl}/api/admin/users/${userId}`,{method:'PUT',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken},body:JSON.stringify(userData)});const data=await response.json();if(data.success){this.showNotification('✅ Benutzer erfolgreich aktualisiert!','success');document.getElementById('user-modal').remove();setTimeout(()=>{window.location.reload();},1000);}else{this.showNotification(`❌ Fehler:${data.error}`,'error');}}catch(error){console.error('Fehler beim Aktualisieren des Benutzers:',error);this.showNotification('❌ Fehler beim Aktualisieren des Benutzers','error');}finally{submitBtn.innerHTML=originalText;submitBtn.disabled=false;}}
editUser(userId){console.log(`✏️ Benutzer ${userId}wird bearbeitet`);this.showUserModal(userId);}
async deleteUser(userId,userName){if(!confirm(`🗑️ Möchten Sie den Benutzer"${userName}"wirklich löschen?\n\nDiese Aktion kann nicht rückgängig gemacht werden!`)){return;}
try{this.showNotification(`🔄 Benutzer"${userName}"wird gelöscht...`,'info');const response=await fetch(`${this.apiBaseUrl}/api/admin/users/${userId}`,{method:'DELETE',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification(`✅ Benutzer"${userName}"erfolgreich gelöscht!`,'success');setTimeout(()=>{window.location.reload();},1000);}else{this.showNotification(`❌ Fehler beim Löschen:${data.error}`,'error');}}catch(error){console.error('Fehler beim Löschen des Benutzers:',error);this.showNotification('❌ Fehler beim Löschen des Benutzers','error');}}
showPrinterModal(){console.log('🖨️ Drucker-Modal wird angezeigt');this.showNotification('Drucker-Funktionen werden geladen...','info');}
managePrinter(printerId){console.log(`🔧 Drucker ${printerId}wird verwaltet`);this.showNotification(`Drucker ${printerId}wird verwaltet...`,'info');}
showPrinterSettings(printerId){console.log(`⚙️ Drucker-Einstellungen ${printerId}werden angezeigt`);this.showNotification(`Drucker-Einstellungen werden geladen...`,'info');}
async togglePrinterPower(printerId,printerName,button){console.log(`🔌 Smart-Plug Toggle für Drucker ${printerId}(${printerName})`);const confirmMessage=`Möchten Sie die Steckdose für"${printerName}"umschalten?\n\nDies schaltet den Drucker ein/aus.`;if(!confirm(confirmMessage))return;const originalContent=button.innerHTML;button.disabled=true;button.innerHTML=`<div class="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div><span class="hidden lg:inline">Schaltet...</span>`;try{const response=await fetch(`${this.apiBaseUrl}/api/admin/printers/${printerId}/toggle`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken},body:JSON.stringify({reason:'Admin-Panel Smart-Plug Toggle'})});const data=await response.json();if(response.ok&&data.success){const action=data.action||'umgeschaltet';this.showNotification(`✅ Steckdose für"${printerName}"erfolgreich ${action}`,'success');button.classList.remove('from-orange-500','to-red-500');button.classList.add('from-green-500','to-green-600');setTimeout(()=>{button.classList.remove('from-green-500','to-green-600');button.classList.add('from-orange-500','to-red-500');},2000);setTimeout(()=>{this.loadLiveStats();},1000);}else{const errorMsg=data.error||'Unbekannter Fehler beim Schalten der Steckdose';this.showNotification(`❌ Fehler:${errorMsg}`,'error');console.error('Smart-Plug Toggle Fehler:',data);}}catch(error){console.error('Netzwerkfehler beim Smart-Plug Toggle:',error);this.showNotification(`❌ Netzwerkfehler beim Schalten der Steckdose für"${printerName}"`,'error');}finally{button.disabled=false;button.innerHTML=originalContent;}}
handleJobAction(action,jobId){console.log(`📋 Job-Aktion"${action}"für Job ${jobId}`);this.showNotification(`Job-Aktion"${action}"wird ausgeführt...`,'info');}
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);}}catch(error){console.error('Fehler bei System-Health-Check:',error);}}
updateHealthDisplay(data){const statusIndicator=document.getElementById('db-status-indicator');const statusText=document.getElementById('db-status-text');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';}}
this.updateElement('last-migration',data.last_migration||'Unbekannt');this.updateElement('schema-integrity',data.schema_integrity||'Prüfung');this.updateElement('recent-errors-count',data.recent_errors_count||0);}
updateErrorAlerts(data){const alertContainer=document.getElementById('critical-errors-alert');if(!alertContainer)return;const allErrors=[...(data.critical_errors||[]),...(data.warnings||[])];if(allErrors.length>0){alertContainer.classList.remove('hidden');}else{alertContainer.classList.add('hidden');}}
async fixErrors(){if(!confirm('🔧 Möchten Sie die automatische Fehlerkorrektur durchführen?'))return;this.showNotification('🔄 Fehler werden automatisch behoben...','info');if(!this.csrfToken){console.error('❌ CSRF Token fehlt! Versuche Token neu zu laden...');this.csrfToken=this.extractCSRFToken();if(!this.csrfToken){this.showNotification('❌ Sicherheitsfehler: CSRF Token nicht verfügbar','error');return;}}
console.log('🔧 Starte automatische Fehlerkorrektur...');console.log('🔒 CSRF Token für Request:',this.csrfToken.substring(0,10)+'...');try{const requestOptions={method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken}};console.log('📡 Sende Request an:','/api/admin/fix-errors');console.log('📝 Request Headers:',requestOptions.headers);const response=await fetch('/api/admin/fix-errors',requestOptions);console.log('📡 Response Status:',response.status);console.log('📡 Response Headers:',Object.fromEntries(response.headers.entries()));if(!response.ok){const errorText=await response.text();console.error('❌ Response Error:',errorText);throw new Error(`HTTP ${response.status}:${response.statusText}-${errorText}`);}
const data=await response.json();console.log('✅ Response Data:',data);if(data.success){this.showNotification('✅ Automatische Reparatur erfolgreich!','success');setTimeout(()=>this.checkSystemHealth(),2000);}else{this.showNotification(`❌ Automatische Reparatur fehlgeschlagen:${data.message||'Unbekannter Fehler'}`,'error');}}catch(error){console.error('❌ Fehler bei automatischer Reparatur:',error);this.showNotification(`❌ Fehler bei der automatischen Reparatur:${error.message}`,'error');}}
dismissErrors(){const alertContainer=document.getElementById('critical-errors-alert');if(alertContainer){alertContainer.classList.add('hidden');}}
showNotification(message,type='info'){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';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 transition-all duration-300 ${colors[type]}`;notification.textContent=message;notification.style.transform='translateX(0)';setTimeout(()=>{if(notification){notification.style.transform='translateX(100%)';setTimeout(()=>{if(notification&&notification.parentNode){notification.parentNode.removeChild(notification);}},300);}},3000);}
async loadLogs(level=null){const logsContainer=document.getElementById('logs-container');if(!logsContainer)return;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-blue-600 dark:border-blue-400"></div><span class="ml-3 text-slate-600 dark:text-slate-400">Logs werden geladen...</span></div>`;try{const filter=level||document.getElementById('log-level-filter')?.value||'all';const url=`${this.apiBaseUrl}/api/admin/logs?level=${filter}&limit=100`;const response=await fetch(url,{headers:{'X-CSRFToken':this.csrfToken}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
const data=await response.json();this.displayLogs(data.logs||[]);console.log('📋 Logs erfolgreich geladen');}catch(error){console.error('Fehler beim Laden der Logs:',error);logsContainer.innerHTML=`<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl p-6 text-center"><svg class="w-12 h-12 text-red-500 mx-auto mb-4"fill="none"stroke="currentColor"viewBox="0 0 24 24"><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.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg><h3 class="text-lg font-semibold text-red-800 dark:text-red-200 mb-2">Fehler beim Laden der Logs</h3><p class="text-red-600 dark:text-red-400 mb-4">${error.message}</p><button onclick="adminDashboard.loadLogs()"class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">🔄 Erneut versuchen</button></div>`;}}
displayLogs(logs){const logsContainer=document.getElementById('logs-container');if(!logsContainer)return;if(!logs||logs.length===0){logsContainer.innerHTML=`<div class="text-center py-12"><svg class="w-16 h-16 mx-auto text-slate-400 dark:text-slate-500 mb-4"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg><h3 class="text-lg font-medium text-slate-900 dark:text-white mb-2">Keine Logs gefunden</h3><p class="text-slate-500 dark:text-slate-400">Es sind keine Logs für die ausgewählten Kriterien vorhanden.</p></div>`;return;}
const logsHtml=logs.map(log=>{const levelColors={'error':'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-200','warning':'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-200','info':'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200','debug':'bg-gray-50 dark:bg-gray-900/20 border-gray-200 dark:border-gray-800 text-gray-800 dark:text-gray-200','critical':'bg-red-100 dark:bg-red-900/40 border-red-300 dark:border-red-700 text-red-900 dark:text-red-100'};const levelIcons={'error':'❌','warning':'⚠️','info':'','debug':'🔍','critical':'🚨'};const levelClass=levelColors[log.level]||levelColors['info'];const levelIcon=levelIcons[log.level]||'';return`<div class="border rounded-xl p-4 transition-all duration-200 hover:shadow-md ${levelClass}"><div class="flex items-start justify-between mb-2"><div class="flex items-center space-x-2"><span class="text-lg">${levelIcon}</span><span class="font-semibold text-sm uppercase tracking-wide">${log.level}</span><span class="text-xs opacity-75">${log.component||'System'}</span></div><div class="text-xs opacity-75">${this.formatLogTimestamp(log.timestamp)}</div></div><div class="mb-2"><p class="font-medium">${this.escapeHtml(log.message)}</p>${log.details?`<p class="text-sm opacity-75 mt-1">${this.escapeHtml(log.details)}</p>`:''}</div>${log.user?`<div class="text-xs opacity-75"><span class="font-medium">Benutzer:</span>${this.escapeHtml(log.user)}</div>`:''}
${log.ip_address?`<div class="text-xs opacity-75"><span class="font-medium">IP:</span>${this.escapeHtml(log.ip_address)}</div>`:''}
${log.request_id?`<div class="text-xs opacity-75 mt-2 font-mono"><span class="font-medium">Request-ID:</span>${this.escapeHtml(log.request_id)}</div>`:''}</div>`;}).join('');logsContainer.innerHTML=logsHtml;}
formatLogTimestamp(timestamp){if(!timestamp)return'Unbekannt';try{const date=new Date(timestamp);return date.toLocaleString('de-DE',{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'});}catch(error){return timestamp;}}
escapeHtml(text){if(!text)return'';const div=document.createElement('div');div.textContent=text;return div.innerHTML;}
async exportLogs(){try{this.showNotification('📥 Logs werden exportiert...','info');const filter=document.getElementById('log-level-filter')?.value||'all';const url=`${this.apiBaseUrl}/api/admin/logs/export?level=${filter}`;const response=await fetch(url,{headers:{'X-CSRFToken':this.csrfToken}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
const blob=await response.blob();const downloadUrl=window.URL.createObjectURL(blob);const a=document.createElement('a');a.href=downloadUrl;a.download=`system-logs-${new Date().toISOString().split('T')[0]}.csv`;document.body.appendChild(a);a.click();document.body.removeChild(a);window.URL.revokeObjectURL(downloadUrl);this.showNotification('✅ Logs erfolgreich exportiert!','success');}catch(error){console.error('Fehler beim Exportieren der Logs:',error);this.showNotification('❌ Fehler beim Exportieren der Logs: '+error.message,'error');}}
testButtons(){console.log('🧪 Teste Button-Funktionalität...');const fixBtn=document.querySelector('#fix-errors-btn');if(fixBtn){console.log('✅ Fix-Errors Button gefunden:',fixBtn);console.log('🔗 Event-Listener-Status:',fixBtn.dataset.listenerAttached);fixBtn.addEventListener('click',(e)=>{console.log('🖱️ Fix-Errors Button wurde geklickt (manueller Listener)');e.preventDefault();e.stopPropagation();this.testFixErrors();});}else{console.error('❌ Fix-Errors Button NICHT gefunden!');}
const viewBtn=document.querySelector('#view-error-details-btn');if(viewBtn){console.log('✅ View-Details Button gefunden:',viewBtn);console.log('🔗 Event-Listener-Status:',viewBtn.dataset.listenerAttached);viewBtn.addEventListener('click',(e)=>{console.log('🖱️ View-Details Button wurde geklickt (manueller Listener)');e.preventDefault();e.stopPropagation();console.log('🔄 Weiterleitung zu Logs-Tab...');window.location.href='/admin-dashboard?tab=logs';});}else{console.error('❌ View-Details Button NICHT gefunden!');}}
async testFixErrors(){console.log('🧪 TEST: Fix-Errors wird ausgeführt...');console.log('🔒 Aktueller CSRF Token:',this.csrfToken);if(!this.csrfToken){console.error('❌ CSRF Token fehlt - versuche neu zu laden...');this.csrfToken=this.extractCSRFToken();console.log('🔒 Neu geladener Token:',this.csrfToken);}
this.showNotification('🧪 TEST: Starte automatische Fehlerkorrektur...','info');try{const requestOptions={method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken,'X-Requested-With':'XMLHttpRequest'}};console.log('📡 TEST Request an:','/api/admin/fix-errors');console.log('📝 TEST Headers:',requestOptions.headers);const response=await fetch('/api/admin/fix-errors',requestOptions);console.log('📡 TEST Response Status:',response.status);console.log('📡 TEST Response Headers:',Object.fromEntries(response.headers.entries()));if(!response.ok){const errorText=await response.text();console.error('❌ TEST Response Error:',errorText);this.showNotification(`❌ TEST Fehler:${response.status}-${errorText}`,'error');return;}
const data=await response.json();console.log('✅ TEST Response Data:',data);if(data.success){this.showNotification('✅ TEST: Automatische Reparatur erfolgreich!','success');}else{this.showNotification(`❌ TEST:Reparatur fehlgeschlagen-${data.message}`,'error');}}catch(error){console.error('❌ TEST Fehler:',error);this.showNotification(`❌ TEST Netzwerk-Fehler:${error.message}`,'error');}}}
let adminDashboardInstance=null;document.addEventListener('DOMContentLoaded',function(){if(!adminDashboardInstance){adminDashboardInstance=new AdminDashboard();window.AdminDashboard=adminDashboardInstance;console.log('🎯 Admin Dashboard erfolgreich initialisiert (unified)');}});window.AdminDashboard=AdminDashboard;

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,79 @@
(function(){'use strict';window.MYP=window.MYP||{};window.MYP.Advanced=window.MYP.Advanced||{};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;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);}}
class FileUpload{constructor(container,options={}){this.container=typeof container==='string'?document.querySelector(container):container;this.options={multiple:false,accept:'*/*',maxSize:50*1024*1024,maxFiles:10,dragDrop:true,showProgress:true,showPreview:true,uploadUrl:'/api/upload',chunkSize:1024*1024,...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');fileInput.addEventListener('change',(e)=>{this.handleFiles(Array.from(e.target.files));});if(this.options.dragDrop){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){if(file.size>this.options.maxSize){this.showError(`Datei"${file.name}"ist zu groß.Maximum:${this.formatFileSize(this.options.maxSize)}`);return false;}
if(!this.options.multiple&&this.files.length>0){this.files=[];}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);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('');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();}}
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');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;}
return days.join('');}
setupEventListeners(){this.input.addEventListener('click',()=>{this.toggle();});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));}});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);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='';}}
window.MYP.Advanced={ProgressBar,FileUpload,DatePicker,createProgressBar:(container,options)=>new ProgressBar(container,options),createFileUpload:(container,options)=>new FileUpload(container,options),createDatePicker:(input,options)=>new DatePicker(input,options)};document.addEventListener('DOMContentLoaded',function(){document.querySelectorAll('[data-datepicker]').forEach(input=>{const options=JSON.parse(input.dataset.datepicker||'{}');new DatePicker(input,options);});document.querySelectorAll('[data-file-upload]').forEach(container=>{const options=JSON.parse(container.dataset.fileUpload||'{}');new FileUpload(container,options);});console.log('🚀 MYP Advanced Components geladen');});})();

Binary file not shown.

Binary file not shown.

14
backend/static/js/auto-logout.min.js vendored Normal file
View File

@@ -0,0 +1,14 @@
class AutoLogoutManager{constructor(){this.timer=null;this.warningTimer=null;this.timeout=60;this.warningTime=5;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;}}}}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';}}
document.addEventListener('DOMContentLoaded',function(){if(!window.location.pathname.includes('/login')){window.autoLogoutManager=new AutoLogoutManager();}});

Binary file not shown.

Binary file not shown.

25
backend/static/js/charts.min.js vendored Normal file
View File

@@ -0,0 +1,25 @@
window.statsCharts={};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)'};}
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}}}};}
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;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');}}
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;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');}}
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;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');}}
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;if(window.statsCharts.userActivity){window.statsCharts.userActivity.destroy();}
const options=getDefaultChartOptions();options.indexAxis='y';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');}}
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>`;}}
async function initializeAllCharts(){showChartLoading();await Promise.allSettled([createJobStatusChart(),createPrinterUsageChart(),createJobsTimelineChart(),createUserActivityChart()]);}
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>`;}});}
async function refreshAllCharts(){console.log('Aktualisiere alle Diagramme...');Object.values(window.statsCharts).forEach(chart=>{if(chart&&typeof chart.destroy==='function'){chart.destroy();}});await initializeAllCharts();console.log('Alle Diagramme aktualisiert');}
function updateChartsTheme(){refreshAllCharts();}
let chartRefreshInterval;function startChartAutoRefresh(){if(chartRefreshInterval){clearInterval(chartRefreshInterval);}
chartRefreshInterval=setInterval(()=>{refreshAllCharts();},5*60*1000);}
function stopChartAutoRefresh(){if(chartRefreshInterval){clearInterval(chartRefreshInterval);chartRefreshInterval=null;}}
function cleanup(){stopChartAutoRefresh();Object.values(window.statsCharts).forEach(chart=>{if(chart&&typeof chart.destroy==='function'){chart.destroy();}});window.statsCharts={};}
window.refreshAllCharts=refreshAllCharts;window.updateChartsTheme=updateChartsTheme;window.startChartAutoRefresh=startChartAutoRefresh;window.stopChartAutoRefresh=stopChartAutoRefresh;window.cleanup=cleanup;document.addEventListener('DOMContentLoaded',function(){if(document.getElementById('job-status-chart')){initializeAllCharts();startChartAutoRefresh();}});if(typeof window.addEventListener!=='undefined'){window.addEventListener('darkModeChanged',function(e){updateChartsTheme();});}
window.addEventListener('beforeunload',cleanup);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,86 @@
class CountdownTimer{constructor(options={}){this.config={name:options.name||'default_timer',duration:options.duration||1800,autoStart:options.autoStart||false,container:options.container||'countdown-timer',size:options.size||'large',theme:options.theme||'primary',showProgress:options.showProgress!==false,showControls:options.showControls!==false,warningThreshold:options.warningThreshold||30,showWarning:options.showWarning!==false,warningMessage:options.warningMessage||'Timer läuft ab!',forceQuitEnabled:options.forceQuitEnabled!==false,forceQuitAction:options.forceQuitAction||'logout',customEndpoint:options.customEndpoint||null,onTick:options.onTick||null,onWarning:options.onWarning||null,onExpired:options.onExpired||null,onForceQuit:options.onForceQuit||null,apiBase:options.apiBase||'/api/timers',updateInterval:options.updateInterval||1000,syncWithServer:options.syncWithServer!==false};this.state={remaining:this.config.duration,total:this.config.duration,status:'stopped',warningShown:false,lastServerSync:null};this.elements={};this.intervals={countdown:null,serverSync:null};this.listeners=new Map();this.init();}
init(){this.createUI();this.attachEventListeners();if(this.config.syncWithServer){this.syncWithServer();this.startServerSync();}
if(this.config.autoStart){this.start();}
console.log(`Timer'${this.config.name}'initialisiert`);}
createUI(){const container=document.getElementById(this.config.container);if(!container){console.error(`Container'${this.config.container}'nicht gefunden`);return;}
const timerWrapper=document.createElement('div');timerWrapper.className=`countdown-timer-wrapper size-${this.config.size}theme-${this.config.theme}`;timerWrapper.innerHTML=this.getTimerHTML();container.appendChild(timerWrapper);this.elements={wrapper:timerWrapper,display:timerWrapper.querySelector('.timer-display'),timeText:timerWrapper.querySelector('.time-text'),progressBar:timerWrapper.querySelector('.progress-fill'),progressText:timerWrapper.querySelector('.progress-text'),statusIndicator:timerWrapper.querySelector('.status-indicator'),warningBox:timerWrapper.querySelector('.warning-box'),warningText:timerWrapper.querySelector('.warning-text'),controls:timerWrapper.querySelector('.timer-controls'),startBtn:timerWrapper.querySelector('.btn-start'),pauseBtn:timerWrapper.querySelector('.btn-pause'),stopBtn:timerWrapper.querySelector('.btn-stop'),resetBtn:timerWrapper.querySelector('.btn-reset'),extendBtn:timerWrapper.querySelector('.btn-extend')};this.updateDisplay();}
getTimerHTML(){return`<div class="countdown-timer-container"><!--Timer-Display--><div class="timer-display"><div class="time-display"><span class="time-text">${this.formatTime(this.state.remaining)}</span><span class="time-label">verbleibend</span></div><div class="status-indicator"><i class="fas fa-circle"></i><span class="status-text">Gestoppt</span></div></div><!--Fortschrittsbalken-->${this.config.showProgress?`<div class="progress-container"><div class="progress-bar"><div class="progress-fill"style="width: 0%"></div></div><div class="progress-text">0%abgelaufen</div></div>`:''}<!--Warnungsbereich--><div class="warning-box"style="display: none;"><div class="warning-content"><i class="fas fa-exclamation-triangle"></i><span class="warning-text">${this.config.warningMessage}</span></div></div><!--Steuerungsbuttons-->${this.config.showControls?`<div class="timer-controls"><button class="btn btn-success btn-start"title="Timer starten"><i class="fas fa-play"></i><span>Start</span></button><button class="btn btn-warning btn-pause"title="Timer pausieren"style="display: none;"><i class="fas fa-pause"></i><span>Pause</span></button><button class="btn btn-danger btn-stop"title="Timer stoppen"><i class="fas fa-stop"></i><span>Stop</span></button><button class="btn btn-secondary btn-reset"title="Timer zurücksetzen"><i class="fas fa-redo"></i><span>Reset</span></button><button class="btn btn-info btn-extend"title="Timer verlängern"><i class="fas fa-plus"></i><span>+5min</span></button></div>`:''}</div>`;}
attachEventListeners(){if(this.elements.startBtn){this.elements.startBtn.addEventListener('click',()=>this.start());}
if(this.elements.pauseBtn){this.elements.pauseBtn.addEventListener('click',()=>this.pause());}
if(this.elements.stopBtn){this.elements.stopBtn.addEventListener('click',()=>this.stop());}
if(this.elements.resetBtn){this.elements.resetBtn.addEventListener('click',()=>this.reset());}
if(this.elements.extendBtn){this.elements.extendBtn.addEventListener('click',()=>this.extend(300));}
document.addEventListener('keydown',(e)=>this.handleKeyboardShortcuts(e));document.addEventListener('visibilitychange',()=>this.handleVisibilityChange());window.addEventListener('beforeunload',(e)=>this.handleBeforeUnload(e));}
async start(){try{if(this.state.status==='running'){return true;}
if(this.config.syncWithServer){const response=await this.apiCall('start','POST');if(!response.success){this.showError('Fehler beim Starten des Timers');return false;}}
this.state.status='running';this.startCountdown();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'gestartet`);return true;}catch(error){console.error('Fehler beim Starten des Timers:',error);this.showError('Timer konnte nicht gestartet werden');return false;}}
async pause(){try{if(this.state.status!=='running'){return true;}
if(this.config.syncWithServer){const response=await this.apiCall('pause','POST');if(!response.success){this.showError('Fehler beim Pausieren des Timers');return false;}}
this.state.status='paused';this.stopCountdown();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'pausiert`);return true;}catch(error){console.error('Fehler beim Pausieren des Timers:',error);this.showError('Timer konnte nicht pausiert werden');return false;}}
async stop(){try{if(this.config.syncWithServer){const response=await this.apiCall('stop','POST');if(!response.success){this.showError('Fehler beim Stoppen des Timers');return false;}}
this.state.status='stopped';this.state.remaining=this.state.total;this.state.warningShown=false;this.stopCountdown();this.hideWarning();this.updateDisplay();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'gestoppt`);return true;}catch(error){console.error('Fehler beim Stoppen des Timers:',error);this.showError('Timer konnte nicht gestoppt werden');return false;}}
async reset(){try{if(this.config.syncWithServer){const response=await this.apiCall('reset','POST');if(!response.success){this.showError('Fehler beim Zurücksetzen des Timers');return false;}}
this.stop();this.state.remaining=this.state.total;this.updateDisplay();console.log(`Timer'${this.config.name}'zurückgesetzt`);return true;}catch(error){console.error('Fehler beim Zurücksetzen des Timers:',error);this.showError('Timer konnte nicht zurückgesetzt werden');return false;}}
async extend(seconds){try{if(this.config.syncWithServer){const response=await this.apiCall('extend','POST',{seconds});if(!response.success){this.showError('Fehler beim Verlängern des Timers');return false;}}
this.state.remaining+=seconds;this.state.total+=seconds;this.state.warningShown=false;this.hideWarning();this.updateDisplay();this.showToast(`Timer um ${Math.floor(seconds/60)}Minuten verlängert`,'success');console.log(`Timer'${this.config.name}'um ${seconds}Sekunden verlängert`);return true;}catch(error){console.error('Fehler beim Verlängern des Timers:',error);this.showError('Timer konnte nicht verlängert werden');return false;}}
startCountdown(){this.stopCountdown();this.intervals.countdown=setInterval(()=>{this.tick();},this.config.updateInterval);}
stopCountdown(){if(this.intervals.countdown){clearInterval(this.intervals.countdown);this.intervals.countdown=null;}}
tick(){if(this.state.status!=='running'){return;}
this.state.remaining=Math.max(0,this.state.remaining-1);this.updateDisplay();if(this.config.onTick){this.config.onTick(this.state.remaining,this.state.total);}
if(!this.state.warningShown&&this.state.remaining<=this.config.warningThreshold&&this.state.remaining>0){this.showWarning();}
if(this.state.remaining<=0){this.handleExpired();}}
async handleExpired(){console.warn(`Timer'${this.config.name}'ist abgelaufen`);this.state.status='expired';this.stopCountdown();this.updateDisplay();this.updateStatusIndicator();if(this.config.onExpired){this.config.onExpired();}
if(this.config.forceQuitEnabled){await this.executeForceQuit();}}
async executeForceQuit(){try{console.warn(`Force-Quit für Timer'${this.config.name}'wird ausgeführt...`);if(this.config.onForceQuit){const shouldContinue=this.config.onForceQuit(this.config.forceQuitAction);if(!shouldContinue){return;}}
if(this.config.syncWithServer){const response=await this.apiCall('force-quit','POST');if(!response.success){console.error('Force-Quit-API-Aufruf fehlgeschlagen');}}
switch(this.config.forceQuitAction){case'logout':this.performLogout();break;case'redirect':this.performRedirect();break;case'refresh':this.performRefresh();break;case'custom':this.performCustomAction();break;default:console.warn(`Unbekannte Force-Quit-Aktion:${this.config.forceQuitAction}`);}}catch(error){console.error('Fehler bei Force-Quit-Ausführung:',error);}}
performLogout(){this.showModal('Session abgelaufen','Sie werden automatisch abgemeldet...','warning');setTimeout(()=>{window.location.href='/auth/logout';},2000);}
performRedirect(){const redirectUrl=this.config.redirectUrl||'/';this.showModal('Umleitung','Sie werden weitergeleitet...','info');setTimeout(()=>{window.location.href=redirectUrl;},2000);}
performRefresh(){this.showModal('Seite wird aktualisiert','Die Seite wird automatisch neu geladen...','info');setTimeout(()=>{window.location.reload();},2000);}
performCustomAction(){if(this.config.customEndpoint){fetch(this.config.customEndpoint,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()},body:JSON.stringify({timer_name:this.config.name,action:'force_quit'})}).catch(error=>{console.error('Custom-Action-Request fehlgeschlagen:',error);});}}
showWarning(){if(!this.config.showWarning||this.state.warningShown){return;}
this.state.warningShown=true;if(this.elements.warningBox){this.elements.warningBox.style.display='block';this.elements.warningBox.classList.add('pulse');}
if(this.config.onWarning){this.config.onWarning(this.state.remaining);}
this.showNotification('Timer-Warnung',this.config.warningMessage);console.warn(`Timer-Warnung für'${this.config.name}':${this.state.remaining}Sekunden verbleiben`);}
hideWarning(){this.state.warningShown=false;if(this.elements.warningBox){this.elements.warningBox.style.display='none';this.elements.warningBox.classList.remove('pulse');}}
updateDisplay(){if(this.elements.timeText){this.elements.timeText.textContent=this.formatTime(this.state.remaining);}
if(this.config.showProgress&&this.elements.progressBar){const progress=((this.state.total-this.state.remaining)/this.state.total)*100;this.elements.progressBar.style.width=`${progress}%`;if(this.elements.progressText){this.elements.progressText.textContent=`${Math.round(progress)}%abgelaufen`;}}
this.updateTheme();}
updateTheme(){if(!this.elements.wrapper)return;const progress=(this.state.total-this.state.remaining)/this.state.total;this.elements.wrapper.classList.remove('theme-primary','theme-warning','theme-danger');if(progress<0.7){this.elements.wrapper.classList.add('theme-primary');}else if(progress<0.9){this.elements.wrapper.classList.add('theme-warning');}else{this.elements.wrapper.classList.add('theme-danger');}}
updateControls(){if(!this.config.showControls)return;const isRunning=this.state.status==='running';const isPaused=this.state.status==='paused';const isStopped=this.state.status==='stopped';if(this.elements.startBtn){this.elements.startBtn.style.display=(isStopped||isPaused)?'inline-flex':'none';}
if(this.elements.pauseBtn){this.elements.pauseBtn.style.display=isRunning?'inline-flex':'none';}
if(this.elements.stopBtn){this.elements.stopBtn.disabled=isStopped;}
if(this.elements.resetBtn){this.elements.resetBtn.disabled=isRunning;}
if(this.elements.extendBtn){this.elements.extendBtn.disabled=this.state.status==='expired';}}
updateStatusIndicator(){if(!this.elements.statusIndicator)return;const statusText=this.elements.statusIndicator.querySelector('.status-text');const statusIcon=this.elements.statusIndicator.querySelector('i');if(statusText&&statusIcon){switch(this.state.status){case'running':statusText.textContent='Läuft';statusIcon.className='fas fa-circle text-success';break;case'paused':statusText.textContent='Pausiert';statusIcon.className='fas fa-circle text-warning';break;case'expired':statusText.textContent='Abgelaufen';statusIcon.className='fas fa-circle text-danger';break;default:statusText.textContent='Gestoppt';statusIcon.className='fas fa-circle text-secondary';}}}
formatTime(seconds){const minutes=Math.floor(seconds/60);const secs=seconds%60;return`${minutes.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')}`;}
async syncWithServer(){try{const response=await this.apiCall('status','GET');if(response.success&&response.data){const serverState=response.data;this.state.remaining=serverState.remaining_seconds||this.state.remaining;this.state.total=serverState.duration_seconds||this.state.total;this.state.status=serverState.status||this.state.status;this.updateDisplay();this.updateControls();this.updateStatusIndicator();this.state.lastServerSync=new Date();}}catch(error){console.error('Server-Synchronisation fehlgeschlagen:',error);}}
startServerSync(){if(this.intervals.serverSync){clearInterval(this.intervals.serverSync);}
this.intervals.serverSync=setInterval(()=>{this.syncWithServer();},30000);}
async apiCall(action,method='GET',data=null){const url=`${this.config.apiBase}/${this.config.name}/${action}`;const options={method,headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()}};if(data&&(method==='POST'||method==='PUT')){options.body=JSON.stringify(data);}
const response=await fetch(url,options);return await response.json();}
getCSRFToken(){const token=document.querySelector('meta[name="csrf-token"]');return token?token.getAttribute('content'):'';}
showNotification(title,message){if('Notification'in window&&Notification.permission==='granted'){new Notification(title,{body:message,icon:'/static/icons/timer-icon.png'});}}
showToast(message,type='info'){console.log(`Toast[${type}]:${message}`);}
showError(message){this.showModal('Fehler',message,'danger');}
showModal(title,message,type='info'){console.log(`Modal[${type}]${title}:${message}`);}
handleKeyboardShortcuts(e){if(e.ctrlKey||e.metaKey){switch(e.key){case' ':e.preventDefault();if(this.state.status==='running'){this.pause();}else{this.start();}
break;case'r':e.preventDefault();this.reset();break;case's':e.preventDefault();this.stop();break;}}}
handleVisibilityChange(){if(document.hidden){this.config._wasRunning=this.state.status==='running';}else{if(this.config.syncWithServer){this.syncWithServer();}}}
handleBeforeUnload(e){if(this.state.status==='running'&&this.state.remaining>0){e.preventDefault();e.returnValue='Timer läuft noch. Möchten Sie die Seite wirklich verlassen?';return e.returnValue;}}
destroy(){this.stopCountdown();if(this.intervals.serverSync){clearInterval(this.intervals.serverSync);}
if(this.elements.wrapper){this.elements.wrapper.remove();}
this.listeners.forEach((listener,element)=>{element.removeEventListener(listener.event,listener.handler);});console.log(`Timer'${this.config.name}'zerstört`);}}
const timerStyles=`<style id="countdown-timer-styles">.countdown-timer-wrapper{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:400px;margin:0 auto;text-align:center;border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.15);background:white;overflow:hidden;}.countdown-timer-container{padding:24px;}
.size-small{max-width:250px;font-size:0.9em;}.size-medium{max-width:350px;}.size-large{max-width:450px;font-size:1.1em;}
.theme-primary{border-left:4px solid#007bff;}.theme-warning{border-left:4px solid#ffc107;}.theme-danger{border-left:4px solid#dc3545;}.theme-success{border-left:4px solid#28a745;}
.timer-display{margin-bottom:20px;}.time-display{margin-bottom:8px;}.time-text{font-size:3em;font-weight:bold;color:#2c3e50;font-family:'Courier New',monospace;}.time-label{display:block;font-size:0.9em;color:#6c757d;margin-top:4px;}.status-indicator{display:flex;align-items:center;justify-content:center;gap:8px;font-size:0.9em;color:#6c757d;}
.progress-container{margin-bottom:20px;}.progress-bar{height:8px;background-color:#e9ecef;border-radius:4px;overflow:hidden;margin-bottom:8px;}.progress-fill{height:100%;background:linear-gradient(90deg,#007bff,#0056b3);transition:width 0.5s ease-in-out;border-radius:4px;}.progress-text{font-size:0.85em;color:#6c757d;}
.warning-box{background:linear-gradient(135deg,#fff3cd,#ffeaa7);border:1px solid#ffc107;border-radius:8px;padding:12px;margin-bottom:20px;animation:pulse 2s infinite;}.warning-content{display:flex;align-items:center;justify-content:center;gap:8px;color:#856404;font-weight:500;}.warning-content i{color:#ffc107;}
.timer-controls{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;}.timer-controls.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border:none;border-radius:6px;font-size:0.9em;font-weight:500;cursor:pointer;transition:all 0.2s ease;text-decoration:none;}.timer-controls.btn:hover{transform:translateY(-1px);box-shadow:0 2px 8px rgba(0,0,0,0.2);}.timer-controls.btn:disabled{opacity:0.6;cursor:not-allowed;transform:none;}.btn-success{background:#28a745;color:white;}.btn-warning{background:#ffc107;color:#212529;}.btn-danger{background:#dc3545;color:white;}.btn-secondary{background:#6c757d;color:white;}.btn-info{background:#17a2b8;color:white;}
@keyframes pulse{0%,100%{opacity:1;}
50%{opacity:0.7;}}
@media(max-width:480px){.countdown-timer-wrapper{margin:0 16px;}.time-text{font-size:2.5em;}.timer-controls{flex-direction:column;}.timer-controls.btn{width:100%;justify-content:center;}}
@media(prefers-color-scheme:dark){.countdown-timer-wrapper{background:#2c3e50;color:white;}.time-text{color:#ecf0f1;}.progress-bar{background-color:#34495e;}}</style>`;if(!document.getElementById('countdown-timer-styles')){document.head.insertAdjacentHTML('beforeend',timerStyles);}
window.CountdownTimer=CountdownTimer;window.TimerManager={timers:new Map(),create(name,options){if(this.timers.has(name)){console.warn(`Timer'${name}'existiert bereits`);return this.timers.get(name);}
const timer=new CountdownTimer({...options,name:name});this.timers.set(name,timer);return timer;},get(name){return this.timers.get(name);},destroy(name){const timer=this.timers.get(name);if(timer){timer.destroy();this.timers.delete(name);}},destroyAll(){this.timers.forEach(timer=>timer.destroy());this.timers.clear();}};

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,20 @@
class CSPViolationHandler{constructor(){this.violations=[];this.init();}
init(){document.addEventListener('securitypolicyviolation',this.handleViolation.bind(this));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');}
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);this.reportViolation(violation);}
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();}
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();}
async reportViolation(violation){try{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);}}
getViolations(){return this.violations;}
getStats(){const stats={total:this.violations.length,byDirective:{},byURI:{},recent:this.violations.slice(-10)};this.violations.forEach(violation=>{const directive=violation.violatedDirective;stats.byDirective[directive]=(stats.byDirective[directive]||0)+1;const uri=violation.blockedURI;stats.byURI[uri]=(stats.byURI[uri]||0)+1;});return stats;}
enableDebugMode(){this.createDebugPanel();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');}
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);document.addEventListener('keydown',(event)=>{if(event.ctrlKey&&event.shiftKey&&event.key==='C'){panel.style.display=panel.style.display==='none'?'block':'none';this.updateDebugPanel();}});}
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('');}
clearViolations(){this.violations=[];this.updateDebugPanel();console.log('🗑️ CSP Violations gelöscht');}
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');}}
const cspHandler=new CSPViolationHandler();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');}
window.cspHandler=cspHandler;console.log('🛡️ CSP Violation Handler geladen');

Binary file not shown.

View File

@@ -0,0 +1,123 @@
/**
* MYP Platform - CSS Cache Manager
* Integration und Management des CSS-Caching Service Workers
*/
class CSSCacheManager {
constructor() {
this.serviceWorker = null;
this.registration = null;
this.isSupported = 'serviceWorker' in navigator;
this.cacheStats = null;
this.init();
}
async init() {
if (!this.isSupported) {
console.warn('[CSS-Cache] Service Worker wird nicht unterstützt');
return;
}
try {
this.registration = await navigator.serviceWorker.register(
'/static/js/css-cache-service-worker.js',
{ scope: '/static/css/' }
);
console.log('[CSS-Cache] Service Worker registriert');
if (this.registration.active) {
this.serviceWorker = this.registration.active;
}
this.startPerformanceMonitoring();
} catch (error) {
console.error('[CSS-Cache] Fehler bei Service Worker Registrierung:', error);
}
}
async clearCache() {
if (!this.serviceWorker) return false;
try {
const messageChannel = new MessageChannel();
return new Promise((resolve) => {
messageChannel.port1.onmessage = (event) => {
resolve(event.data.success);
};
this.serviceWorker.postMessage(
{ type: 'CLEAR_CSS_CACHE' },
[messageChannel.port2]
);
});
} catch (error) {
console.error('[CSS-Cache] Fehler beim Cache leeren:', error);
return false;
}
}
async getCacheStats() {
if (!this.serviceWorker) return null;
try {
const messageChannel = new MessageChannel();
return new Promise((resolve) => {
messageChannel.port1.onmessage = (event) => {
this.cacheStats = event.data;
resolve(event.data);
};
this.serviceWorker.postMessage(
{ type: 'GET_CACHE_STATS' },
[messageChannel.port2]
);
});
} catch (error) {
console.error('[CSS-Cache] Fehler beim Abrufen der Stats:', error);
return null;
}
}
startPerformanceMonitoring() {
setInterval(async () => {
const stats = await this.getCacheStats();
if (stats) {
console.log('[CSS-Cache] Performance-Stats:', stats);
}
}, 5 * 60 * 1000);
setTimeout(async () => {
await this.getCacheStats();
}, 10000);
}
debug() {
console.group('[CSS-Cache] Debug-Informationen');
console.log('Service Worker unterstützt:', this.isSupported);
console.log('Service Worker aktiv:', !!this.serviceWorker);
console.log('Registration:', this.registration);
console.log('Cache-Stats:', this.cacheStats);
console.groupEnd();
}
}
// Globale Instanz erstellen
window.cssCache = new CSSCacheManager();
// Entwicklungs-Hilfsfunktionen
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
window.clearCSSCache = () => window.cssCache.clearCache();
window.getCSSStats = () => window.cssCache.getCacheStats();
console.log('[CSS-Cache] Entwicklungsmodus: Debug-Funktionen verfügbar');
console.log('- cssCache.debug() - Debug-Informationen anzeigen');
console.log('- clearCSSCache() - CSS-Cache leeren');
console.log('- getCSSStats() - Cache-Statistiken abrufen');
}
export default CSSCacheManager;

Binary file not shown.

View File

@@ -0,0 +1,10 @@
class CSSCacheManager{constructor(){this.serviceWorker=null;this.registration=null;this.isSupported='serviceWorker'in navigator;this.cacheStats=null;this.init();}
async init(){if(!this.isSupported){console.warn('[CSS-Cache] Service Worker wird nicht unterstützt');return;}
try{this.registration=await navigator.serviceWorker.register('/static/js/css-cache-service-worker.js',{scope:'/static/css/'});console.log('[CSS-Cache] Service Worker registriert');if(this.registration.active){this.serviceWorker=this.registration.active;}
this.startPerformanceMonitoring();}catch(error){console.error('[CSS-Cache] Fehler bei Service Worker Registrierung:',error);}}
async clearCache(){if(!this.serviceWorker)return false;try{const messageChannel=new MessageChannel();return new Promise((resolve)=>{messageChannel.port1.onmessage=(event)=>{resolve(event.data.success);};this.serviceWorker.postMessage({type:'CLEAR_CSS_CACHE'},[messageChannel.port2]);});}catch(error){console.error('[CSS-Cache] Fehler beim Cache leeren:',error);return false;}}
async getCacheStats(){if(!this.serviceWorker)return null;try{const messageChannel=new MessageChannel();return new Promise((resolve)=>{messageChannel.port1.onmessage=(event)=>{this.cacheStats=event.data;resolve(event.data);};this.serviceWorker.postMessage({type:'GET_CACHE_STATS'},[messageChannel.port2]);});}catch(error){console.error('[CSS-Cache] Fehler beim Abrufen der Stats:',error);return null;}}
startPerformanceMonitoring(){setInterval(async()=>{const stats=await this.getCacheStats();if(stats){console.log('[CSS-Cache] Performance-Stats:',stats);}},5*60*1000);setTimeout(async()=>{await this.getCacheStats();},10000);}
debug(){console.group('[CSS-Cache] Debug-Informationen');console.log('Service Worker unterstützt:',this.isSupported);console.log('Service Worker aktiv:',!!this.serviceWorker);console.log('Registration:',this.registration);console.log('Cache-Stats:',this.cacheStats);console.groupEnd();}}
window.cssCache=new CSSCacheManager();if(window.location.hostname==='localhost'||window.location.hostname==='127.0.0.1'){window.clearCSSCache=()=>window.cssCache.clearCache();window.getCSSStats=()=>window.cssCache.getCacheStats();console.log('[CSS-Cache] Entwicklungsmodus: Debug-Funktionen verfügbar');console.log('- cssCache.debug() - Debug-Informationen anzeigen');console.log('- clearCSSCache() - CSS-Cache leeren');console.log('- getCSSStats() - Cache-Statistiken abrufen');}
export default CSSCacheManager;

Binary file not shown.

View File

@@ -0,0 +1,372 @@
/**
* MYP Platform - CSS Caching Service Worker
* Intelligentes Caching für optimierte CSS-Performance
*/
const CACHE_NAME = 'myp-css-cache-v1.0';
const CSS_CACHE_NAME = 'myp-css-resources-v1.0';
// Kritische CSS-Ressourcen für sofortiges Caching
const CRITICAL_CSS_RESOURCES = [
'/static/css/caching-optimizations.css',
'/static/css/optimization-animations.css',
'/static/css/glassmorphism.css',
'/static/css/professional-theme.css',
'/static/css/tailwind.min.css'
];
// Nicht-kritische CSS-Ressourcen für Prefetching
const NON_CRITICAL_CSS_RESOURCES = [
'/static/css/components.css',
'/static/css/printers.css',
'/static/fontawesome/css/all.min.css'
];
// CSS-spezifische Konfiguration
const CSS_CACHE_CONFIG = {
maxAge: 24 * 60 * 60 * 1000, // 24 Stunden
maxEntries: 50,
networkTimeoutSeconds: 5
};
// Service Worker Installation
self.addEventListener('install', event => {
console.log('[CSS-SW] Service Worker wird installiert...');
event.waitUntil(
caches.open(CSS_CACHE_NAME)
.then(cache => {
console.log('[CSS-SW] Kritische CSS-Ressourcen werden gecacht...');
return cache.addAll(CRITICAL_CSS_RESOURCES);
})
.then(() => {
console.log('[CSS-SW] Installation abgeschlossen');
return self.skipWaiting();
})
.catch(error => {
console.error('[CSS-SW] Fehler bei Installation:', error);
})
);
});
// Service Worker Aktivierung
self.addEventListener('activate', event => {
console.log('[CSS-SW] Service Worker wird aktiviert...');
event.waitUntil(
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
// Alte Caches löschen
if (cacheName !== CACHE_NAME && cacheName !== CSS_CACHE_NAME) {
console.log('[CSS-SW] Alter Cache wird gelöscht:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log('[CSS-SW] Aktivierung abgeschlossen');
return self.clients.claim();
})
.then(() => {
// Nicht-kritische Ressourcen im Hintergrund prefetchen
return prefetchNonCriticalResources();
})
);
});
// CSS-Request-Behandlung mit Cache-First-Strategie
self.addEventListener('fetch', event => {
const { request } = event;
// Nur CSS-Requests verarbeiten
if (!request.url.includes('.css') && !request.url.includes('/static/css/')) {
return;
}
event.respondWith(
handleCSSRequest(request)
);
});
// CSS-Request-Handler mit intelligenter Cache-Strategie
async function handleCSSRequest(request) {
const url = new URL(request.url);
try {
// 1. Cache-First für CSS-Dateien
const cachedResponse = await caches.match(request);
if (cachedResponse) {
console.log('[CSS-SW] Cache-Hit für:', url.pathname);
// Hintergrund-Update für kritische Ressourcen
if (CRITICAL_CSS_RESOURCES.some(resource => url.pathname.includes(resource))) {
updateCacheInBackground(request);
}
return cachedResponse;
}
// 2. Network-Request mit Timeout
const networkResponse = await fetchWithTimeout(request, CSS_CACHE_CONFIG.networkTimeoutSeconds * 1000);
if (networkResponse && networkResponse.ok) {
// Response für Cache klonen
const responseToCache = networkResponse.clone();
// Asynchron cachen
cacheResponse(request, responseToCache);
console.log('[CSS-SW] Network-Response für:', url.pathname);
return networkResponse;
}
// 3. Fallback für kritische CSS
return await getFallbackCSS(request);
} catch (error) {
console.error('[CSS-SW] Fehler bei CSS-Request:', error);
return await getFallbackCSS(request);
}
}
// Network-Request mit Timeout
function fetchWithTimeout(request, timeout) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Network timeout'));
}, timeout);
fetch(request)
.then(response => {
clearTimeout(timer);
resolve(response);
})
.catch(error => {
clearTimeout(timer);
reject(error);
});
});
}
// Response asynchron cachen
async function cacheResponse(request, response) {
try {
const cache = await caches.open(CSS_CACHE_NAME);
await cache.put(request, response);
// Cache-Größe prüfen und ggf. alte Einträge löschen
await maintainCacheSize();
} catch (error) {
console.error('[CSS-SW] Fehler beim Cachen:', error);
}
}
// Cache-Größe überwachen und bereinigen
async function maintainCacheSize() {
try {
const cache = await caches.open(CSS_CACHE_NAME);
const requests = await cache.keys();
if (requests.length > CSS_CACHE_CONFIG.maxEntries) {
console.log('[CSS-SW] Cache-Bereinigung wird durchgeführt...');
// Älteste Einträge löschen (LRU-ähnlich)
const excessCount = requests.length - CSS_CACHE_CONFIG.maxEntries;
for (let i = 0; i < excessCount; i++) {
await cache.delete(requests[i]);
}
}
} catch (error) {
console.error('[CSS-SW] Fehler bei Cache-Wartung:', error);
}
}
// Hintergrund-Update für kritische Ressourcen
async function updateCacheInBackground(request) {
try {
const response = await fetch(request);
if (response && response.ok) {
const cache = await caches.open(CSS_CACHE_NAME);
await cache.put(request, response.clone());
console.log('[CSS-SW] Hintergrund-Update für:', request.url);
}
} catch (error) {
console.log('[CSS-SW] Hintergrund-Update fehlgeschlagen:', error);
}
}
// Fallback-CSS für kritische Requests
async function getFallbackCSS(request) {
const url = new URL(request.url);
// Minimales Fallback-CSS für kritische Komponenten
const fallbackCSS = `
/* Fallback CSS für Offline-Nutzung */
body {
font-family: system-ui, sans-serif;
margin: 0;
padding: 20px;
background: #f8fafc;
color: #1f2937;
}
.offline-notice {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
text-align: center;
}
.btn {
background: #0073ce;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
`;
return new Response(fallbackCSS, {
headers: {
'Content-Type': 'text/css',
'Cache-Control': 'no-cache'
}
});
}
// Nicht-kritische Ressourcen im Hintergrund prefetchen
async function prefetchNonCriticalResources() {
try {
const cache = await caches.open(CSS_CACHE_NAME);
for (const resource of NON_CRITICAL_CSS_RESOURCES) {
try {
const request = new Request(resource);
const cachedResponse = await cache.match(request);
if (!cachedResponse) {
const response = await fetch(request);
if (response && response.ok) {
await cache.put(request, response);
console.log('[CSS-SW] Prefetch erfolgreich für:', resource);
}
}
} catch (error) {
console.log('[CSS-SW] Prefetch fehlgeschlagen für:', resource);
}
}
} catch (error) {
console.error('[CSS-SW] Fehler beim Prefetching:', error);
}
}
// Message-Handler für Cache-Management
self.addEventListener('message', event => {
const { type, data } = event.data;
switch (type) {
case 'SKIP_WAITING':
self.skipWaiting();
break;
case 'CLEAR_CSS_CACHE':
clearCSSCache().then(() => {
event.ports[0].postMessage({ success: true });
});
break;
case 'PREFETCH_CSS':
prefetchSpecificCSS(data.urls).then(() => {
event.ports[0].postMessage({ success: true });
});
break;
case 'GET_CACHE_STATS':
getCacheStats().then(stats => {
event.ports[0].postMessage(stats);
});
break;
}
});
// CSS-Cache leeren
async function clearCSSCache() {
try {
await caches.delete(CSS_CACHE_NAME);
console.log('[CSS-SW] CSS-Cache wurde geleert');
} catch (error) {
console.error('[CSS-SW] Fehler beim Leeren des CSS-Cache:', error);
}
}
// Spezifische CSS-Dateien prefetchen
async function prefetchSpecificCSS(urls) {
try {
const cache = await caches.open(CSS_CACHE_NAME);
for (const url of urls) {
try {
const response = await fetch(url);
if (response && response.ok) {
await cache.put(url, response);
console.log('[CSS-SW] Spezifisches Prefetch für:', url);
}
} catch (error) {
console.log('[CSS-SW] Spezifisches Prefetch fehlgeschlagen für:', url);
}
}
} catch (error) {
console.error('[CSS-SW] Fehler beim spezifischen Prefetching:', error);
}
}
// Cache-Statistiken abrufen
async function getCacheStats() {
try {
const cache = await caches.open(CSS_CACHE_NAME);
const requests = await cache.keys();
return {
cssEntries: requests.length,
maxEntries: CSS_CACHE_CONFIG.maxEntries,
cacheUtilization: (requests.length / CSS_CACHE_CONFIG.maxEntries) * 100,
cachedUrls: requests.map(req => req.url)
};
} catch (error) {
console.error('[CSS-SW] Fehler beim Abrufen der Cache-Stats:', error);
return { error: error.message };
}
}
// Performance-Monitoring
self.addEventListener('install', () => {
console.log('[CSS-SW] Installation gestartet um:', new Date().toISOString());
});
self.addEventListener('activate', () => {
console.log('[CSS-SW] Aktivierung abgeschlossen um:', new Date().toISOString());
});
// Globaler Error-Handler
self.addEventListener('error', event => {
console.error('[CSS-SW] Globaler Fehler:', event.error);
});
self.addEventListener('unhandledrejection', event => {
console.error('[CSS-SW] Unbehandelte Promise-Rejection:', event.reason);
});
console.log('[CSS-SW] CSS-Caching Service Worker geladen');

Binary file not shown.

View File

@@ -0,0 +1,15 @@
const CACHE_NAME='myp-css-cache-v1.0';const CSS_CACHE_NAME='myp-css-resources-v1.0';const CRITICAL_CSS_RESOURCES=['/static/css/caching-optimizations.css','/static/css/optimization-animations.css','/static/css/glassmorphism.css','/static/css/professional-theme.css','/static/css/tailwind.min.css'];const NON_CRITICAL_CSS_RESOURCES=['/static/css/components.css','/static/css/printers.css','/static/fontawesome/css/all.min.css'];const CSS_CACHE_CONFIG={maxAge:24*60*60*1000,maxEntries:50,networkTimeoutSeconds:5};self.addEventListener('install',event=>{console.log('[CSS-SW] Service Worker wird installiert...');event.waitUntil(caches.open(CSS_CACHE_NAME).then(cache=>{console.log('[CSS-SW] Kritische CSS-Ressourcen werden gecacht...');return cache.addAll(CRITICAL_CSS_RESOURCES);}).then(()=>{console.log('[CSS-SW] Installation abgeschlossen');return self.skipWaiting();}).catch(error=>{console.error('[CSS-SW] Fehler bei Installation:',error);}));});self.addEventListener('activate',event=>{console.log('[CSS-SW] Service Worker wird aktiviert...');event.waitUntil(caches.keys().then(cacheNames=>{return Promise.all(cacheNames.map(cacheName=>{if(cacheName!==CACHE_NAME&&cacheName!==CSS_CACHE_NAME){console.log('[CSS-SW] Alter Cache wird gelöscht:',cacheName);return caches.delete(cacheName);}}));}).then(()=>{console.log('[CSS-SW] Aktivierung abgeschlossen');return self.clients.claim();}).then(()=>{return prefetchNonCriticalResources();}));});self.addEventListener('fetch',event=>{const{request}=event;if(!request.url.includes('.css')&&!request.url.includes('/static/css/')){return;}
event.respondWith(handleCSSRequest(request));});async function handleCSSRequest(request){const url=new URL(request.url);try{const cachedResponse=await caches.match(request);if(cachedResponse){console.log('[CSS-SW] Cache-Hit für:',url.pathname);if(CRITICAL_CSS_RESOURCES.some(resource=>url.pathname.includes(resource))){updateCacheInBackground(request);}
return cachedResponse;}
const networkResponse=await fetchWithTimeout(request,CSS_CACHE_CONFIG.networkTimeoutSeconds*1000);if(networkResponse&&networkResponse.ok){const responseToCache=networkResponse.clone();cacheResponse(request,responseToCache);console.log('[CSS-SW] Network-Response für:',url.pathname);return networkResponse;}
return await getFallbackCSS(request);}catch(error){console.error('[CSS-SW] Fehler bei CSS-Request:',error);return await getFallbackCSS(request);}}
function fetchWithTimeout(request,timeout){return new Promise((resolve,reject)=>{const timer=setTimeout(()=>{reject(new Error('Network timeout'));},timeout);fetch(request).then(response=>{clearTimeout(timer);resolve(response);}).catch(error=>{clearTimeout(timer);reject(error);});});}
async function cacheResponse(request,response){try{const cache=await caches.open(CSS_CACHE_NAME);await cache.put(request,response);await maintainCacheSize();}catch(error){console.error('[CSS-SW] Fehler beim Cachen:',error);}}
async function maintainCacheSize(){try{const cache=await caches.open(CSS_CACHE_NAME);const requests=await cache.keys();if(requests.length>CSS_CACHE_CONFIG.maxEntries){console.log('[CSS-SW] Cache-Bereinigung wird durchgeführt...');const excessCount=requests.length-CSS_CACHE_CONFIG.maxEntries;for(let i=0;i<excessCount;i++){await cache.delete(requests[i]);}}}catch(error){console.error('[CSS-SW] Fehler bei Cache-Wartung:',error);}}
async function updateCacheInBackground(request){try{const response=await fetch(request);if(response&&response.ok){const cache=await caches.open(CSS_CACHE_NAME);await cache.put(request,response.clone());console.log('[CSS-SW] Hintergrund-Update für:',request.url);}}catch(error){console.log('[CSS-SW] Hintergrund-Update fehlgeschlagen:',error);}}
async function getFallbackCSS(request){const url=new URL(request.url);const fallbackCSS=`body{font-family:system-ui,sans-serif;margin:0;padding:20px;background:#f8fafc;color:#1f2937;}.offline-notice{background:#fef3c7;border:1px solid#f59e0b;border-radius:8px;padding:16px;margin-bottom:20px;text-align:center;}.btn{background:#0073ce;color:white;border:none;padding:8px 16px;border-radius:4px;cursor:pointer;}.card{background:white;border:1px solid#e5e7eb;border-radius:8px;padding:16px;margin-bottom:16px;}`;return new Response(fallbackCSS,{headers:{'Content-Type':'text/css','Cache-Control':'no-cache'}});}
async function prefetchNonCriticalResources(){try{const cache=await caches.open(CSS_CACHE_NAME);for(const resource of NON_CRITICAL_CSS_RESOURCES){try{const request=new Request(resource);const cachedResponse=await cache.match(request);if(!cachedResponse){const response=await fetch(request);if(response&&response.ok){await cache.put(request,response);console.log('[CSS-SW] Prefetch erfolgreich für:',resource);}}}catch(error){console.log('[CSS-SW] Prefetch fehlgeschlagen für:',resource);}}}catch(error){console.error('[CSS-SW] Fehler beim Prefetching:',error);}}
self.addEventListener('message',event=>{const{type,data}=event.data;switch(type){case'SKIP_WAITING':self.skipWaiting();break;case'CLEAR_CSS_CACHE':clearCSSCache().then(()=>{event.ports[0].postMessage({success:true});});break;case'PREFETCH_CSS':prefetchSpecificCSS(data.urls).then(()=>{event.ports[0].postMessage({success:true});});break;case'GET_CACHE_STATS':getCacheStats().then(stats=>{event.ports[0].postMessage(stats);});break;}});async function clearCSSCache(){try{await caches.delete(CSS_CACHE_NAME);console.log('[CSS-SW] CSS-Cache wurde geleert');}catch(error){console.error('[CSS-SW] Fehler beim Leeren des CSS-Cache:',error);}}
async function prefetchSpecificCSS(urls){try{const cache=await caches.open(CSS_CACHE_NAME);for(const url of urls){try{const response=await fetch(url);if(response&&response.ok){await cache.put(url,response);console.log('[CSS-SW] Spezifisches Prefetch für:',url);}}catch(error){console.log('[CSS-SW] Spezifisches Prefetch fehlgeschlagen für:',url);}}}catch(error){console.error('[CSS-SW] Fehler beim spezifischen Prefetching:',error);}}
async function getCacheStats(){try{const cache=await caches.open(CSS_CACHE_NAME);const requests=await cache.keys();return{cssEntries:requests.length,maxEntries:CSS_CACHE_CONFIG.maxEntries,cacheUtilization:(requests.length/CSS_CACHE_CONFIG.maxEntries)*100,cachedUrls:requests.map(req=>req.url)};}catch(error){console.error('[CSS-SW] Fehler beim Abrufen der Cache-Stats:',error);return{error:error.message};}}
self.addEventListener('install',()=>{console.log('[CSS-SW] Installation gestartet um:',new Date().toISOString());});self.addEventListener('activate',()=>{console.log('[CSS-SW] Aktivierung abgeschlossen um:',new Date().toISOString());});self.addEventListener('error',event=>{console.error('[CSS-SW] Globaler Fehler:',event.error);});self.addEventListener('unhandledrejection',event=>{console.error('[CSS-SW] Unbehandelte Promise-Rejection:',event.reason);});console.log('[CSS-SW] CSS-Caching Service Worker geladen');

Binary file not shown.

Binary file not shown.

11
backend/static/js/dark-mode-fix.min.js vendored Normal file
View File

@@ -0,0 +1,11 @@
document.addEventListener('DOMContentLoaded',function(){const darkModeToggle=document.getElementById('darkModeToggle');const html=document.documentElement;const STORAGE_KEY='myp-dark-mode';function isDarkMode(){const savedMode=localStorage.getItem(STORAGE_KEY);if(savedMode!==null){return savedMode==='true';}
return window.matchMedia('(prefers-color-scheme: dark)').matches;}
function updateIcons(isDark){if(!darkModeToggle)return;const sunIcon=darkModeToggle.querySelector('.sun-icon');const moonIcon=darkModeToggle.querySelector('.moon-icon');if(!sunIcon||!moonIcon){console.warn('Premium Dark Mode Icons nicht gefunden');return;}
if(isDark){sunIcon.style.opacity='0';sunIcon.style.transform='scale(0.75) rotate(90deg)';moonIcon.style.opacity='1';moonIcon.style.transform='scale(1) rotate(0deg)';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{sunIcon.style.opacity='1';sunIcon.style.transform='scale(1) rotate(0deg)';moonIcon.style.opacity='0';moonIcon.style.transform='scale(0.75) rotate(-90deg)';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');}
sunIcon.classList.toggle('icon-enter',!isDark);moonIcon.classList.toggle('icon-enter',isDark);}
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');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');updateIcons(false);}}
localStorage.setItem(STORAGE_KEY,enable.toString());const metaThemeColor=document.getElementById('metaThemeColor');if(metaThemeColor){metaThemeColor.setAttribute('content',enable?'#000000':'#ffffff');}
window.dispatchEvent(new CustomEvent('darkModeChanged',{detail:{isDark:enable}}));console.log(`${enable?'🌙':'☀️'}Premium Design umgeschaltet auf:${enable?'Dark Mode':'Light Mode'}`);}
function toggleDarkMode(){const currentMode=isDarkMode();setDarkMode(!currentMode);if(darkModeToggle){const container=darkModeToggle.querySelector('div');if(container){container.style.transform='scale(0.95)';setTimeout(()=>{container.style.transform='';},150);}}}
if(darkModeToggle){console.log('🎨 Premium Dark Mode Toggle Button gefunden - initialisiere...');const newDarkModeToggle=darkModeToggle.cloneNode(true);darkModeToggle.parentNode.replaceChild(newDarkModeToggle,darkModeToggle);newDarkModeToggle.addEventListener('click',function(e){e.preventDefault();e.stopPropagation();toggleDarkMode();});const updatedToggle=document.getElementById('darkModeToggle');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!');}
document.addEventListener('keydown',function(e){if(e.ctrlKey&&e.shiftKey&&e.key==='D'){toggleDarkMode();e.preventDefault();}});document.addEventListener('keydown',function(e){if(e.altKey&&e.key==='t'){toggleDarkMode();e.preventDefault();}});window.toggleDarkMode=toggleDarkMode;window.isDarkMode=isDarkMode;window.setDarkMode=setDarkMode;window.premiumDarkMode={toggle:toggleDarkMode,isDark:isDarkMode,setMode:setDarkMode,version:'3.0.0-premium'};console.log('🎨 Premium Dark Mode System geladen - Version 3.0.0');});

Binary file not shown.

Binary file not shown.

15
backend/static/js/dark-mode.min.js vendored Normal file
View File

@@ -0,0 +1,15 @@
(function(){"use strict";const STORAGE_KEY='myp-dark-mode';let darkModeToggle;const html=document.documentElement;document.addEventListener('DOMContentLoaded',initialize);function shouldUseDarkMode(){const savedMode=localStorage.getItem(STORAGE_KEY);if(savedMode!==null){return savedMode==='true';}
return window.matchMedia('(prefers-color-scheme: dark)').matches;}
function setDarkMode(enable){html.classList.add('disable-transitions');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';}
localStorage.setItem(STORAGE_KEY,enable);updateMetaThemeColor(enable);if(darkModeToggle){updateDarkModeToggle(enable);}
window.dispatchEvent(new CustomEvent('darkModeChanged',{detail:{isDark:enable,source:'dark-mode-toggle',timestamp:new Date().toISOString()}}));const eventName=enable?'darkModeEnabled':'darkModeDisabled';window.dispatchEvent(new CustomEvent(eventName,{detail:{timestamp:new Date().toISOString()}}));setTimeout(function(){html.classList.remove('disable-transitions');},100);console.log(`${enable?'🌙':'☀️'}${enable?'Dark Mode aktiviert - Augenschonender Modus aktiv':'Light Mode aktiviert - Heller Modus aktiv'}`);}
function updateMetaThemeColor(isDark){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)"]')];const darkColor=getComputedStyle(document.documentElement).getPropertyValue('--color-bg')||'#0f172a';const lightColor=getComputedStyle(document.documentElement).getPropertyValue('--color-bg')||'#ffffff';metaTags.forEach(tag=>{if(tag){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{tag.setAttribute('content',isDark?darkColor:lightColor);}}});}
function updateDarkModeToggle(isDark){darkModeToggle.setAttribute('aria-pressed',isDark.toString());darkModeToggle.title=isDark?"Light Mode aktivieren":"Dark Mode aktivieren";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{const icon=darkModeToggle.querySelector('svg');if(icon){icon.classList.add('animate-spin-once');setTimeout(()=>{icon.classList.remove('animate-spin-once');},300);const pathElement=icon.querySelector('path');if(pathElement){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");}}}}}
function initialize(){darkModeToggle=document.getElementById('darkModeToggle');if(!darkModeToggle){console.log('🔧 Dark Mode Toggle nicht gefunden - erstelle automatisch einen neuen Button');createDarkModeToggle();}
if(darkModeToggle){darkModeToggle.addEventListener('click',function(){const isDark=!shouldUseDarkMode();console.log(`👆 Dark Mode Toggle:Wechsel zu ${isDark?'🌙 dunkel':'☀️ hell'}angefordert`);setDarkMode(isDark);});}
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();}});const darkModeMediaQuery=window.matchMedia('(prefers-color-scheme: dark)');try{darkModeMediaQuery.addEventListener('change',function(e){if(localStorage.getItem(STORAGE_KEY)===null){console.log(`🖥️ Systemeinstellung geändert:${e.matches?'🌙 dunkel':'☀️ hell'}`);setDarkMode(e.matches);}});}catch(error){darkModeMediaQuery.addListener(function(e){if(localStorage.getItem(STORAGE_KEY)===null){console.log(`🖥️ Systemeinstellung geändert(Legacy-Browser):${e.matches?'🌙 dunkel':'☀️ hell'}`);setDarkMode(e.matches);}});}
const initialState=shouldUseDarkMode();console.log(`🔍 Ermittelter Ausgangszustand:${initialState?'🌙 Dark Mode':'☀️ Light Mode'}`);setDarkMode(initialState);const animClass=initialState?'dark-mode-transition':'light-mode-transition';document.body.classList.add(animClass);setTimeout(()=>{document.body.classList.remove(animClass);},300);console.log('🚀 Dark Mode Handler erfolgreich initialisiert');}
function createDarkModeToggle(){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;}
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');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");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");sunIcon.appendChild(sunPath);moonIcon.appendChild(moonPath);darkModeToggle.appendChild(sunIcon);darkModeToggle.appendChild(moonIcon);container.appendChild(darkModeToggle);console.log('✅ Dark Mode Toggle Button erfolgreich erstellt und zur Benutzeroberfläche hinzugefügt');}
const isDark=shouldUseDarkMode();console.log(`🏃‍♂️ Sofortige Anwendung:${isDark?'🌙 Dark Mode':'☀️ Light Mode'}(vor DOM-Ladung)`);setDarkMode(isDark);})();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');}

Binary file not shown.

Binary file not shown.

32
backend/static/js/dashboard.min.js vendored Normal file
View File

@@ -0,0 +1,32 @@
let dashboardData={};let updateInterval;const elements={activeJobs:null,scheduledJobs:null,availablePrinters:null,totalPrintTime:null,schedulerStatus:null,recentJobsList:null,recentActivitiesList:null,refreshBtn:null,schedulerToggleBtn:null};document.addEventListener('DOMContentLoaded',function(){initializeDashboard();});function initializeDashboard(){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');if(elements.refreshBtn){elements.refreshBtn.addEventListener('click',refreshDashboard);}
if(elements.schedulerToggleBtn){elements.schedulerToggleBtn.addEventListener('click',toggleScheduler);}
loadDashboardData();loadRecentJobs();loadRecentActivities();loadSchedulerStatus();updateInterval=setInterval(function(){loadDashboardData();loadRecentJobs();loadRecentActivities();loadSchedulerStatus();},30000);}
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');}}
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`;}}
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>';}}}
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;}
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>';}}}
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;}
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>';}}}
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';}}
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);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');}}
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';}});}
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){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);setTimeout(()=>{if(alertDiv.parentNode){alertDiv.parentNode.removeChild(alertDiv);}},5000);}
window.addEventListener('beforeunload',function(){if(updateInterval){clearInterval(updateInterval);}});

Binary file not shown.

Binary file not shown.

10
backend/static/js/debug-fix.min.js vendored Normal file
View File

@@ -0,0 +1,10 @@
(function(){'use strict';console.log('🔧 Debug Fix Script wird geladen...');window.MYP=window.MYP||{};window.MYP.UI=window.MYP.UI||{};window.MVP=window.MVP||{};window.MVP.UI=window.MVP.UI||{};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;}
return{init:function(){console.log('DarkModeManager Fallback init');},setDarkMode:function(){console.log('DarkModeManager Fallback setDarkMode');},isDarkMode:function(){return false;}};};document.addEventListener('DOMContentLoaded',function(){console.log('🚀 Debug Fix: DOM Content geladen');setTimeout(()=>{try{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');}
if(!window.jobManager&&window.JobManager){window.jobManager=new window.JobManager();console.log('✅ JobManager Instanz erstellt');}
if(window.jobManager&&!window.jobManager.setupFormHandlers){window.jobManager.setupFormHandlers=function(){console.log('✅ setupFormHandlers Fallback aufgerufen');};}
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();}};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);});window.addEventListener('error',function(e){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:',JSON.stringify(errorInfo,null,2));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');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;}
if(e.message.includes('showToast is not defined')){console.log('🔧 showToast Fehler erkannt - verwende Fallback');e.preventDefault();return false;}});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');})();

Binary file not shown.

Binary file not shown.

32
backend/static/js/event-handlers.min.js vendored Normal file
View File

@@ -0,0 +1,32 @@
class GlobalEventManager{constructor(){this.init();}
init(){document.addEventListener('click',this.handleClick.bind(this));document.addEventListener('DOMContentLoaded',this.setupEventListeners.bind(this));if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',this.setupEventListeners.bind(this));}else{this.setupEventListeners();}}
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);}
parseActionParams(element){const params={};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;}
executeAction(action,params,element){console.log(`🎯 Führe Action aus:${action}`,params);switch(action){case'refresh-dashboard':if(typeof refreshDashboard==='function')refreshDashboard();break;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;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;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;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;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;case'reset-form':if(typeof resetForm==='function')resetForm();else this.resetNearestForm(element);break;case'clear-file':if(typeof clearFile==='function')clearFile();break;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;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;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;case'refresh-stats':if(typeof refreshStats==='function')refreshStats();break;case'export-stats':if(typeof exportStats==='function')exportStats();break;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}`);if(typeof window[action]==='function'){window[action](params);}
break;}}
closeModal(modalElement){if(modalElement){modalElement.classList.add('hidden');modalElement.remove();}}
resetNearestForm(element){const form=element.closest('form');if(form){form.reset();}}
setupEventListeners(){this.setupAutoRefresh();this.setupKeyboardShortcuts();this.setupFormValidation();console.log('🔧 Globale Event-Handler initialisiert');}
setupAutoRefresh(){const currentPath=window.location.pathname;if(currentPath.includes('/dashboard')){setInterval(()=>{if(typeof refreshDashboard==='function'){refreshDashboard();}},30000);}
if(currentPath.includes('/jobs')){setInterval(()=>{if(typeof refreshJobs==='function'){refreshJobs();}},15000);}}
setupKeyboardShortcuts(){document.addEventListener('keydown',(event)=>{if(event.key==='Escape'){const openModal=document.querySelector('.fixed:not(.hidden)');if(openModal){this.closeModal(openModal);}}
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();}}});}
setupFormValidation(){const forms=document.querySelectorAll('form[data-validate]');forms.forEach(form=>{form.addEventListener('submit',this.validateForm.bind(this));});}
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');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;}}
const globalEventManager=new GlobalEventManager();if(typeof module!=='undefined'&&module.exports){module.exports=GlobalEventManager;}
console.log('🌍 Globaler Event Manager geladen');

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,80 @@
class GlassmorphismNotificationSystem{constructor(){this.notifications=new Map();this.toastCounter=0;this.soundEnabled=localStorage.getItem('myp-notification-sound')!=='false';this.animationsEnabled=!window.matchMedia('(prefers-reduced-motion: reduce)').matches;this.actionCallbacks=new Map();this.callbackCounter=0;this.init();this.setupGlobalFunctions();this.injectStyles();}
init(){this.createToastContainer();this.setupEventListeners();console.log('🎨 Glassmorphism Notification System initialisiert');}
createToastContainer(){if(!document.getElementById('glassmorphism-toast-container')){const container=document.createElement('div');container.id='glassmorphism-toast-container';container.className='notifications-container';document.body.appendChild(container);}}
setupGlobalFunctions(){window.showFlashMessage=this.showToast.bind(this);window.showToast=this.showToast.bind(this);window.showNotification=this.showToast.bind(this);window.showSuccessMessage=(msg,duration)=>this.showToast(msg,'success',duration);window.showErrorMessage=(msg,duration)=>this.showToast(msg,'error',duration);window.showWarningMessage=(msg,duration)=>this.showToast(msg,'warning',duration);window.showInfoMessage=(msg,duration)=>this.showToast(msg,'info',duration);window.showSuccessNotification=(msg)=>this.showToast(msg,'success');window.showPersistentAlert=this.showPersistentAlert.bind(this);window.showConfirmationToast=this.showConfirmationToast.bind(this);window.showProgressToast=this.showProgressToast.bind(this);window.executeNotificationCallback=this.executeCallback.bind(this);}
setupEventListeners(){document.addEventListener('keydown',(e)=>{if(e.key==='Escape'){this.closeAllToasts();}
if((e.ctrlKey||e.metaKey)&&e.shiftKey&&e.key==='N'){this.showNotificationSettings();}});window.addEventListener('orientationchange',()=>{setTimeout(()=>this.repositionAllToasts(),200);});}
showToast(message,type='info',duration=5000,options={}){if(window.dndManager?.isEnabled){window.dndManager.suppressNotification(message,type);return null;}
const toastId=`glass-toast-${++this.toastCounter}`;const toast=this.createToastElement(toastId,message,type,duration,options);this.addToContainer(toast);this.animateIn(toast);this.scheduleRemoval(toastId,duration,options.persistent);if(this.soundEnabled&&options.playSound!==false){this.playNotificationSound(type);}
if(options.browserNotification&&'Notification'in window){this.showBrowserNotification(message,type,options);}
return toastId;}
createToastElement(toastId,message,type,duration,options){const toast=document.createElement('div');toast.id=toastId;toast.className=`glassmorphism-toast notification notification-${type}`;toast.setAttribute('role','alert');toast.setAttribute('aria-live','polite');toast.setAttribute('aria-atomic','true');const iconSvg=this.getIconSvg(type);const progressBar=duration>0&&!options.persistent?this.createProgressBar(duration):'';toast.innerHTML=`<div class="toast-content"><div class="toast-header"><div class="toast-icon">${iconSvg}</div><div class="toast-body">${options.title?`<div class="toast-title">${options.title}</div>`:''}<div class="toast-message">${message}</div></div><div class="toast-actions">${options.actions?this.createActionButtons(options.actions,toastId):''}<button class="toast-close"onclick="glassNotificationSystem.closeToast('${toastId}')"
aria-label="Benachrichtigung schließen"title="Schließen"><svg class="w-4 h-4"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>${progressBar}</div>`;if(duration>0&&!options.persistent){this.setupHoverEvents(toast,toastId);}
this.notifications.set(toastId,{element:toast,type,message,timestamp:Date.now(),options});return toast;}
createProgressBar(duration){return`<div class="toast-progress"><div class="toast-progress-bar"style="animation: toast-progress ${duration}ms linear forwards;"></div></div>`;}
createActionButtons(actions,toastId){return actions.map(action=>{let callbackId='';if(action.callback&&typeof action.callback==='function'){callbackId=`callback-${++this.callbackCounter}`;this.actionCallbacks.set(callbackId,action.callback);}
return`<button class="toast-action-btn toast-action-${action.type || 'secondary'}"
onclick="glassNotificationSystem.handleActionClick('${callbackId}', '${toastId}', ${action.closeAfter !== false})"
title="${action.title || action.text}">${action.icon?`<svg class="w-4 h-4">${action.icon}</svg>`:''}
${action.text}</button>`;}).join('');}
getIconSvg(type){const icons={success:`<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="M5 13l4 4L19 7"/></svg>`,error:`<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>`,warning:`<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>`,info:`<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`,loading:`<svg class="w-5 h-5 animate-spin"fill="none"viewBox="0 0 24 24"><circle cx="12"cy="12"r="10"stroke="currentColor"stroke-width="4"class="opacity-25"></circle><path 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"class="opacity-75"></path></svg>`};return icons[type]||icons.info;}
addToContainer(toast){const container=document.getElementById('glassmorphism-toast-container');if(container){container.appendChild(toast);this.repositionAllToasts();}}
animateIn(toast){if(!this.animationsEnabled){toast.classList.add('show');return;}
toast.style.transform='translateX(120%) scale(0.8) rotateY(15deg)';toast.style.opacity='0';toast.style.filter='blur(8px)';requestAnimationFrame(()=>{toast.style.transition='all 0.8s cubic-bezier(0.34, 1.56, 0.64, 1)';toast.style.transform='translateX(0) scale(1) rotateY(0deg)';toast.style.opacity='1';toast.style.filter='blur(0px)';toast.classList.add('show');});}
setupHoverEvents(toast,toastId){let timeoutId;let isPaused=false;const notification=this.notifications.get(toastId);if(!notification)return;const pauseTimer=()=>{isPaused=true;clearTimeout(timeoutId);const progressBar=toast.querySelector('.toast-progress-bar');if(progressBar){progressBar.style.animationPlayState='paused';}
toast.style.transform='translateY(-4px) scale(1.03)';toast.style.filter='brightness(1.1) saturate(1.1)';};const resumeTimer=()=>{isPaused=false;const progressBar=toast.querySelector('.toast-progress-bar');if(progressBar){progressBar.style.animationPlayState='running';}
toast.style.transform='translateY(0) scale(1)';toast.style.filter='brightness(1) saturate(1)';};toast.addEventListener('mouseenter',pauseTimer);toast.addEventListener('mouseleave',resumeTimer);toast.addEventListener('focus',pauseTimer);toast.addEventListener('blur',resumeTimer);}
scheduleRemoval(toastId,duration,persistent=false){if(persistent||duration<=0)return;setTimeout(()=>{this.closeToast(toastId);},duration);}
closeToast(toastId){const notification=this.notifications.get(toastId);if(!notification)return;const toast=notification.element;if(this.animationsEnabled){toast.style.transition='all 0.6s cubic-bezier(0.4, 0, 1, 1)';toast.style.transform='translateX(120%) scale(0.8) rotateY(-15deg)';toast.style.opacity='0';toast.style.filter='blur(4px)';setTimeout(()=>{this.removeToast(toastId);},600);}else{this.removeToast(toastId);}}
removeToast(toastId){const notification=this.notifications.get(toastId);if(!notification)return;if(notification.element.parentNode){notification.element.parentNode.removeChild(notification.element);}
this.notifications.delete(toastId);this.actionCallbacks.forEach((callback,callbackId)=>{if(callbackId.includes(toastId)){this.actionCallbacks.delete(callbackId);}});this.repositionAllToasts();}
repositionAllToasts(){const container=document.getElementById('glassmorphism-toast-container');if(!container)return;const toasts=Array.from(container.children);toasts.forEach((toast,index)=>{toast.style.top=`${1+index*3.75}rem`;toast.style.zIndex=1000-index;if(index>0){toast.style.transform=`scale(${1-index*0.015})`;toast.style.opacity=`${1-index*0.08}`;}});}
closeAllToasts(){const toastIds=Array.from(this.notifications.keys());toastIds.forEach((id,index)=>{setTimeout(()=>this.closeToast(id),index*100);});}
showConfirmationToast(message,onConfirm,onCancel=null,options={}){const confirmCallback=()=>{if(typeof onConfirm==='function'){try{onConfirm();}catch(error){console.error('Fehler beim Ausführen der Bestätigungslogik:',error);}}};const cancelCallback=()=>{if(typeof onCancel==='function'){try{onCancel();}catch(error){console.error('Fehler beim Ausführen der Abbruchlogik:',error);}}};const actions=[{text:options.confirmText||'Bestätigen',type:'primary',callback:confirmCallback,closeAfter:true}];if(onCancel||options.cancelText){actions.push({text:options.cancelText||'Abbrechen',type:'secondary',callback:cancelCallback,closeAfter:true});}
return this.showToast(message,'warning',0,{persistent:true,title:options.title||'Bestätigung erforderlich',actions:actions,...options});}
showProgressToast(message,type='info',options={}){const toastId=this.showToast(message,'loading',0,{persistent:true,title:options.title||'Verarbeitung...',...options});return{id:toastId,updateProgress:(percent)=>this.updateProgressToast(toastId,percent),updateMessage:(newMessage)=>this.updateToastMessage(toastId,newMessage),complete:(finalMessage,finalType='success')=>{this.closeToast(toastId);if(finalMessage){this.showToast(finalMessage,finalType,3000);}}};}
updateProgressToast(toastId,percent){const notification=this.notifications.get(toastId);if(!notification)return;let progressBar=notification.element.querySelector('.toast-progress-bar');if(!progressBar){const progressContainer=document.createElement('div');progressContainer.className='toast-progress';progressContainer.innerHTML=`<div class="toast-progress-bar"></div>`;notification.element.querySelector('.toast-content').appendChild(progressContainer);progressBar=progressContainer.querySelector('.toast-progress-bar');}
progressBar.style.width=`${Math.min(100,Math.max(0,percent))}%`;}
updateToastMessage(toastId,newMessage){const notification=this.notifications.get(toastId);if(!notification)return;const messageEl=notification.element.querySelector('.toast-message');if(messageEl){messageEl.textContent=newMessage;}}
showPersistentAlert(message,type='warning',options={}){return this.showToast(message,type,0,{persistent:true,title:options.title||'Wichtiger Hinweis',actions:[{text:'Verstanden',type:'primary',onClick:''}],...options});}
async showBrowserNotification(message,type,options={}){if(!('Notification'in window))return null;if(Notification.permission==='granted'){return new Notification(options.title||'MYP Platform',{body:message,icon:'/static/icons/notification-icon.png',badge:'/static/icons/badge-icon.png',tag:`myp-${type}`,...options.browserOptions});}else if(Notification.permission==='default'){const permission=await Notification.requestPermission();if(permission==='granted'){return this.showBrowserNotification(message,type,options);}}
return null;}
playNotificationSound(type){if(!this.soundEnabled)return;try{const audioContext=new(window.AudioContext||window.webkitAudioContext)();const frequencies={success:[523.25,659.25,783.99,880],error:[440,370,311],warning:[493.88,587.33,659.25],info:[523.25,659.25],loading:[392,440,493.88,523.25]};const freq=frequencies[type]||frequencies.info;freq.forEach((f,i)=>{setTimeout(()=>{const oscillator=audioContext.createOscillator();const gainNode=audioContext.createGain();const filterNode=audioContext.createBiquadFilter();oscillator.connect(filterNode);filterNode.connect(gainNode);gainNode.connect(audioContext.destination);filterNode.type='lowpass';filterNode.frequency.setValueAtTime(2000,audioContext.currentTime);filterNode.Q.setValueAtTime(1,audioContext.currentTime);oscillator.frequency.setValueAtTime(f,audioContext.currentTime);oscillator.type=type==='error'?'triangle':'sine';const baseVolume=type==='error'?0.06:0.08;gainNode.gain.setValueAtTime(0,audioContext.currentTime);gainNode.gain.linearRampToValueAtTime(baseVolume,audioContext.currentTime+0.1);gainNode.gain.exponentialRampToValueAtTime(0.001,audioContext.currentTime+0.3);oscillator.start(audioContext.currentTime);oscillator.stop(audioContext.currentTime+0.3);},i*120);});if(type==='success'){setTimeout(()=>{const reverb=audioContext.createConvolver();const impulse=audioContext.createBuffer(2,audioContext.sampleRate*0.5,audioContext.sampleRate);for(let channel=0;channel<impulse.numberOfChannels;channel++){const channelData=impulse.getChannelData(channel);for(let i=0;i<channelData.length;i++){channelData[i]=(Math.random()*2-1)*Math.pow(1-i/channelData.length,2);}}
reverb.buffer=impulse;const finalOsc=audioContext.createOscillator();const finalGain=audioContext.createGain();finalOsc.connect(reverb);reverb.connect(finalGain);finalGain.connect(audioContext.destination);finalOsc.frequency.setValueAtTime(1046.5,audioContext.currentTime);finalOsc.type='sine';finalGain.gain.setValueAtTime(0,audioContext.currentTime);finalGain.gain.linearRampToValueAtTime(0.03,audioContext.currentTime+0.1);finalGain.gain.exponentialRampToValueAtTime(0.001,audioContext.currentTime+0.8);finalOsc.start(audioContext.currentTime);finalOsc.stop(audioContext.currentTime+0.8);},freq.length*120);}}catch(error){}}
toggleSound(){this.soundEnabled=!this.soundEnabled;localStorage.setItem('myp-notification-sound',this.soundEnabled.toString());this.showToast(`Benachrichtigungstöne ${this.soundEnabled?'aktiviert':'deaktiviert'}`,'info',2000);}
showNotificationSettings(){const settingsHTML=`<div class="notification-settings"><h3>Benachrichtigungseinstellungen</h3><label class="setting-item"><input type="checkbox"${this.soundEnabled?'checked':''}
onchange="glassNotificationSystem.toggleSound()"><span>Benachrichtigungstöne</span></label><label class="setting-item"><input type="checkbox"${this.animationsEnabled?'checked':''}
onchange="glassNotificationSystem.toggleAnimations()"><span>Animationen</span></label></div>`;this.showToast(settingsHTML,'info',0,{persistent:true,title:'Einstellungen',actions:[{text:'Schließen',type:'secondary',onClick:''}]});}
toggleAnimations(){this.animationsEnabled=!this.animationsEnabled;this.showToast(`Animationen ${this.animationsEnabled?'aktiviert':'deaktiviert'}`,'info',2000);}
injectStyles(){if(document.getElementById('glassmorphism-notification-styles'))return;const styles=document.createElement('style');styles.id='glassmorphism-notification-styles';styles.textContent=`.glassmorphism-toast{margin-bottom:0.625rem;transform:translateX(100%);opacity:0;transition:all 0.7s cubic-bezier(0.34,1.56,0.64,1);will-change:transform,opacity,filter;backdrop-filter:blur(50px)saturate(200%)brightness(120%)contrast(110%);-webkit-backdrop-filter:blur(50px)saturate(200%)brightness(120%)contrast(110%);box-shadow:0 32px 64px rgba(0,0,0,0.1),0 16px 32px rgba(0,0,0,0.06),0 6px 12px rgba(0,0,0,0.05),inset 0 2px 0 rgba(255,255,255,0.4),inset 0 1px 2px rgba(255,255,255,0.7),0 0 0 1px rgba(255,255,255,0.18);border-radius:1.5rem;overflow:hidden;position:relative;background:linear-gradient(145deg,rgba(255,255,255,0.12)0%,rgba(255,255,255,0.06)25%,rgba(255,255,255,0.1)50%,rgba(255,255,255,0.05)75%,rgba(255,255,255,0.08)100%);}
.glassmorphism-toast::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at 25%25%,rgba(255,255,255,0.25)0%,transparent 35%),radial-gradient(circle at 75%75%,rgba(255,255,255,0.15)0%,transparent 35%),radial-gradient(circle at 50%10%,rgba(255,255,255,0.1)0%,transparent 40%),linear-gradient(135deg,rgba(255,255,255,0.15)0%,transparent 60%),conic-gradient(from 180deg at 50%50%,rgba(255,255,255,0.05)0deg,rgba(255,255,255,0.1)45deg,rgba(255,255,255,0.05)90deg,rgba(255,255,255,0.08)135deg,rgba(255,255,255,0.05)180deg,rgba(255,255,255,0.12)225deg,rgba(255,255,255,0.05)270deg,rgba(255,255,255,0.08)315deg,rgba(255,255,255,0.05)360deg);pointer-events:none;opacity:0;transition:opacity 0.4s cubic-bezier(0.4,0,0.2,1);animation:subtle-shimmer 8s ease-in-out infinite;}
@keyframes subtle-shimmer{0%,100%{opacity:0;transform:scale(1)rotate(0deg);}
25%{opacity:0.3;transform:scale(1.01)rotate(1deg);}
50%{opacity:0.6;transform:scale(1.02)rotate(0deg);}
75%{opacity:0.3;transform:scale(1.01)rotate(-1deg);}}.glassmorphism-toast:hover::before{opacity:1;animation-play-state:paused;}
.glassmorphism-toast::after{content:'';position:absolute;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(circle at 20%20%,rgba(255,255,255,0.3)1px,transparent 1px),radial-gradient(circle at 60%80%,rgba(255,255,255,0.2)1px,transparent 1px),radial-gradient(circle at 80%30%,rgba(255,255,255,0.25)1px,transparent 1px),radial-gradient(circle at 30%70%,rgba(255,255,255,0.15)1px,transparent 1px);opacity:0;transition:opacity 0.6s ease;animation:floating-particles 12s linear infinite;pointer-events:none;}@keyframes floating-particles{0%{transform:translate(0,0)rotate(0deg);}
25%{transform:translate(-10px,-10px)rotate(90deg);}
50%{transform:translate(0,-20px)rotate(180deg);}
75%{transform:translate(10px,-10px)rotate(270deg);}
100%{transform:translate(0,0)rotate(360deg);}}.glassmorphism-toast:hover::after{opacity:1;}.glassmorphism-toast.show{transform:translateX(0);opacity:1;}
.glassmorphism-toast.notification-success{background:linear-gradient(145deg,rgba(34,197,94,0.18)0%,rgba(134,239,172,0.12)20%,rgba(16,185,129,0.15)40%,rgba(34,197,94,0.08)60%,rgba(134,239,172,0.1)80%,rgba(16,185,129,0.06)100%);border:1px solid rgba(34,197,94,0.3);box-shadow:0 40px 80px rgba(34,197,94,0.15),0 20px 40px rgba(34,197,94,0.08),0 8px 16px rgba(16,185,129,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(134,239,172,0.4),0 0 0 1px rgba(34,197,94,0.2);color:rgba(5,95,70,0.95);}.dark.glassmorphism-toast.notification-success{color:rgba(134,239,172,0.95);background:linear-gradient(145deg,rgba(34,197,94,0.25)0%,rgba(134,239,172,0.15)30%,rgba(34,197,94,0.12)70%,rgba(16,185,129,0.08)100%);}.glassmorphism-toast.notification-error{background:linear-gradient(145deg,rgba(239,68,68,0.18)0%,rgba(252,165,165,0.12)20%,rgba(220,38,38,0.15)40%,rgba(239,68,68,0.08)60%,rgba(252,165,165,0.1)80%,rgba(220,38,38,0.06)100%);border:1px solid rgba(239,68,68,0.3);box-shadow:0 40px 80px rgba(239,68,68,0.15),0 20px 40px rgba(239,68,68,0.08),0 8px 16px rgba(220,38,38,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(252,165,165,0.4),0 0 0 1px rgba(239,68,68,0.2);color:rgba(153,27,27,0.95);}.dark.glassmorphism-toast.notification-error{color:rgba(252,165,165,0.95);background:linear-gradient(145deg,rgba(239,68,68,0.25)0%,rgba(252,165,165,0.15)30%,rgba(239,68,68,0.12)70%,rgba(220,38,38,0.08)100%);}.glassmorphism-toast.notification-warning{background:linear-gradient(145deg,rgba(245,158,11,0.18)0%,rgba(252,211,77,0.12)20%,rgba(217,119,6,0.15)40%,rgba(245,158,11,0.08)60%,rgba(252,211,77,0.1)80%,rgba(217,119,6,0.06)100%);border:1px solid rgba(245,158,11,0.3);box-shadow:0 40px 80px rgba(245,158,11,0.15),0 20px 40px rgba(245,158,11,0.08),0 8px 16px rgba(217,119,6,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(252,211,77,0.4),0 0 0 1px rgba(245,158,11,0.2);color:rgba(146,64,14,0.95);}.dark.glassmorphism-toast.notification-warning{color:rgba(252,211,77,0.95);background:linear-gradient(145deg,rgba(245,158,11,0.25)0%,rgba(252,211,77,0.15)30%,rgba(245,158,11,0.12)70%,rgba(217,119,6,0.08)100%);}.glassmorphism-toast.notification-info{background:linear-gradient(145deg,rgba(59,130,246,0.18)0%,rgba(147,197,253,0.12)20%,rgba(37,99,235,0.15)40%,rgba(59,130,246,0.08)60%,rgba(147,197,253,0.1)80%,rgba(37,99,235,0.06)100%);border:1px solid rgba(59,130,246,0.3);box-shadow:0 40px 80px rgba(59,130,246,0.15),0 20px 40px rgba(59,130,246,0.08),0 8px 16px rgba(37,99,235,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(147,197,253,0.4),0 0 0 1px rgba(59,130,246,0.2);color:rgba(30,64,175,0.95);}.dark.glassmorphism-toast.notification-info{color:rgba(147,197,253,0.95);background:linear-gradient(145deg,rgba(59,130,246,0.25)0%,rgba(147,197,253,0.15)30%,rgba(59,130,246,0.12)70%,rgba(37,99,235,0.08)100%);}.glassmorphism-toast.notification-loading{background:linear-gradient(145deg,rgba(99,102,241,0.18)0%,rgba(165,180,252,0.12)20%,rgba(79,70,229,0.15)40%,rgba(99,102,241,0.08)60%,rgba(165,180,252,0.1)80%,rgba(79,70,229,0.06)100%);border:1px solid rgba(99,102,241,0.3);box-shadow:0 40px 80px rgba(99,102,241,0.15),0 20px 40px rgba(99,102,241,0.08),0 8px 16px rgba(79,70,229,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(165,180,252,0.4),0 0 0 1px rgba(99,102,241,0.2);color:rgba(55,48,163,0.95);}.dark.glassmorphism-toast.notification-loading{color:rgba(165,180,252,0.95);background:linear-gradient(145deg,rgba(99,102,241,0.25)0%,rgba(165,180,252,0.15)30%,rgba(99,102,241,0.12)70%,rgba(79,70,229,0.08)100%);}
.dark.glassmorphism-toast{backdrop-filter:blur(80px)saturate(200%)brightness(115%)contrast(125%);-webkit-backdrop-filter:blur(80px)saturate(200%)brightness(115%)contrast(125%);box-shadow:0 40px 80px rgba(0,0,0,0.4),0 20px 40px rgba(0,0,0,0.3),0 8px 16px rgba(0,0,0,0.2),inset 0 3px 0 rgba(255,255,255,0.15),inset 0 1px 2px rgba(255,255,255,0.25),0 0 0 1px rgba(255,255,255,0.08);background:linear-gradient(145deg,rgba(0,0,0,0.25)0%,rgba(15,15,15,0.18)25%,rgba(0,0,0,0.22)50%,rgba(10,10,10,0.15)75%,rgba(0,0,0,0.2)100%);}.dark.glassmorphism-toast::before{background:radial-gradient(circle at 25%25%,rgba(255,255,255,0.12)0%,transparent 35%),radial-gradient(circle at 75%75%,rgba(255,255,255,0.08)0%,transparent 35%),radial-gradient(circle at 50%10%,rgba(255,255,255,0.06)0%,transparent 40%),linear-gradient(135deg,rgba(255,255,255,0.08)0%,transparent 60%),conic-gradient(from 180deg at 50%50%,rgba(255,255,255,0.03)0deg,rgba(255,255,255,0.06)45deg,rgba(255,255,255,0.03)90deg,rgba(255,255,255,0.05)135deg,rgba(255,255,255,0.03)180deg,rgba(255,255,255,0.07)225deg,rgba(255,255,255,0.03)270deg,rgba(255,255,255,0.05)315deg,rgba(255,255,255,0.03)360deg);}.dark.glassmorphism-toast::after{background:radial-gradient(circle at 20%20%,rgba(255,255,255,0.15)1px,transparent 1px),radial-gradient(circle at 60%80%,rgba(255,255,255,0.1)1px,transparent 1px),radial-gradient(circle at 80%30%,rgba(255,255,255,0.12)1px,transparent 1px),radial-gradient(circle at 30%70%,rgba(255,255,255,0.08)1px,transparent 1px);}.toast-content{position:relative;overflow:hidden;padding:1rem;border-radius:inherit;}.toast-header{display:flex;align-items:flex-start;gap:0.875rem;}.toast-icon{flex-shrink:0;width:2.25rem;height:2.25rem;display:flex;align-items:center;justify-content:center;border-radius:50%;background:linear-gradient(145deg,rgba(255,255,255,0.35)0%,rgba(255,255,255,0.2)50%,rgba(255,255,255,0.3)100%);backdrop-filter:blur(16px)saturate(140%);-webkit-backdrop-filter:blur(16px)saturate(140%);border:1px solid rgba(255,255,255,0.4);box-shadow:0 8px 16px rgba(0,0,0,0.06),0 2px 4px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.6),inset 0-1px 0 rgba(0,0,0,0.03);transition:all 0.3s cubic-bezier(0.4,0,0.2,1);position:relative;overflow:hidden;}
.toast-icon::before{content:'';position:absolute;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(circle,rgba(255,255,255,0.25)0%,transparent 70%);opacity:0;transition:opacity 0.3s ease;animation:icon-pulse 3s ease-in-out infinite;}@keyframes icon-pulse{0%,100%{opacity:0;transform:scale(0.8);}
50%{opacity:0.4;transform:scale(1.1);}}.toast-icon:hover::before{opacity:1;animation-play-state:paused;}.toast-icon:hover{transform:scale(1.08)rotate(8deg);box-shadow:0 12px 24px rgba(0,0,0,0.08),0 4px 8px rgba(0,0,0,0.06),inset 0 1px 0 rgba(255,255,255,0.7),inset 0-1px 0 rgba(0,0,0,0.05);}.dark.toast-icon{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.3)100%);border:1px solid rgba(255,255,255,0.12);box-shadow:0 8px 16px rgba(0,0,0,0.25),0 2px 4px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.12),inset 0-1px 0 rgba(255,255,255,0.03);}.dark.toast-icon::before{background:radial-gradient(circle,rgba(255,255,255,0.12)0%,transparent 70%);}.toast-body{flex:1;min-width:0;}.toast-title{font-weight:600;font-size:0.875rem;margin-bottom:0.375rem;line-height:1.3;letter-spacing:0.01em;text-shadow:0 1px 2px rgba(0,0,0,0.06);background:linear-gradient(135deg,currentColor 0%,currentColor 100%);-webkit-background-clip:text;background-clip:text;}.toast-message{font-size:0.8125rem;line-height:1.4;opacity:0.9;font-weight:450;text-shadow:0 1px 2px rgba(0,0,0,0.04);}.toast-actions{display:flex;gap:0.5rem;align-items:flex-start;flex-shrink:0;}.toast-action-btn{padding:0.5rem 0.875rem;border-radius:0.75rem;font-size:0.75rem;font-weight:500;border:none;cursor:pointer;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);display:flex;align-items:center;gap:0.375rem;background:linear-gradient(145deg,rgba(255,255,255,0.25)0%,rgba(255,255,255,0.12)50%,rgba(255,255,255,0.2)100%);color:inherit;backdrop-filter:blur(16px)saturate(130%);-webkit-backdrop-filter:blur(16px)saturate(130%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 4px 8px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.5);text-shadow:0 1px 2px rgba(0,0,0,0.06);position:relative;overflow:hidden;}
.toast-action-btn::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.3)50%,transparent 100%);transition:left 0.4s cubic-bezier(0.4,0,0.2,1);}.toast-action-btn:hover::before{left:100%;}.toast-action-btn:hover{background:linear-gradient(145deg,rgba(255,255,255,0.35)0%,rgba(255,255,255,0.2)50%,rgba(255,255,255,0.3)100%);transform:translateY(-2px)scale(1.04);box-shadow:0 8px 16px rgba(0,0,0,0.08),0 2px 4px rgba(0,0,0,0.06),inset 0 1px 0 rgba(255,255,255,0.6);border-color:rgba(255,255,255,0.5);}.toast-action-btn:active{transform:translateY(-1px)scale(1.02);transition:transform 0.1s ease;}.toast-action-primary{background:linear-gradient(145deg,rgba(59,130,246,0.8)0%,rgba(37,99,235,0.85)50%,rgba(59,130,246,0.75)100%);color:white;border-color:rgba(59,130,246,0.6);box-shadow:0 4px 12px rgba(59,130,246,0.2),0 1px 3px rgba(59,130,246,0.12),inset 0 1px 0 rgba(255,255,255,0.25);text-shadow:0 1px 2px rgba(0,0,0,0.15);}.toast-action-primary::before{background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.25)50%,transparent 100%);}.toast-action-primary:hover{background:linear-gradient(145deg,rgba(59,130,246,0.9)0%,rgba(37,99,235,0.95)50%,rgba(59,130,246,0.85)100%);box-shadow:0 8px 20px rgba(59,130,246,0.25),0 2px 6px rgba(59,130,246,0.18),inset 0 1px 0 rgba(255,255,255,0.3);}.dark.toast-action-btn{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.3)100%);border:1px solid rgba(255,255,255,0.12);box-shadow:0 4px 8px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.08);}.dark.toast-action-btn:hover{background:linear-gradient(145deg,rgba(0,0,0,0.45)0%,rgba(15,15,15,0.35)50%,rgba(0,0,0,0.4)100%);box-shadow:0 8px 16px rgba(0,0,0,0.2),0 2px 4px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.12);}.toast-close{padding:0.375rem;border-radius:0.625rem;border:none;background:linear-gradient(145deg,rgba(255,255,255,0.18)0%,rgba(255,255,255,0.08)50%,rgba(255,255,255,0.12)100%);color:inherit;cursor:pointer;opacity:0.75;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);display:flex;align-items:center;justify-content:center;backdrop-filter:blur(16px)saturate(110%);-webkit-backdrop-filter:blur(16px)saturate(110%);border:1px solid rgba(255,255,255,0.2);box-shadow:0 2px 4px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.3);position:relative;overflow:hidden;}
.toast-close::after{content:'';position:absolute;top:50%;left:50%;width:0;height:0;border-radius:50%;background:rgba(255,255,255,0.3);transform:translate(-50%,-50%);transition:width 0.25s ease,height 0.25s ease;}.toast-close:hover::after{width:100%;height:100%;}.toast-close:hover{opacity:1;background:linear-gradient(145deg,rgba(255,255,255,0.28)0%,rgba(255,255,255,0.15)50%,rgba(255,255,255,0.22)100%);transform:scale(1.1)rotate(90deg);box-shadow:0 4px 8px rgba(0,0,0,0.08),0 1px 2px rgba(0,0,0,0.06),inset 0 1px 0 rgba(255,255,255,0.5);}.toast-close:active{transform:scale(1.05)rotate(90deg);transition:transform 0.1s ease;}.dark.toast-close{background:linear-gradient(145deg,rgba(0,0,0,0.25)0%,rgba(15,15,15,0.15)50%,rgba(0,0,0,0.2)100%);border:1px solid rgba(255,255,255,0.08);box-shadow:0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.06);}.dark.toast-close::after{background:rgba(255,255,255,0.15);}.dark.toast-close:hover{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.4)100%);box-shadow:0 4px 8px rgba(0,0,0,0.25),0 1px 2px rgba(0,0,0,0.18),inset 0 1px 0 rgba(255,255,255,0.1);}.toast-progress{position:absolute;bottom:0;left:0;right:0;height:3px;background:linear-gradient(90deg,rgba(255,255,255,0.08)0%,rgba(255,255,255,0.04)50%,rgba(255,255,255,0.08)100%);overflow:hidden;border-radius:0 0 1.75rem 1.75rem;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);}.toast-progress-bar{height:100%;background:linear-gradient(90deg,rgba(255,255,255,0.6)0%,rgba(255,255,255,0.8)25%,rgba(255,255,255,0.9)50%,rgba(255,255,255,0.8)75%,rgba(255,255,255,0.6)100%);width:0%;transition:width 0.25s ease;position:relative;border-radius:inherit;box-shadow:0 0 8px rgba(255,255,255,0.4),0 0 4px rgba(255,255,255,0.3),inset 0 1px 0 rgba(255,255,255,0.7);overflow:hidden;}
.toast-progress-bar::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.4)25%,rgba(255,255,255,0.6)50%,rgba(255,255,255,0.4)75%,transparent 100%);animation:progress-shimmer 2s ease-in-out infinite;}.toast-progress-bar::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(45deg,transparent 25%,rgba(255,255,255,0.2)25%,rgba(255,255,255,0.2)50%,transparent 50%,transparent 75%,rgba(255,255,255,0.2)75%);background-size:12px 12px;animation:progress-stripes 0.8s linear infinite;}@keyframes progress-shimmer{0%{transform:translateX(-100%);}
100%{transform:translateX(100%);}}@keyframes progress-stripes{0%{background-position:0 0;}
100%{background-position:12px 0;}}@keyframes toast-progress{from{width:100%;}
to{width:0%;}}.notification-settings{max-width:320px;padding:0.875rem;background:linear-gradient(145deg,rgba(255,255,255,0.12)0%,rgba(255,255,255,0.06)50%,rgba(255,255,255,0.1)100%);border-radius:1.25rem;backdrop-filter:blur(24px)saturate(140%);-webkit-backdrop-filter:blur(24px)saturate(140%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 16px 32px rgba(0,0,0,0.08),0 6px 12px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.4);}.dark.notification-settings{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.3)100%);border:1px solid rgba(255,255,255,0.08);box-shadow:0 16px 32px rgba(0,0,0,0.25),0 6px 12px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.06);}.notification-settings h3{margin-bottom:1rem;font-size:1rem;font-weight:600;color:inherit;text-align:center;background:linear-gradient(135deg,currentColor 0%,currentColor 100%);-webkit-background-clip:text;background-clip:text;text-shadow:0 1px 2px rgba(0,0,0,0.08);}.setting-item{display:flex;align-items:center;gap:0.75rem;margin:1rem 0;cursor:pointer;font-size:0.875rem;font-weight:450;padding:0.75rem;border-radius:0.875rem;transition:all 0.25s cubic-bezier(0.4,0,0.2,1);background:linear-gradient(145deg,rgba(255,255,255,0.08)0%,rgba(255,255,255,0.04)100%);border:1px solid rgba(255,255,255,0.12);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);position:relative;overflow:hidden;}
.setting-item::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.08)50%,transparent 100%);transition:left 0.3s ease;}.setting-item:hover::before{left:100%;}.setting-item:hover{background:linear-gradient(145deg,rgba(255,255,255,0.16)0%,rgba(255,255,255,0.08)100%);transform:translateX(6px)scale(1.01);box-shadow:0 6px 12px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.25);}.dark.setting-item{background:linear-gradient(145deg,rgba(0,0,0,0.18)0%,rgba(15,15,15,0.12)100%);border:1px solid rgba(255,255,255,0.06);}.dark.setting-item:hover{background:linear-gradient(145deg,rgba(0,0,0,0.25)0%,rgba(15,15,15,0.18)100%);box-shadow:0 6px 12px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.08);}.setting-item input[type="checkbox"]{margin:0;width:1.25rem;height:1.25rem;accent-color:currentColor;cursor:pointer;border-radius:0.3rem;transition:all 0.2s ease;}.setting-item input[type="checkbox"]:checked{transform:scale(1.05);box-shadow:0 0 6px rgba(59,130,246,0.3);}
@media(max-width:640px){.notifications-container{left:0.5rem;right:0.5rem;top:0.5rem;}.glassmorphism-toast{margin-bottom:0.5rem;border-radius:1.25rem;}.toast-content{padding:0.875rem;}.toast-header{gap:0.75rem;}.toast-icon{width:2rem;height:2rem;}.toast-title{font-size:0.8125rem;}.toast-message{font-size:0.75rem;}.toast-action-btn{padding:0.4rem 0.7rem;font-size:0.7rem;}.toast-close{padding:0.3rem;}}
@media(prefers-contrast:high){.glassmorphism-toast{border:2px solid currentColor;backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(255,255,255,0.95);}.dark.glassmorphism-toast{background:rgba(0,0,0,0.95);}}
@media(prefers-reduced-motion:reduce){.glassmorphism-toast,.toast-icon,.toast-action-btn,.toast-close{transition:none!important;animation:none!important;}.glassmorphism-toast:hover{transform:none!important;}}`;document.head.appendChild(styles);}
handleActionClick(callbackId,toastId,shouldClose=true){if(callbackId&&this.actionCallbacks.has(callbackId)){const callback=this.actionCallbacks.get(callbackId);try{callback();}catch(error){console.error('Fehler beim Ausführen des Action-Callbacks:',error);}
this.actionCallbacks.delete(callbackId);}
if(shouldClose){this.closeToast(toastId);}}
executeCallback(callbackId){if(this.actionCallbacks.has(callbackId)){const callback=this.actionCallbacks.get(callbackId);try{callback();}catch(error){console.error('Fehler beim Ausführen des Callbacks:',error);}
this.actionCallbacks.delete(callbackId);}}}
const glassNotificationSystem=new GlassmorphismNotificationSystem();if(typeof window!=='undefined'){window.glassNotificationSystem=glassNotificationSystem;window.GlassmorphismNotificationSystem=GlassmorphismNotificationSystem;}
document.addEventListener('DOMContentLoaded',()=>{console.log('🎨 Glassmorphism Notification System bereit');});

Binary file not shown.

View File

@@ -0,0 +1,47 @@
function safeUpdateElement(elementId,value,options={}){const{fallbackValue='-',logWarning=true,attribute='textContent',transform=null}=options;const element=document.getElementById(elementId);if(!element){if(logWarning){console.warn(`🔍 Element mit ID'${elementId}'nicht gefunden`);}
return false;}
try{const finalValue=value!==undefined&&value!==null?value:fallbackValue;const displayValue=transform?transform(finalValue):finalValue;element[attribute]=displayValue;return true;}catch(error){console.error(`❌ Fehler beim Aktualisieren von Element'${elementId}':`,error);return false;}}
function safeBatchUpdate(updates,options={}){const results={};Object.entries(updates).forEach(([elementId,value])=>{results[elementId]=safeUpdateElement(elementId,value,options);});const successful=Object.values(results).filter(Boolean).length;const total=Object.keys(updates).length;console.log(`📊 Batch-Update:${successful}/${total}Elemente erfolgreich aktualisiert`);return results;}
function elementExists(elementId){return document.getElementById(elementId)!==null;}
window.refreshDashboard=async function(){const refreshButton=document.getElementById('refreshDashboard');if(refreshButton){refreshButton.disabled=true;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.add('animate-spin');}}
try{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){updateDashboardStats(data.stats);showToast('✅ Dashboard erfolgreich aktualisiert','success');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{if(refreshButton){refreshButton.disabled=false;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.remove('animate-spin');}}}};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{if(typeof loadBasicStats==='function'){await loadBasicStats();}else{const response=await fetch('/api/stats');const data=await response.json();if(response.ok){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');}}
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');}}}};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{console.log('🔄 Starte Jobs-Refresh...');let refreshSuccess=false;if(typeof window.jobManager!=='undefined'&&window.jobManager&&typeof window.jobManager.loadJobs==='function'){console.log('📝 Verwende window.jobManager.loadJobs()');await window.jobManager.loadJobs();refreshSuccess=true;}else if(typeof jobManager!=='undefined'&&jobManager&&typeof jobManager.loadJobs==='function'){console.log('📝 Verwende lokalen jobManager.loadJobs()');await jobManager.loadJobs();refreshSuccess=true;}else{console.log('📝 JobManager nicht verfügbar - verwende direkten API-Aufruf');const response=await fetch('/api/jobs',{headers:{'Content-Type':'application/json','X-CSRFToken':getCSRFToken()}});if(!response.ok){throw new Error(`API-Fehler:${response.status}${response.statusText}`);}
const data=await response.json();console.log('📝 API-Response erhalten:',data);if(!data||typeof data!=='object'){throw new Error('Ungültige API-Response: Keine Daten erhalten');}
let jobs=[];if(Array.isArray(data.jobs)){jobs=data.jobs;}else if(Array.isArray(data)){jobs=data;}else if(data.success&&Array.isArray(data.data)){jobs=data.data;}else{console.warn('⚠️ Keine Jobs-Array in API-Response gefunden:',data);jobs=[];}
console.log(`📝 ${jobs.length}Jobs aus API extrahiert:`,jobs);const jobsContainers=['.jobs-container','#jobs-container','.job-grid','#jobs-list','#jobs-grid'];let containerFound=false;for(const selector of jobsContainers){const container=document.querySelector(selector);if(container){containerFound=true;console.log(`📝 Container gefunden:${selector}`);if(jobs.length===0){container.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>`;}else{const jobCards=jobs.map(job=>{if(!job||typeof job!=='object'){console.warn('⚠️ Ungültiges Job-Objekt übersprungen:',job);return'';}
return`<div class="job-card p-4 border rounded-lg bg-white dark:bg-slate-800 shadow-sm hover:shadow-md transition-shadow"><h3 class="font-semibold text-gray-900 dark:text-white mb-2">${job.filename||job.title||job.name||'Unbekannter Job'}</h3><div class="text-sm text-gray-600 dark:text-gray-400 space-y-1"><p><span class="font-medium">ID:</span>${job.id||'N/A'}</p><p><span class="font-medium">Status:</span>${job.status||'Unbekannt'}</p>${job.printer_name?`<p><span class="font-medium">Drucker:</span>${job.printer_name}</p>`:''}
${job.created_at?`<p><span class="font-medium">Erstellt:</span>${new Date(job.created_at).toLocaleDateString('de-DE')}</p>`:''}</div></div>`;}).filter(card=>card!=='').join('');container.innerHTML=jobCards||`<div class="text-center py-8"><p class="text-gray-500 dark:text-gray-400">Keine gültigen Jobs zum Anzeigen</p></div>`;}
break;}}
if(!containerFound){console.warn('⚠️ Kein Jobs-Container gefunden. Verfügbare Container:',jobsContainers);}
refreshSuccess=true;}
if(refreshSuccess){showToast('✅ Druckaufträge erfolgreich aktualisiert','success');}}catch(error){console.error('❌ Jobs-Refresh Fehler:',error);let errorMessage;if(error.message.includes('undefined')){errorMessage='Jobs-Daten nicht verfügbar';}else if(error.message.includes('fetch')){errorMessage='Netzwerkfehler beim Laden der Jobs';}else if(error.message.includes('API')){errorMessage='Server-Fehler beim Laden der Jobs';}else{errorMessage=error.message||'Unbekannter Fehler beim Laden der Jobs';}
showToast(`❌ Fehler:${errorMessage}`,'error');const container=document.querySelector('.jobs-container, #jobs-container, .job-grid, #jobs-list');if(container){container.innerHTML=`<div class="text-center py-12"><div class="text-red-400 dark:text-red-600 text-6xl mb-4">⚠️</div><h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Fehler beim Laden</h3><p class="text-gray-500 dark:text-gray-400 mb-4">${errorMessage}</p><button onclick="refreshJobs()"class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">Erneut versuchen</button></div>`;}}finally{if(refreshButton){refreshButton.disabled=false;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.remove('animate-spin');}}}};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{if(typeof calendar!=='undefined'&&calendar.refetchEvents){calendar.refetchEvents();showToast('✅ Kalender erfolgreich aktualisiert','success');}else{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');}}}};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{if(typeof printerManager!=='undefined'&&printerManager.loadPrinters){await printerManager.loadPrinters();}else{const response=await fetch('/api/printers/status/live',{headers:{'X-CSRFToken':getCSRFToken()}});if(response.ok){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');}}}};function updateDashboardStats(stats){const activeJobsEl=document.querySelector('[data-stat="active-jobs"]');if(activeJobsEl){activeJobsEl.textContent=stats.active_jobs||0;}
const availablePrintersEl=document.querySelector('[data-stat="available-printers"]');if(availablePrintersEl){availablePrintersEl.textContent=stats.available_printers||0;}
const totalJobsEl=document.querySelector('[data-stat="total-jobs"]');if(totalJobsEl){totalJobsEl.textContent=stats.total_jobs||0;}
const successRateEl=document.querySelector('[data-stat="success-rate"]');if(successRateEl){successRateEl.textContent=(stats.success_rate||0)+'%';}
console.log('📊 Dashboard-Statistiken aktualisiert:',stats);}
function updateStatsCounter(elementId,value,animate=true){const element=document.getElementById(elementId);if(!element){console.warn(`Element mit ID'${elementId}'nicht gefunden`);return;}
if(value===null||value===undefined){console.warn(`Ungültiger Wert für Element'${elementId}':`,value);value=0;}
if(animate){const currentValue=parseInt(element.textContent.replace(/[^\d]/g,''))||0;const targetValue=parseInt(value.toString().replace(/[^\d]/g,''))||0;if(currentValue!==targetValue){const finalTextValue=value!==null&&value!==undefined?value.toString():'0';animateCounter(element,currentValue,targetValue,finalTextValue);}}else{element.textContent=value!==null&&value!==undefined?value.toString():'0';}}
function animateCounter(element,start,end,finalText){if(!element){console.warn('animateCounter: Kein gültiges Element übergeben');return;}
if(typeof finalText!=='string'){if(finalText===null||finalText===undefined||(typeof finalText==='object'&&finalText!==null)){console.warn('animateCounter: Problematischer finalText-Wert:',finalText);}
finalText=finalText!==null&&finalText!==undefined?String(finalText):'0';}
start=parseInt(start)||0;end=parseInt(end)||0;const duration=1000;const startTime=performance.now();function updateCounter(currentTime){const elapsed=currentTime-startTime;const progress=Math.min(elapsed/duration,1);const easeOut=1-Math.pow(1-progress,3);const currentValue=Math.round(start+(end-start)*easeOut);try{if(typeof finalText==='string'&&finalText.includes('%')){element.textContent=currentValue+'%';}else{element.textContent=currentValue;}}catch(error){console.warn('animateCounter: Fehler bei finalText.includes:',error,'finalText:',finalText);element.textContent=currentValue;}
if(progress<1){requestAnimationFrame(updateCounter);}else{try{element.textContent=finalText;}catch(error){console.warn('animateCounter: Fehler bei finaler Zuweisung:',error);element.textContent=String(end);}}}
requestAnimationFrame(updateCounter);}
function getCSRFToken(){const token=document.querySelector('meta[name="csrf-token"]');return token?token.getAttribute('content'):'';}
function showToast(message,type='info'){if(typeof optimizationManager!=='undefined'&&optimizationManager.showToast){optimizationManager.showToast(message,type);return;}
const emoji={success:'✅',error:'❌',warning:'⚠️',info:''};console.log(`${emoji[type]||''}${message}`);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);setTimeout(()=>{toast.classList.remove('translate-x-full');},100);setTimeout(()=>{toast.classList.add('translate-x-full');setTimeout(()=>{toast.remove();},300);},3000);}catch(error){console.warn('Toast-Erstellung fehlgeschlagen:',error);}}
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{window.location.reload();}};class AutoRefreshManager{constructor(){this.isEnabled=false;this.interval=null;this.intervalTime=30000;}
start(){if(this.isEnabled)return;this.isEnabled=true;this.interval=setInterval(()=>{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();}}}
window.autoRefreshManager=new AutoRefreshManager();document.addEventListener('keydown',function(e){if(e.key==='F5'||(e.ctrlKey&&e.key==='r')){e.preventDefault();universalRefresh();}
if(e.ctrlKey&&e.shiftKey&&e.key==='R'){e.preventDefault();autoRefreshManager.toggle();showToast(autoRefreshManager.isEnabled?'🔄 Auto-Refresh aktiviert':'⏸️ Auto-Refresh deaktiviert','info');}});document.addEventListener('visibilitychange',function(){if(!document.hidden&&autoRefreshManager.isEnabled){setTimeout(universalRefresh,1000);}});console.log('🔄 Globale Refresh-Funktionen geladen');

Binary file not shown.

Binary file not shown.

67
backend/static/js/job-manager.min.js vendored Normal file
View File

@@ -0,0 +1,67 @@
(function(){'use strict';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...');}
async init(){try{console.log('🚀 JobManager-Initialisierung gestartet');this.setupEventListeners();this.setupFormHandlers();await this.loadJobs();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');}}
setupEventListeners(){console.log('📡 Event-Listener werden eingerichtet...');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);}});const refreshBtn=document.getElementById('refresh-jobs');if(refreshBtn){refreshBtn.addEventListener('click',()=>this.loadJobs());}
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');}
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');}
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();if(data&&typeof data==='object'){this.jobs=Array.isArray(data.jobs)?data.jobs:[];this.currentPage=Number(data.current_page)||1;this.totalPages=Number(data.total_pages)||1;console.log(`${this.jobs.length}Jobs erfolgreich geladen`,this.jobs);}else{console.warn('⚠️ Unerwartete API-Response-Struktur:',data);this.jobs=[];this.currentPage=1;this.totalPages=1;}
this.renderJobs();this.updatePagination();console.log(`${this.jobs.length}Jobs erfolgreich geladen und gerendert`);}catch(error){console.error('❌ Fehler beim Laden der Jobs:',error);this.showToast('Fehler beim Laden der Jobs','error');this.jobs=[];this.currentPage=1;this.totalPages=1;this.renderJobs();}finally{this.isLoading=false;this.showLoadingState(false);}}
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');}}
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');}}
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');}}
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');}}
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');}}
openJobDetails(jobId){console.log(`📄 Öffne Details für Job ${jobId}...`);const detailsUrl=`/jobs/${jobId}`;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{window.location.href=detailsUrl;}}
renderJobs(){const jobsList=document.getElementById('jobs-list');if(!jobsList){console.warn('⚠️ Jobs-Liste Element nicht gefunden');return;}
if(!Array.isArray(this.jobs)){console.warn('⚠️ this.jobs ist kein Array:',this.jobs);this.jobs=[];}
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;}
try{const jobsHTML=this.jobs.map(job=>{if(!job||typeof job!=='object'){console.warn('⚠️ Ungültiges Job-Objekt:',job);return'';}
return this.renderJobCard(job);}).filter(html=>html!=='').join('');jobsList.innerHTML=jobsHTML;console.log(`📋 ${this.jobs.length}Jobs gerendert`);}catch(error){console.error('❌ Fehler beim Rendern der Jobs:',error);jobsList.innerHTML=`<div class="text-center py-12"><div class="text-red-400 dark:text-red-600 text-6xl mb-4">⚠️</div><h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Fehler beim Laden</h3><p class="text-gray-500 dark:text-gray-400">Es gab einen Fehler beim Darstellen der Jobs.</p><button onclick="window.jobManager.loadJobs()"class="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Erneut versuchen</button></div>`;}}
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>`;}
renderJobActions(job){const actions=[];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>`);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('');}
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'];}
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';}
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';}}
getCSRFToken(){const token=document.querySelector('meta[name="csrf-token"]');return token?token.getAttribute('content'):'';}
showToast(message,type='info'){if(typeof window.showToast==='function'){window.showToast(message,type);}else{console.log(`${type.toUpperCase()}:${message}`);}}
startAutoRefresh(){this.stopAutoRefresh();this.refreshInterval=setInterval(()=>{if(!this.isLoading){this.loadJobs(this.currentPage);}},30000);console.log('🔄 Auto-Refresh gestartet (30s Intervall)');}
stopAutoRefresh(){if(this.refreshInterval){clearInterval(this.refreshInterval);this.refreshInterval=null;console.log('⏹️ Auto-Refresh gestoppt');}}
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='';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>`;}
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>`;}
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;}
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');await this.loadJobs();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;}}
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');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;}}}
window.JobManager=JobManager;document.addEventListener('DOMContentLoaded',function(){if(typeof window.jobManager==='undefined'){window.jobManager=new JobManager();if(document.getElementById('jobs-list')||document.querySelector('[data-job-action]')){window.jobManager.init();}}});console.log('✅ JobManager-Modul geladen');})();

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,21 @@
(function(){'use strict';console.log('🛡️ Jobs Safety Fix wird geladen...');if(typeof window.jobsData==='undefined'){window.jobsData=[];}
if(typeof window.filteredJobs==='undefined'){window.filteredJobs=[];}
window.safeJobsOperations={getJobs:function(){if(window.jobManager&&Array.isArray(window.jobManager.jobs)){return window.jobManager.jobs;}
if(Array.isArray(window.jobsData)){return window.jobsData;}
return[];},getFilteredJobs:function(){if(Array.isArray(window.filteredJobs)){return window.filteredJobs;}
return this.getJobs();},setJobs:function(jobs){if(!Array.isArray(jobs)){console.warn('⚠️ Jobs ist kein Array, konvertiere zu leerem Array');jobs=[];}
if(window.jobManager){window.jobManager.jobs=jobs;}
window.jobsData=jobs;console.log(`${jobs.length}Jobs sicher gesetzt`);},findJob:function(jobId){const jobs=this.getJobs();return jobs.find(job=>job&&job.id&&job.id.toString()===jobId.toString())||null;},filterJobs:function(filterFn){const jobs=this.getJobs();if(typeof filterFn!=='function'){return jobs;}
try{return jobs.filter(job=>{if(!job||typeof job!=='object'){return false;}
return filterFn(job);});}catch(error){console.error('❌ Fehler beim Filtern der Jobs:',error);return jobs;}}};function ensureJobManagerSafety(){if(typeof window.jobManager!=='undefined'&&window.jobManager){if(!Array.isArray(window.jobManager.jobs)){console.warn('⚠️ JobManager.jobs ist kein Array, korrigiere...');window.jobManager.jobs=[];}
if(window.jobManager.loadJobs&&typeof window.jobManager.loadJobs==='function'){const originalLoadJobs=window.jobManager.loadJobs;window.jobManager.loadJobs=async function(...args){try{await originalLoadJobs.apply(this,args);if(!Array.isArray(this.jobs)){this.jobs=[];}}catch(error){console.error('❌ Fehler beim sicheren Laden der Jobs:',error);this.jobs=[];throw error;}};}
if(window.jobManager.renderJobs&&typeof window.jobManager.renderJobs==='function'){const originalRenderJobs=window.jobManager.renderJobs;window.jobManager.renderJobs=function(...args){try{if(!Array.isArray(this.jobs)){console.warn('⚠️ Jobs ist kein Array vor dem Rendern, korrigiere...');this.jobs=[];}
return originalRenderJobs.apply(this,args);}catch(error){console.error('❌ Fehler beim sicheren Rendern der Jobs:',error);const jobsList=document.getElementById('jobs-list');if(jobsList){jobsList.innerHTML=`<div class="text-center py-12"><div class="text-red-400 dark:text-red-600 text-6xl mb-4">⚠️</div><h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Renderingfehler</h3><p class="text-gray-500 dark:text-gray-400 mb-4">Jobs konnten nicht angezeigt werden</p><button onclick="window.jobManager.loadJobs()"class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Erneut laden</button></div>`;}}};}}}
function setupGlobalVariableWatching(){let _jobsData=[];Object.defineProperty(window,'jobsData',{get:function(){return _jobsData;},set:function(value){if(!Array.isArray(value)){console.warn('⚠️ Versuche jobsData mit Non-Array zu setzen:',value);_jobsData=[];}else{_jobsData=value;}},enumerable:true,configurable:true});let _filteredJobs=[];Object.defineProperty(window,'filteredJobs',{get:function(){return _filteredJobs;},set:function(value){if(!Array.isArray(value)){console.warn('⚠️ Versuche filteredJobs mit Non-Array zu setzen:',value);_filteredJobs=[];}else{_filteredJobs=value;}},enumerable:true,configurable:true});}
window.validateJobsResponse=function(data){if(!data||typeof data!=='object'){console.warn('⚠️ Ungültige API-Response:',data);return{jobs:[],total:0};}
let jobs=[];if(Array.isArray(data.jobs)){jobs=data.jobs;}else if(Array.isArray(data.data)){jobs=data.data;}else if(Array.isArray(data)){jobs=data;}else if(data.success&&Array.isArray(data.jobs)){jobs=data.jobs;}
jobs=jobs.filter(job=>{if(!job||typeof job!=='object'){console.warn('⚠️ Ungültiges Job-Objekt gefiltert:',job);return false;}
if(!job.id){console.warn('⚠️ Job ohne ID gefiltert:',job);return false;}
return true;});console.log(`${jobs.length}Jobs validiert`);return{jobs:jobs,total:jobs.length,current_page:data.current_page||1,total_pages:data.total_pages||1};};window.addEventListener('error',function(e){if(e.message&&e.message.includes('jobs')&&e.message.includes('undefined')){console.log('🛡️ Jobs undefined Fehler abgefangen:',e.message);window.safeJobsOperations.setJobs([]);e.preventDefault();return false;}});window.addEventListener('unhandledrejection',function(e){if(e.reason&&e.reason.message&&e.reason.message.includes('jobs')){console.log('🛡️ Jobs Promise rejection abgefangen:',e.reason);window.safeJobsOperations.setJobs([]);e.preventDefault();}});if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',function(){setupGlobalVariableWatching();ensureJobManagerSafety();});}else{setupGlobalVariableWatching();ensureJobManagerSafety();}
setInterval(function(){ensureJobManagerSafety();if(!Array.isArray(window.jobsData)){console.warn('⚠️ jobsData ist kein Array mehr, repariere...');window.jobsData=[];}
if(!Array.isArray(window.filteredJobs)){console.warn('⚠️ filteredJobs ist kein Array mehr, repariere...');window.filteredJobs=[];}},10000);console.log('✅ Jobs Safety Fix erfolgreich geladen');})();

Binary file not shown.

Binary file not shown.

40
backend/static/js/notifications.min.js vendored Normal file
View File

@@ -0,0 +1,40 @@
class ModernNotificationManager{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=[];this.activeToasts=new Map();this.toastCounter=0;this.csrfToken=this.getCSRFToken();this.init();this.setupGlobalNotificationSystem();}
getCSRFToken(){const metaTag=document.querySelector('meta[name="csrf-token"]');return metaTag?metaTag.getAttribute('content'):'';}
getAPIHeaders(){const headers={'Content-Type':'application/json',};if(this.csrfToken){headers['X-CSRFToken']=this.csrfToken;}
return headers;}
init(){if(!this.notificationToggle)return;this.notificationToggle.addEventListener('click',(e)=>{e.stopPropagation();this.toggleDropdown();});if(this.markAllReadBtn){this.markAllReadBtn.addEventListener('click',()=>{this.markAllAsRead();});}
document.addEventListener('click',(e)=>{if(!this.notificationDropdown.contains(e.target)&&!this.notificationToggle.contains(e.target)){this.closeDropdown();}});this.loadNotifications();setInterval(()=>{this.loadNotifications();},30000);}
toggleDropdown(){if(this.isOpen){this.closeDropdown();}else{this.openDropdown();}}
openDropdown(){this.notificationDropdown.classList.remove('hidden');this.notificationToggle.setAttribute('aria-expanded','true');this.isOpen=true;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;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){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){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);}}
setupGlobalNotificationSystem(){window.showFlashMessage=this.showGlassToast.bind(this);window.showToast=this.showGlassToast.bind(this);window.showNotification=this.showGlassToast.bind(this);window.showSuccessMessage=(message)=>this.showGlassToast(message,'success');window.showErrorMessage=(message)=>this.showGlassToast(message,'error');window.showWarningMessage=(message)=>this.showGlassToast(message,'warning');window.showInfoMessage=(message)=>this.showGlassToast(message,'info');}
showGlassToast(message,type='info',duration=5000,options={}){if(window.dndManager&&window.dndManager.isEnabled){window.dndManager.suppressNotification(message,type);return;}
const toastId=`toast-${++this.toastCounter}`;const toast=document.createElement('div');toast.id=toastId;toast.className=`glass-toast notification notification-${type}show`;const iconMap={success:`<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="M5 13l4 4L19 7"/></svg>`,error:`<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="M6 18L18 6M6 6l12 12"/></svg>`,warning:`<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>`,info:`<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`};toast.innerHTML=`<div class="flex items-center"><div class="notification-icon">${iconMap[type]||iconMap.info}</div><div class="notification-content">${options.title?`<div class="notification-title">${options.title}</div>`:''}<div class="notification-message">${message}</div></div><button class="notification-close"onclick="modernNotificationManager.closeToast('${toastId}')"><svg class="w-4 h-4"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>`;let container=document.getElementById('toast-container');if(!container){container=document.createElement('div');container.id='toast-container';container.className='notifications-container';document.body.appendChild(container);}
const existingToasts=container.children.length;toast.style.top=`${1+existingToasts*5}rem`;container.appendChild(toast);this.activeToasts.set(toastId,toast);let isPaused=false;let timeoutId;const startTimer=()=>{if(!isPaused&&duration>0){timeoutId=setTimeout(()=>{this.closeToast(toastId);},duration);}};toast.addEventListener('mouseenter',()=>{isPaused=true;clearTimeout(timeoutId);toast.style.transform='translateY(-2px) scale(1.02)';});toast.addEventListener('mouseleave',()=>{isPaused=false;toast.style.transform='translateY(0) scale(1)';startTimer();});setTimeout(startTimer,100);if(options.playSound!==false){this.playNotificationSound(type);}
return toastId;}
closeToast(toastId){const toast=this.activeToasts.get(toastId);if(!toast)return;toast.classList.add('hiding');setTimeout(()=>{if(toast.parentNode){toast.parentNode.removeChild(toast);}
this.activeToasts.delete(toastId);this.repositionToasts();},400);}
repositionToasts(){let index=0;this.activeToasts.forEach((toast)=>{if(toast.parentNode){toast.style.top=`${1+index*5}rem`;index++;}});}
playNotificationSound(type){try{if(typeof AudioContext!=='undefined'||typeof webkitAudioContext!=='undefined'){const audioContext=new(window.AudioContext||window.webkitAudioContext)();const oscillator=audioContext.createOscillator();const gainNode=audioContext.createGain();oscillator.connect(gainNode);gainNode.connect(audioContext.destination);const frequencies={success:[523.25,659.25,783.99],error:[440,415.3],warning:[493.88,523.25],info:[523.25]};const freq=frequencies[type]||frequencies.info;freq.forEach((f,i)=>{setTimeout(()=>{const osc=audioContext.createOscillator();const gain=audioContext.createGain();osc.connect(gain);gain.connect(audioContext.destination);osc.frequency.setValueAtTime(f,audioContext.currentTime);gain.gain.setValueAtTime(0.1,audioContext.currentTime);gain.gain.exponentialRampToValueAtTime(0.01,audioContext.currentTime+0.1);osc.start(audioContext.currentTime);osc.stop(audioContext.currentTime+0.1);},i*100);});}}catch(error){}}
showBrowserNotification(title,message,options={}){if('Notification'in window){if(Notification.permission==='granted'){const notification=new Notification(title,{body:message,icon:options.icon||'/static/icons/static/icons/notification-icon.png',badge:options.badge||'/static/icons/static/icons/badge-icon.png',tag:options.tag||'myp-notification',requireInteraction:options.requireInteraction||false,...options});notification.onclick=options.onClick||(()=>{window.focus();notification.close();});return notification;}else if(Notification.permission==='default'){Notification.requestPermission().then(permission=>{if(permission==='granted'){this.showBrowserNotification(title,message,options);}});}}
return null;}
showAlert(message,type='info',options={}){const alertId=`alert-${Date.now()}`;const alert=document.createElement('div');alert.id=alertId;alert.className=`alert alert-${type}`;alert.innerHTML=`<div class="flex items-start"><div class="notification-icon mr-3">${this.getIconForType(type)}</div><div class="flex-1">${options.title?`<h4 class="font-semibold mb-2">${options.title}</h4>`:''}<p>${message}</p></div>${options.dismissible!==false?`<button class="ml-3 p-1 rounded-lg opacity-70 hover:opacity-100 transition-opacity"onclick="document.getElementById('${alertId}').remove()"><svg class="w-4 h-4"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>`;const container=options.container||document.querySelector('.flash-messages')||document.body;container.appendChild(alert);if(options.autoDismiss!==false){setTimeout(()=>{if(alert.parentNode){alert.style.opacity='0';alert.style.transform='translateY(-20px)';setTimeout(()=>alert.remove(),300);}},options.duration||7000);}
return alertId;}
getIconForType(type){const icons={success:`<svg class="w-5 h-5 text-green-600"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>`,error:`<svg class="w-5 h-5 text-red-600"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>`,warning:`<svg class="w-5 h-5 text-yellow-600"fill="none"stroke="currentColor"viewBox="0 0 24 24"><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.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>`,info:`<svg class="w-5 h-5 text-blue-600"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>`};return icons[type]||icons.info;}}
class NotificationManager extends ModernNotificationManager{constructor(){super();console.warn('NotificationManager ist deprecated. Verwenden Sie ModernNotificationManager.');}}
const modernNotificationManager=new ModernNotificationManager();if(typeof window!=='undefined'){window.notificationManager=modernNotificationManager;window.modernNotificationManager=modernNotificationManager;}
const toastStyles=`<style>.glass-toast{position:relative;transform:translateX(0)translateY(0)scale(1);animation:toast-slide-in 0.6s cubic-bezier(0.4,0,0.2,1);}.glass-toast:hover{transform:translateY(-2px)scale(1.02)!important;transition:transform 0.2s ease;}@keyframes toast-slide-in{0%{opacity:0;transform:translateX(100%)translateY(-20px)scale(0.9);}
50%{opacity:0.8;transform:translateX(20px)translateY(-10px)scale(1.05);}
100%{opacity:1;transform:translateX(0)translateY(0)scale(1);}}.notification-item.unread{background:linear-gradient(135deg,rgba(59,130,246,0.08)0%,rgba(147,197,253,0.05)100%);border-left:3px solid rgb(59,130,246);}.dark.notification-item.unread{background:linear-gradient(135deg,rgba(59,130,246,0.15)0%,rgba(147,197,253,0.08)100%);}</style>`;if(typeof document!=='undefined'&&!document.getElementById('toast-styles')){const styleElement=document.createElement('div');styleElement.id='toast-styles';styleElement.innerHTML=toastStyles;document.head.appendChild(styleElement);}

Binary file not shown.

Binary file not shown.

28
backend/static/js/offline-app.min.js vendored Normal file
View File

@@ -0,0 +1,28 @@
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();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'}`);}
setupOfflineDetection(){window.addEventListener('online',()=>{this.isOffline=false;document.body.classList.remove('offline-mode');console.log('Netzwerkverbindung wiederhergestellt!');this.syncOfflineData();window.dispatchEvent(new CustomEvent('networkStatusChange',{detail:{isOffline:false}}));});window.addEventListener('offline',()=>{this.isOffline=true;document.body.classList.add('offline-mode');console.log('Netzwerkverbindung verloren!');window.dispatchEvent(new CustomEvent('networkStatusChange',{detail:{isOffline:true}}));});if(this.isOffline){document.body.classList.add('offline-mode');}}
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);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');}
registration.addEventListener('updatefound',()=>{const newWorker=registration.installing;newWorker.addEventListener('statechange',()=>{console.log(`Service Worker Status:${newWorker.state}`);if(newWorker.state==='activated'){this.fetchAndCacheAppData();}});});registration.update();}).catch(error=>{console.error('Service Worker Registrierung fehlgeschlagen:',error);this.setupLocalCache();});navigator.serviceWorker.addEventListener('controllerchange',()=>{console.log('Service Worker Controller hat gewechselt');});}else{console.warn('Service Worker werden von diesem Browser nicht unterstützt');this.setupLocalCache();}}
setupLocalCache(){console.log('Verwende lokalen Cache als Fallback');this.fetchAndCacheAppData();setInterval(()=>{if(navigator.onLine){this.fetchAndCacheAppData();}},15*60*1000);}
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);});}}
setupLocalStorage(){if(!localStorage.getItem('offlineChanges')){localStorage.setItem('offlineChanges',JSON.stringify([]));}
if(!localStorage.getItem('appConfig')){const defaultConfig={theme:'system',notifications:true,dataSync:true,lastSync:null};localStorage.setItem('appConfig',JSON.stringify(defaultConfig));}
this.cleanupOldCache(7);}
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);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);}}}}
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...`);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=>{const failedChanges=results.filter(result=>!result.success).map(result=>result.change);localStorage.setItem('offlineChanges',JSON.stringify(failedChanges));const appConfig=JSON.parse(localStorage.getItem('appConfig')||'{}');appConfig.lastSync=new Date().toISOString();localStorage.setItem('appConfig',JSON.stringify(appConfig));document.body.classList.remove('syncing');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`);});}
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}`);}
setupThemeListeners(){window.addEventListener('darkModeChanged',(e)=>{this.darkMode=e.detail.isDark;this.updateAppTheme();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'}`);});window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',(e)=>{const appConfig=JSON.parse(localStorage.getItem('appConfig')||'{}');if(appConfig.theme==='system'){this.darkMode=e.matches;this.updateAppTheme();console.log(`Systemthema geändert:${this.darkMode?'Dark Mode':'Light Mode'}`);}});}
updateAppTheme(){const metaThemeColor=document.getElementById('metaThemeColor')||document.querySelector('meta[name="theme-color"]');if(metaThemeColor){metaThemeColor.setAttribute('content',this.darkMode?'#0f172a':'#ffffff');}
this.updateUIForTheme();}
updateUIForTheme(){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');}}
}
setupUI(){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);if(this.isOffline){banner.classList.remove('hidden');}
window.addEventListener('online',()=>banner.classList.add('hidden'));window.addEventListener('offline',()=>banner.classList.remove('hidden'));}
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);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});}
this.setupMobileOptimizations();}
setupMobileOptimizations(){if('serviceWorker'in navigator&&navigator.serviceWorker.controller){this.setupTouchFeedback();this.fixMobileViewportHeight();if('scrollRestoration'in history){history.scrollRestoration='manual';}}}
setupTouchFeedback(){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});}
fixMobileViewportHeight(){const setViewportHeight=()=>{const vh=window.innerHeight*0.01;document.documentElement.style.setProperty('--vh',`${vh}px`);};setViewportHeight();window.addEventListener('resize',setViewportHeight);window.addEventListener('orientationchange',()=>{setTimeout(setViewportHeight,100);});}}
document.addEventListener('DOMContentLoaded',()=>{window.myp=new MYPApp();});

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,71 @@
class OptimizationManager{constructor(){this.isAutoOptimizationEnabled=false;this.isBatchModeEnabled=false;this.selectedJobs=new Set();this.optimizationSettings={algorithm:'round_robin',considerDistance:true,minimizeChangeover:true,maxBatchSize:10,timeWindow:24};this.init();}
init(){this.setupEventListeners();this.loadSavedSettings();this.updateUI();}
setupEventListeners(){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();}});}
toggleAutoOptimization(){this.isAutoOptimizationEnabled=!this.isAutoOptimizationEnabled;const button=document.getElementById('auto-opt-toggle');if(button){this.updateAutoOptimizationButton(button);}
localStorage.setItem('myp-auto-optimization',this.isAutoOptimizationEnabled);this.showOptimizationNotification(this.isAutoOptimizationEnabled?'aktiviert':'deaktiviert','auto-optimization');if(this.isAutoOptimizationEnabled){this.performAutoOptimization();}
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.style.transform='scale(1.05)';setTimeout(()=>{button.style.transform='';},200);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';}}
toggleBatchMode(){this.isBatchModeEnabled=!this.isBatchModeEnabled;const button=document.getElementById('batch-toggle');if(button){this.updateBatchModeButton(button);}
this.toggleBatchSelection();localStorage.setItem('myp-batch-mode',this.isBatchModeEnabled);this.showOptimizationNotification(this.isBatchModeEnabled?'aktiviert':'deaktiviert','batch-mode');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.style.transform='scale(1.05)';setTimeout(()=>{button.style.transform='';},200);}else{button.classList.remove('btn-warning');button.classList.add('btn-secondary');span.textContent='Mehrfachauswahl';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){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';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();});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})`;}}
async performAutoOptimization(){try{this.showOptimizationLoading();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();this.hideOptimizationLoading();if(data.success){this.showRewardModal(data);this.refreshCurrentView();}else{this.showErrorMessage(`Optimierung fehlgeschlagen:${data.error}`);}}catch(error){this.hideOptimizationLoading();console.error('Auto-Optimierung Fehler:',error);this.showErrorMessage('Netzwerkfehler bei der Auto-Optimierung');}}
showRewardModal(data){const existingModal=document.getElementById('optimization-reward-modal');if(existingModal){existingModal.remove();}
const modal=document.createElement('div');modal.id='optimization-reward-modal';modal.className='fixed inset-0 bg-black/70 backdrop-blur-md z-50 flex items-center justify-center p-4 animate-fade-in';const optimizedCount=data.optimized_jobs||0;let celebration='🎉';let message='Optimierung erfolgreich!';if(optimizedCount===0){celebration='✅';message='System bereits optimal!';}else if(optimizedCount<=3){celebration='🚀';message='Kleine Verbesserungen durchgeführt!';}else if(optimizedCount<=10){celebration='⚡';message='Deutliche Optimierung erreicht!';}else{celebration='💎';message='Exzellente Optimierung!';}
modal.innerHTML=`<div class="relative bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-md w-full p-8 transform animate-bounce-in"><!--Konfetti Animation--><div class="absolute inset-0 pointer-events-none overflow-hidden rounded-2xl"><div class="confetti-container">${this.generateConfetti()}</div></div><!--Schließen Button--><button onclick="this.closest('#optimization-reward-modal').remove()"
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors z-10"><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><!--Haupt-Content--><div class="text-center relative z-10"><!--Animiertes Erfolgs-Icon--><div class="relative mb-6"><div class="text-8xl animate-pulse-scale mx-auto">${celebration}</div><div class="absolute -top-2 -right-2 text-4xl animate-float">✨</div><div class="absolute -bottom-2 -left-2 text-3xl animate-float-delay">⭐</div></div><!--Erfolgs-Nachricht--><h2 class="text-2xl font-bold text-slate-800 dark:text-white mb-2 animate-slide-up">${message}</h2><!--Statistiken--><div class="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20
rounded-xl p-6 mb-6 animate-slide-up-delay"><div class="grid grid-cols-2 gap-4"><div class="text-center"><div class="text-3xl font-bold text-green-600 dark:text-green-400 animate-count-up">${optimizedCount}</div><div class="text-sm text-green-700 dark:text-green-300 font-medium">Jobs optimiert</div></div><div class="text-center"><div class="text-lg font-semibold text-slate-700 dark:text-slate-300 capitalize">${data.algorithm?.replace('_',' ')||'Standard'}</div><div class="text-sm text-slate-600 dark:text-slate-400">Algorithmus</div></div></div></div><!--Belohnungs-Badge--><div class="inline-flex items-center gap-2 bg-gradient-to-r from-blue-500 to-purple-600
text-white px-6 py-3 rounded-full text-sm font-semibold mb-6 animate-glow"><svg class="w-5 h-5"fill="currentColor"viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>Effizienz-Boost erreicht!</div><!--Action Buttons--><div class="flex gap-3"><button onclick="this.closest('#optimization-reward-modal').remove()"
class="flex-1 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300
py-3 px-6 rounded-xl font-semibold transition-all hover:bg-slate-200
dark:hover:bg-slate-600 animate-slide-up-delay-2">Schließen</button><button onclick="optimizationManager.showOptimizationSettings();
this.closest('#optimization-reward-modal').remove();"
class="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white
py-3 px-6 rounded-xl font-semibold transition-all hover:from-blue-600
hover:to-blue-700 animate-slide-up-delay-3">Einstellungen</button></div></div></div>`;document.body.appendChild(modal);this.playSuccessSound();setTimeout(()=>{if(modal&&modal.parentNode){modal.style.opacity='0';modal.style.transform='scale(0.95)';setTimeout(()=>modal.remove(),300);}},20000);}
generateConfetti(){const colors=['#FFD700','#FF6B6B','#4ECDC4','#45B7D1','#96CEB4','#FFEAA7'];let confetti='';for(let i=0;i<50;i++){const color=colors[Math.floor(Math.random()*colors.length)];const delay=Math.random()*5;const duration=4+Math.random()*3;const left=Math.random()*100;confetti+=`<div class="confetti-piece"style="
background-color: ${color};
left: ${left}%;
animation-delay: ${delay}s;
animation-duration: ${duration}s;
"></div>`;}
return confetti;}
showOptimizationLoading(){const loader=document.createElement('div');loader.id='optimization-loader';loader.className='fixed inset-0 bg-black/50 backdrop-blur-sm z-40 flex items-center justify-center';loader.innerHTML=`<div class="bg-white dark:bg-slate-800 rounded-2xl p-8 text-center max-w-sm mx-4 shadow-2xl"><div class="relative mb-6"><div class="w-16 h-16 mx-auto"><svg class="animate-spin text-blue-500"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><div class="absolute -top-1 -right-1 text-2xl animate-bounce">⚡</div></div><h3 class="text-xl font-semibold text-slate-800 dark:text-white mb-2">Optimierung läuft...</h3><p class="text-slate-600 dark:text-slate-400">Jobs werden intelligent verteilt</p></div>`;document.body.appendChild(loader);}
hideOptimizationLoading(){const loader=document.getElementById('optimization-loader');if(loader){loader.style.opacity='0';setTimeout(()=>loader.remove(),200);}}
playSuccessSound(){try{const audioContext=new(window.AudioContext||window.webkitAudioContext)();const oscillator=audioContext.createOscillator();const gainNode=audioContext.createGain();oscillator.connect(gainNode);gainNode.connect(audioContext.destination);oscillator.frequency.setValueAtTime(523.25,audioContext.currentTime);oscillator.frequency.setValueAtTime(659.25,audioContext.currentTime+0.1);oscillator.frequency.setValueAtTime(783.99,audioContext.currentTime+0.2);gainNode.gain.setValueAtTime(0.1,audioContext.currentTime);gainNode.gain.exponentialRampToValueAtTime(0.01,audioContext.currentTime+0.5);oscillator.start(audioContext.currentTime);oscillator.stop(audioContext.currentTime+0.5);}catch(e){console.log('Audio-API nicht verfügbar');}}
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');}}
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');if(this.isAutoOptimizationEnabled){this.performAutoOptimization();}}
loadSavedSettings(){const savedAutoOpt=localStorage.getItem('myp-auto-optimization');if(savedAutoOpt!==null){this.isAutoOptimizationEnabled=savedAutoOpt==='true';}
const savedBatchMode=localStorage.getItem('myp-batch-mode');if(savedBatchMode!==null){this.isBatchModeEnabled=savedBatchMode==='true';}
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(){const autoOptButton=document.getElementById('auto-opt-toggle');if(autoOptButton){this.updateAutoOptimizationButton(autoOptButton);}
const batchButton=document.getElementById('batch-toggle');if(batchButton){this.updateBatchModeButton(batchButton);}
if(this.isBatchModeEnabled){this.toggleBatchSelection();}}
getCSRFToken(){const token=document.querySelector('meta[name="csrf-token"]');return token?token.getAttribute('content'):'';}
refreshCurrentView(){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){if(typeof showFlashMessage==='function'){showFlashMessage(message,'success');}else{console.log('Success:',message);}}
showErrorMessage(message){if(typeof showFlashMessage==='function'){showFlashMessage(message,'error');}else{console.error('Error:',message);}}
showWarningMessage(message){if(typeof showFlashMessage==='function'){showFlashMessage(message,'warning');}else{console.warn('Warning:',message);}}
showToast(message,type='info'){if(typeof showFlashMessage==='function'){showFlashMessage(message,type);}else{console.log(`${type.toUpperCase()}:${message}`);}}}
let optimizationManager;window.toggleAutoOptimization=function(){if(!optimizationManager){optimizationManager=new OptimizationManager();}
optimizationManager.toggleAutoOptimization();};window.toggleBatchMode=function(){if(!optimizationManager){optimizationManager=new OptimizationManager();}
optimizationManager.toggleBatchMode();};window.openBatchPlanningModal=function(){if(!optimizationManager){optimizationManager=new OptimizationManager();}
optimizationManager.showOptimizationSettings();};window.showOptimizationSettings=function(){if(!optimizationManager){optimizationManager=new OptimizationManager();}
optimizationManager.showOptimizationSettings();};document.addEventListener('DOMContentLoaded',function(){optimizationManager=new OptimizationManager();console.log('🎯 Optimierungs-Manager initialisiert');});

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,25 @@
const CACHE_VERSION='v2.0';const STATIC_CACHE=`myp-static-${CACHE_VERSION}`;const DYNAMIC_CACHE=`myp-dynamic-${CACHE_VERSION}`;const FONTS_CACHE=`myp-fonts-${CACHE_VERSION}`;const IMAGES_CACHE=`myp-images-${CACHE_VERSION}`;const CRITICAL_ASSETS=['/','/static/css/critical-inline.css','/static/css/app-bundle.min.css','/static/js/performance-service-worker.js','/static/js/app-bundle.min.js'];const CACHE_STRATEGIES={css:{cache:STATIC_CACHE,strategy:'cache-first',maxAge:86400000},js:{cache:STATIC_CACHE,strategy:'cache-first',maxAge:86400000},fonts:{cache:FONTS_CACHE,strategy:'cache-first',maxAge:2592000000},images:{cache:IMAGES_CACHE,strategy:'cache-first',maxAge:604800000},html:{cache:DYNAMIC_CACHE,strategy:'network-first',maxAge:3600000},api:{cache:DYNAMIC_CACHE,strategy:'network-first',maxAge:300000}};self.addEventListener('install',event=>{console.log('[SW] Installing Performance Service Worker...');event.waitUntil(Promise.all([caches.open(STATIC_CACHE).then(cache=>{console.log('[SW] Pre-caching critical assets...');return cache.addAll(CRITICAL_ASSETS);}),self.skipWaiting()]));});self.addEventListener('activate',event=>{console.log('[SW] Activating Performance Service Worker...');event.waitUntil(Promise.all([caches.keys().then(cacheNames=>{return Promise.all(cacheNames.map(cacheName=>{if(!cacheName.includes(CACHE_VERSION)){console.log('[SW] Deleting old cache:',cacheName);return caches.delete(cacheName);}}));}),self.clients.claim()]));});self.addEventListener('fetch',event=>{if(event.request.method!=='GET')return;const url=new URL(event.request.url);const strategy=getResourceStrategy(url);event.respondWith(handleRequest(event.request,strategy));});function getResourceStrategy(url){const pathname=url.pathname;if(pathname.endsWith('.css'))return CACHE_STRATEGIES.css;if(pathname.endsWith('.js'))return CACHE_STRATEGIES.js;if(pathname.match(/\.(woff2?|ttf|eot)$/))return CACHE_STRATEGIES.fonts;if(pathname.match(/\.(jpg|jpeg|png|gif|webp|svg|ico)$/))return CACHE_STRATEGIES.images;if(pathname.startsWith('/api/'))return CACHE_STRATEGIES.api;return CACHE_STRATEGIES.html;}
async function handleRequest(request,strategy){const cacheName=strategy.cache;switch(strategy.strategy){case'cache-first':return cacheFirst(request,cacheName,strategy.maxAge);case'network-first':return networkFirst(request,cacheName,strategy.maxAge);default:return fetch(request);}}
async function cacheFirst(request,cacheName,maxAge){try{const cache=await caches.open(cacheName);const cachedResponse=await cache.match(request);if(cachedResponse&&!isExpired(cachedResponse,maxAge)){if(isCriticalResource(request.url)){updateInBackground(request,cache);}
return cachedResponse;}
const networkResponse=await fetchWithTimeout(request,5000);if(networkResponse&&networkResponse.ok){await cache.put(request,networkResponse.clone());return networkResponse;}
return cachedResponse||createFallbackResponse(request);}catch(error){console.warn('[SW] Cache-first failed:',error);const cache=await caches.open(cacheName);const cachedResponse=await cache.match(request);return cachedResponse||createFallbackResponse(request);}}
async function networkFirst(request,cacheName,maxAge){try{const networkResponse=await fetchWithTimeout(request,3000);if(networkResponse&&networkResponse.ok){const cache=await caches.open(cacheName);await cache.put(request,networkResponse.clone());return networkResponse;}
throw new Error('Network response not ok');}catch(error){console.warn('[SW] Network-first fallback to cache:',error);const cache=await caches.open(cacheName);const cachedResponse=await cache.match(request);if(cachedResponse&&!isExpired(cachedResponse,maxAge)){return cachedResponse;}
return createFallbackResponse(request);}}
function fetchWithTimeout(request,timeout){return new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error('Timeout')),timeout);fetch(request).then(response=>{clearTimeout(timer);resolve(response);}).catch(error=>{clearTimeout(timer);reject(error);});});}
function isExpired(response,maxAge){const date=response.headers.get('date');if(!date)return false;const responseTime=new Date(date).getTime();const now=Date.now();return(now-responseTime)>maxAge;}
function isCriticalResource(url){return CRITICAL_ASSETS.some(asset=>url.includes(asset));}
async function updateInBackground(request,cache){try{const response=await fetch(request);if(response&&response.ok){await cache.put(request,response);console.log('[SW] Background update completed for:',request.url);}}catch(error){console.warn('[SW] Background update failed:',error);}}
function createFallbackResponse(request){const url=new URL(request.url);if(url.pathname.endsWith('.css')){return new Response(getFallbackCSS(),{headers:{'Content-Type':'text/css'}});}
if(url.pathname.endsWith('.js')){return new Response('console.log("Fallback JS loaded");',{headers:{'Content-Type':'application/javascript'}});}
if(url.pathname.match(/\.(jpg|jpeg|png|gif|webp)$/)){return new Response(getFallbackImage(),{headers:{'Content-Type':'image/svg+xml'}});}
return new Response('Offline',{status:503});}
function getFallbackCSS(){return`body{font-family:system-ui,sans-serif;margin:0;padding:20px;background:#f8fafc;color:#1f2937}.offline{background:#fef3c7;border:1px solid#f59e0b;border-radius:8px;padding:16px;text-align:center;margin:20px 0}.btn{background:#0073ce;color:white;border:none;padding:8px 16px;border-radius:4px;cursor:pointer}.card{background:white;border:1px solid#e5e7eb;border-radius:8px;padding:16px;margin:16px 0}`;}
function getFallbackImage(){return`<svg width="300"height="200"xmlns="http://www.w3.org/2000/svg"><rect width="100%"height="100%"fill="#f3f4f6"/><text x="50%"y="50%"text-anchor="middle"dy=".3em"fill="#6b7280">Bild offline</text></svg>`;}
self.addEventListener('message',event=>{const{type,data}=event.data;switch(type){case'SKIP_WAITING':self.skipWaiting();break;case'CLEAR_ALL_CACHES':clearAllCaches().then(()=>{event.ports[0].postMessage({success:true});});break;case'PREFETCH_URLS':prefetchUrls(data.urls).then(()=>{event.ports[0].postMessage({success:true});});break;case'GET_CACHE_INFO':getCacheInfo().then(info=>{event.ports[0].postMessage(info);});break;}});async function clearAllCaches(){const cacheNames=await caches.keys();await Promise.all(cacheNames.map(name=>caches.delete(name)));console.log('[SW] All caches cleared');}
async function prefetchUrls(urls){for(const url of urls){try{const response=await fetch(url);if(response.ok){const strategy=getResourceStrategy(new URL(url));const cache=await caches.open(strategy.cache);await cache.put(url,response);console.log('[SW] Prefetched:',url);}}catch(error){console.warn('[SW] Prefetch failed for:',url,error);}}}
async function getCacheInfo(){const cacheNames=await caches.keys();const info={};for(const name of cacheNames){const cache=await caches.open(name);const keys=await cache.keys();info[name]={entries:keys.length,urls:keys.map(req=>req.url)};}
return info;}
let performanceData={cacheHits:0,cacheMisses:0,networkRequests:0,backgroundUpdates:0};self.addEventListener('sync',event=>{if(event.tag==='background-sync'){console.log('[SW] Background sync triggered');event.waitUntil(doBackgroundSync());}});async function doBackgroundSync(){console.log('[SW] Background sync completed');}
console.log('[SW] Performance Service Worker loaded and ready');setInterval(async()=>{console.log('[SW] Starting automatic cache cleanup...');await cleanupOldCaches();},24*60*60*1000);async function cleanupOldCaches(){const cacheNames=await caches.keys();const oldCaches=cacheNames.filter(name=>!name.includes(CACHE_VERSION));await Promise.all(oldCaches.map(name=>{console.log('[SW] Cleaning up old cache:',name);return caches.delete(name);}));}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,34 @@
class PrinterMonitor{constructor(){this.refreshInterval=30000;this.fastRefreshInterval=5000;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;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');}
start(){if(this.isActive){console.log('⚠️ PrinterMonitor läuft bereits');return;}
this.isActive=true;this.errorCount=0;console.log('🚀 Starte PrinterMonitor');this.updatePrinterStatus();this.startInterval();document.addEventListener('visibilitychange',this.handleVisibilityChange.bind(this));}
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');}
startInterval(){if(this.currentInterval){clearInterval(this.currentInterval);}
this.currentInterval=setInterval(()=>{this.updatePrinterStatus();},this.refreshInterval);}
handleVisibilityChange(){if(document.hidden){this.refreshInterval=60000;}else{this.refreshInterval=30000;this.updatePrinterStatus();}
if(this.isActive){this.startInterval();}}
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;}else{throw new Error(data.error||'Unbekannter Fehler');}}catch(error){this.errorCount++;console.error('❌ Fehler beim Abrufen des Drucker-Status:',error);if(this.errorCount>=this.maxErrors){this.refreshInterval=Math.min(this.refreshInterval*2,300000);this.startInterval();this.notifyCallbacks({type:'error',message:`Drucker-Status nicht verfügbar(${this.errorCount}Fehler)`,timestamp:new Date().toISOString()});}}}
processPrinterData(data){const previousPrinters=new Map(this.printers);this.printers.clear();let printersData=null;if(data&&data.printers&&typeof data.printers==='object'){printersData=data.printers;}else if(data&&data.status&&typeof data.status==='object'){printersData=data.status;}else if(data&&typeof data==='object'&&!data.success&&!data.error){printersData=data;}
if(printersData&&typeof printersData==='object'){Object.values(printersData).forEach(printer=>{if(printer&&typeof printer==='object'&&printer.id){this.printers.set(printer.id,{...printer,statusInfo:this.statusCategories[printer.status]||this.statusCategories['offline']});}else{console.warn('⚠️ Ungültiges Drucker-Objekt übersprungen:',printer);}});console.log(`${this.printers.size}Drucker erfolgreich verarbeitet`);}else{console.warn('⚠️ Keine gültigen Drucker-Daten in Response-Struktur gefunden');console.debug('Response-Struktur:',data);this.notifyCallbacks({type:'warning',message:'Keine Drucker-Daten verfügbar',data:data});return;}
this.lastUpdate=new Date(data.timestamp||Date.now());const changes=this.detectChanges(previousPrinters,this.printers);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`);}
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;}
notifyCallbacks(data){this.callbacks.forEach(callback=>{try{callback(data);}catch(error){console.error('❌ Fehler in PrinterMonitor Callback:',error);}});}
onUpdate(callback){if(typeof callback==='function'){this.callbacks.add(callback);}}
offUpdate(callback){this.callbacks.delete(callback);}
async forceUpdate(){const oldUseCache=this.useCache;this.useCache=false;try{await this.updatePrinterStatus();}finally{this.useCache=oldUseCache;}}
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);}}
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;}
getPrinter(id){return this.printers.get(id);}
getAllPrinters(){return Array.from(this.printers.values());}
getPrintersByStatus(status){return this.getAllPrinters().filter(printer=>printer.status===status);}
getStatusSummary(){const summary={total:this.printers.size,online:0,offline:0,printing:0,standby:0,unreachable:0,unconfigured:0,error:0};this.printers.forEach(printer=>{const status=printer.status;if(summary.hasOwnProperty(status)){summary[status]++;}else{summary.offline++;}});return summary;}
enableFastUpdates(){this.refreshInterval=this.fastRefreshInterval;if(this.isActive){this.startInterval();}
console.log('⚡ Schnelle Updates aktiviert');}
disableFastUpdates(){this.refreshInterval=30000;if(this.isActive){this.startInterval();}
console.log('🐌 Normale Updates aktiviert');}
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);this.notifyCallbacks({type:'initialization',results:data.results,statistics:data.statistics,message:data.message});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;}}}
window.printerMonitor=new PrinterMonitor();document.addEventListener('DOMContentLoaded',()=>{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();}});window.addEventListener('beforeunload',()=>{if(window.printerMonitor){window.printerMonitor.stop();}});if(typeof module!=='undefined'&&module.exports){module.exports=PrinterMonitor;}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
const CACHE_NAME='myp-platform-backup-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/favicon.ico'];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();}));});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();}));});self.addEventListener('fetch',(event)=>{event.respondWith(caches.match(event.request).then((response)=>{if(response){return response;}
return fetch(event.request).then((fetchResponse)=>{if(!fetchResponse||fetchResponse.status!==200||fetchResponse.type!=='basic'){return fetchResponse;}
const responseToCache=fetchResponse.clone();caches.open(CACHE_NAME).then((cache)=>{cache.put(event.request,responseToCache);});return fetchResponse;});}));});

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,29 @@
class SessionManager{constructor(){this.isAuthenticated=false;this.maxInactiveMinutes=30;this.heartbeatInterval=5*60*1000;this.warningTime=5*60*1000;this.checkInterval=30*1000;this.heartbeatTimer=null;this.statusCheckTimer=null;this.warningShown=false;this.sessionWarningModal=null;this.init();}
async init(){try{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(){this.heartbeatTimer=setInterval(()=>{this.sendHeartbeat();},this.heartbeatInterval);this.statusCheckTimer=setInterval(()=>{this.checkSessionStatus();},this.checkInterval);setTimeout(()=>this.sendHeartbeat(),1000);}
async sendHeartbeat(){try{const csrfToken=document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');const headers={'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'};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})});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');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);if(timeLeftSeconds<=this.warningTime/1000&&timeLeftSeconds>0){if(!this.warningShown){this.showSessionWarning(timeLeftMinutes);this.warningShown=true;}}else if(timeLeftSeconds<=0){this.handleSessionExpired('Session time expired');}else{this.warningShown=false;this.hideSessionWarning();}
this.updateSessionStatusDisplay(sessionData);}}catch(error){console.error('❌ Session-Status-Check fehlgeschlagen:',error);}}
showSessionWarning(minutesLeft){this.hideSessionWarning();this.showToast('Session läuft ab',`Ihre Session läuft in ${minutesLeft}Minuten ab.Möchten Sie verlängern?`,'warning',10000,[{text:'Verlängern',action:()=>this.extendSession()},{text:'Abmelden',action:()=>this.logout()}]);if(this.sessionWarningModal){this.sessionWarningModal.show();this.updateWarningModal(minutesLeft);}
console.log(`⚠️ Session-Warnung:${minutesLeft}Minuten verbleibend`);}
hideSessionWarning(){if(this.sessionWarningModal){this.sessionWarningModal.hide();}}
createWarningModal(){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>`;document.body.insertAdjacentHTML('beforeend',modalHTML);document.getElementById('extendSessionBtn').addEventListener('click',()=>{this.extendSession();this.hideSessionWarning();});document.getElementById('logoutBtn').addEventListener('click',()=>{this.logout();});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();const response=await fetch('/auth/logout',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'}});if(response.ok){window.location.href='/auth/login';}else{window.location.href='/auth/login';}}catch(error){console.error('❌ Logout-Fehler:',error);window.location.href='/auth/login';}}
handleSessionExpired(reason){console.log('🕒 Session abgelaufen:',reason);this.stopSessionMonitoring();this.isAuthenticated=false;this.showToast('Session abgelaufen','Sie wurden automatisch abgemeldet. Bitte melden Sie sich erneut an.','warning',8000);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){const statusElement=document.getElementById('sessionStatus');if(statusElement){const timeLeftMinutes=Math.floor(sessionData.session.time_left_seconds/60);statusElement.textContent=`Session:${timeLeftMinutes}min`;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=[]){if(window.showToast){window.showToast(message,type,duration);return;}
if(type==='error'||type==='warning'){alert(`${title}:${message}`);}else{console.log(`${title}:${message}`);}}
isLoggedIn(){return this.isAuthenticated;}
start(){if(!this.heartbeatTimer&&this.isAuthenticated){this.startSessionMonitoring();}}
stop(){this.stopSessionMonitoring();}
async extend(minutes=30){return await this.extendSession(minutes);}
async logoutUser(){return await this.logout();}}
document.addEventListener('DOMContentLoaded',()=>{if(!window.location.pathname.includes('/auth/login')){window.sessionManager=new SessionManager();window.addEventListener('beforeunload',()=>{if(window.sessionManager){window.sessionManager.stop();}});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');setTimeout(()=>window.sessionManager.checkSessionStatus(),1000);}}});}});window.SessionManager=SessionManager;

Binary file not shown.

BIN
backend/static/js/sw.js.gz Normal file

Binary file not shown.

25
backend/static/js/sw.min.js vendored Normal file
View File

@@ -0,0 +1,25 @@
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'];const STATIC_PATTERNS=[/\.css$/,/\.js$/,/\.svg$/,/\.png$/,/\.ico$/,/\.woff2?$/];const API_PATTERNS=[/^\/api\//,/^\/auth\//,/^\/api\/jobs/,/^\/api\/printers/,/^\/api\/stats/];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);}));});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();}));});self.addEventListener('fetch',(event)=>{const{request}=event;const url=new URL(request.url);if(request.method!=='GET'||(url.protocol!=='http:'&&url.protocol!=='https:')){return;}
event.respondWith(fetch(request).then((response)=>{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(()=>{return caches.match(request);}));});function isStaticFile(pathname){return STATIC_PATTERNS.some(pattern=>pattern.test(pathname));}
function isAPIRequest(pathname){return API_PATTERNS.some(pattern=>pattern.test(pathname));}
function isPageRequest(request){return request.mode==='navigate';}
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'];const API_CACHE_PATTERNS=[/^\/api\/dashboard/,/^\/api\/printers/,/^\/api\/jobs/,/^\/api\/stats/];async function handleStaticFile(request){try{const cachedResponse=await caches.match(request);if(cachedResponse){return cachedResponse;}
const networkResponse=await fetch(request);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);const cachedResponse=await caches.match(request);if(cachedResponse){return cachedResponse;}
return new Response('Offline - Datei nicht verfügbar',{status:503,statusText:'Service Unavailable'});}}
async function handleAPIRequest(request){const url=new URL(request.url);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{const networkResponse=await fetch(request);if(networkResponse.ok){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');const cachedResponse=await caches.match(request);if(cachedResponse){const response=cachedResponse.clone();response.headers.set('X-Served-By','ServiceWorker-Cache');return 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'}});}}
async function handlePageRequest(request){try{const networkResponse=await fetch(request);if(networkResponse.ok){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');const cachedResponse=await caches.match(request);if(cachedResponse){return cachedResponse;}
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'}});}}
function shouldCacheAPIResponse(pathname){return API_CACHE_PATTERNS.some(pattern=>pattern.test(pathname));}
self.addEventListener('sync',(event)=>{console.log('Service Worker: Background sync triggered',event.tag);if(event.tag==='background-sync'){event.waitUntil(doBackgroundSync());}});async function doBackgroundSync(){try{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);}}
async function getPendingRequests(){return[];}
async function removePendingRequest(id){console.log('Service Worker: Removing pending request',id);}
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));});self.addEventListener('notificationclick',(event)=>{console.log('Service Worker: Notification clicked');event.notification.close();if(event.action==='explore'){event.waitUntil(clients.openWindow('/dashboard'));}});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));}});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);}}
self.addEventListener('periodicsync',(event)=>{console.log('Service Worker: Periodic sync triggered',event.tag);if(event.tag==='content-sync'){event.waitUntil(syncContent());}});async function syncContent(){try{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');

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,2 @@
(function(){'use strict';if(!window.showToast){window.showToast=function(message,type='info',duration=5000){console.log(`🔧 Fallback showToast:[${type.toUpperCase()}]${message}`);};}
window.MYP=window.MYP||{};window.MYP.UI=window.MYP.UI||{};console.log('🎨 MYP UI Components werden geladen...');})();

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More