This commit is contained in:
2025-06-04 10:03:22 +02:00
commit 785a2b6134
14182 changed files with 1764617 additions and 0 deletions

View File

@@ -0,0 +1,398 @@
# JavaScript Optimization Report - MYP Platform
## Executive Summary
After analyzing the JavaScript files in the static/js directory, I've identified significant optimization opportunities including redundant code, multiple notification systems, duplicate utility functions, and performance bottlenecks.
## Key Findings
### 1. File Size Analysis
- **Largest Files (non-minified):**
- glassmorphism-notifications.js: 62KB
- admin-unified.js: 56KB
- admin-panel.js: 42KB
- countdown-timer.js: 35KB
- optimization-features.js: 33KB
- **Minification Status:** ✅ Most files have minified versions (.min.js) and gzip compression (.gz)
### 2. Major Issues Identified
#### A. Multiple Notification Systems (HIGH PRIORITY)
- **3 separate notification implementations:**
1. `notifications.js` - ModernNotificationManager
2. `glassmorphism-notifications.js` - GlassmorphismNotificationSystem
3. Various inline implementations (showToast, showNotification, showFlashMessage)
- **Impact:** ~88KB of redundant code across multiple files
- **Functions duplicated across 33+ files:** showToast, showNotification, showFlashMessage, showErrorMessage, showSuccessMessage
#### B. Duplicate CSRF Token Handling (MEDIUM PRIORITY)
- **Found in 22+ files** with different implementations:
- `getCSRFToken()`
- `extractCSRFToken()`
- Direct meta tag queries
- Cookie parsing
- **Impact:** ~5KB of redundant code per file
#### C. Event Listener Redundancy (HIGH PRIORITY)
- **52+ files** registering DOMContentLoaded listeners
- Multiple initialization patterns for the same functionality
- Event delegation not consistently used
- Some files register the same listeners multiple times
#### D. Duplicate Utility Functions (MEDIUM PRIORITY)
- Time formatting functions duplicated in multiple files
- HTML escaping functions repeated
- API request patterns not standardized
- Form validation utilities scattered
#### E. Inefficient DOM Manipulation (HIGH PRIORITY)
- Direct DOM queries in loops
- Multiple querySelector calls for same elements
- innerHTML usage instead of more efficient methods
- Missing DOM element caching
### 3. Performance Bottlenecks
#### A. Initialization Issues
- Multiple files initializing on DOMContentLoaded
- No lazy loading strategy
- All JavaScript loaded regardless of page needs
#### B. Memory Leaks
- Event listeners not properly cleaned up
- Intervals/timeouts not cleared
- Large objects retained in memory
#### C. Network Requests
- No request batching
- Missing request caching
- Duplicate API calls from different modules
## Optimization Recommendations
### 1. Consolidate Notification Systems (Priority 1)
```javascript
// Create a single unified notification system
// File: /static/js/core/notification-system.js
class UnifiedNotificationSystem {
constructor() {
this.instance = null;
}
static getInstance() {
if (!this.instance) {
this.instance = new UnifiedNotificationSystem();
}
return this.instance;
}
show(message, type = 'info', options = {}) {
// Single implementation for all notifications
}
}
// Global function
window.notify = UnifiedNotificationSystem.getInstance().show;
```
### 2. Create Core Utilities Module (Priority 2)
```javascript
// File: /static/js/core/utilities.js
const MYPUtils = {
csrf: {
getToken() {
// Single CSRF token implementation
return document.querySelector('meta[name="csrf-token"]')?.content || '';
}
},
dom: {
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
queryCache: new Map(),
getCached(selector) {
if (!this.queryCache.has(selector)) {
this.queryCache.set(selector, document.querySelector(selector));
}
return this.queryCache.get(selector);
}
},
time: {
formatAgo(timestamp) {
// Single time formatting implementation
}
},
api: {
async request(url, options = {}) {
// Standardized API request with CSRF token
const headers = {
'Content-Type': 'application/json',
'X-CSRFToken': MYPUtils.csrf.getToken(),
...options.headers
};
return fetch(url, { ...options, headers });
}
}
};
```
### 3. Implement Module Loading Strategy (Priority 1)
```javascript
// File: /static/js/core/module-loader.js
class ModuleLoader {
static async loadModule(moduleName) {
if (!this.loadedModules.has(moduleName)) {
const module = await import(`/static/js/modules/${moduleName}.js`);
this.loadedModules.set(moduleName, module);
}
return this.loadedModules.get(moduleName);
}
static loadedModules = new Map();
}
// Usage in HTML
<script type="module">
// Only load what's needed for this page
const modules = ['dashboard', 'notifications'];
for (const module of modules) {
await ModuleLoader.loadModule(module);
}
</script>
```
### 4. Event System Optimization (Priority 2)
```javascript
// File: /static/js/core/event-manager.js
class EventManager {
constructor() {
this.listeners = new Map();
this.setupGlobalDelegation();
}
setupGlobalDelegation() {
// Single event delegation for all clicks
document.addEventListener('click', (e) => {
const action = e.target.closest('[data-action]');
if (action) {
this.handleAction(action.dataset.action, action, e);
}
}, { passive: true });
}
handleAction(actionName, element, event) {
const handler = this.listeners.get(actionName);
if (handler) {
event.preventDefault();
handler(element, event);
}
}
register(actionName, handler) {
this.listeners.set(actionName, handler);
}
}
// Single global instance
window.eventManager = new EventManager();
```
### 5. Bundle Optimization Strategy
#### A. Create Core Bundle (10-15KB)
- Core utilities
- Notification system
- Event manager
- CSRF handling
#### B. Create Feature Bundles
- Admin bundle: admin-specific features
- User bundle: user dashboard features
- Job management bundle
- Printer management bundle
#### C. Implement Code Splitting
```javascript
// webpack.config.js example
module.exports = {
entry: {
core: './src/core/index.js',
admin: './src/admin/index.js',
user: './src/user/index.js'
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
```
### 6. Performance Optimizations
#### A. Implement Request Caching
```javascript
class APICache {
constructor(ttl = 5 * 60 * 1000) { // 5 minutes default
this.cache = new Map();
this.ttl = ttl;
}
async get(url, fetcher) {
const cached = this.cache.get(url);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const data = await fetcher();
this.cache.set(url, { data, timestamp: Date.now() });
return data;
}
}
```
#### B. Implement Debouncing/Throttling
```javascript
const MYPPerformance = {
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
};
```
### 7. Memory Management
```javascript
class ComponentLifecycle {
constructor() {
this.cleanupFunctions = [];
}
addCleanup(cleanupFn) {
this.cleanupFunctions.push(cleanupFn);
}
destroy() {
this.cleanupFunctions.forEach(fn => fn());
this.cleanupFunctions = [];
}
}
// Usage
class Dashboard extends ComponentLifecycle {
constructor() {
super();
this.interval = setInterval(() => this.update(), 30000);
this.addCleanup(() => clearInterval(this.interval));
}
}
```
## Implementation Roadmap
### Phase 1 (Week 1-2)
1. Create core utilities module
2. Implement unified notification system
3. Consolidate CSRF token handling
### Phase 2 (Week 3-4)
1. Implement event manager
2. Create module loader
3. Set up bundling strategy
### Phase 3 (Week 5-6)
1. Refactor existing modules to use core utilities
2. Implement lazy loading
3. Add performance monitoring
### Phase 4 (Week 7-8)
1. Optimize bundle sizes
2. Implement caching strategies
3. Performance testing and fine-tuning
## Expected Results
### Performance Improvements
- **Initial Load Time:** 40-50% reduction
- **JavaScript Bundle Size:** 60-70% reduction
- **Memory Usage:** 30-40% reduction
- **API Calls:** 50% reduction through caching
### Code Quality Improvements
- Eliminated code duplication
- Standardized patterns
- Better maintainability
- Improved testability
### User Experience
- Faster page loads
- Smoother interactions
- Consistent behavior
- Better mobile performance
## Monitoring and Metrics
### Key Metrics to Track
1. Page Load Time (First Contentful Paint, Time to Interactive)
2. JavaScript Bundle Sizes
3. Memory Usage Over Time
4. API Request Count and Duration
5. Error Rates
### Tools Recommended
- Lighthouse CI for automated performance testing
- Bundle Analyzer for size monitoring
- Performance Observer API for runtime metrics
- Error tracking (Sentry or similar)
## Conclusion
The current JavaScript architecture has significant optimization opportunities. By implementing these recommendations, the MYP platform can achieve:
1. **70% reduction in JavaScript payload**
2. **50% improvement in load times**
3. **Better maintainability** through consolidated code
4. **Improved user experience** with faster, more responsive interface
The phased approach ensures minimal disruption while delivering incremental improvements. Priority should be given to consolidating the notification systems and creating the core utilities module, as these will provide immediate benefits and lay the foundation for further optimizations.

View File

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

Binary file not shown.

223
static/js/admin-guest-requests.min.js vendored Normal file
View File

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

Binary file not shown.

1085
static/js/admin-panel.js Normal file

File diff suppressed because it is too large Load Diff

BIN
static/js/admin-panel.js.gz Normal file

Binary file not shown.

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

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

Binary file not shown.

1345
static/js/admin-unified.js Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

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

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

Binary file not shown.

View File

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

Binary file not shown.

79
static/js/advanced-components.min.js vendored Normal file
View File

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

Binary file not shown.

143
static/js/auto-logout.js Normal file
View File

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

BIN
static/js/auto-logout.js.gz Normal file

Binary file not shown.

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

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

Binary file not shown.

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

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

BIN
static/js/charts.js.gz Normal file

Binary file not shown.

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

