🌟 🎉 Major Update:
This commit is contained in:
BIN
backend/static/js/admin-guest-requests.js.gz
Normal file
BIN
backend/static/js/admin-guest-requests.js.gz
Normal file
Binary file not shown.
223
backend/static/js/admin-guest-requests.min.js
vendored
Normal file
223
backend/static/js/admin-guest-requests.min.js
vendored
Normal 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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
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'
|
BIN
backend/static/js/admin-guest-requests.min.js.gz
Normal file
BIN
backend/static/js/admin-guest-requests.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/admin-panel.js.gz
Normal file
BIN
backend/static/js/admin-panel.js.gz
Normal file
Binary file not shown.
77
backend/static/js/admin-panel.min.js
vendored
Normal file
77
backend/static/js/admin-panel.min.js
vendored
Normal 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);}
|
BIN
backend/static/js/admin-panel.min.js.gz
Normal file
BIN
backend/static/js/admin-panel.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/admin-unified.js.gz
Normal file
BIN
backend/static/js/admin-unified.js.gz
Normal file
Binary file not shown.
101
backend/static/js/admin-unified.min.js
vendored
Normal file
101
backend/static/js/admin-unified.min.js
vendored
Normal 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&¤tValue!==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&¬ification.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;
|
BIN
backend/static/js/admin-unified.min.js.gz
Normal file
BIN
backend/static/js/admin-unified.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/advanced-components.js.gz
Normal file
BIN
backend/static/js/advanced-components.js.gz
Normal file
Binary file not shown.
79
backend/static/js/advanced-components.min.js
vendored
Normal file
79
backend/static/js/advanced-components.min.js
vendored
Normal 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');});})();
|
BIN
backend/static/js/advanced-components.min.js.gz
Normal file
BIN
backend/static/js/advanced-components.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/auto-logout.js.gz
Normal file
BIN
backend/static/js/auto-logout.js.gz
Normal file
Binary file not shown.
14
backend/static/js/auto-logout.min.js
vendored
Normal file
14
backend/static/js/auto-logout.min.js
vendored
Normal 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();}});
|
BIN
backend/static/js/auto-logout.min.js.gz
Normal file
BIN
backend/static/js/auto-logout.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/charts.js.gz
Normal file
BIN
backend/static/js/charts.js.gz
Normal file
Binary file not shown.
25
backend/static/js/charts.min.js
vendored
Normal file
25
backend/static/js/charts.min.js
vendored
Normal 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);
|
BIN
backend/static/js/charts.min.js.gz
Normal file
BIN
backend/static/js/charts.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/charts/apexcharts.min.js.gz
Normal file
BIN
backend/static/js/charts/apexcharts.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/charts/chart-adapter.js.gz
Normal file
BIN
backend/static/js/charts/chart-adapter.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/charts/chart-config.js.gz
Normal file
BIN
backend/static/js/charts/chart-config.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/charts/chart-renderer.js.gz
Normal file
BIN
backend/static/js/charts/chart-renderer.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/charts/chart.min.js.gz
Normal file
BIN
backend/static/js/charts/chart.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/countdown-timer.js.gz
Normal file
BIN
backend/static/js/countdown-timer.js.gz
Normal file
Binary file not shown.
86
backend/static/js/countdown-timer.min.js
vendored
Normal file
86
backend/static/js/countdown-timer.min.js
vendored
Normal 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();}};
|
BIN
backend/static/js/countdown-timer.min.js.gz
Normal file
BIN
backend/static/js/countdown-timer.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/csp-violation-handler.js.gz
Normal file
BIN
backend/static/js/csp-violation-handler.js.gz
Normal file
Binary file not shown.
20
backend/static/js/csp-violation-handler.min.js
vendored
Normal file
20
backend/static/js/csp-violation-handler.min.js
vendored
Normal 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;">×</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');
|
BIN
backend/static/js/csp-violation-handler.min.js.gz
Normal file
BIN
backend/static/js/csp-violation-handler.min.js.gz
Normal file
Binary file not shown.
123
backend/static/js/css-cache-manager.js
Normal file
123
backend/static/js/css-cache-manager.js
Normal 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;
|
BIN
backend/static/js/css-cache-manager.js.gz
Normal file
BIN
backend/static/js/css-cache-manager.js.gz
Normal file
Binary file not shown.
10
backend/static/js/css-cache-manager.min.js
vendored
Normal file
10
backend/static/js/css-cache-manager.min.js
vendored
Normal 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;
|
BIN
backend/static/js/css-cache-manager.min.js.gz
Normal file
BIN
backend/static/js/css-cache-manager.min.js.gz
Normal file
Binary file not shown.
372
backend/static/js/css-cache-service-worker.js
Normal file
372
backend/static/js/css-cache-service-worker.js
Normal 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');
|
BIN
backend/static/js/css-cache-service-worker.js.gz
Normal file
BIN
backend/static/js/css-cache-service-worker.js.gz
Normal file
Binary file not shown.
15
backend/static/js/css-cache-service-worker.min.js
vendored
Normal file
15
backend/static/js/css-cache-service-worker.min.js
vendored
Normal 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');
|
BIN
backend/static/js/css-cache-service-worker.min.js.gz
Normal file
BIN
backend/static/js/css-cache-service-worker.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/dark-mode-fix.js.gz
Normal file
BIN
backend/static/js/dark-mode-fix.js.gz
Normal file
Binary file not shown.
11
backend/static/js/dark-mode-fix.min.js
vendored
Normal file
11
backend/static/js/dark-mode-fix.min.js
vendored
Normal 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');});
|
BIN
backend/static/js/dark-mode-fix.min.js.gz
Normal file
BIN
backend/static/js/dark-mode-fix.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/dark-mode.js.gz
Normal file
BIN
backend/static/js/dark-mode.js.gz
Normal file
Binary file not shown.
15
backend/static/js/dark-mode.min.js
vendored
Normal file
15
backend/static/js/dark-mode.min.js
vendored
Normal 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');}
|
BIN
backend/static/js/dark-mode.min.js.gz
Normal file
BIN
backend/static/js/dark-mode.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/dashboard.js.gz
Normal file
BIN
backend/static/js/dashboard.js.gz
Normal file
Binary file not shown.
32
backend/static/js/dashboard.min.js
vendored
Normal file
32
backend/static/js/dashboard.min.js
vendored
Normal 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);}});
|
BIN
backend/static/js/dashboard.min.js.gz
Normal file
BIN
backend/static/js/dashboard.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/debug-fix.js.gz
Normal file
BIN
backend/static/js/debug-fix.js.gz
Normal file
Binary file not shown.
10
backend/static/js/debug-fix.min.js
vendored
Normal file
10
backend/static/js/debug-fix.min.js
vendored
Normal 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');})();
|
BIN
backend/static/js/debug-fix.min.js.gz
Normal file
BIN
backend/static/js/debug-fix.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/event-handlers.js.gz
Normal file
BIN
backend/static/js/event-handlers.js.gz
Normal file
Binary file not shown.
32
backend/static/js/event-handlers.min.js
vendored
Normal file
32
backend/static/js/event-handlers.min.js
vendored
Normal 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'&¶ms.id){jobManager.startJob(params.id);}
|
||||
break;case'pause-job':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.pauseJob(params.id);}
|
||||
break;case'resume-job':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.resumeJob(params.id);}
|
||||
break;case'delete-job':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.deleteJob(params.id);}
|
||||
break;case'open-job-details':if(typeof jobManager!=='undefined'&¶ms.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'&¶ms.id){printerManager.editPrinter(params.id);}
|
||||
break;case'connect-printer':if(typeof printerManager!=='undefined'&¶ms.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'&¶ms.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');
|
BIN
backend/static/js/event-handlers.min.js.gz
Normal file
BIN
backend/static/js/event-handlers.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/fullcalendar/core.min.js.gz
Normal file
BIN
backend/static/js/fullcalendar/core.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/fullcalendar/daygrid.min.js.gz
Normal file
BIN
backend/static/js/fullcalendar/daygrid.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/fullcalendar/interaction.min.js.gz
Normal file
BIN
backend/static/js/fullcalendar/interaction.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/fullcalendar/list.min.js.gz
Normal file
BIN
backend/static/js/fullcalendar/list.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/fullcalendar/main.min.css.gz
Normal file
BIN
backend/static/js/fullcalendar/main.min.css.gz
Normal file
Binary file not shown.
BIN
backend/static/js/fullcalendar/timegrid.min.js.gz
Normal file
BIN
backend/static/js/fullcalendar/timegrid.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/glassmorphism-notifications.js.gz
Normal file
BIN
backend/static/js/glassmorphism-notifications.js.gz
Normal file
Binary file not shown.
80
backend/static/js/glassmorphism-notifications.min.js
vendored
Normal file
80
backend/static/js/glassmorphism-notifications.min.js
vendored
Normal 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');});
|
BIN
backend/static/js/glassmorphism-notifications.min.js.gz
Normal file
BIN
backend/static/js/glassmorphism-notifications.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/global-refresh-functions.js.gz
Normal file
BIN
backend/static/js/global-refresh-functions.js.gz
Normal file
Binary file not shown.
47
backend/static/js/global-refresh-functions.min.js
vendored
Normal file
47
backend/static/js/global-refresh-functions.min.js
vendored
Normal 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');
|
BIN
backend/static/js/global-refresh-functions.min.js.gz
Normal file
BIN
backend/static/js/global-refresh-functions.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/job-manager.js.gz
Normal file
BIN
backend/static/js/job-manager.js.gz
Normal file
Binary file not shown.
67
backend/static/js/job-manager.min.js
vendored
Normal file
67
backend/static/js/job-manager.min.js
vendored
Normal 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');})();
|
BIN
backend/static/js/job-manager.min.js.gz
Normal file
BIN
backend/static/js/job-manager.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/jobs-safety-fix.js.gz
Normal file
BIN
backend/static/js/jobs-safety-fix.js.gz
Normal file
Binary file not shown.
21
backend/static/js/jobs-safety-fix.min.js
vendored
Normal file
21
backend/static/js/jobs-safety-fix.min.js
vendored
Normal 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');})();
|
BIN
backend/static/js/jobs-safety-fix.min.js.gz
Normal file
BIN
backend/static/js/jobs-safety-fix.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/notifications.js.gz
Normal file
BIN
backend/static/js/notifications.js.gz
Normal file
Binary file not shown.
40
backend/static/js/notifications.min.js
vendored
Normal file
40
backend/static/js/notifications.min.js
vendored
Normal 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);}
|
BIN
backend/static/js/notifications.min.js.gz
Normal file
BIN
backend/static/js/notifications.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/offline-app.js.gz
Normal file
BIN
backend/static/js/offline-app.js.gz
Normal file
Binary file not shown.
28
backend/static/js/offline-app.min.js
vendored
Normal file
28
backend/static/js/offline-app.min.js
vendored
Normal 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();});
|
BIN
backend/static/js/offline-app.min.js.gz
Normal file
BIN
backend/static/js/offline-app.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/optimization-features.js.gz
Normal file
BIN
backend/static/js/optimization-features.js.gz
Normal file
Binary file not shown.
71
backend/static/js/optimization-features.min.js
vendored
Normal file
71
backend/static/js/optimization-features.min.js
vendored
Normal 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');});
|
BIN
backend/static/js/optimization-features.min.js.gz
Normal file
BIN
backend/static/js/optimization-features.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/performance-service-worker.js.gz
Normal file
BIN
backend/static/js/performance-service-worker.js.gz
Normal file
Binary file not shown.
25
backend/static/js/performance-service-worker.min.js
vendored
Normal file
25
backend/static/js/performance-service-worker.min.js
vendored
Normal 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);}));}
|
BIN
backend/static/js/performance-service-worker.min.js.gz
Normal file
BIN
backend/static/js/performance-service-worker.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/printer_monitor.js.gz
Normal file
BIN
backend/static/js/printer_monitor.js.gz
Normal file
Binary file not shown.
34
backend/static/js/printer_monitor.min.js
vendored
Normal file
34
backend/static/js/printer_monitor.min.js
vendored
Normal 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;}
|
BIN
backend/static/js/printer_monitor.min.js.gz
Normal file
BIN
backend/static/js/printer_monitor.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/service-worker.js.gz
Normal file
BIN
backend/static/js/service-worker.js.gz
Normal file
Binary file not shown.
3
backend/static/js/service-worker.min.js
vendored
Normal file
3
backend/static/js/service-worker.min.js
vendored
Normal 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;});}));});
|
BIN
backend/static/js/service-worker.min.js.gz
Normal file
BIN
backend/static/js/service-worker.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/session-manager.js.gz
Normal file
BIN
backend/static/js/session-manager.js.gz
Normal file
Binary file not shown.
29
backend/static/js/session-manager.min.js
vendored
Normal file
29
backend/static/js/session-manager.min.js
vendored
Normal 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;
|
BIN
backend/static/js/session-manager.min.js.gz
Normal file
BIN
backend/static/js/session-manager.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/sw.js.gz
Normal file
BIN
backend/static/js/sw.js.gz
Normal file
Binary file not shown.
25
backend/static/js/sw.min.js
vendored
Normal file
25
backend/static/js/sw.min.js
vendored
Normal 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');
|
BIN
backend/static/js/sw.min.js.gz
Normal file
BIN
backend/static/js/sw.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/ui-components.js.gz
Normal file
BIN
backend/static/js/ui-components.js.gz
Normal file
Binary file not shown.
2
backend/static/js/ui-components.min.js
vendored
Normal file
2
backend/static/js/ui-components.min.js
vendored
Normal 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...');})();
|
BIN
backend/static/js/ui-components.min.js.gz
Normal file
BIN
backend/static/js/ui-components.min.js.gz
Normal file
Binary file not shown.
BIN
backend/static/js/user-dropdown.js.gz
Normal file
BIN
backend/static/js/user-dropdown.js.gz
Normal file
Binary file not shown.
0
backend/static/js/user-dropdown.min.js
vendored
Normal file
0
backend/static/js/user-dropdown.min.js
vendored
Normal file
BIN
backend/static/js/user-dropdown.min.js.gz
Normal file
BIN
backend/static/js/user-dropdown.min.js.gz
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user