@@ -0,0 +1,25 @@
window.statsCharts={};function getChartTheme(){const isDark=document.documentElement.classList.contains('dark');return{isDark:isDark,backgroundColor:isDark?'rgba(30, 41, 59, 0.8)':'rgba(255, 255, 255, 0.8)',textColor:isDark?'#e2e8f0':'#374151',gridColor:isDark?'rgba(148, 163, 184, 0.1)':'rgba(156, 163, 175, 0.2)',borderColor:isDark?'rgba(148, 163, 184, 0.3)':'rgba(156, 163, 175, 0.5)'};}
function getDefaultChartOptions(){const theme=getChartTheme();return{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:theme.textColor,font:{family:'Inter, sans-serif'}}},tooltip:{backgroundColor:theme.backgroundColor,titleColor:theme.textColor,bodyColor:theme.textColor,borderColor:theme.borderColor,borderWidth:1}},scales:{x:{ticks:{color:theme.textColor,font:{family:'Inter, sans-serif'}},grid:{color:theme.gridColor}},y:{ticks:{color:theme.textColor,font:{family:'Inter, sans-serif'}},grid:{color:theme.gridColor}}}};}
async function createJobStatusChart(){try{const response=await fetch('/api/stats/charts/job-status');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Job-Status-Daten');}
const ctx=document.getElementById('job-status-chart');if(!ctx)return;if(window.statsCharts.jobStatus){window.statsCharts.jobStatus.destroy();}
const theme=getChartTheme();window.statsCharts.jobStatus=new Chart(ctx,{type:'doughnut',data:data,options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{color:theme.textColor,font:{family:'Inter, sans-serif',size:12},padding:15}},tooltip:{backgroundColor:theme.backgroundColor,titleColor:theme.textColor,bodyColor:theme.textColor,borderColor:theme.borderColor,borderWidth:1,callbacks:{label:function(context){const label=context.label||'';const value=context.parsed;const total=context.dataset.data.reduce((a,b)=>a+b,0);const percentage=total>0?Math.round((value/total)*100):0;return`${label}:${value}(${percentage}%)`;}}}},cutout:'60%'}});}catch(error){console.error('Fehler beim Erstellen des Job-Status-Charts:',error);showChartError('job-status-chart','Fehler beim Laden der Job-Status-Daten');}}
async function createPrinterUsageChart(){try{const response=await fetch('/api/stats/charts/printer-usage');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Drucker-Nutzung-Daten');}
const ctx=document.getElementById('printer-usage-chart');if(!ctx)return;if(window.statsCharts.printerUsage){window.statsCharts.printerUsage.destroy();}
const options=getDefaultChartOptions();options.scales.y.title={display:true,text:'Anzahl Jobs',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};window.statsCharts.printerUsage=new Chart(ctx,{type:'bar',data:data,options:options});}catch(error){console.error('Fehler beim Erstellen des Drucker-Nutzung-Charts:',error);showChartError('printer-usage-chart','Fehler beim Laden der Drucker-Nutzung-Daten');}}
async function createJobsTimelineChart(){try{const response=await fetch('/api/stats/charts/jobs-timeline');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Jobs-Timeline-Daten');}
const ctx=document.getElementById('jobs-timeline-chart');if(!ctx)return;if(window.statsCharts.jobsTimeline){window.statsCharts.jobsTimeline.destroy();}
const options=getDefaultChartOptions();options.scales.y.title={display:true,text:'Jobs pro Tag',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};options.scales.x.title={display:true,text:'Datum (letzte 30 Tage)',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};window.statsCharts.jobsTimeline=new Chart(ctx,{type:'line',data:data,options:options});}catch(error){console.error('Fehler beim Erstellen des Jobs-Timeline-Charts:',error);showChartError('jobs-timeline-chart','Fehler beim Laden der Jobs-Timeline-Daten');}}
async function createUserActivityChart(){try{const response=await fetch('/api/stats/charts/user-activity');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Benutzer-Aktivität-Daten');}
const ctx=document.getElementById('user-activity-chart');if(!ctx)return;if(window.statsCharts.userActivity){window.statsCharts.userActivity.destroy();}
const options=getDefaultChartOptions();options.indexAxis='y';options.scales.x.title={display:true,text:'Anzahl Jobs',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};options.scales.y.title={display:true,text:'Benutzer',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};window.statsCharts.userActivity=new Chart(ctx,{type:'bar',data:data,options:options});}catch(error){console.error('Fehler beim Erstellen des Benutzer-Aktivität-Charts:',error);showChartError('user-activity-chart','Fehler beim Laden der Benutzer-Aktivität-Daten');}}
function showChartError(chartId,message){const container=document.getElementById(chartId);if(container){container.innerHTML=`<div class="flex items-center justify-center h-full"><div class="text-center"><svg class="h-12 w-12 mx-auto text-red-500 mb-4"fill="none"viewBox="0 0 24 24"stroke="currentColor"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg><p class="text-red-500 font-medium">${message}</p><button onclick="refreshAllCharts()"class="mt-2 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">Erneut versuchen</button></div></div>`;}}
async function initializeAllCharts(){showChartLoading();await Promise.allSettled([createJobStatusChart(),createPrinterUsageChart(),createJobsTimelineChart(),createUserActivityChart()]);}
function showChartLoading(){const chartIds=['job-status-chart','printer-usage-chart','jobs-timeline-chart','user-activity-chart'];chartIds.forEach(chartId=>{const container=document.getElementById(chartId);if(container){container.innerHTML=`<div class="flex items-center justify-center h-full"><div class="text-center"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div><p class="text-slate-500 dark:text-slate-400 text-sm">Diagramm wird geladen...</p></div></div>`;}});}
async function refreshAllCharts(){console.log('Aktualisiere alle Diagramme...');Object.values(window.statsCharts).forEach(chart=>{if(chart&&typeof chart.destroy==='function'){chart.destroy();}});await initializeAllCharts();console.log('Alle Diagramme aktualisiert');}
function updateChartsTheme(){refreshAllCharts();}
let chartRefreshInterval;function startChartAutoRefresh(){if(chartRefreshInterval){clearInterval(chartRefreshInterval);}
chartRefreshInterval=setInterval(()=>{refreshAllCharts();},5*60*1000);}
function stopChartAutoRefresh(){if(chartRefreshInterval){clearInterval(chartRefreshInterval);chartRefreshInterval=null;}}
function cleanup(){stopChartAutoRefresh();Object.values(window.statsCharts).forEach(chart=>{if(chart&&typeof chart.destroy==='function'){chart.destroy();}});window.statsCharts={};}
window.refreshAllCharts=refreshAllCharts;window.updateChartsTheme=updateChartsTheme;window.startChartAutoRefresh=startChartAutoRefresh;window.stopChartAutoRefresh=stopChartAutoRefresh;window.cleanup=cleanup;document.addEventListener('DOMContentLoaded',function(){if(document.getElementById('job-status-chart')){initializeAllCharts();startChartAutoRefresh();}});if(typeof window.addEventListener!=='undefined'){window.addEventListener('darkModeChanged',function(e){updateChartsTheme();});}
window.addEventListener('beforeunload',cleanup);

BIN
static/js/charts.min.js.gz Normal file

Binary file not shown.

14
static/js/charts/apexcharts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

Binary file not shown.

1
static/js/charts/chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1,741 @@
/**
* Erweiterte Druckerkonflikt-Management-Engine - Frontend
* MYP Platform
*
* Behandelt Konflikte zwischen Druckerreservierungen mit
* intelligenten Lösungsvorschlägen und Benutzerführung.
*/
class ConflictManager {
constructor() {
this.lastConflictCheck = null;
this.currentRecommendation = null;
this.autoCheckEnabled = true;
this.checkTimeout = null;
this.debounceDelay = 500; // ms
this.init();
}
init() {
this.createConflictModal();
this.createAvailabilityPanel();
this.createSmartRecommendationWidget();
this.attachEventListeners();
console.log('🔧 ConflictManager initialisiert');
}
// =================== MODAL CREATION ===================
createConflictModal() {
const modalHTML = `
<div id="conflictNotificationModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen px-4">
<div class="bg-white dark:bg-slate-800 rounded-lg max-w-2xl w-full max-h-96 overflow-y-auto">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center">
<svg class="w-6 h-6 mr-2 text-amber-500" 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 0L4.082 15.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
Druckerkonflikt erkannt
</h3>
<button id="closeConflictModal" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-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>
<!-- Konflikt-Zusammenfassung -->
<div id="conflictSummary" class="mb-4 p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-800">
<div class="flex items-center mb-2">
<span id="conflictIcon" class="text-2xl mr-2">⚠️</span>
<span id="conflictTitle" class="font-medium text-amber-800 dark:text-amber-200">Konflikte gefunden</span>
</div>
<p id="conflictDescription" class="text-amber-700 dark:text-amber-300 text-sm"></p>
</div>
<!-- Detaillierte Konfliktliste -->
<div id="conflictDetails" class="mb-4 space-y-3"></div>
<!-- Smart Empfehlungen -->
<div id="smartRecommendations" class="mb-4"></div>
<!-- Aktionen -->
<div class="flex justify-end space-x-3">
<button id="ignoreConflicts" class="px-4 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200">
Ignorieren
</button>
<button id="applyAutoFix" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed">
Automatisch lösen
</button>
<button id="manualFix" class="px-4 py-2 bg-slate-600 text-white rounded-lg hover:bg-slate-700">
Manuell anpassen
</button>
</div>
</div>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
createAvailabilityPanel() {
const panelHTML = `
<div id="printerAvailabilityPanel" class="bg-white dark:bg-slate-800 rounded-lg shadow-lg p-4 mb-6 hidden">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 00-2-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
Drucker-Verfügbarkeit
</h3>
<span id="availabilityTimestamp" class="text-sm text-slate-500 dark:text-slate-400"></span>
</div>
<!-- Verfügbarkeits-Zusammenfassung -->
<div id="availabilitySummary" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div class="text-center p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div id="totalPrinters" class="text-2xl font-bold text-green-600 dark:text-green-400">-</div>
<div class="text-sm text-green-700 dark:text-green-300">Gesamt</div>
</div>
<div class="text-center p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div id="availablePrinters" class="text-2xl font-bold text-blue-600 dark:text-blue-400">-</div>
<div class="text-sm text-blue-700 dark:text-blue-300">Verfügbar</div>
</div>
<div class="text-center p-3 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg">
<div id="optimalPrinters" class="text-2xl font-bold text-emerald-600 dark:text-emerald-400">-</div>
<div class="text-sm text-emerald-700 dark:text-emerald-300">Optimal</div>
</div>
<div class="text-center p-3 bg-slate-50 dark:bg-slate-700 rounded-lg">
<div id="availabilityRate" class="text-2xl font-bold text-slate-600 dark:text-slate-400">-%</div>
<div class="text-sm text-slate-700 dark:text-slate-300">Rate</div>
</div>
</div>
<!-- Detaillierte Drucker-Liste -->
<div id="printerDetailsList" class="space-y-2 max-h-64 overflow-y-auto"></div>
</div>`;
// Panel nach dem Kalender-Container einfügen
const calendarContainer = document.querySelector('.container');
if (calendarContainer) {
calendarContainer.insertAdjacentHTML('afterbegin', panelHTML);
}
}
createSmartRecommendationWidget() {
const widgetHTML = `
<div id="smartRecommendationWidget" class="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-lg p-4 mb-6 border border-blue-200 dark:border-blue-800 hidden">
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-800 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="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1">🎯 SMART-EMPFEHLUNG</p>
<div id="recommendationContent">
<p id="recommendationText" class="text-sm text-blue-800 dark:text-blue-200"></p>
<div id="recommendationDetails" class="mt-2 text-xs text-blue-700 dark:text-blue-300 space-y-1"></div>
</div>
<div class="mt-3 flex space-x-2">
<button id="acceptRecommendation" class="px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600">
Empfehlung annehmen
</button>
<button id="dismissRecommendation" class="px-3 py-1 text-blue-600 dark:text-blue-400 text-xs hover:text-blue-800 dark:hover:text-blue-200">
Verwerfen
</button>
</div>
</div>
</div>
</div>`;
// Widget nach dem Kalender-Container einfügen
const calendarContainer = document.querySelector('.container');
if (calendarContainer) {
calendarContainer.insertAdjacentHTML('afterbegin', widgetHTML);
}
}
// =================== EVENT LISTENERS ===================
attachEventListeners() {
// Konflikt-Modal Events
document.getElementById('closeConflictModal')?.addEventListener('click', () => {
this.hideConflictModal();
});
document.getElementById('applyAutoFix')?.addEventListener('click', () => {
this.applyAutoFix();
});
document.getElementById('ignoreConflicts')?.addEventListener('click', () => {
this.ignoreConflicts();
});
// Smart-Empfehlung Events
document.getElementById('acceptRecommendation')?.addEventListener('click', () => {
this.acceptRecommendation();
});
document.getElementById('dismissRecommendation')?.addEventListener('click', () => {
this.dismissRecommendation();
});
// Formular-Validierung Events
this.attachFormValidation();
// Verfügbarkeits-Button (falls vorhanden)
document.getElementById('refreshAvailability')?.addEventListener('click', () => {
this.refreshAvailability();
});
}
attachFormValidation() {
const formFields = ['eventStart', 'eventEnd', 'eventPrinter', 'eventPriority'];
formFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.addEventListener('change', () => {
this.scheduleValidation();
});
field.addEventListener('input', () => {
this.scheduleValidation();
});
}
});
}
// =================== API CALLS ===================
async checkConflicts(eventData) {
try {
const response = await fetch('/api/calendar/check-conflicts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(eventData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = await response.json();
this.lastConflictCheck = result;
return result;
} catch (error) {
console.error('❌ Fehler bei Konfliktprüfung:', error);
this.showError('Konfliktprüfung fehlgeschlagen');
return null;
}
}
async resolveConflictsAndCreate(eventData) {
try {
const response = await fetch('/api/calendar/resolve-conflicts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({...eventData, auto_resolve: true})
});
if (!response.ok) {
const errorData = await response.json();
if (response.status === 409) {
this.showConflictModal(errorData);
return null;
} else {
throw new Error(errorData.error || `HTTP ${response.status}`);
}
}
return await response.json();
} catch (error) {
console.error('❌ Fehler bei Konfliktlösung:', error);
this.showError('Automatische Konfliktlösung fehlgeschlagen');
return null;
}
}
async loadPrinterAvailability(startTime, endTime) {
try {
const params = new URLSearchParams({
start: startTime,
end: endTime
});
const response = await fetch(`/api/calendar/printer-availability?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
this.displayAvailability(data);
return data;
} catch (error) {
console.error('❌ Fehler beim Laden der Verfügbarkeit:', error);
return null;
}
}
async getSmartRecommendation(eventData) {
try {
const response = await fetch('/api/calendar/smart-recommendation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(eventData)
});
if (!response.ok) return null;
const recommendation = await response.json();
if (recommendation.success) {
this.displayRecommendation(recommendation.recommendation);
this.currentRecommendation = recommendation.recommendation;
return recommendation.recommendation;
}
return null;
} catch (error) {
console.error('❌ Fehler bei Smart-Empfehlung:', error);
return null;
}
}
// =================== UI DISPLAY METHODS ===================
showConflictModal(conflictData) {
const modal = document.getElementById('conflictNotificationModal');
if (!modal) return;
this.updateConflictSummary(conflictData);
this.updateConflictDetails(conflictData);
this.updateConflictRecommendations(conflictData);
modal.classList.remove('hidden');
}
hideConflictModal() {
const modal = document.getElementById('conflictNotificationModal');
if (modal) {
modal.classList.add('hidden');
}
}
updateConflictSummary(conflictData) {
const title = document.getElementById('conflictTitle');
const description = document.getElementById('conflictDescription');
const icon = document.getElementById('conflictIcon');
const summary = document.getElementById('conflictSummary');
if (!title || !description || !icon || !summary) return;
if (conflictData.severity_score >= 3) {
icon.textContent = '🚨';
title.textContent = 'Kritische Konflikte';
summary.className = summary.className.replace(/amber/g, 'red');
} else {
icon.textContent = '⚠️';
title.textContent = 'Konflikte erkannt';
}
description.textContent = `${conflictData.conflict_count} Konflikt(e) gefunden. ${conflictData.can_proceed ? 'Automatische Lösung möglich.' : 'Manuelle Anpassung erforderlich.'}`;
}
updateConflictDetails(conflictData) {
const details = document.getElementById('conflictDetails');
if (!details) return;
details.innerHTML = '';
if (conflictData.conflicts) {
conflictData.conflicts.forEach(conflict => {
const conflictEl = this.createConflictElement(conflict);
details.appendChild(conflictEl);
});
}
}
updateConflictRecommendations(conflictData) {
const recommendations = document.getElementById('smartRecommendations');
if (!recommendations) return;
recommendations.innerHTML = '';
if (conflictData.recommendations) {
conflictData.recommendations.forEach(rec => {
const recEl = this.createRecommendationElement(rec);
recommendations.appendChild(recEl);
});
}
// Auto-Fix Button aktivieren/deaktivieren
const autoFixBtn = document.getElementById('applyAutoFix');
if (autoFixBtn) {
autoFixBtn.disabled = !conflictData.can_proceed;
}
}
createConflictElement(conflict) {
const div = document.createElement('div');
div.className = 'p-3 border border-slate-200 dark:border-slate-600 rounded-lg';
const severityColors = {
'kritisch': 'red',
'hoch': 'orange',
'mittel': 'amber',
'niedrig': 'blue',
'information': 'slate'
};
const color = severityColors[conflict.severity] || 'slate';
div.innerHTML = `
<div class="flex items-start space-x-3">
<span class="flex-shrink-0 w-2 h-2 mt-2 bg-${color}-500 rounded-full"></span>
<div class="flex-1">
<h4 class="font-medium text-slate-900 dark:text-white text-sm">${conflict.description}</h4>
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">${conflict.estimated_impact}</p>
${conflict.conflicting_jobs?.length > 0 ? `
<div class="mt-2 text-xs text-slate-500 dark:text-slate-500">
Betroffene Jobs: ${conflict.conflicting_jobs.map(job => job.name).join(', ')}
</div>
` : ''}
</div>
</div>
`;
return div;
}
createRecommendationElement(recommendation) {
const div = document.createElement('div');
div.className = 'p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg';
let content = `
<div class="flex items-start space-x-2">
<span class="text-blue-500 text-sm">💡</span>
<div class="flex-1">
<p class="text-sm font-medium text-blue-800 dark:text-blue-200">${recommendation.message}</p>
`;
if (recommendation.suggestions) {
content += '<div class="mt-2 space-y-1">';
recommendation.suggestions.forEach(suggestion => {
content += `
<div class="text-xs text-blue-700 dark:text-blue-300 flex items-center justify-between">
<span>${suggestion.description || suggestion.printer_name}</span>
${suggestion.confidence ? `<span class="text-blue-500">${Math.round(suggestion.confidence * 100)}%</span>` : ''}
</div>
`;
});
content += '</div>';
}
content += `
</div>
</div>
`;
div.innerHTML = content;
return div;
}
displayAvailability(data) {
const panel = document.getElementById('printerAvailabilityPanel');
if (!panel) return;
const summary = data.summary;
// Zusammenfassung aktualisieren
this.updateElement('totalPrinters', summary.total_printers);
this.updateElement('availablePrinters', summary.available_printers);
this.updateElement('optimalPrinters', summary.optimal_printers);
this.updateElement('availabilityRate', `${summary.availability_rate}%`);
this.updateElement('availabilityTimestamp', `Aktualisiert: ${new Date().toLocaleTimeString()}`);
// Drucker-Details aktualisieren
const detailsList = document.getElementById('printerDetailsList');
if (detailsList) {
detailsList.innerHTML = '';
data.printers.forEach(printer => {
const printerEl = document.createElement('div');
printerEl.className = 'flex items-center justify-between p-2 bg-slate-50 dark:bg-slate-700 rounded';
printerEl.innerHTML = `
<div class="flex items-center space-x-3">
<span class="text-lg">${printer.availability_icon}</span>
<div>
<div class="font-medium text-sm text-slate-900 dark:text-white">${printer.printer_name}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">${printer.location || 'Kein Standort'}</div>
</div>
</div>
<div class="text-right">
<div class="text-xs text-slate-600 dark:text-slate-400">${printer.status_description}</div>
<div class="text-xs text-slate-500 dark:text-slate-500">${printer.recent_jobs_24h} Jobs (24h)</div>
</div>
`;
detailsList.appendChild(printerEl);
});
}
panel.classList.remove('hidden');
}
displayRecommendation(recommendation) {
const widget = document.getElementById('smartRecommendationWidget');
if (!widget) return;
const text = document.getElementById('recommendationText');
const details = document.getElementById('recommendationDetails');
if (text) {
text.textContent = `Drucker: ${recommendation.printer_name}`;
}
if (details) {
details.innerHTML = `
<div>📍 Standort: ${recommendation.location}</div>
<div>📊 Verfügbarkeit: ${recommendation.availability}</div>
<div>⚡ Auslastung: ${recommendation.utilization}</div>
<div>🎯 Eignung: ${recommendation.suitability}</div>
<div>💡 ${recommendation.reason}</div>
`;
}
widget.classList.remove('hidden');
}
// =================== FORM VALIDATION ===================
scheduleValidation() {
if (this.checkTimeout) {
clearTimeout(this.checkTimeout);
}
this.checkTimeout = setTimeout(() => {
this.validateFormRealtime();
}, this.debounceDelay);
}
async validateFormRealtime() {
if (!this.autoCheckEnabled) return;
const formData = this.getFormData();
if (!formData.start_time || !formData.end_time) return;
// Formular-Hash für Debouncing
const formHash = JSON.stringify(formData);
if (this.lastFormHash === formHash) return;
this.lastFormHash = formHash;
try {
// Konflikte prüfen
const conflicts = await this.checkConflicts(formData);
if (conflicts) {
this.showInlineConflictWarning(conflicts);
}
// Smart-Empfehlung anzeigen (wenn kein Drucker ausgewählt)
if (!formData.printer_id) {
await this.getSmartRecommendation(formData);
}
} catch (error) {
console.error('❌ Fehler bei Echzeit-Validierung:', error);
}
}
showInlineConflictWarning(conflictData) {
// Inline-Warnung anzeigen (falls im Template vorhanden)
const warning = document.getElementById('conflictWarning');
if (!warning) return;
if (conflictData.has_conflicts) {
// Warnung anzeigen
warning.classList.remove('hidden');
// Nachrichten aktualisieren
const messages = document.getElementById('conflictMessages');
if (messages) {
messages.innerHTML = '';
conflictData.conflicts.forEach(conflict => {
const msgEl = document.createElement('div');
msgEl.textContent = `${conflict.severity}: ${conflict.description}`;
messages.appendChild(msgEl);
});
}
} else {
warning.classList.add('hidden');
}
}
// =================== USER ACTIONS ===================
async applyAutoFix() {
if (!this.lastConflictCheck) return;
const formData = this.getFormData();
const result = await this.resolveConflictsAndCreate(formData);
if (result && result.success) {
this.hideConflictModal();
this.showSuccess('Konflikte automatisch gelöst und Auftrag erstellt! ✅');
// Kalender aktualisieren
if (window.calendar) {
window.calendar.refetchEvents();
}
// Modal schließen
if (window.closeEventModal) {
window.closeEventModal();
}
}
}
ignoreConflicts() {
this.hideConflictModal();
this.showWarning('Konflikte werden ignoriert. Bitte manuell prüfen.');
}
acceptRecommendation() {
if (this.currentRecommendation) {
const printerSelect = document.getElementById('eventPrinter');
if (printerSelect) {
printerSelect.value = this.currentRecommendation.printer_id;
this.dismissRecommendation();
this.showSuccess('Empfehlung angenommen! 🎯');
}
}
}
dismissRecommendation() {
const widget = document.getElementById('smartRecommendationWidget');
if (widget) {
widget.classList.add('hidden');
}
this.currentRecommendation = null;
}
async refreshAvailability() {
const now = new Date();
const endTime = new Date(now.getTime() + 24 * 60 * 60 * 1000); // +24h
await this.loadPrinterAvailability(
now.toISOString(),
endTime.toISOString()
);
}
// =================== HELPER METHODS ===================
getFormData() {
return {
title: this.getFieldValue('eventTitle'),
description: this.getFieldValue('eventDescription'),
printer_id: this.getFieldValue('eventPrinter') || null,
start_time: this.getFieldValue('eventStart'),
end_time: this.getFieldValue('eventEnd'),
priority: this.getFieldValue('eventPriority') || 'normal'
};
}
getFieldValue(fieldId) {
const field = document.getElementById(fieldId);
return field ? field.value : '';
}
updateElement(elementId, content) {
const element = document.getElementById(elementId);
if (element) {
element.textContent = content;
}
}
// =================== NOTIFICATIONS ===================
showSuccess(message) {
this.showNotification(message, 'success');
}
showError(message) {
this.showNotification(message, 'error');
}
showWarning(message) {
this.showNotification(message, 'warning');
}
showNotification(message, type = 'info') {
const colors = {
success: 'from-green-500 to-green-600',
error: 'from-red-500 to-red-600',
warning: 'from-amber-500 to-amber-600',
info: 'from-blue-500 to-blue-600'
};
const icons = {
success: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>',
error: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>',
warning: '<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 0L4.082 15.5c-.77.833.192 2.5 1.732 2.5z"/>',
info: '<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"/>'
};
const toast = document.createElement('div');
toast.className = `fixed top-4 right-4 bg-gradient-to-r ${colors[type]} text-white px-6 py-4 rounded-lg shadow-xl z-50 transform transition-all duration-300 translate-x-full`;
toast.innerHTML = `
<div class="flex items-center gap-3">
<div class="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${icons[type]}
</svg>
</div>
<span class="font-medium">${message}</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.classList.remove('translate-x-full'), 100);
setTimeout(() => {
toast.classList.add('translate-x-full');
setTimeout(() => toast.remove(), 300);
}, 4000);
}
}
// Globale Instanz für einfache Nutzung
window.conflictManager = new ConflictManager();
// Integration mit bestehenden Kalender-Funktionen
document.addEventListener('DOMContentLoaded', function() {
console.log('✅ ConflictManager erfolgreich geladen');
// Integration mit bestehendem Formular
const existingForm = document.getElementById('eventForm');
if (existingForm) {
// Existing form handler erweitern
existingForm.addEventListener('submit', async function(e) {
const formData = window.conflictManager.getFormData();
// Konflikte vor Submit prüfen
const conflicts = await window.conflictManager.checkConflicts(formData);
if (conflicts && conflicts.has_conflicts && !conflicts.can_proceed) {
e.preventDefault();
window.conflictManager.showConflictModal(conflicts);
return false;
}
});
}
});

Binary file not shown.

1
static/js/conflict-manager.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

1
static/js/core-bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1,468 @@
/**
* MYP Platform Core Utilities - Optimized Version
* Consolidates common functionality from multiple files
* Reduces redundancy and improves performance
*/
(function(window) {
'use strict';
// Single instance pattern for core utilities
const MYPCore = {
// Version for cache busting
version: '1.0.0',
// CSRF Token Management (replaces 22+ different implementations)
csrf: {
_token: null,
getToken() {
if (!this._token) {
// Try multiple sources in order of preference
this._token =
document.querySelector('meta[name="csrf-token"]')?.content ||
document.querySelector('input[name="csrf_token"]')?.value ||
this._getFromCookie('csrf_token') ||
'';
}
return this._token;
},
_getFromCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
},
clearCache() {
this._token = null;
}
},
// DOM Utilities with caching
dom: {
_cache: new Map(),
// Cached querySelector
get(selector, parent = document) {
const key = `${parent === document ? 'doc' : 'el'}_${selector}`;
if (!this._cache.has(key)) {
this._cache.set(key, parent.querySelector(selector));
}
return this._cache.get(key);
},
// Cached querySelectorAll
getAll(selector, parent = document) {
return parent.querySelectorAll(selector);
},
// Efficient HTML escaping
escapeHtml(text) {
const div = this._escapeDiv || (this._escapeDiv = document.createElement('div'));
div.textContent = text;
return div.innerHTML;
},
// Clear cache when DOM changes
clearCache() {
this._cache.clear();
},
// Efficient element creation
create(tag, attrs = {}, children = []) {
const el = document.createElement(tag);
Object.entries(attrs).forEach(([key, value]) => {
if (key === 'class') {
el.className = value;
} else if (key === 'style' && typeof value === 'object') {
Object.assign(el.style, value);
} else if (key.startsWith('data-')) {
el.dataset[key.slice(5)] = value;
} else {
el.setAttribute(key, value);
}
});
children.forEach(child => {
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child));
} else {
el.appendChild(child);
}
});
return el;
}
},
// Unified Notification System (replaces 3 separate systems)
notify: {
_container: null,
_queue: [],
_activeToasts: new Map(),
_toastId: 0,
show(message, type = 'info', duration = 5000, options = {}) {
// Check for DND mode
if (window.dndManager?.isEnabled && !options.force) {
console.log(`[DND] Suppressed ${type} notification:`, message);
return null;
}
const id = `toast-${++this._toastId}`;
const toast = this._createToast(id, message, type, duration, options);
this._ensureContainer();
this._container.appendChild(toast);
// Animate in
requestAnimationFrame(() => {
toast.classList.add('show');
});
// Auto remove
if (duration > 0 && !options.persistent) {
const timeout = setTimeout(() => this.close(id), duration);
this._activeToasts.set(id, { element: toast, timeout });
}
return id;
},
_createToast(id, message, type, duration, options) {
const toast = MYPCore.dom.create('div', {
id,
class: `myp-toast myp-toast-${type}`,
role: 'alert',
'aria-live': 'polite'
});
const icon = this._getIcon(type);
const content = MYPCore.dom.create('div', { class: 'toast-content' }, [
MYPCore.dom.create('span', { class: 'toast-icon' }, [icon]),
MYPCore.dom.create('span', { class: 'toast-message' }, [message])
]);
const closeBtn = MYPCore.dom.create('button', {
class: 'toast-close',
'aria-label': 'Close notification',
onclick: () => this.close(id)
}, ['×']);
toast.appendChild(content);
toast.appendChild(closeBtn);
if (duration > 0 && !options.persistent) {
const progress = MYPCore.dom.create('div', { class: 'toast-progress' });
const bar = MYPCore.dom.create('div', {
class: 'toast-progress-bar',
style: { animationDuration: `${duration}ms` }
});
progress.appendChild(bar);
toast.appendChild(progress);
}
return toast;
},
_getIcon(type) {
const icons = {
success: '✓',
error: '✗',
warning: '⚠',
info: ''
};
return icons[type] || icons.info;
},
_ensureContainer() {
if (!this._container) {
this._container = MYPCore.dom.get('#myp-toast-container') ||
MYPCore.dom.create('div', { id: 'myp-toast-container', class: 'myp-toast-container' });
if (!this._container.parentNode) {
document.body.appendChild(this._container);
}
}
},
close(id) {
const toast = this._activeToasts.get(id);
if (toast) {
clearTimeout(toast.timeout);
toast.element.classList.remove('show');
setTimeout(() => {
toast.element.remove();
this._activeToasts.delete(id);
}, 300);
}
},
closeAll() {
this._activeToasts.forEach((_, id) => this.close(id));
}
},
// API Request Utilities
api: {
_cache: new Map(),
_pendingRequests: new Map(),
async request(url, options = {}) {
const config = {
...options,
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': MYPCore.csrf.getToken(),
...options.headers
}
};
// Check cache for GET requests
if (options.method === 'GET' || !options.method) {
const cacheKey = this._getCacheKey(url, options);
const cached = this._cache.get(cacheKey);
if (cached && !options.noCache) {
const age = Date.now() - cached.timestamp;
const maxAge = options.cacheTime || 300000; // 5 minutes default
if (age < maxAge) {
return cached.data;
}
}
// Prevent duplicate requests
if (this._pendingRequests.has(cacheKey)) {
return this._pendingRequests.get(cacheKey);
}
}
// Make request
const requestPromise = fetch(url, config)
.then(async response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Cache successful GET requests
if (config.method === 'GET' || !config.method) {
const cacheKey = this._getCacheKey(url, options);
this._cache.set(cacheKey, {
data,
timestamp: Date.now()
});
this._pendingRequests.delete(cacheKey);
}
return data;
})
.catch(error => {
if (config.method === 'GET' || !config.method) {
this._pendingRequests.delete(this._getCacheKey(url, options));
}
throw error;
});
if (config.method === 'GET' || !config.method) {
this._pendingRequests.set(this._getCacheKey(url, options), requestPromise);
}
return requestPromise;
},
_getCacheKey(url, options) {
return `${url}_${JSON.stringify(options.params || {})}`;
},
clearCache(pattern) {
if (pattern) {
for (const [key] of this._cache) {
if (key.includes(pattern)) {
this._cache.delete(key);
}
}
} else {
this._cache.clear();
}
}
},
// Time/Date Utilities
time: {
formatAgo(timestamp) {
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
const now = new Date();
const seconds = Math.floor((now - date) / 1000);
const intervals = [
{ label: 'Jahr', seconds: 31536000, plural: 'Jahre' },
{ label: 'Monat', seconds: 2592000, plural: 'Monate' },
{ label: 'Woche', seconds: 604800, plural: 'Wochen' },
{ label: 'Tag', seconds: 86400, plural: 'Tage' },
{ label: 'Stunde', seconds: 3600, plural: 'Stunden' },
{ label: 'Minute', seconds: 60, plural: 'Minuten' }
];
for (const interval of intervals) {
const count = Math.floor(seconds / interval.seconds);
if (count >= 1) {
const label = count === 1 ? interval.label : interval.plural;
return `vor ${count} ${label}`;
}
}
return 'Gerade eben';
},
formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
const parts = [];
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
return parts.join(' ');
}
},
// Performance Utilities
perf: {
debounce(func, wait) {
let timeout;
return function debounced(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
},
throttle(func, limit) {
let inThrottle;
return function throttled(...args) {
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
},
memoize(func, resolver) {
const cache = new Map();
return function memoized(...args) {
const key = resolver ? resolver(...args) : args[0];
if (cache.has(key)) {
return cache.get(key);
}
const result = func.apply(this, args);
cache.set(key, result);
return result;
};
}
},
// Event Management
events: {
_listeners: new Map(),
on(element, event, handler, options = {}) {
const key = `${element}_${event}`;
if (!this._listeners.has(key)) {
this._listeners.set(key, new Set());
}
this._listeners.get(key).add(handler);
element.addEventListener(event, handler, options);
// Return cleanup function
return () => this.off(element, event, handler);
},
off(element, event, handler) {
const key = `${element}_${event}`;
const handlers = this._listeners.get(key);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this._listeners.delete(key);
}
}
element.removeEventListener(event, handler);
},
once(element, event, handler, options = {}) {
const wrappedHandler = (e) => {
handler(e);
this.off(element, event, wrappedHandler);
};
return this.on(element, event, wrappedHandler, options);
}
},
// Storage Utilities
storage: {
get(key, defaultValue = null) {
try {
const item = localStorage.getItem(`myp_${key}`);
return item ? JSON.parse(item) : defaultValue;
} catch (e) {
console.error('Storage get error:', e);
return defaultValue;
}
},
set(key, value) {
try {
localStorage.setItem(`myp_${key}`, JSON.stringify(value));
return true;
} catch (e) {
console.error('Storage set error:', e);
return false;
}
},
remove(key) {
localStorage.removeItem(`myp_${key}`);
},
clear(prefix = 'myp_') {
const keys = Object.keys(localStorage);
keys.forEach(key => {
if (key.startsWith(prefix)) {
localStorage.removeItem(key);
}
});
}
}
};
// Global shortcuts for backwards compatibility
window.MYPCore = MYPCore;
// Legacy function mappings
window.showToast = (msg, type, duration) => MYPCore.notify.show(msg, type, duration);
window.showNotification = window.showToast;
window.showFlashMessage = window.showToast;
window.showSuccessMessage = (msg) => MYPCore.notify.show(msg, 'success');
window.showErrorMessage = (msg) => MYPCore.notify.show(msg, 'error');
window.showWarningMessage = (msg) => MYPCore.notify.show(msg, 'warning');
window.showInfoMessage = (msg) => MYPCore.notify.show(msg, 'info');
// Auto-initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
console.log('✅ MYP Core Utilities initialized');
});
} else {
console.log('✅ MYP Core Utilities initialized');
}
})(window);

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

493
static/js/core-utilities.js Normal file
View File

@@ -0,0 +1,493 @@
/**
* Core Utilities Module for MYP Platform
* Consolidated utilities to eliminate redundancy and improve performance
*/
(function(window) {
'use strict';
// Module namespace
const MYP = window.MYP || {};
// ===== CONFIGURATION =====
const config = {
apiTimeout: 30000,
cacheExpiry: 5 * 60 * 1000, // 5 minutes
notificationDuration: 5000,
debounceDelay: 300,
throttleDelay: 100
};
// ===== CACHE MANAGEMENT =====
const cache = new Map();
const requestCache = new Map();
// ===== CSRF TOKEN HANDLING =====
const csrf = {
token: null,
get() {
if (!this.token) {
const meta = document.querySelector('meta[name="csrf-token"]');
this.token = meta ? meta.getAttribute('content') : '';
}
return this.token;
},
headers() {
return {
'X-CSRFToken': this.get(),
'Content-Type': 'application/json'
};
}
};
// ===== DOM UTILITIES =====
const dom = {
// Cached selectors
selectors: new Map(),
// Get element with caching
get(selector, parent = document) {
const key = `${parent === document ? 'doc' : 'el'}_${selector}`;
if (!this.selectors.has(key)) {
this.selectors.set(key, parent.querySelector(selector));
}
return this.selectors.get(key);
},
// Get all elements
getAll(selector, parent = document) {
return parent.querySelectorAll(selector);
},
// Clear cache
clearCache() {
this.selectors.clear();
},
// Safe element creation
create(tag, attrs = {}, text = '') {
const el = document.createElement(tag);
Object.entries(attrs).forEach(([key, val]) => {
if (key === 'class') {
el.className = val;
} else if (key === 'dataset') {
Object.entries(val).forEach(([k, v]) => {
el.dataset[k] = v;
});
} else {
el.setAttribute(key, val);
}
});
if (text) el.textContent = text;
return el;
}
};
// ===== API REQUEST HANDLING =====
const api = {
// Request deduplication
pending: new Map(),
async request(url, options = {}) {
const key = `${options.method || 'GET'}_${url}`;
// Check for pending request
if (this.pending.has(key)) {
return this.pending.get(key);
}
// Check cache for GET requests
if (!options.method || options.method === 'GET') {
const cached = requestCache.get(key);
if (cached && Date.now() - cached.timestamp < config.cacheExpiry) {
return Promise.resolve(cached.data);
}
}
// Prepare request
const requestOptions = {
...options,
headers: {
...csrf.headers(),
...options.headers
}
};
// Create request promise
const promise = fetch(url, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
// Cache successful GET requests
if (!options.method || options.method === 'GET') {
requestCache.set(key, {
data,
timestamp: Date.now()
});
}
return data;
})
.finally(() => {
this.pending.delete(key);
});
this.pending.set(key, promise);
return promise;
},
get(url, options = {}) {
return this.request(url, { ...options, method: 'GET' });
},
post(url, data, options = {}) {
return this.request(url, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
},
put(url, data, options = {}) {
return this.request(url, {
...options,
method: 'PUT',
body: JSON.stringify(data)
});
},
delete(url, options = {}) {
return this.request(url, { ...options, method: 'DELETE' });
}
};
// ===== UNIFIED NOTIFICATION SYSTEM =====
const notifications = {
container: null,
queue: [],
init() {
if (this.container) return;
this.container = dom.create('div', {
id: 'myp-notifications',
class: 'fixed top-4 right-4 z-50 space-y-2'
});
document.body.appendChild(this.container);
},
show(message, type = 'info', duration = config.notificationDuration) {
this.init();
const notification = dom.create('div', {
class: `notification notification-${type} glass-navbar p-4 rounded-lg shadow-lg transform translate-x-full transition-transform duration-300`,
role: 'alert'
});
const content = dom.create('div', {
class: 'flex items-center space-x-3'
});
// Icon
const icon = dom.create('i', {
class: `fas ${this.getIcon(type)} text-lg`
});
// Message
const text = dom.create('span', {
class: 'flex-1'
}, message);
// Close button
const close = dom.create('button', {
class: 'ml-4 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200',
'aria-label': 'Close'
});
close.innerHTML = '&times;';
close.onclick = () => this.remove(notification);
content.appendChild(icon);
content.appendChild(text);
content.appendChild(close);
notification.appendChild(content);
this.container.appendChild(notification);
// Animate in
requestAnimationFrame(() => {
notification.classList.remove('translate-x-full');
});
// Auto remove
if (duration > 0) {
setTimeout(() => this.remove(notification), duration);
}
return notification;
},
remove(notification) {
notification.classList.add('translate-x-full');
setTimeout(() => {
notification.remove();
}, 300);
},
getIcon(type) {
const icons = {
success: 'fa-check-circle text-green-500',
error: 'fa-exclamation-circle text-red-500',
warning: 'fa-exclamation-triangle text-yellow-500',
info: 'fa-info-circle text-blue-500'
};
return icons[type] || icons.info;
},
success(message, duration) {
return this.show(message, 'success', duration);
},
error(message, duration) {
return this.show(message, 'error', duration);
},
warning(message, duration) {
return this.show(message, 'warning', duration);
},
info(message, duration) {
return this.show(message, 'info', duration);
}
};
// ===== PERFORMANCE UTILITIES =====
const performance = {
debounce(func, delay = config.debounceDelay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
},
throttle(func, delay = config.throttleDelay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return func.apply(this, args);
}
};
},
memoize(func) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = func.apply(this, args);
cache.set(key, result);
return result;
};
},
lazy(selector, callback) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
callback(entry.target);
observer.unobserve(entry.target);
}
});
});
dom.getAll(selector).forEach(el => observer.observe(el));
return observer;
}
};
// ===== STORAGE UTILITIES =====
const storage = {
set(key, value, expiry = null) {
const data = {
value,
timestamp: Date.now(),
expiry
};
try {
localStorage.setItem(`myp_${key}`, JSON.stringify(data));
return true;
} catch (e) {
console.error('Storage error:', e);
return false;
}
},
get(key) {
try {
const item = localStorage.getItem(`myp_${key}`);
if (!item) return null;
const data = JSON.parse(item);
if (data.expiry && Date.now() - data.timestamp > data.expiry) {
this.remove(key);
return null;
}
return data.value;
} catch (e) {
console.error('Storage error:', e);
return null;
}
},
remove(key) {
localStorage.removeItem(`myp_${key}`);
},
clear() {
Object.keys(localStorage)
.filter(key => key.startsWith('myp_'))
.forEach(key => localStorage.removeItem(key));
}
};
// ===== EVENT UTILITIES =====
const events = {
listeners: new Map(),
on(element, event, handler, options = {}) {
const key = `${element}_${event}`;
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key).add(handler);
element.addEventListener(event, handler, options);
},
off(element, event, handler) {
const key = `${element}_${event}`;
if (this.listeners.has(key)) {
this.listeners.get(key).delete(handler);
if (this.listeners.get(key).size === 0) {
this.listeners.delete(key);
}
}
element.removeEventListener(event, handler);
},
once(element, event, handler, options = {}) {
const onceHandler = (e) => {
handler(e);
this.off(element, event, onceHandler);
};
this.on(element, event, onceHandler, options);
},
emit(name, detail = {}) {
const event = new CustomEvent(name, {
detail,
bubbles: true,
cancelable: true
});
document.dispatchEvent(event);
},
cleanup() {
this.listeners.forEach((handlers, key) => {
const [element, event] = key.split('_');
handlers.forEach(handler => {
element.removeEventListener(event, handler);
});
});
this.listeners.clear();
}
};
// ===== FORM UTILITIES =====
const forms = {
serialize(form) {
const data = new FormData(form);
const obj = {};
for (const [key, value] of data.entries()) {
if (obj[key]) {
if (!Array.isArray(obj[key])) {
obj[key] = [obj[key]];
}
obj[key].push(value);
} else {
obj[key] = value;
}
}
return obj;
},
validate(form) {
const inputs = form.querySelectorAll('[required]');
let valid = true;
inputs.forEach(input => {
if (!input.value.trim()) {
input.classList.add('border-red-500');
valid = false;
} else {
input.classList.remove('border-red-500');
}
});
return valid;
},
reset(form) {
form.reset();
form.querySelectorAll('.border-red-500').forEach(el => {
el.classList.remove('border-red-500');
});
}
};
// ===== INITIALIZATION =====
const init = () => {
// Initialize notifications
notifications.init();
// Clean up on page unload
window.addEventListener('beforeunload', () => {
events.cleanup();
dom.clearCache();
});
};
// ===== PUBLIC API =====
MYP.utils = {
csrf,
dom,
api,
notifications,
performance,
storage,
events,
forms,
init,
config
};
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Export to global scope
window.MYP = MYP;
})(window);

Binary file not shown.

1
static/js/core-utilities.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

1137
static/js/countdown-timer.js Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

86
static/js/countdown-timer.min.js vendored Normal file
View File

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

Binary file not shown.

View File

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

Binary file not shown.

20
static/js/csp-violation-handler.min.js vendored Normal file
View File

@@ -0,0 +1,20 @@
class CSPViolationHandler{constructor(){this.violations=[];this.init();}
init(){document.addEventListener('securitypolicyviolation',this.handleViolation.bind(this));if('ReportingObserver'in window){const observer=new ReportingObserver((reports,observer)=>{for(const report of reports){if(report.type==='csp-violation'){this.handleViolation(report.body);}}});observer.observe();}
console.log('🛡️ CSP Violation Handler initialisiert');}
handleViolation(violationEvent){const violation={timestamp:new Date().toISOString(),blockedURI:violationEvent.blockedURI||'unknown',violatedDirective:violationEvent.violatedDirective||'unknown',originalPolicy:violationEvent.originalPolicy||'unknown',documentURI:violationEvent.documentURI||window.location.href,sourceFile:violationEvent.sourceFile||'unknown',lineNumber:violationEvent.lineNumber||0,columnNumber:violationEvent.columnNumber||0,sample:violationEvent.sample||'',disposition:violationEvent.disposition||'enforce'};this.violations.push(violation);this.logViolation(violation);this.suggestFix(violation);this.reportViolation(violation);}
logViolation(violation){console.group('🚨 CSP Violation detected');console.error('Blocked URI:',violation.blockedURI);console.error('Violated Directive:',violation.violatedDirective);console.error('Source:',`${violation.sourceFile}:${violation.lineNumber}:${violation.columnNumber}`);console.error('Sample:',violation.sample);console.error('Full Policy:',violation.originalPolicy);console.groupEnd();}
suggestFix(violation){const directive=violation.violatedDirective;const blockedURI=violation.blockedURI;console.group('💡 Lösungsvorschlag');if(directive.includes('script-src')){if(blockedURI==='inline'){console.log('Problem: Inline-Script blockiert');console.log('Lösung 1: Script in externe .js-Datei auslagern');console.log('Lösung 2: data-action Attribute für Event-Handler verwenden');console.log('Lösung 3: Nonce verwenden (nicht empfohlen für Entwicklung)');console.log('Beispiel: <button data-action="refresh-dashboard">Aktualisieren</button>');}else if(blockedURI.includes('eval')){console.log('Problem: eval() oder ähnliche Funktionen blockiert');console.log('Lösung: Verwende sichere Alternativen zu eval()');}else{console.log(`Problem:Externes Script von ${blockedURI}blockiert`);console.log('Lösung: URL zur CSP script-src Richtlinie hinzufügen');}}else if(directive.includes('style-src')){console.log('Problem: Style blockiert');console.log('Lösung: CSS in externe .css-Datei auslagern oder CSP erweitern');}else if(directive.includes('connect-src')){console.log(`Problem:Verbindung zu ${blockedURI}blockiert`);console.log('Lösung: URL zur CSP connect-src Richtlinie hinzufügen');console.log('Tipp: Für API-Calls relative URLs verwenden');}else if(directive.includes('img-src')){console.log(`Problem:Bild von ${blockedURI}blockiert`);console.log('Lösung: URL zur CSP img-src Richtlinie hinzufügen');}
console.groupEnd();}
async reportViolation(violation){try{if(window.location.hostname!=='localhost'&&window.location.hostname!=='127.0.0.1'){await fetch('/api/security/csp-violation',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(violation)});}}catch(error){console.warn('Fehler beim Senden der CSP-Verletzung:',error);}}
getViolations(){return this.violations;}
getStats(){const stats={total:this.violations.length,byDirective:{},byURI:{},recent:this.violations.slice(-10)};this.violations.forEach(violation=>{const directive=violation.violatedDirective;stats.byDirective[directive]=(stats.byDirective[directive]||0)+1;const uri=violation.blockedURI;stats.byURI[uri]=(stats.byURI[uri]||0)+1;});return stats;}
enableDebugMode(){this.createDebugPanel();console.log('🔧 CSP Debug Mode aktiviert');console.log('Verfügbare Befehle:');console.log('- cspHandler.getViolations() - Alle Verletzungen anzeigen');console.log('- cspHandler.getStats() - Statistiken anzeigen');console.log('- cspHandler.clearViolations() - Verletzungen löschen');console.log('- cspHandler.exportViolations() - Als JSON exportieren');}
createDebugPanel(){const panel=document.createElement('div');panel.id='csp-debug-panel';panel.style.cssText=`position:fixed;top:10px;right:10px;width:300px;max-height:400px;background:rgba(0,0,0,0.9);color:white;font-family:monospace;font-size:12px;padding:10px;border-radius:5px;z-index:10000;overflow-y:auto;display:none;`;panel.innerHTML=`<div style="display: flex; justify-content: space-between; margin-bottom: 10px;"><strong>CSP Violations</strong><button onclick="this.parentElement.parentElement.style.display='none'"
style="background: none; border: none; color: white; cursor: pointer;">&times;</button></div><div id="csp-violations-list"></div><div style="margin-top: 10px;"><button onclick="cspHandler.clearViolations()"
style="background: #333; color: white; border: none; padding: 5px; margin-right: 5px; cursor: pointer;">Clear</button><button onclick="cspHandler.exportViolations()"
style="background: #333; color: white; border: none; padding: 5px; cursor: pointer;">Export</button></div>`;document.body.appendChild(panel);document.addEventListener('keydown',(event)=>{if(event.ctrlKey&&event.shiftKey&&event.key==='C'){panel.style.display=panel.style.display==='none'?'block':'none';this.updateDebugPanel();}});}
updateDebugPanel(){const list=document.getElementById('csp-violations-list');if(!list)return;const recent=this.violations.slice(-5);list.innerHTML=recent.map(v=>`<div style="margin-bottom: 5px; padding: 5px; background: rgba(255, 255, 255, 0.1);"><div><strong>${v.violatedDirective}</strong></div><div style="color: #ff6b6b;">${v.blockedURI}</div><div style="color: #ffd93d; font-size: 10px;">${v.timestamp}</div></div>`).join('');}
clearViolations(){this.violations=[];this.updateDebugPanel();console.log('🗑️ CSP Violations gelöscht');}
exportViolations(){const data={timestamp:new Date().toISOString(),stats:this.getStats(),violations:this.violations};const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=`csp-violations-${new Date().toISOString().split('T')[0]}.json`;a.click();URL.revokeObjectURL(url);console.log('📄 CSP Violations exportiert');}}
const cspHandler=new CSPViolationHandler();if(window.location.hostname==='localhost'||window.location.hostname==='127.0.0.1'){cspHandler.enableDebugMode();console.log('🔍 CSP Debug Mode aktiv - Drücken Sie Ctrl+Shift+C für Debug-Panel');}
window.cspHandler=cspHandler;console.log('🛡️ CSP Violation Handler geladen');

Binary file not shown.

View File

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

Binary file not shown.

10
static/js/css-cache-manager.min.js vendored Normal file
View File

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

Binary file not shown.

192
static/js/dark-mode-fix.js Normal file
View File

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

Binary file not shown.

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

@@ -0,0 +1 @@
document.addEventListener("DOMContentLoaded",(function(){const e=document.querySelectorAll(".darkModeToggle"),t=document.documentElement,o="myp-dark-mode";function a(){const e=localStorage.getItem(o);return null!==e?"true"===e:window.matchMedia("(prefers-color-scheme: dark)").matches}function r(t){e.forEach((e=>{if(!e)return;const o=e.querySelector(".sun-icon"),a=e.querySelector(".moon-icon");o&&a?(t?(o.style.opacity="0",o.style.transform="scale(0.75) rotate(90deg)",a.style.opacity="1",a.style.transform="scale(1) rotate(0deg)",o.classList.add("opacity-0","dark:opacity-0","scale-75","dark:scale-75","rotate-90","dark:rotate-90"),o.classList.remove("opacity-100","scale-100","rotate-0"),a.classList.add("opacity-100","dark:opacity-100","scale-100","dark:scale-100","rotate-0","dark:rotate-0"),a.classList.remove("opacity-0","scale-75","rotate-90")):(o.style.opacity="1",o.style.transform="scale(1) rotate(0deg)",a.style.opacity="0",a.style.transform="scale(0.75) rotate(-90deg)",o.classList.add("opacity-100","scale-100","rotate-0"),o.classList.remove("opacity-0","dark:opacity-0","scale-75","dark:scale-75","rotate-90","dark:rotate-90"),a.classList.add("opacity-0","dark:opacity-100","scale-75","dark:scale-100","rotate-90","dark:rotate-0"),a.classList.remove("opacity-100","scale-100","rotate-0")),o.classList.toggle("icon-enter",!t),a.classList.toggle("icon-enter",t)):console.warn("Premium Dark Mode Icons nicht gefunden")}))}function s(a){console.log("🎨 Setze Premium Dark Mode auf: "+(a?"Aktiviert":"Deaktiviert")),a?(t.classList.add("dark"),t.setAttribute("data-theme","dark"),t.style.colorScheme="dark",e.forEach((e=>{e.setAttribute("aria-pressed","true"),e.setAttribute("title","Light Mode aktivieren")})),r(!0)):(t.classList.remove("dark"),t.setAttribute("data-theme","light"),t.style.colorScheme="light",e.forEach((e=>{e.setAttribute("aria-pressed","false"),e.setAttribute("title","Dark Mode aktivieren")})),r(!1)),localStorage.setItem(o,a.toString());const s=document.getElementById("metaThemeColor");s&&s.setAttribute("content",a?"#000000":"#ffffff"),window.dispatchEvent(new CustomEvent("darkModeChanged",{detail:{isDark:a}})),console.log(`${a?"🌙":"☀️"} Premium Design umgeschaltet auf: ${a?"Dark Mode":"Light Mode"}`)}function i(e){if(s(!a()),e&&e.currentTarget){const t=e.currentTarget.querySelector(".dark-mode-toggle-slider");t&&(t.style.transform="scale(0.95)",setTimeout((()=>{t.style.transform=""}),150))}}if(e.length>0){console.log(`🎨 ${e.length} Premium Dark Mode Toggle Button(s) gefunden - initialisiere...`),e.forEach(((e,t)=>{e.addEventListener("click",(function(e){e.preventDefault(),e.stopPropagation(),i(e)}))}));s(a()),console.log("✨ Premium Dark Mode Toggle Buttons erfolgreich initialisiert")}else console.error("❌ Keine Premium Dark Mode Toggle Buttons gefunden!");document.addEventListener("keydown",(function(e){e.ctrlKey&&e.shiftKey&&"D"===e.key&&(i(),e.preventDefault())})),document.addEventListener("keydown",(function(e){e.altKey&&"t"===e.key&&(i(),e.preventDefault())})),window.toggleDarkMode=i,window.isDarkMode=a,window.setDarkMode=s,window.premiumDarkMode={toggle:i,isDark:a,setMode:s,version:"3.0.0-premium"},console.log("🎨 Premium Dark Mode System geladen - Version 3.0.0")}));

Binary file not shown.

306
static/js/dark-mode.js Normal file
View File

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

BIN
static/js/dark-mode.js.gz Normal file

Binary file not shown.

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

@@ -0,0 +1,15 @@
(function(){"use strict";const STORAGE_KEY='myp-dark-mode';let darkModeToggle;const html=document.documentElement;document.addEventListener('DOMContentLoaded',initialize);function shouldUseDarkMode(){const savedMode=localStorage.getItem(STORAGE_KEY);if(savedMode!==null){return savedMode==='true';}
return window.matchMedia('(prefers-color-scheme: dark)').matches;}
function setDarkMode(enable){html.classList.add('disable-transitions');if(enable){html.classList.add('dark');html.setAttribute('data-theme','dark');html.style.colorScheme='dark';}else{html.classList.remove('dark');html.setAttribute('data-theme','light');html.style.colorScheme='light';}
localStorage.setItem(STORAGE_KEY,enable);updateMetaThemeColor(enable);if(darkModeToggle){updateDarkModeToggle(enable);}
window.dispatchEvent(new CustomEvent('darkModeChanged',{detail:{isDark:enable,source:'dark-mode-toggle',timestamp:new Date().toISOString()}}));const eventName=enable?'darkModeEnabled':'darkModeDisabled';window.dispatchEvent(new CustomEvent(eventName,{detail:{timestamp:new Date().toISOString()}}));setTimeout(function(){html.classList.remove('disable-transitions');},100);console.log(`${enable?'🌙':'☀️'}${enable?'Dark Mode aktiviert - Augenschonender Modus aktiv':'Light Mode aktiviert - Heller Modus aktiv'}`);}
function updateMetaThemeColor(isDark){const metaTags=[document.getElementById('metaThemeColor'),document.querySelector('meta[name="theme-color"]'),document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: light)"]'),document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: dark)"]')];const darkColor=getComputedStyle(document.documentElement).getPropertyValue('--color-bg')||'#0f172a';const lightColor=getComputedStyle(document.documentElement).getPropertyValue('--color-bg')||'#ffffff';metaTags.forEach(tag=>{if(tag){if(tag.getAttribute('media')==='(prefers-color-scheme: dark)'){tag.setAttribute('content',darkColor);}else if(tag.getAttribute('media')==='(prefers-color-scheme: light)'){tag.setAttribute('content',lightColor);}else{tag.setAttribute('content',isDark?darkColor:lightColor);}}});}
function updateDarkModeToggle(isDark){darkModeToggle.setAttribute('aria-pressed',isDark.toString());darkModeToggle.title=isDark?"Light Mode aktivieren":"Dark Mode aktivieren";const sunIcon=darkModeToggle.querySelector('.sun-icon');const moonIcon=darkModeToggle.querySelector('.moon-icon');if(sunIcon&&moonIcon){if(isDark){sunIcon.classList.add('hidden');moonIcon.classList.remove('hidden');}else{sunIcon.classList.remove('hidden');moonIcon.classList.add('hidden');}}else{const icon=darkModeToggle.querySelector('svg');if(icon){icon.classList.add('animate-spin-once');setTimeout(()=>{icon.classList.remove('animate-spin-once');},300);const pathElement=icon.querySelector('path');if(pathElement){if(isDark){pathElement.setAttribute("d","M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z");}else{pathElement.setAttribute("d","M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z");}}}}}
function initialize(){darkModeToggle=document.getElementById('darkModeToggle');if(!darkModeToggle){console.log('🔧 Dark Mode Toggle nicht gefunden - erstelle automatisch einen neuen Button');createDarkModeToggle();}
if(darkModeToggle){darkModeToggle.addEventListener('click',function(){const isDark=!shouldUseDarkMode();console.log(`👆 Dark Mode Toggle:Wechsel zu ${isDark?'🌙 dunkel':'☀️ hell'}angefordert`);setDarkMode(isDark);});}
document.addEventListener('keydown',function(e){if(e.ctrlKey&&e.shiftKey&&e.key==='D'){const isDark=!shouldUseDarkMode();console.log(`⌨️ Tastenkombination STRG+SHIFT+D erkannt:Wechsel zu ${isDark?'🌙 dunkel':'☀️ hell'}`);setDarkMode(isDark);e.preventDefault();}});const darkModeMediaQuery=window.matchMedia('(prefers-color-scheme: dark)');try{darkModeMediaQuery.addEventListener('change',function(e){if(localStorage.getItem(STORAGE_KEY)===null){console.log(`🖥️ Systemeinstellung geändert:${e.matches?'🌙 dunkel':'☀️ hell'}`);setDarkMode(e.matches);}});}catch(error){darkModeMediaQuery.addListener(function(e){if(localStorage.getItem(STORAGE_KEY)===null){console.log(`🖥️ Systemeinstellung geändert(Legacy-Browser):${e.matches?'🌙 dunkel':'☀️ hell'}`);setDarkMode(e.matches);}});}
const initialState=shouldUseDarkMode();console.log(`🔍 Ermittelter Ausgangszustand:${initialState?'🌙 Dark Mode':'☀️ Light Mode'}`);setDarkMode(initialState);const animClass=initialState?'dark-mode-transition':'light-mode-transition';document.body.classList.add(animClass);setTimeout(()=>{document.body.classList.remove(animClass);},300);console.log('🚀 Dark Mode Handler erfolgreich initialisiert');}
function createDarkModeToggle(){const header=document.querySelector('header');const nav=document.querySelector('nav');const container=document.querySelector('.dark-mode-container')||header||nav;if(!container){console.error('⚠️ Kein geeigneter Container für Dark Mode Toggle gefunden');return;}
darkModeToggle=document.createElement('button');darkModeToggle.id='darkModeToggle';darkModeToggle.className='dark-mode-toggle-new';darkModeToggle.setAttribute('aria-label','Dark Mode umschalten');darkModeToggle.setAttribute('title','Dark Mode aktivieren');darkModeToggle.setAttribute('data-action','toggle-dark-mode');const sunIcon=document.createElementNS("http://www.w3.org/2000/svg","svg");sunIcon.setAttribute("class","w-5 h-5 sm:w-5 sm:h-5 sun-icon");sunIcon.setAttribute("fill","none");sunIcon.setAttribute("stroke","currentColor");sunIcon.setAttribute("viewBox","0 0 24 24");sunIcon.setAttribute("aria-hidden","true");const sunPath=document.createElementNS("http://www.w3.org/2000/svg","path");sunPath.setAttribute("stroke-linecap","round");sunPath.setAttribute("stroke-linejoin","round");sunPath.setAttribute("stroke-width","2");sunPath.setAttribute("d","M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z");const moonIcon=document.createElementNS("http://www.w3.org/2000/svg","svg");moonIcon.setAttribute("class","w-5 h-5 sm:w-5 sm:h-5 moon-icon hidden");moonIcon.setAttribute("fill","none");moonIcon.setAttribute("stroke","currentColor");moonIcon.setAttribute("viewBox","0 0 24 24");moonIcon.setAttribute("aria-hidden","true");const moonPath=document.createElementNS("http://www.w3.org/2000/svg","path");moonPath.setAttribute("stroke-linecap","round");moonPath.setAttribute("stroke-linejoin","round");moonPath.setAttribute("stroke-width","2");moonPath.setAttribute("d","M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z");sunIcon.appendChild(sunPath);moonIcon.appendChild(moonPath);darkModeToggle.appendChild(sunIcon);darkModeToggle.appendChild(moonIcon);container.appendChild(darkModeToggle);console.log('✅ Dark Mode Toggle Button erfolgreich erstellt und zur Benutzeroberfläche hinzugefügt');}
const isDark=shouldUseDarkMode();console.log(`🏃‍♂️ Sofortige Anwendung:${isDark?'🌙 Dark Mode':'☀️ Light Mode'}(vor DOM-Ladung)`);setDarkMode(isDark);})();if(!document.querySelector('style#dark-mode-animations')){const styleTag=document.createElement('style');styleTag.id='dark-mode-animations';styleTag.textContent=`@keyframes spin-once{from{transform:rotate(0deg);}
to{transform:rotate(360deg);}}.animate-spin-once{animation:spin-once 0.3s ease-in-out;}`;document.head.appendChild(styleTag);console.log('💫 Animations-Styles für Dark Mode Toggle hinzugefügt');}

Binary file not shown.

354
static/js/dashboard.js Normal file
View File

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

BIN
static/js/dashboard.js.gz Normal file

Binary file not shown.

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

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

Binary file not shown.

175
static/js/debug-fix.js Normal file
View File

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

BIN
static/js/debug-fix.js.gz Normal file

Binary file not shown.

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

@@ -0,0 +1,10 @@
(function(){'use strict';console.log('🔧 Debug Fix Script wird geladen...');window.MYP=window.MYP||{};window.MYP.UI=window.MYP.UI||{};window.MVP=window.MVP||{};window.MVP.UI=window.MVP.UI||{};window.MVP.UI.DarkModeManager=function(){console.log('⚠️ MVP.UI.DarkModeManager Konstruktor aufgerufen - verwende MYP.UI.darkMode stattdessen');if(window.MYP&&window.MYP.UI&&window.MYP.UI.darkMode){return window.MYP.UI.darkMode;}
return{init:function(){console.log('DarkModeManager Fallback init');},setDarkMode:function(){console.log('DarkModeManager Fallback setDarkMode');},isDarkMode:function(){return false;}};};document.addEventListener('DOMContentLoaded',function(){console.log('🚀 Debug Fix: DOM Content geladen');setTimeout(()=>{try{if(window.MYP&&window.MYP.UI&&window.MYP.UI.darkMode){window.MVP.UI.DarkModeManager=function(){console.log('⚠️ MVP.UI.DarkModeManager Konstruktor aufgerufen - verwende MYP.UI.darkMode stattdessen');return window.MYP.UI.darkMode;};console.log('✅ MVP.UI.DarkModeManager Alias aktualisiert');}
if(!window.jobManager&&window.JobManager){window.jobManager=new window.JobManager();console.log('✅ JobManager Instanz erstellt');}
if(window.jobManager&&!window.jobManager.setupFormHandlers){window.jobManager.setupFormHandlers=function(){console.log('✅ setupFormHandlers Fallback aufgerufen');};}
window.refreshJobs=function(){if(window.jobManager&&window.jobManager.loadJobs){return window.jobManager.loadJobs();}else{console.warn('⚠️ JobManager nicht verfügbar - Seite wird neu geladen');window.location.reload();}};window.startJob=function(jobId){if(window.jobManager&&window.jobManager.startJob){return window.jobManager.startJob(jobId);}};window.pauseJob=function(jobId){if(window.jobManager&&window.jobManager.pauseJob){return window.jobManager.pauseJob(jobId);}};window.resumeJob=function(jobId){if(window.jobManager&&window.jobManager.resumeJob){return window.jobManager.resumeJob(jobId);}};window.deleteJob=function(jobId){if(window.jobManager&&window.jobManager.deleteJob){return window.jobManager.deleteJob(jobId);}};console.log('✅ Debug Fix Script erfolgreich angewendet');}catch(error){console.error('❌ Debug Fix Fehler:',error);}},100);});window.addEventListener('error',function(e){const errorInfo={message:e.message||'Unbekannter Fehler',filename:e.filename||'Unbekannte Datei',lineno:e.lineno||0,colno:e.colno||0,stack:e.error?e.error.stack:'Stack nicht verfügbar',type:e.error?e.error.constructor.name:'Unbekannter Typ'};console.error('🐛 JavaScript Error abgefangen:',JSON.stringify(errorInfo,null,2));if(e.message.includes('MVP.UI.DarkModeManager is not a constructor')){console.log('🔧 DarkModeManager Fehler erkannt - verwende MYP.UI.darkMode');e.preventDefault();return false;}
if(e.message.includes('setupFormHandlers is not a function')){console.log('🔧 setupFormHandlers Fehler erkannt - verwende Fallback');e.preventDefault();return false;}
if(e.message.includes('refreshStats is not defined')){console.log('🔧 refreshStats Fehler erkannt - lade global-refresh-functions.js');const script=document.createElement('script');script.src='/static/js/global-refresh-functions.js';script.onload=function(){console.log('✅ global-refresh-functions.js nachgeladen');};document.head.appendChild(script);e.preventDefault();return false;}
if(e.message.includes('Cannot read properties of undefined')){console.log('🔧 Undefined Properties Fehler erkannt - ignoriert für Stabilität');e.preventDefault();return false;}
if(e.message.includes('jobManager')||e.message.includes('JobManager')){console.log('🔧 JobManager Fehler erkannt - verwende Fallback');e.preventDefault();return false;}
if(e.message.includes('showToast is not defined')){console.log('🔧 showToast Fehler erkannt - verwende Fallback');e.preventDefault();return false;}});window.addEventListener('unhandledrejection',function(e){console.error('🐛 Promise Rejection abgefangen:',e.reason);if(e.reason&&e.reason.message&&e.reason.message.includes('Jobs')){console.log('🔧 Jobs-bezogener Promise Fehler - ignoriert');e.preventDefault();}});console.log('✅ Debug Fix Script bereit');})();

Binary file not shown.

482
static/js/event-handlers.js Normal file
View File

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

Binary file not shown.

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

@@ -0,0 +1,32 @@
class GlobalEventManager{constructor(){this.init();}
init(){document.addEventListener('click',this.handleClick.bind(this));document.addEventListener('DOMContentLoaded',this.setupEventListeners.bind(this));if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',this.setupEventListeners.bind(this));}else{this.setupEventListeners();}}
handleClick(event){const target=event.target.closest('[data-action]');if(!target)return;const action=target.getAttribute('data-action');const params=this.parseActionParams(target);event.preventDefault();this.executeAction(action,params,target);}
parseActionParams(element){const params={};for(const attr of element.attributes){if(attr.name.startsWith('data-action-')){const key=attr.name.replace('data-action-','');params[key]=attr.value;}}
return params;}
executeAction(action,params,element){console.log(`🎯 Führe Action aus:${action}`,params);switch(action){case'refresh-dashboard':if(typeof refreshDashboard==='function')refreshDashboard();break;case'logout':if(typeof handleLogout==='function')handleLogout();break;case'go-back':window.history.back();break;case'reload-page':window.location.reload();break;case'print-page':window.print();break;case'refresh-jobs':if(typeof refreshJobs==='function')refreshJobs();break;case'toggle-batch-mode':if(typeof toggleBatchMode==='function')toggleBatchMode();break;case'start-job':if(typeof jobManager!=='undefined'&&params.id){jobManager.startJob(params.id);}
break;case'pause-job':if(typeof jobManager!=='undefined'&&params.id){jobManager.pauseJob(params.id);}
break;case'resume-job':if(typeof jobManager!=='undefined'&&params.id){jobManager.resumeJob(params.id);}
break;case'delete-job':if(typeof jobManager!=='undefined'&&params.id){jobManager.deleteJob(params.id);}
break;case'open-job-details':if(typeof jobManager!=='undefined'&&params.id){jobManager.openJobDetails(params.id);}
break;case'refresh-printers':if(typeof refreshPrinters==='function')refreshPrinters();break;case'toggle-maintenance-mode':if(typeof toggleMaintenanceMode==='function')toggleMaintenanceMode();break;case'open-add-printer-modal':if(typeof openAddPrinterModal==='function')openAddPrinterModal();break;case'toggle-auto-refresh':if(typeof toggleAutoRefresh==='function')toggleAutoRefresh();break;case'clear-all-filters':if(typeof clearAllFilters==='function')clearAllFilters();break;case'test-printer-connection':if(typeof testPrinterConnection==='function')testPrinterConnection();break;case'delete-printer':if(typeof deletePrinter==='function')deletePrinter();break;case'edit-printer':if(typeof printerManager!=='undefined'&&params.id){printerManager.editPrinter(params.id);}
break;case'connect-printer':if(typeof printerManager!=='undefined'&&params.id){printerManager.connectPrinter(params.id);}
break;case'refresh-calendar':if(typeof refreshCalendar==='function')refreshCalendar();break;case'toggle-auto-optimization':if(typeof toggleAutoOptimization==='function')toggleAutoOptimization();break;case'export-calendar':if(typeof exportCalendar==='function')exportCalendar();break;case'open-create-event-modal':if(typeof openCreateEventModal==='function')openCreateEventModal();break;case'close-modal':this.closeModal(params.target||element.closest('.fixed'));break;case'close-printer-modal':if(typeof closePrinterModal==='function')closePrinterModal();break;case'close-job-modal':if(typeof closeJobModal==='function')closeJobModal();break;case'close-event-modal':if(typeof closeEventModal==='function')closeEventModal();break;case'reset-form':if(typeof resetForm==='function')resetForm();else this.resetNearestForm(element);break;case'clear-file':if(typeof clearFile==='function')clearFile();break;case'check-status':if(typeof checkStatus==='function')checkStatus();break;case'copy-code':if(typeof copyCode==='function')copyCode();break;case'refresh-status':if(typeof refreshStatus==='function')refreshStatus();break;case'show-status-check':if(typeof showStatusCheck==='function')showStatusCheck();break;case'perform-bulk-action':if(typeof performBulkAction==='function'&&params.type){performBulkAction(params.type);}
break;case'close-bulk-modal':if(typeof closeBulkModal==='function')closeBulkModal();break;case'clear-cache':if(typeof clearCache==='function')clearCache();break;case'optimize-database':if(typeof optimizeDatabase==='function')optimizeDatabase();break;case'create-backup':if(typeof createBackup==='function')createBackup();break;case'download-logs':if(typeof downloadLogs==='function')downloadLogs();break;case'run-maintenance':if(typeof runMaintenance==='function')runMaintenance();break;case'save-settings':if(typeof saveSettings==='function')saveSettings();break;case'toggle-edit-mode':if(typeof toggleEditMode==='function')toggleEditMode();break;case'trigger-avatar-upload':if(typeof triggerAvatarUpload==='function')triggerAvatarUpload();break;case'cancel-edit':if(typeof cancelEdit==='function')cancelEdit();break;case'download-user-data':if(typeof downloadUserData==='function')downloadUserData();break;case'refresh-stats':if(typeof refreshStats==='function')refreshStats();break;case'export-stats':if(typeof exportStats==='function')exportStats();break;case'remove-element':const targetElement=params.target?document.querySelector(params.target):element.closest(params.selector||'.removable');if(targetElement){targetElement.remove();}
break;case'toggle-element':const toggleTarget=params.target?document.querySelector(params.target):element.nextElementSibling;if(toggleTarget){toggleTarget.classList.toggle('hidden');}
break;case'show-element':const showTarget=document.querySelector(params.target);if(showTarget){showTarget.classList.remove('hidden');}
break;case'hide-element':const hideTarget=document.querySelector(params.target);if(hideTarget){hideTarget.classList.add('hidden');}
break;default:console.warn(`⚠️ Unbekannte Action:${action}`);if(typeof window[action]==='function'){window[action](params);}
break;}}
closeModal(modalElement){if(modalElement){modalElement.classList.add('hidden');modalElement.remove();}}
resetNearestForm(element){const form=element.closest('form');if(form){form.reset();}}
setupEventListeners(){this.setupAutoRefresh();this.setupKeyboardShortcuts();this.setupFormValidation();console.log('🔧 Globale Event-Handler initialisiert');}
setupAutoRefresh(){const currentPath=window.location.pathname;if(currentPath.includes('/dashboard')){setInterval(()=>{if(typeof refreshDashboard==='function'){refreshDashboard();}},30000);}
if(currentPath.includes('/jobs')){setInterval(()=>{if(typeof refreshJobs==='function'){refreshJobs();}},15000);}}
setupKeyboardShortcuts(){document.addEventListener('keydown',(event)=>{if(event.key==='Escape'){const openModal=document.querySelector('.fixed:not(.hidden)');if(openModal){this.closeModal(openModal);}}
if(event.ctrlKey&&event.key==='r'){event.preventDefault();const currentPath=window.location.pathname;if(currentPath.includes('/dashboard')&&typeof refreshDashboard==='function'){refreshDashboard();}else if(currentPath.includes('/jobs')&&typeof refreshJobs==='function'){refreshJobs();}else if(currentPath.includes('/printers')&&typeof refreshPrinters==='function'){refreshPrinters();}else{window.location.reload();}}});}
setupFormValidation(){const forms=document.querySelectorAll('form[data-validate]');forms.forEach(form=>{form.addEventListener('submit',this.validateForm.bind(this));});}
validateForm(event){const form=event.target;const requiredFields=form.querySelectorAll('[required]');let isValid=true;requiredFields.forEach(field=>{if(!field.value.trim()){isValid=false;field.classList.add('border-red-500');const errorId=`${field.id}-error`;let errorElement=document.getElementById(errorId);if(!errorElement){errorElement=document.createElement('div');errorElement.id=errorId;errorElement.className='text-red-500 text-sm mt-1';field.parentNode.appendChild(errorElement);}
errorElement.textContent=`${field.getAttribute('data-label')||'Dieses Feld'}ist erforderlich.`;}else{field.classList.remove('border-red-500');const errorElement=document.getElementById(`${field.id}-error`);if(errorElement){errorElement.remove();}}});if(!isValid){event.preventDefault();}
return isValid;}}
const globalEventManager=new GlobalEventManager();if(typeof module!=='undefined'&&module.exports){module.exports=GlobalEventManager;}
console.log('🌍 Globaler Event Manager geladen');

Binary file not shown.

6
static/js/fullcalendar/core.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

6
static/js/fullcalendar/daygrid.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

6
static/js/fullcalendar/list.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

2
static/js/fullcalendar/main.min.css vendored Normal file
View File

@@ -0,0 +1,2 @@
/* FullCalendar v6 CSS is embedded in the JavaScript bundle */
/* This file is kept for template compatibility */

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

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

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