FIN INIT
This commit is contained in:
398
static/js/JS_OPTIMIZATION_REPORT.md
Normal file
398
static/js/JS_OPTIMIZATION_REPORT.md
Normal 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.
|
||||
875
static/js/admin-guest-requests.js
Normal file
875
static/js/admin-guest-requests.js
Normal 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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text ? String(text).replace(/[&<>"']/g, m => map[m]) : '';
|
||||
}
|
||||
|
||||
function formatDateTime(dateString) {
|
||||
if (!dateString) return 'Unbekannt';
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeAgo(dateString) {
|
||||
if (!dateString) return 'Unbekannt';
|
||||
|
||||
const now = new Date();
|
||||
const date = new Date(dateString);
|
||||
const diffMs = now - date;
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffDays > 0) {
|
||||
return `vor ${diffDays} Tag${diffDays === 1 ? '' : 'en'}`;
|
||||
} else if (diffHours > 0) {
|
||||
return `vor ${diffHours} Stunde${diffHours === 1 ? '' : 'n'}`;
|
||||
} else {
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
||||
return `vor ${Math.max(1, diffMinutes)} Minute${diffMinutes === 1 ? '' : 'n'}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Funktionen für onclick-Handler
|
||||
window.showRequestDetail = showRequestDetail;
|
||||
window.approveRequest = approveRequest;
|
||||
window.rejectRequest = rejectRequest;
|
||||
window.deleteRequest = deleteRequest;
|
||||
window.closeDetailModal = closeDetailModal;
|
||||
window.closeBulkModal = closeBulkModal;
|
||||
window.performBulkAction = performBulkAction;
|
||||
|
||||
console.log('📋 Admin Guest Requests JavaScript vollständig geladen');
|
||||
BIN
static/js/admin-guest-requests.js.gz
Normal file
BIN
static/js/admin-guest-requests.js.gz
Normal file
Binary file not shown.
223
static/js/admin-guest-requests.min.js
vendored
Normal file
223
static/js/admin-guest-requests.min.js
vendored
Normal file
@@ -0,0 +1,223 @@
|
||||
let currentRequests=[];let filteredRequests=[];let currentPage=0;let totalPages=0;let totalRequests=0;let refreshInterval=null;let csrfToken='';function detectApiBaseUrl(){return'';}
|
||||
const API_BASE_URL=detectApiBaseUrl();document.addEventListener('DOMContentLoaded',function(){csrfToken=document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')||'';initEventListeners();loadGuestRequests();startAutoRefresh();console.log('🎯 Admin Guest Requests Management geladen');});function initEventListeners(){const searchInput=document.getElementById('search-requests');if(searchInput){searchInput.addEventListener('input',debounce(handleSearch,300));}
|
||||
const statusFilter=document.getElementById('status-filter');if(statusFilter){statusFilter.addEventListener('change',handleFilterChange);}
|
||||
const sortOrder=document.getElementById('sort-order');if(sortOrder){sortOrder.addEventListener('change',handleSortChange);}
|
||||
const refreshBtn=document.getElementById('refresh-btn');if(refreshBtn){refreshBtn.addEventListener('click',()=>{loadGuestRequests();showNotification('🔄 Gastaufträge aktualisiert','info');});}
|
||||
const exportBtn=document.getElementById('export-btn');if(exportBtn){exportBtn.addEventListener('click',handleExport);}
|
||||
const bulkActionsBtn=document.getElementById('bulk-actions-btn');if(bulkActionsBtn){bulkActionsBtn.addEventListener('click',showBulkActionsModal);}
|
||||
const selectAllCheckbox=document.getElementById('select-all');if(selectAllCheckbox){selectAllCheckbox.addEventListener('change',handleSelectAll);}}
|
||||
async function loadGuestRequests(){try{showLoading(true);const url=`${API_BASE_URL}/api/admin/guest-requests`;const response=await fetch(url,{method:'GET',headers:{'Content-Type':'application/json','X-CSRFToken':csrfToken}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
|
||||
const data=await response.json();if(data.success){currentRequests=data.requests||[];totalRequests=data.total||0;updateStats(data.stats||{});applyFiltersAndSort();console.log(`✅ ${currentRequests.length}Gastaufträge geladen`);}else{throw new Error(data.message||'Fehler beim Laden der Gastaufträge');}}catch(error){console.error('Fehler beim Laden der Gastaufträge:',error);showNotification('❌ Fehler beim Laden der Gastaufträge: '+error.message,'error');showEmptyState();}finally{showLoading(false);}}
|
||||
function updateStats(stats){const elements={'pending-count':stats.pending||0,'approved-count':stats.approved||0,'rejected-count':stats.rejected||0,'total-count':stats.total||0};Object.entries(elements).forEach(([id,value])=>{const element=document.getElementById(id);if(element){animateCounter(element,value);}});}
|
||||
function animateCounter(element,targetValue){const currentValue=parseInt(element.textContent)||0;const difference=targetValue-currentValue;const steps=20;const stepValue=difference/steps;let step=0;const interval=setInterval(()=>{step++;const value=Math.round(currentValue+(stepValue*step));element.textContent=value;if(step>=steps){clearInterval(interval);element.textContent=targetValue;}},50);}
|
||||
function applyFiltersAndSort(){let requests=[...currentRequests];const statusFilter=document.getElementById('status-filter')?.value;if(statusFilter&&statusFilter!=='all'){requests=requests.filter(req=>req.status===statusFilter);}
|
||||
const searchTerm=document.getElementById('search-requests')?.value.toLowerCase();if(searchTerm){requests=requests.filter(req=>req.name?.toLowerCase().includes(searchTerm)||req.email?.toLowerCase().includes(searchTerm)||req.file_name?.toLowerCase().includes(searchTerm)||req.reason?.toLowerCase().includes(searchTerm));}
|
||||
const sortOrder=document.getElementById('sort-order')?.value;requests.sort((a,b)=>{switch(sortOrder){case'oldest':return new Date(a.created_at)-new Date(b.created_at);case'priority':return getPriorityValue(b)-getPriorityValue(a);case'newest':default:return new Date(b.created_at)-new Date(a.created_at);}});filteredRequests=requests;renderRequestsTable();}
|
||||
function getPriorityValue(request){const now=new Date();const created=new Date(request.created_at);const hoursOld=(now-created)/(1000*60*60);let priority=0;if(request.status==='pending')priority+=10;else if(request.status==='approved')priority+=5;if(hoursOld>24)priority+=5;else if(hoursOld>8)priority+=3;else if(hoursOld>2)priority+=1;return priority;}
|
||||
function renderRequestsTable(){const tableBody=document.getElementById('requests-table-body');const emptyState=document.getElementById('empty-state');if(!tableBody)return;if(filteredRequests.length===0){tableBody.innerHTML='';showEmptyState();return;}
|
||||
hideEmptyState();const requestsHtml=filteredRequests.map(request=>createRequestRow(request)).join('');tableBody.innerHTML=requestsHtml;addRowEventListeners();}
|
||||
function createRequestRow(request){const statusColor=getStatusColor(request.status);const priorityLevel=getPriorityLevel(request);const timeAgo=getTimeAgo(request.created_at);return`<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors duration-200"data-request-id="${request.id}"><td class="px-6 py-4"><input type="checkbox"class="request-checkbox rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
||||
value="${request.id}"></td><td class="px-6 py-4"><div class="flex items-center"><div class="flex-shrink-0 h-10 w-10"><div class="h-10 w-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">${request.name?request.name[0].toUpperCase():'G'}</div></div><div class="ml-4"><div class="text-sm font-medium text-slate-900 dark:text-white">${escapeHtml(request.name||'Unbekannt')}</div><div class="text-sm text-slate-500 dark:text-slate-400">${escapeHtml(request.email||'Keine E-Mail')}</div></div></div></td><td class="px-6 py-4"><div class="text-sm text-slate-900 dark:text-white font-medium">${escapeHtml(request.file_name||'Keine Datei')}</div><div class="text-sm text-slate-500 dark:text-slate-400">${request.duration_minutes?`${request.duration_minutes}Min.`:'Unbekannte Dauer'}
|
||||
${request.copies?`• ${request.copies}Kopien`:''}</div>${request.reason?`<div class="text-xs text-slate-400 dark:text-slate-500 mt-1 truncate max-w-xs">${escapeHtml(request.reason)}</div>`:''}</td><td class="px-6 py-4"><span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full ${statusColor}"><span class="w-2 h-2 mr-1 rounded-full ${getStatusDot(request.status)}"></span>${getStatusText(request.status)}</span></td><td class="px-6 py-4"><div class="text-sm text-slate-900 dark:text-white">${timeAgo}</div><div class="text-xs text-slate-500 dark:text-slate-400">${formatDateTime(request.created_at)}</div></td><td class="px-6 py-4"><div class="flex items-center">${getPriorityBadge(priorityLevel)}
|
||||
${request.is_urgent?'<span class="ml-2 text-red-500 text-xs">🔥 Dringend</span>':''}</div></td><td class="px-6 py-4"><div class="flex items-center space-x-2"><button onclick="showRequestDetail(${request.id})"
|
||||
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
|
||||
title="Details anzeigen"><svg class="w-5 h-5"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg></button>${request.status==='pending'?`<button onclick="approveRequest(${request.id})"
|
||||
class="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300 transition-colors"
|
||||
title="Genehmigen"><svg class="w-5 h-5"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></button><button onclick="rejectRequest(${request.id})"
|
||||
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||
title="Ablehnen"><svg class="w-5 h-5"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></button>`:''}<button onclick="deleteRequest(${request.id})"
|
||||
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 transition-colors"
|
||||
title="Löschen"><svg class="w-5 h-5"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg></button></div></td></tr>`;}
|
||||
function getStatusColor(status){const colors={'pending':'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300','approved':'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300','rejected':'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300','expired':'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300'};return colors[status]||'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300';}
|
||||
function getStatusDot(status){const dots={'pending':'bg-yellow-400 dark:bg-yellow-300','approved':'bg-green-400 dark:bg-green-300','rejected':'bg-red-400 dark:bg-red-300','expired':'bg-gray-400 dark:bg-gray-300'};return dots[status]||'bg-gray-400 dark:bg-gray-300';}
|
||||
function getStatusText(status){const texts={'pending':'Wartend','approved':'Genehmigt','rejected':'Abgelehnt','expired':'Abgelaufen'};return texts[status]||status;}
|
||||
function getPriorityLevel(request){const priority=getPriorityValue(request);if(priority>=15)return'high';if(priority>=8)return'medium';return'low';}
|
||||
function getPriorityBadge(level){const badges={'high':'<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300">🔴 Hoch</span>','medium':'<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300">🟡 Mittel</span>','low':'<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">🟢 Niedrig</span>'};return badges[level]||badges['low'];}
|
||||
async function approveRequest(requestId){if(!confirm('Möchten Sie diesen Gastauftrag wirklich genehmigen?'))return;try{showLoading(true);const url=`${API_BASE_URL}/api/guest-requests/${requestId}/approve`;const response=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':csrfToken},body:JSON.stringify({})});const data=await response.json();if(data.success){showNotification('✅ Gastauftrag erfolgreich genehmigt','success');loadGuestRequests();}else{throw new Error(data.message||'Fehler beim Genehmigen');}}catch(error){console.error('Fehler beim Genehmigen:',error);showNotification('❌ Fehler beim Genehmigen: '+error.message,'error');}finally{showLoading(false);}}
|
||||
async function rejectRequest(requestId){const reason=prompt('Grund für die Ablehnung:');if(!reason)return;try{showLoading(true);const url=`${API_BASE_URL}/api/guest-requests/${requestId}/reject`;const response=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':csrfToken},body:JSON.stringify({reason})});const data=await response.json();if(data.success){showNotification('✅ Gastauftrag erfolgreich abgelehnt','success');loadGuestRequests();}else{throw new Error(data.message||'Fehler beim Ablehnen');}}catch(error){console.error('Fehler beim Ablehnen:',error);showNotification('❌ Fehler beim Ablehnen: '+error.message,'error');}finally{showLoading(false);}}
|
||||
async function deleteRequest(requestId){if(!confirm('Möchten Sie diesen Gastauftrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.'))return;try{showLoading(true);const url=`${API_BASE_URL}/api/guest-requests/${requestId}`;const response=await fetch(url,{method:'DELETE',headers:{'Content-Type':'application/json','X-CSRFToken':csrfToken}});const data=await response.json();if(data.success){showNotification('✅ Gastauftrag erfolgreich gelöscht','success');loadGuestRequests();}else{throw new Error(data.message||'Fehler beim Löschen');}}catch(error){console.error('Fehler beim Löschen:',error);showNotification('❌ Fehler beim Löschen: '+error.message,'error');}finally{showLoading(false);}}
|
||||
function showRequestDetail(requestId){const request=currentRequests.find(req=>req.id===requestId);if(!request)return;const modal=document.getElementById('detail-modal');const content=document.getElementById('modal-content');content.innerHTML=`<div class="p-6 border-b border-gray-200 dark:border-gray-700"><div class="flex justify-between items-center"><h3 class="text-xl font-bold text-gray-900 dark:text-white">Gastauftrag Details</h3><button onclick="closeDetailModal()"class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"><svg class="w-6 h-6"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M6 18L18 6M6 6l12 12"/></svg></button></div></div><div class="p-6"><div class="grid grid-cols-1 md:grid-cols-2 gap-6"><div class="space-y-4"><h4 class="text-lg font-semibold text-gray-900 dark:text-white">Antragsteller</h4><div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4"><p><strong>Name:</strong>${escapeHtml(request.name||'Unbekannt')}</p><p><strong>E-Mail:</strong>${escapeHtml(request.email||'Keine E-Mail')}</p><p><strong>Erstellt am:</strong>${formatDateTime(request.created_at)}</p></div></div><div class="space-y-4"><h4 class="text-lg font-semibold text-gray-900 dark:text-white">Auftrag Details</h4><div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4"><p><strong>Datei:</strong>${escapeHtml(request.file_name||'Keine Datei')}</p><p><strong>Dauer:</strong>${request.duration_minutes||'Unbekannt'}Minuten</p><p><strong>Kopien:</strong>${request.copies||1}</p><p><strong>Status:</strong>${getStatusText(request.status)}</p></div></div></div>${request.reason?`<div class="mt-6"><h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Begründung</h4><div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4"><p class="text-gray-700 dark:text-gray-300">${escapeHtml(request.reason)}</p></div></div>`:''}<div class="mt-8 flex justify-end space-x-3">${request.status==='pending'?`<button onclick="approveRequest(${request.id}); closeDetailModal();"
|
||||
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">Genehmigen</button><button onclick="rejectRequest(${request.id}); closeDetailModal();"
|
||||
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">Ablehnen</button>`:''}<button onclick="closeDetailModal()"
|
||||
class="px-4 py-2 bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors">Schließen</button></div></div>`;modal.classList.remove('hidden');}
|
||||
function closeDetailModal(){const modal=document.getElementById('detail-modal');modal.classList.add('hidden');}
|
||||
function showBulkActionsModal(){const selectedIds=getSelectedRequestIds();if(selectedIds.length===0){showNotification('⚠️ Bitte wählen Sie mindestens einen Gastauftrag aus','warning');return;}
|
||||
const modal=document.getElementById('bulk-modal');modal.classList.remove('hidden');}
|
||||
function closeBulkModal(){const modal=document.getElementById('bulk-modal');modal.classList.add('hidden');}
|
||||
async function performBulkAction(action){const selectedIds=getSelectedRequestIds();if(selectedIds.length===0)return;const confirmMessages={'approve':`Möchten Sie ${selectedIds.length}Gastaufträge genehmigen?`,'reject':`Möchten Sie ${selectedIds.length}Gastaufträge ablehnen?`,'delete':`Möchten Sie ${selectedIds.length}Gastaufträge löschen?`};if(!confirm(confirmMessages[action]))return;try{showLoading(true);closeBulkModal();const promises=selectedIds.map(async(id)=>{const url=`${API_BASE_URL}/api/guest-requests/${id}/${action}`;const method=action==='delete'?'DELETE':'POST';return fetch(url,{method,headers:{'Content-Type':'application/json','X-CSRFToken':csrfToken}});});const results=await Promise.allSettled(promises);const successCount=results.filter(r=>r.status==='fulfilled'&&r.value.ok).length;showNotification(`✅ ${successCount}von ${selectedIds.length}Aktionen erfolgreich`,'success');loadGuestRequests();document.getElementById('select-all').checked=false;document.querySelectorAll('.request-checkbox').forEach(cb=>cb.checked=false);}catch(error){console.error('Fehler bei Bulk-Aktion:',error);showNotification('❌ Fehler bei der Bulk-Aktion: '+error.message,'error');}finally{showLoading(false);}}
|
||||
function getSelectedRequestIds(){const checkboxes=document.querySelectorAll('.request-checkbox:checked');return Array.from(checkboxes).map(cb=>parseInt(cb.value));}
|
||||
function handleSearch(){applyFiltersAndSort();}
|
||||
function handleFilterChange(){applyFiltersAndSort();}
|
||||
function handleSortChange(){applyFiltersAndSort();}
|
||||
function handleSelectAll(event){const checkboxes=document.querySelectorAll('.request-checkbox');checkboxes.forEach(checkbox=>{checkbox.checked=event.target.checked;});}
|
||||
function handleExport(){const selectedIds=getSelectedRequestIds();const exportData=selectedIds.length>0?filteredRequests.filter(req=>selectedIds.includes(req.id)):filteredRequests;if(exportData.length===0){showNotification('⚠️ Keine Daten zum Exportieren verfügbar','warning');return;}
|
||||
exportToCSV(exportData);}
|
||||
function exportToCSV(data){const headers=['ID','Name','E-Mail','Datei','Status','Erstellt','Dauer (Min)','Kopien','Begründung'];const rows=data.map(req=>[req.id,req.name||'',req.email||'',req.file_name||'',getStatusText(req.status),formatDateTime(req.created_at),req.duration_minutes||'',req.copies||'',req.reason||'']);const csvContent=[headers,...rows].map(row=>row.map(field=>`"${String(field).replace(/"/g,'""')}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
|
||||
if (link.download !== undefined) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `gastauftraege_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
showNotification('📄 CSV-Export erfolgreich erstellt', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Refresh
|
||||
*/
|
||||
function startAutoRefresh() {
|
||||
// Refresh alle 30 Sekunden
|
||||
refreshInterval = setInterval(() => {
|
||||
loadGuestRequests();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility-Funktionen
|
||||
*/
|
||||
function addRowEventListeners() {
|
||||
// Falls notwendig, können hier zusätzliche Event Listener hinzugefügt werden
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
const loadingElement = document.getElementById('table-loading');
|
||||
const tableBody = document.getElementById('requests-table-body');
|
||||
|
||||
if (loadingElement) {
|
||||
loadingElement.classList.toggle('hidden', !show);
|
||||
}
|
||||
|
||||
if (show && tableBody) {
|
||||
tableBody.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function showEmptyState() {
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function hideEmptyState() {
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 px-6 py-4 rounded-xl shadow-2xl z-50 transform transition-all duration-500 translate-x-full ${
|
||||
type === 'success' ? 'bg-green-500 text-white' :
|
||||
type === 'error' ? 'bg-red-500 text-white' :
|
||||
type === 'warning' ? 'bg-yellow-500 text-black' :
|
||||
'bg-blue-500 text-white'
|
||||
}`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="text-lg">
|
||||
${type === 'success' ? '✅' :
|
||||
type === 'error' ? '❌' :
|
||||
type === 'warning' ? '⚠️' : 'ℹ️'}
|
||||
</span>
|
||||
<span class="font-medium">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animation einblenden
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// Nach 5 Sekunden entfernen
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-x-full');
|
||||
setTimeout(() => notification.remove(), 5000);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text ? String(text).replace(/[&<>"']/g, m => map[m]) : '';
|
||||
}
|
||||
|
||||
function formatDateTime(dateString) {
|
||||
if (!dateString) return 'Unbekannt';
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeAgo(dateString) {
|
||||
if (!dateString) return 'Unbekannt';
|
||||
|
||||
const now = new Date();
|
||||
const date = new Date(dateString);
|
||||
const diffMs = now - date;
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffDays > 0) {
|
||||
return `vor ${diffDays} Tag${diffDays === 1 ? '' : 'en'}`;
|
||||
} else if (diffHours > 0) {
|
||||
return `vor ${diffHours} Stunde${diffHours === 1 ? '' : 'n'}`;
|
||||
} else {
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
||||
return `vor ${Math.max(1, diffMinutes)} Minute${diffMinutes === 1 ? '' : 'n'}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Funktionen für onclick-Handler
|
||||
window.showRequestDetail = showRequestDetail;
|
||||
window.approveRequest = approveRequest;
|
||||
window.rejectRequest = rejectRequest;
|
||||
window.deleteRequest = deleteRequest;
|
||||
window.closeDetailModal = closeDetailModal;
|
||||
window.closeBulkModal = closeBulkModal;
|
||||
window.performBulkAction = performBulkAction;
|
||||
|
||||
console.log('📋 Admin Guest Requests JavaScript vollständig geladen'
|
||||
BIN
static/js/admin-guest-requests.min.js.gz
Normal file
BIN
static/js/admin-guest-requests.min.js.gz
Normal file
Binary file not shown.
1085
static/js/admin-panel.js
Normal file
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
BIN
static/js/admin-panel.js.gz
Normal file
Binary file not shown.
77
static/js/admin-panel.min.js
vendored
Normal file
77
static/js/admin-panel.min.js
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
document.addEventListener('DOMContentLoaded',function(){initializeAdminPanel();loadAdminStats();initializeActiveTab();initializeEventHandlers();setInterval(function(){if(document.visibilityState==='visible'){refreshActiveTabData();}},30000);});function initializeActiveTab(){const hash=window.location.hash.substring(1);if(hash){activateTab(hash);}else{const defaultTab=document.querySelector('.nav-tab');if(defaultTab){const tabName=defaultTab.getAttribute('data-tab');activateTab(tabName);}}}
|
||||
function activateTab(tabName){const tabs=document.querySelectorAll('.nav-tab');tabs.forEach(tab=>{if(tab.getAttribute('data-tab')===tabName){tab.classList.add('active');}else{tab.classList.remove('active');}});const tabPanes=document.querySelectorAll('.tab-pane');tabPanes.forEach(pane=>{if(pane.id===`${tabName}-tab`){pane.classList.add('active');pane.classList.remove('hidden');}else{pane.classList.remove('active');pane.classList.add('hidden');}});switch(tabName){case'users':loadUsers();break;case'printers':loadPrinters();break;case'scheduler':loadSchedulerStatus();break;case'system':loadSystemStats();break;case'logs':loadLogs();break;}
|
||||
window.location.hash=tabName;}
|
||||
function refreshActiveTabData(){const activeTab=document.querySelector('.nav-tab.active');if(!activeTab)return;const tabName=activeTab.getAttribute('data-tab');switch(tabName){case'users':loadUsers();break;case'printers':loadPrinters();break;case'scheduler':loadSchedulerStatus();break;case'system':loadSystemStats();break;case'logs':loadLogs();break;}
|
||||
loadAdminStats();}
|
||||
function initializeEventHandlers(){const addUserBtn=document.getElementById('add-user-btn');if(addUserBtn){addUserBtn.addEventListener('click',showAddUserModal);}
|
||||
const addPrinterBtn=document.getElementById('add-printer-btn');if(addPrinterBtn){addPrinterBtn.addEventListener('click',showAddPrinterModal);}
|
||||
const refreshLogsBtn=document.getElementById('refresh-logs-btn');if(refreshLogsBtn){refreshLogsBtn.addEventListener('click',refreshLogs);}
|
||||
const closeDeleteModalBtn=document.getElementById('close-delete-modal');if(closeDeleteModalBtn){closeDeleteModalBtn.addEventListener('click',closeDeleteModal);}
|
||||
const cancelDeleteBtn=document.getElementById('cancel-delete');if(cancelDeleteBtn){cancelDeleteBtn.addEventListener('click',closeDeleteModal);}
|
||||
const closeAddUserBtn=document.getElementById('close-add-user-modal');if(closeAddUserBtn){closeAddUserBtn.addEventListener('click',closeAddUserModal);}
|
||||
const cancelAddUserBtn=document.getElementById('cancel-add-user');if(cancelAddUserBtn){cancelAddUserBtn.addEventListener('click',closeAddUserModal);}
|
||||
const confirmAddUserBtn=document.getElementById('confirm-add-user');if(confirmAddUserBtn){confirmAddUserBtn.addEventListener('click',addUser);}
|
||||
const closeAddPrinterBtn=document.getElementById('close-add-printer-modal');if(closeAddPrinterBtn){closeAddPrinterBtn.addEventListener('click',closeAddPrinterModal);}
|
||||
const cancelAddPrinterBtn=document.getElementById('cancel-add-printer');if(cancelAddPrinterBtn){cancelAddPrinterBtn.addEventListener('click',closeAddPrinterModal);}
|
||||
const confirmAddPrinterBtn=document.getElementById('confirm-add-printer');if(confirmAddPrinterBtn){confirmAddPrinterBtn.addEventListener('click',addPrinter);}
|
||||
const startSchedulerBtn=document.getElementById('start-scheduler');if(startSchedulerBtn){startSchedulerBtn.addEventListener('click',startScheduler);}
|
||||
const stopSchedulerBtn=document.getElementById('stop-scheduler');if(stopSchedulerBtn){stopSchedulerBtn.addEventListener('click',stopScheduler);}
|
||||
const refreshButton=document.createElement('button');refreshButton.id='refresh-admin-btn';refreshButton.className='fixed bottom-8 right-8 p-3 bg-primary text-white rounded-full shadow-lg z-40';refreshButton.innerHTML=`<svg class="w-6 h-6"fill="none"viewBox="0 0 24 24"stroke="currentColor"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>`;refreshButton.title='Alle Daten aktualisieren';refreshButton.addEventListener('click',refreshAllData);if(!document.getElementById('refresh-admin-btn')){document.body.appendChild(refreshButton);}}
|
||||
function showAddUserModal(){const modal=document.getElementById('add-user-modal');if(modal){const form=document.getElementById('add-user-form');if(form){form.reset();}
|
||||
modal.classList.remove('hidden');modal.classList.add('show');modal.style.display='flex';}}
|
||||
function closeAddUserModal(){const modal=document.getElementById('add-user-modal');if(modal){modal.classList.remove('show');modal.classList.add('hidden');modal.style.display='none';}}
|
||||
async function addUser(){const form=document.getElementById('add-user-form');if(!form)return;const userData={email:document.getElementById('user-email').value,name:document.getElementById('user-name').value,password:document.getElementById('user-password').value,role:document.getElementById('user-role').value,status:document.getElementById('user-status').value};try{const response=await fetch('/api/users',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':getCSRFToken()},body:JSON.stringify(userData)});if(!response.ok){throw new Error('Fehler beim Hinzufügen des Benutzers');}
|
||||
showNotification('Benutzer erfolgreich hinzugefügt','success');closeAddUserModal();loadUsers();}catch(error){console.error('Error adding user:',error);showNotification(error.message,'error');}}
|
||||
function showAddPrinterModal(){const modal=document.getElementById('add-printer-modal');if(modal){const form=document.getElementById('add-printer-form');if(form){form.reset();}
|
||||
modal.classList.remove('hidden');modal.classList.add('show');modal.style.display='flex';}}
|
||||
function closeAddPrinterModal(){const modal=document.getElementById('add-printer-modal');if(modal){modal.classList.remove('show');modal.classList.add('hidden');modal.style.display='none';}}
|
||||
async function addPrinter(){const form=document.getElementById('add-printer-form');if(!form)return;const printerData={name:document.getElementById('printer-name').value,model:document.getElementById('printer-model').value,ip_address:document.getElementById('printer-ip').value,location:document.getElementById('printer-location').value,status:document.getElementById('printer-status').value};try{const response=await fetch('/api/printers',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':getCSRFToken()},body:JSON.stringify(printerData)});if(!response.ok){throw new Error('Fehler beim Hinzufügen des Druckers');}
|
||||
showNotification('Drucker erfolgreich hinzugefügt','success');closeAddPrinterModal();loadPrinters();}catch(error){console.error('Error adding printer:',error);showNotification(error.message,'error');}}
|
||||
function refreshLogs(){loadLogs();showNotification('Logs aktualisiert','info');}
|
||||
function closeDeleteModal(){const modal=document.getElementById('delete-confirm-modal');if(modal){modal.classList.remove('show');modal.style.display='none';}}
|
||||
function showDeleteConfirmation(id,type,name,callback){const modal=document.getElementById('delete-confirm-modal');const messageEl=document.getElementById('delete-message');const idField=document.getElementById('delete-id');const typeField=document.getElementById('delete-type');const confirmBtn=document.getElementById('confirm-delete');if(modal&&messageEl&&idField&&typeField&&confirmBtn){messageEl.textContent=`Möchten Sie ${type==='user'?'den Benutzer':'den Drucker'}"${name}"wirklich löschen?`;idField.value=id;typeField.value=type;confirmBtn.onclick=function(){if(typeof callback==='function'){callback(id,name);}
|
||||
closeDeleteModal();};modal.classList.add('show');modal.style.display='flex';}}
|
||||
function initializeAdminPanel(){const tabs=document.querySelectorAll('.nav-tab');tabs.forEach(tab=>{tab.addEventListener('click',function(){const tabName=this.getAttribute('data-tab');if(!tabName)return;activateTab(tabName);});});const logLevelFilter=document.getElementById('log-level-filter');if(logLevelFilter){logLevelFilter.addEventListener('change',function(){if(window.logsData){const level=this.value;if(level==='all'){window.filteredLogs=[...window.logsData];}else{window.filteredLogs=window.logsData.filter(log=>log.level.toLowerCase()===level);}
|
||||
renderLogs();}});}
|
||||
const confirmDeleteBtn=document.getElementById('confirm-delete');if(confirmDeleteBtn){confirmDeleteBtn.addEventListener('click',function(){const id=document.getElementById('delete-id').value;const type=document.getElementById('delete-type').value;if(type==='user'){deleteUser(id);}else if(type==='printer'){deletePrinter(id);}
|
||||
closeDeleteModal();});}}
|
||||
async function loadAdminStats(){try{const response=await fetch('/api/admin/stats');if(!response.ok){throw new Error('Fehler beim Laden der Admin-Statistiken');}
|
||||
const data=await response.json();const statsContainer=document.getElementById('admin-stats');if(!statsContainer){console.warn('Stats-Container nicht gefunden');return;}
|
||||
statsContainer.innerHTML=`<div class="stat-card"><div class="stat-icon"><svg class="w-10 h-10"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg></div><div class="stat-title">Benutzer</div><div class="stat-value"id="admin-total-users-count">${data.total_users||0}</div><div class="stat-desc">Registrierte Benutzer</div></div><div class="stat-card"><div class="stat-icon"><svg class="w-10 h-10"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/></svg></div><div class="stat-title">Drucker</div><div class="stat-value"id="admin-total-printers-count">${data.total_printers||0}</div><div class="stat-desc">Verbundene Drucker</div></div><div class="stat-card"><div class="stat-icon"><svg class="w-10 h-10"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg></div><div class="stat-title">Aktive Jobs</div><div class="stat-value"id="admin-active-jobs-count">${data.active_jobs||0}</div><div class="stat-desc">Laufende Druckaufträge</div></div><div class="stat-card"><div class="stat-icon"><svg class="w-10 h-10"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></div><div class="stat-title">Erfolgsrate</div><div class="stat-value"id="admin-success-rate">${data.success_rate||'0%'}</div><div class="stat-desc">Erfolgreiche Druckaufträge</div></div>`;console.log('✅ Admin-Statistiken erfolgreich aktualisiert:',data);}catch(error){console.error('Error loading admin stats:',error);showNotification('Fehler beim Laden der Admin-Statistiken','error');}}
|
||||
async function loadUsers(){try{const response=await fetch('/api/users');if(!response.ok){throw new Error('Fehler beim Laden der Benutzer');}
|
||||
const data=await response.json();const userTableBody=document.querySelector('#users-tab table tbody');if(!userTableBody)return;let html='';if(data.users&&data.users.length>0){data.users.forEach(user=>{html+=`<tr class="border-b border-gray-200 dark:border-gray-700"><td class="px-4 py-3">${user.id}</td><td class="px-4 py-3">${user.name||'-'}</td><td class="px-4 py-3">${user.email}</td><td class="px-4 py-3"><span class="px-2 py-1 text-xs rounded-full ${user.active ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}">${user.active?'Aktiv':'Inaktiv'}</span></td><td class="px-4 py-3">${user.role||'Benutzer'}</td><td class="px-4 py-3"><div class="flex justify-end space-x-2"><button onclick="editUser(${user.id})"class="p-1 text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-200"><svg class="w-5 h-5"fill="none"viewBox="0 0 24 24"stroke="currentColor"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg></button><button onclick="showDeleteConfirmation(${user.id}, 'user', '${user.name || user.email}', deleteUser)"class="p-1 text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-200"><svg class="w-5 h-5"fill="none"viewBox="0 0 24 24"stroke="currentColor"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg></button></div></td></tr>`;});}else{html=`<tr><td colspan="6"class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">Keine Benutzer gefunden</td></tr>`;}
|
||||
userTableBody.innerHTML=html;}catch(error){console.error('Error loading users:',error);showNotification('Fehler beim Laden der Benutzer','error');}}
|
||||
async function loadPrinters(){try{const response=await fetch('/api/printers');if(!response.ok){throw new Error('Fehler beim Laden der Drucker');}
|
||||
const data=await response.json();const printerTableBody=document.querySelector('#printers-tab table tbody');if(!printerTableBody)return;let html='';if(data.printers&&data.printers.length>0){data.printers.forEach(printer=>{html+=`<tr class="border-b border-gray-200 dark:border-gray-700"><td class="px-4 py-3">${printer.id}</td><td class="px-4 py-3">${printer.name}</td><td class="px-4 py-3">${printer.model||'-'}</td><td class="px-4 py-3">${printer.ip_address||'-'}</td><td class="px-4 py-3">${printer.location||'-'}</td><td class="px-4 py-3"><span class="px-2 py-1 text-xs rounded-full ${printer.status === 'online' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}">${printer.status==='online'?'Online':'Offline'}</span></td><td class="px-4 py-3"><div class="flex justify-end space-x-2"><button onclick="editPrinter(${printer.id})"class="p-1 text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-200"><svg class="w-5 h-5"fill="none"viewBox="0 0 24 24"stroke="currentColor"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg></button><button onclick="showDeleteConfirmation(${printer.id}, 'printer', '${printer.name}', deletePrinter)"class="p-1 text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-200"><svg class="w-5 h-5"fill="none"viewBox="0 0 24 24"stroke="currentColor"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg></button></div></td></tr>`;});}else{html=`<tr><td colspan="7"class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">Keine Drucker gefunden</td></tr>`;}
|
||||
printerTableBody.innerHTML=html;}catch(error){console.error('Error loading printers:',error);showNotification('Fehler beim Laden der Drucker','error');}}
|
||||
async function loadSchedulerStatus(){try{const response=await fetch('/api/scheduler/status');if(!response.ok){throw new Error('Fehler beim Laden des Scheduler-Status');}
|
||||
const data=await response.json();const statusIndicator=document.getElementById('scheduler-status-indicator');const statusText=document.getElementById('scheduler-status-text');const startSchedulerBtn=document.getElementById('start-scheduler');const stopSchedulerBtn=document.getElementById('stop-scheduler');if(statusIndicator&&statusText){if(data.active){statusIndicator.classList.remove('bg-red-500');statusIndicator.classList.add('bg-green-500');statusText.textContent='Aktiv';if(startSchedulerBtn&&stopSchedulerBtn){startSchedulerBtn.disabled=true;stopSchedulerBtn.disabled=false;}}else{statusIndicator.classList.remove('bg-green-500');statusIndicator.classList.add('bg-red-500');statusText.textContent='Inaktiv';if(startSchedulerBtn&&stopSchedulerBtn){startSchedulerBtn.disabled=false;stopSchedulerBtn.disabled=true;}}}
|
||||
const schedulerDetails=document.getElementById('scheduler-details');if(schedulerDetails){schedulerDetails.innerHTML=`<div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Letzte Ausführung</h4><p class="text-gray-800 dark:text-gray-100">${data.last_run?new Date(data.last_run).toLocaleString('de-DE'):'Nie'}</p></div><div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Nächste Ausführung</h4><p class="text-gray-800 dark:text-gray-100">${data.next_run?new Date(data.next_run).toLocaleString('de-DE'):'Nicht geplant'}</p></div><div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Ausführungsintervall</h4><p class="text-gray-800 dark:text-gray-100">${data.interval||'60'}Sekunden</p></div><div><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Verarbeitete Jobs</h4><p class="text-gray-800 dark:text-gray-100">${data.processed_jobs||0}Jobs seit dem letzten Neustart</p></div>`;}
|
||||
const jobQueueContainer=document.getElementById('job-queue');if(jobQueueContainer&&data.pending_jobs){let queueHtml='';if(data.pending_jobs.length>0){queueHtml=`<div class="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden"><table class="min-w-full"><thead class="bg-gray-50 dark:bg-gray-700"><tr><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Job ID</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Name</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Geplant für</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Status</th></tr></thead><tbody class="divide-y divide-gray-200 dark:divide-gray-700">`;data.pending_jobs.forEach(job=>{queueHtml+=`<tr><td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">${job.id}</td><td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">${job.name}</td><td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">${new Date(job.start_time).toLocaleString('de-DE')}</td><td class="px-4 py-3 text-sm"><span class="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">Warten</span></td></tr>`;});queueHtml+=`</tbody></table></div>`;}else{queueHtml=`<div class="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6 text-center"><p class="text-gray-500 dark:text-gray-400">Keine ausstehenden Jobs in der Warteschlange</p></div>`;}
|
||||
jobQueueContainer.innerHTML=queueHtml;}}catch(error){console.error('Error loading scheduler status:',error);showNotification('Fehler beim Laden des Scheduler-Status','error');}}
|
||||
async function loadSystemStats(){try{const response=await fetch('/api/system/stats');if(!response.ok){throw new Error('Fehler beim Laden der Systemstatistiken');}
|
||||
const data=await response.json();const cpuUsageElement=document.getElementById('cpu-usage');if(cpuUsageElement){cpuUsageElement.style.width=`${data.cpu_usage||0}%`;cpuUsageElement.textContent=`${data.cpu_usage||0}%`;}
|
||||
const ramUsageElement=document.getElementById('ram-usage');if(ramUsageElement){ramUsageElement.style.width=`${data.ram_usage_percent||0}%`;ramUsageElement.textContent=`${data.ram_usage_percent||0}%`;}
|
||||
const diskUsageElement=document.getElementById('disk-usage');if(diskUsageElement){diskUsageElement.style.width=`${data.disk_usage_percent||0}%`;diskUsageElement.textContent=`${data.disk_usage_percent||0}%`;}
|
||||
const systemDetailsElement=document.getElementById('system-details');if(systemDetailsElement){systemDetailsElement.innerHTML=`<div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">System</h4><p class="text-gray-800 dark:text-gray-100">${data.os_name||'Unbekannt'}${data.os_version||''}</p></div><div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Laufzeit</h4><p class="text-gray-800 dark:text-gray-100">${data.uptime||'Unbekannt'}</p></div><div class="mb-4"><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Python-Version</h4><p class="text-gray-800 dark:text-gray-100">${data.python_version||'Unbekannt'}</p></div><div><h4 class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Server-Zeit</h4><p class="text-gray-800 dark:text-gray-100">${data.server_time?new Date(data.server_time).toLocaleString('de-DE'):'Unbekannt'}</p></div>`;}
|
||||
const systemEventsElement=document.getElementById('system-events');if(systemEventsElement&&data.recent_events){let eventsHtml='';if(data.recent_events.length>0){eventsHtml='<ul class="divide-y divide-gray-200 dark:divide-gray-700">';data.recent_events.forEach(event=>{let eventTypeClass='text-blue-600 dark:text-blue-400';switch(event.type.toLowerCase()){case'error':eventTypeClass='text-red-600 dark:text-red-400';break;case'warning':eventTypeClass='text-yellow-600 dark:text-yellow-400';break;case'success':eventTypeClass='text-green-600 dark:text-green-400';break;}
|
||||
eventsHtml+=`<li class="py-3"><div class="flex items-start"><span class="${eventTypeClass} mr-2"><svg class="h-5 w-5"fill="none"viewBox="0 0 24 24"stroke="currentColor"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></span><div><p class="text-sm text-gray-800 dark:text-gray-200">${event.message}</p><p class="text-xs text-gray-500 dark:text-gray-400">${new Date(event.timestamp).toLocaleString('de-DE')}</p></div></div></li>`;});eventsHtml+='</ul>';}else{eventsHtml='<p class="text-center text-gray-500 dark:text-gray-400 py-4">Keine Ereignisse vorhanden</p>';}
|
||||
systemEventsElement.innerHTML=eventsHtml;}}catch(error){console.error('Error loading system stats:',error);showNotification('Fehler beim Laden der Systemstatistiken','error');}}
|
||||
async function loadLogs(){try{const response=await fetch('/api/logs');if(!response.ok){throw new Error('Fehler beim Laden der Logs');}
|
||||
const data=await response.json();window.logsData=data.logs||[];window.filteredLogs=[...window.logsData];renderLogs();}catch(error){console.error('Error loading logs:',error);showNotification('Fehler beim Laden der Logs','error');}}
|
||||
function renderLogs(){const logsContainer=document.getElementById('logs-container');if(!logsContainer)return;if(!window.filteredLogs||window.filteredLogs.length===0){logsContainer.innerHTML='<div class="p-8 text-center text-gray-500 dark:text-gray-400">Keine Logs gefunden</div>';return;}
|
||||
let html='';window.filteredLogs.forEach(log=>{let levelClass='';switch(log.level.toLowerCase()){case'error':levelClass='bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';break;case'warning':levelClass='bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';break;case'info':levelClass='bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';break;case'debug':levelClass='bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';break;default:levelClass='bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';}
|
||||
html+=`<div class="log-entry border-l-4 border-gray-300 dark:border-gray-600 pl-4 py-3 mb-3"><div class="flex items-center justify-between mb-1"><div><span class="px-2 py-1 text-xs rounded-full ${levelClass} mr-2">${log.level}</span><span class="text-sm text-gray-500 dark:text-gray-400">${log.timestamp?new Date(log.timestamp).toLocaleString('de-DE'):''}</span></div><div class="text-sm text-gray-500 dark:text-gray-400">${log.source||'System'}</div></div><div class="text-sm text-gray-800 dark:text-gray-200">${log.message}</div>${log.details?`<div class="mt-1 text-xs text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 p-2 rounded overflow-auto"><pre>${log.details}</pre></div>`:''}</div>`;});logsContainer.innerHTML=html;}
|
||||
async function deleteUser(userId){try{const response=await fetch(`/api/users/${userId}`,{method:'DELETE',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Löschen des Benutzers');}
|
||||
showNotification('Benutzer erfolgreich gelöscht','success');loadUsers();}catch(error){console.error('Error deleting user:',error);showNotification(error.message,'error');}}
|
||||
async function deletePrinter(printerId){try{const response=await fetch(`/api/printers/${printerId}`,{method:'DELETE',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Löschen des Druckers');}
|
||||
showNotification('Drucker erfolgreich gelöscht','success');loadPrinters();}catch(error){console.error('Error deleting printer:',error);showNotification(error.message,'error');}}
|
||||
async function startScheduler(){try{const response=await fetch('/api/scheduler/start',{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Starten des Schedulers');}
|
||||
showNotification('Scheduler erfolgreich gestartet','success');loadSchedulerStatus();}catch(error){console.error('Error starting scheduler:',error);showNotification(error.message,'error');}}
|
||||
async function stopScheduler(){try{const response=await fetch('/api/scheduler/stop',{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Stoppen des Schedulers');}
|
||||
showNotification('Scheduler erfolgreich gestoppt','success');loadSchedulerStatus();}catch(error){console.error('Error stopping scheduler:',error);showNotification(error.message,'error');}}
|
||||
function getCSRFToken(){return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')||'';}
|
||||
function refreshAllData(){loadAdminStats();const activeTab=document.querySelector('.nav-tab.active');if(activeTab){const tabName=activeTab.getAttribute('data-tab');activateTab(tabName);}
|
||||
showNotification('Daten wurden aktualisiert','success');}
|
||||
function showNotification(message,type='info'){const notification=document.getElementById('notification');const messageEl=document.getElementById('notification-message');const iconEl=document.getElementById('notification-icon');if(!notification||!messageEl||!iconEl)return;messageEl.textContent=message;notification.classList.remove('notification-success','notification-error','notification-warning','notification-info');notification.classList.add(`notification-${type}`);let icon='';switch(type){case'success':icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />';break;case'error':icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />';break;case'warning':icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />';break;case'info':default:icon='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />';break;}
|
||||
iconEl.innerHTML=icon;notification.classList.remove('hidden');notification.classList.add('show');setTimeout(()=>{notification.classList.remove('show');setTimeout(()=>{notification.classList.add('hidden');},300);},5000);}
|
||||
BIN
static/js/admin-panel.min.js.gz
Normal file
BIN
static/js/admin-panel.min.js.gz
Normal file
Binary file not shown.
1345
static/js/admin-unified.js
Normal file
1345
static/js/admin-unified.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/js/admin-unified.js.gz
Normal file
BIN
static/js/admin-unified.js.gz
Normal file
Binary file not shown.
101
static/js/admin-unified.min.js
vendored
Normal file
101
static/js/admin-unified.min.js
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
class AdminDashboard{constructor(){this.csrfToken=null;this.updateInterval=null;this.eventListenersAttached=false;this.apiBaseUrl=this.detectApiBaseUrl();this.retryCount=0;this.maxRetries=3;this.isInitialized=false;this.init();}
|
||||
detectApiBaseUrl(){const currentHost=window.location.hostname;const currentPort=window.location.port;if(currentPort==='5000'){return'';}
|
||||
return`http:}
|
||||
init(){if(this.isInitialized){console.log('🔄 Admin Dashboard bereits initialisiert, überspringe...');return;}
|
||||
console.log('🚀 Initialisiere Mercedes-Benz MYP Admin Dashboard');this.csrfToken=this.extractCSRFToken();console.log('🔒 CSRF Token:',this.csrfToken?'verfügbar':'FEHLT!');this.attachEventListeners();this.startLiveUpdates();this.loadInitialData();this.isInitialized=true;console.log('✅ Admin Dashboard erfolgreich initialisiert');}
|
||||
extractCSRFToken(){const metaToken=document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');if(metaToken){console.log('🔒 CSRF Token aus meta Tag geladen');return metaToken;}
|
||||
const hiddenInput=document.querySelector('input[name="csrf_token"]')?.value;if(hiddenInput){console.log('🔒 CSRF Token aus hidden input geladen');return hiddenInput;}
|
||||
const cookieToken=document.cookie.split('; ').find(row=>row.startsWith('csrf_token='))?.split('=')[1];if(cookieToken){console.log('🔒 CSRF Token aus Cookie geladen');return cookieToken;}
|
||||
const flaskToken=document.querySelector('meta[name="csrf-token"]')?.content;if(flaskToken){console.log('🔒 CSRF Token aus Flask-WTF Meta geladen');return flaskToken;}
|
||||
console.error('❌ CSRF Token konnte nicht gefunden werden!');return null;}
|
||||
attachEventListeners(){if(this.eventListenersAttached){console.log('⚠️ Event-Listener bereits registriert, überspringe...');return;}
|
||||
this.attachSystemButtons();this.attachUserManagement();this.attachPrinterManagement();this.attachJobManagement();this.attachModalEvents();this.eventListenersAttached=true;console.log('📌 Event-Listener erfolgreich registriert');}
|
||||
attachSystemButtons(){this.addEventListenerSafe('#system-status-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showSystemStatus();});this.addEventListenerSafe('#analytics-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showAnalytics();});this.addEventListenerSafe('#clear-cache-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.clearSystemCache();});this.addEventListenerSafe('#optimize-db-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.optimizeDatabase();});this.addEventListenerSafe('#create-backup-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.createSystemBackup();});this.addEventListenerSafe('#force-init-printers-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.forceInitializePrinters();});}
|
||||
attachUserManagement(){this.addEventListenerSafe('#add-user-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showUserModal();});document.addEventListener('click',(e)=>{if(e.target.closest('.edit-user-btn')){e.preventDefault();e.stopPropagation();const userId=e.target.closest('button').dataset.userId;this.editUser(userId);}
|
||||
if(e.target.closest('.delete-user-btn')){e.preventDefault();e.stopPropagation();const userId=e.target.closest('button').dataset.userId;const userName=e.target.closest('button').dataset.userName;this.deleteUser(userId,userName);}});}
|
||||
attachPrinterManagement(){this.addEventListenerSafe('#add-printer-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showPrinterModal();});document.addEventListener('click',(e)=>{if(e.target.closest('.manage-printer-btn')){e.preventDefault();e.stopPropagation();const printerId=e.target.closest('button').dataset.printerId;this.managePrinter(printerId);}
|
||||
if(e.target.closest('.settings-printer-btn')){e.preventDefault();e.stopPropagation();const printerId=e.target.closest('button').dataset.printerId;this.showPrinterSettings(printerId);}
|
||||
if(e.target.closest('.toggle-printer-power-btn')){e.preventDefault();e.stopPropagation();const button=e.target.closest('button');const printerId=button.dataset.printerId;const printerName=button.dataset.printerName;this.togglePrinterPower(printerId,printerName,button);}});}
|
||||
attachJobManagement(){document.addEventListener('click',(e)=>{if(e.target.closest('.job-action-btn')){e.preventDefault();e.stopPropagation();const action=e.target.closest('button').dataset.action;const jobId=e.target.closest('button').dataset.jobId;this.handleJobAction(action,jobId);}});}
|
||||
attachModalEvents(){this.addEventListenerSafe('#fix-errors-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.fixErrors();});this.addEventListenerSafe('#dismiss-errors-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.dismissErrors();});this.addEventListenerSafe('#view-error-details-btn','click',(e)=>{e.preventDefault();e.stopPropagation();window.location.href='/admin-dashboard?tab=logs';});this.addEventListenerSafe('#refresh-logs-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.loadLogs();});this.addEventListenerSafe('#export-logs-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.exportLogs();});this.addEventListenerSafe('#log-level-filter','change',(e)=>{this.loadLogs();});}
|
||||
addEventListenerSafe(selector,event,handler){const element=document.querySelector(selector);if(element&&!element.dataset.listenerAttached){element.addEventListener(event,handler);element.dataset.listenerAttached='true';}}
|
||||
startLiveUpdates(){if(this.updateInterval){clearInterval(this.updateInterval);}
|
||||
this.updateInterval=setInterval(()=>{this.loadLiveStats();},30000);setInterval(()=>{this.updateLiveTime();},1000);setInterval(()=>{this.checkSystemHealth();},30000);console.log('🔄 Live-Updates gestartet');}
|
||||
async loadInitialData(){await this.loadLiveStats();await this.checkSystemHealth();setTimeout(()=>{this.testButtons();},1000);if(window.location.search.includes('tab=logs')||document.querySelector('.tabs [href*="logs"]')?.classList.contains('active')){await this.loadLogs();}
|
||||
const urlParams=new URLSearchParams(window.location.search);const activeTab=urlParams.get('tab');if(activeTab==='logs'){await this.loadLogs();}
|
||||
const logsContainer=document.getElementById('logs-container');if(logsContainer&&logsContainer.offsetParent!==null){await this.loadLogs();}}
|
||||
async loadLiveStats(){try{const url=`${this.apiBaseUrl}/api/stats`;const response=await fetch(url);if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
|
||||
const data=await response.json();this.updateStatsDisplay(data);this.retryCount=0;}catch(error){console.error('Fehler beim Laden der Live-Statistiken:',error);this.retryCount++;if(this.retryCount<=this.maxRetries){setTimeout(()=>this.loadLiveStats(),5000);}}}
|
||||
updateStatsDisplay(data){this.updateElement('live-users-count',data.total_users||0);this.updateElement('live-printers-count',data.total_printers||0);this.updateElement('live-printers-online',`${data.online_printers||0}online`);this.updateElement('live-jobs-active',data.active_jobs||0);this.updateElement('live-jobs-queued',`${data.queued_jobs||0}in Warteschlange`);this.updateElement('live-success-rate',`${data.success_rate||0}%`);this.updateProgressBar('users-progress',data.total_users,20);this.updateProgressBar('printers-progress',data.online_printers,data.total_printers);this.updateProgressBar('jobs-progress',data.active_jobs,10);this.updateProgressBar('success-progress',data.success_rate,100);console.log('📊 Live-Statistiken aktualisiert');}
|
||||
updateElement(elementId,value){const element=document.getElementById(elementId);if(element){element.textContent=value;}}
|
||||
updateProgressBar(progressId,currentValue,maxValue){const progressEl=document.getElementById(progressId);if(progressEl&¤tValue!==undefined&&maxValue>0){const percentage=Math.min(100,Math.max(0,(currentValue/maxValue)*100));progressEl.style.width=`${percentage}%`;}}
|
||||
updateLiveTime(){const timeElement=document.getElementById('live-time');if(timeElement){const now=new Date();timeElement.textContent=now.toLocaleTimeString('de-DE');}}
|
||||
async showSystemStatus(){console.log('🔧 System Status wird angezeigt');this.showNotification('System Status wird geladen...','info');}
|
||||
async showAnalytics(){console.log('📈 Analytics wird angezeigt');this.showNotification('Analytics werden geladen...','info');}
|
||||
async showMaintenance(){console.log('🛠️ Wartung wird angezeigt');const systemTab=document.querySelector('a[href*="tab=system"]');if(systemTab){systemTab.click();}}
|
||||
async clearSystemCache(){if(!confirm('🗑️ Möchten Sie wirklich den System-Cache leeren?'))return;try{const response=await fetch(`${this.apiBaseUrl}/api/admin/cache/clear`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Cache erfolgreich geleert!','success');setTimeout(()=>window.location.reload(),2000);}else{this.showNotification('❌ Fehler beim Leeren des Cache','error');}}catch(error){this.showNotification('❌ Fehler beim Leeren des Cache','error');}}
|
||||
async optimizeDatabase(){if(!confirm('🔧 Möchten Sie die Datenbank optimieren?'))return;this.showNotification('🔄 Datenbank wird optimiert...','info');try{const response=await fetch(`${this.apiBaseUrl}/api/admin/database/optimize`,{method:'POST',headers:{'X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Datenbank erfolgreich optimiert!','success');}else{this.showNotification('❌ Fehler bei der Datenbank-Optimierung','error');}}catch(error){this.showNotification('❌ Fehler bei der Datenbank-Optimierung','error');}}
|
||||
async createSystemBackup(){if(!confirm('💾 Möchten Sie ein System-Backup erstellen?'))return;this.showNotification('🔄 Backup wird erstellt...','info');try{const response=await fetch(`${this.apiBaseUrl}/api/admin/backup/create`,{method:'POST',headers:{'X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Backup erfolgreich erstellt!','success');}else{this.showNotification('❌ Fehler beim Erstellen des Backups','error');}}catch(error){this.showNotification('❌ Fehler beim Erstellen des Backups','error');}}
|
||||
async forceInitializePrinters(){if(!confirm('🔄 Möchten Sie die Drucker-Initialisierung erzwingen?'))return;this.showNotification('🔄 Drucker werden initialisiert...','info');try{const response=await fetch(`${this.apiBaseUrl}/api/admin/printers/force-init`,{method:'POST',headers:{'X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Drucker erfolgreich initialisiert!','success');setTimeout(()=>window.location.reload(),2000);}else{this.showNotification('❌ Fehler bei der Drucker-Initialisierung','error');}}catch(error){this.showNotification('❌ Fehler bei der Drucker-Initialisierung','error');}}
|
||||
showUserModal(userId=null){const isEdit=userId!==null;const title=isEdit?'Benutzer bearbeiten':'Neuer Benutzer';const modalHtml=`<div id="user-modal"class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"><div class="bg-white dark:bg-slate-800 rounded-2xl p-8 max-w-md w-full shadow-2xl transform scale-100 transition-all duration-300"><div class="flex justify-between items-center mb-6"><h3 class="text-2xl font-bold text-slate-900 dark:text-white">${title}</h3><button onclick="this.closest('#user-modal').remove()"class="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg transition-colors"><svg class="w-6 h-6 text-slate-500"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M6 18L18 6M6 6l12 12"/></svg></button></div><form id="user-form"class="space-y-4"><div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">E-Mail-Adresse*</label><input type="email"name="email"id="user-email"required
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all
|
||||
dark:bg-slate-700 dark:text-white bg-slate-50"></div><div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Benutzername</label><input type="text"name="username"id="user-username"
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all
|
||||
dark:bg-slate-700 dark:text-white bg-slate-50"></div><div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Name</label><input type="text"name="name"id="user-name"
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all
|
||||
dark:bg-slate-700 dark:text-white bg-slate-50"></div><div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Passwort ${isEdit?'(leer lassen für keine Änderung)':'*'}</label><input type="password"name="password"id="user-password"${!isEdit?'required':''}
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all
|
||||
dark:bg-slate-700 dark:text-white bg-slate-50"></div><div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Rolle</label><select name="role"id="user-role"
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all
|
||||
dark:bg-slate-700 dark:text-white bg-slate-50"><option value="user">Benutzer</option><option value="admin">Administrator</option></select></div>${isEdit?`<div class="flex items-center space-x-2"><input type="checkbox"name="is_active"id="user-active"
|
||||
class="w-4 h-4 text-blue-600 bg-slate-100 border-slate-300 rounded focus:ring-blue-500"><label for="user-active"class="text-sm font-medium text-slate-700 dark:text-slate-300">Aktiv</label></div>`:''}<div class="flex justify-end space-x-3 mt-8 pt-6 border-t border-slate-200 dark:border-slate-600"><button type="button"onclick="this.closest('#user-modal').remove()"
|
||||
class="px-6 py-3 bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-300
|
||||
rounded-xl hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors font-medium">Abbrechen</button><button type="submit"id="user-submit-btn"
|
||||
class="px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white
|
||||
rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300
|
||||
shadow-lg hover:shadow-xl font-medium">${isEdit?'Aktualisieren':'Erstellen'}</button></div></form></div></div>`;document.body.insertAdjacentHTML('beforeend',modalHtml);const form=document.getElementById('user-form');form.addEventListener('submit',(e)=>{e.preventDefault();e.stopPropagation();if(isEdit){this.updateUser(userId,new FormData(form));}else{this.createUser(new FormData(form));}});if(isEdit){this.loadUserData(userId);}
|
||||
setTimeout(()=>{document.getElementById('user-email').focus();},100);}
|
||||
async loadUserData(userId){try{const response=await fetch(`${this.apiBaseUrl}/api/admin/users/${userId}`);if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
|
||||
const data=await response.json();if(data.success){const user=data.user;document.getElementById('user-email').value=user.email||'';document.getElementById('user-username').value=user.username||'';document.getElementById('user-name').value=user.name||'';document.getElementById('user-role').value=user.is_admin?'admin':'user';const activeCheckbox=document.getElementById('user-active');if(activeCheckbox){activeCheckbox.checked=user.is_active!==false;}}else{this.showNotification('❌ Fehler beim Laden der Benutzerdaten','error');}}catch(error){console.error('Fehler beim Laden der Benutzerdaten:',error);this.showNotification('❌ Fehler beim Laden der Benutzerdaten','error');}}
|
||||
async createUser(formData){const submitBtn=document.getElementById('user-submit-btn');const originalText=submitBtn.innerHTML;try{submitBtn.innerHTML='<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-white mx-auto"></div>';submitBtn.disabled=true;const userData={email:formData.get('email'),username:formData.get('username')||formData.get('email').split('@')[0],name:formData.get('name'),password:formData.get('password'),is_admin:formData.get('role')==='admin'};const response=await fetch(`${this.apiBaseUrl}/api/admin/users`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken},body:JSON.stringify(userData)});const data=await response.json();if(data.success){this.showNotification('✅ Benutzer erfolgreich erstellt!','success');document.getElementById('user-modal').remove();setTimeout(()=>{window.location.reload();},1000);}else{this.showNotification(`❌ Fehler:${data.error}`,'error');}}catch(error){console.error('Fehler beim Erstellen des Benutzers:',error);this.showNotification('❌ Fehler beim Erstellen des Benutzers','error');}finally{submitBtn.innerHTML=originalText;submitBtn.disabled=false;}}
|
||||
async updateUser(userId,formData){const submitBtn=document.getElementById('user-submit-btn');const originalText=submitBtn.innerHTML;try{submitBtn.innerHTML='<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-white mx-auto"></div>';submitBtn.disabled=true;const userData={email:formData.get('email'),username:formData.get('username'),name:formData.get('name'),is_admin:formData.get('role')==='admin',is_active:formData.get('is_active')==='on'};const password=formData.get('password');if(password&&password.trim()){userData.password=password;}
|
||||
const response=await fetch(`${this.apiBaseUrl}/api/admin/users/${userId}`,{method:'PUT',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken},body:JSON.stringify(userData)});const data=await response.json();if(data.success){this.showNotification('✅ Benutzer erfolgreich aktualisiert!','success');document.getElementById('user-modal').remove();setTimeout(()=>{window.location.reload();},1000);}else{this.showNotification(`❌ Fehler:${data.error}`,'error');}}catch(error){console.error('Fehler beim Aktualisieren des Benutzers:',error);this.showNotification('❌ Fehler beim Aktualisieren des Benutzers','error');}finally{submitBtn.innerHTML=originalText;submitBtn.disabled=false;}}
|
||||
editUser(userId){console.log(`✏️ Benutzer ${userId}wird bearbeitet`);this.showUserModal(userId);}
|
||||
async deleteUser(userId,userName){if(!confirm(`🗑️ Möchten Sie den Benutzer"${userName}"wirklich löschen?\n\nDiese Aktion kann nicht rückgängig gemacht werden!`)){return;}
|
||||
try{this.showNotification(`🔄 Benutzer"${userName}"wird gelöscht...`,'info');const response=await fetch(`${this.apiBaseUrl}/api/admin/users/${userId}`,{method:'DELETE',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification(`✅ Benutzer"${userName}"erfolgreich gelöscht!`,'success');setTimeout(()=>{window.location.reload();},1000);}else{this.showNotification(`❌ Fehler beim Löschen:${data.error}`,'error');}}catch(error){console.error('Fehler beim Löschen des Benutzers:',error);this.showNotification('❌ Fehler beim Löschen des Benutzers','error');}}
|
||||
showPrinterModal(){console.log('🖨️ Drucker-Modal wird angezeigt');this.showNotification('Drucker-Funktionen werden geladen...','info');}
|
||||
managePrinter(printerId){console.log(`🔧 Drucker ${printerId}wird verwaltet`);this.showNotification(`Drucker ${printerId}wird verwaltet...`,'info');}
|
||||
showPrinterSettings(printerId){console.log(`⚙️ Drucker-Einstellungen ${printerId}werden angezeigt`);this.showNotification(`Drucker-Einstellungen werden geladen...`,'info');}
|
||||
async togglePrinterPower(printerId,printerName,button){console.log(`🔌 Smart-Plug Toggle für Drucker ${printerId}(${printerName})`);const confirmMessage=`Möchten Sie die Steckdose für"${printerName}"umschalten?\n\nDies schaltet den Drucker ein/aus.`;if(!confirm(confirmMessage))return;const originalContent=button.innerHTML;button.disabled=true;button.innerHTML=`<div class="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div><span class="hidden lg:inline">Schaltet...</span>`;try{const response=await fetch(`${this.apiBaseUrl}/api/admin/printers/${printerId}/toggle`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken},body:JSON.stringify({reason:'Admin-Panel Smart-Plug Toggle'})});const data=await response.json();if(response.ok&&data.success){const action=data.action||'umgeschaltet';this.showNotification(`✅ Steckdose für"${printerName}"erfolgreich ${action}`,'success');button.classList.remove('from-orange-500','to-red-500');button.classList.add('from-green-500','to-green-600');setTimeout(()=>{button.classList.remove('from-green-500','to-green-600');button.classList.add('from-orange-500','to-red-500');},2000);setTimeout(()=>{this.loadLiveStats();},1000);}else{const errorMsg=data.error||'Unbekannter Fehler beim Schalten der Steckdose';this.showNotification(`❌ Fehler:${errorMsg}`,'error');console.error('Smart-Plug Toggle Fehler:',data);}}catch(error){console.error('Netzwerkfehler beim Smart-Plug Toggle:',error);this.showNotification(`❌ Netzwerkfehler beim Schalten der Steckdose für"${printerName}"`,'error');}finally{button.disabled=false;button.innerHTML=originalContent;}}
|
||||
handleJobAction(action,jobId){console.log(`📋 Job-Aktion"${action}"für Job ${jobId}`);this.showNotification(`Job-Aktion"${action}"wird ausgeführt...`,'info');}
|
||||
async checkSystemHealth(){try{const response=await fetch('/api/admin/system-health');const data=await response.json();if(data.success){this.updateHealthDisplay(data);this.updateErrorAlerts(data);}}catch(error){console.error('Fehler bei System-Health-Check:',error);}}
|
||||
updateHealthDisplay(data){const statusIndicator=document.getElementById('db-status-indicator');const statusText=document.getElementById('db-status-text');if(statusIndicator&&statusText){if(data.health_status==='critical'){statusIndicator.className='w-3 h-3 bg-red-500 rounded-full animate-pulse';statusText.textContent='Kritisch';statusText.className='text-sm font-medium text-red-600 dark:text-red-400';}else if(data.health_status==='warning'){statusIndicator.className='w-3 h-3 bg-yellow-500 rounded-full animate-pulse';statusText.textContent='Warnung';statusText.className='text-sm font-medium text-yellow-600 dark:text-yellow-400';}else{statusIndicator.className='w-3 h-3 bg-green-400 rounded-full animate-pulse';statusText.textContent='Gesund';statusText.className='text-sm font-medium text-green-600 dark:text-green-400';}}
|
||||
this.updateElement('last-migration',data.last_migration||'Unbekannt');this.updateElement('schema-integrity',data.schema_integrity||'Prüfung');this.updateElement('recent-errors-count',data.recent_errors_count||0);}
|
||||
updateErrorAlerts(data){const alertContainer=document.getElementById('critical-errors-alert');if(!alertContainer)return;const allErrors=[...(data.critical_errors||[]),...(data.warnings||[])];if(allErrors.length>0){alertContainer.classList.remove('hidden');}else{alertContainer.classList.add('hidden');}}
|
||||
async fixErrors(){if(!confirm('🔧 Möchten Sie die automatische Fehlerkorrektur durchführen?'))return;this.showNotification('🔄 Fehler werden automatisch behoben...','info');if(!this.csrfToken){console.error('❌ CSRF Token fehlt! Versuche Token neu zu laden...');this.csrfToken=this.extractCSRFToken();if(!this.csrfToken){this.showNotification('❌ Sicherheitsfehler: CSRF Token nicht verfügbar','error');return;}}
|
||||
console.log('🔧 Starte automatische Fehlerkorrektur...');console.log('🔒 CSRF Token für Request:',this.csrfToken.substring(0,10)+'...');try{const requestOptions={method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken}};console.log('📡 Sende Request an:','/api/admin/fix-errors');console.log('📝 Request Headers:',requestOptions.headers);const response=await fetch('/api/admin/fix-errors',requestOptions);console.log('📡 Response Status:',response.status);console.log('📡 Response Headers:',Object.fromEntries(response.headers.entries()));if(!response.ok){const errorText=await response.text();console.error('❌ Response Error:',errorText);throw new Error(`HTTP ${response.status}:${response.statusText}-${errorText}`);}
|
||||
const data=await response.json();console.log('✅ Response Data:',data);if(data.success){this.showNotification('✅ Automatische Reparatur erfolgreich!','success');setTimeout(()=>this.checkSystemHealth(),2000);}else{this.showNotification(`❌ Automatische Reparatur fehlgeschlagen:${data.message||'Unbekannter Fehler'}`,'error');}}catch(error){console.error('❌ Fehler bei automatischer Reparatur:',error);this.showNotification(`❌ Fehler bei der automatischen Reparatur:${error.message}`,'error');}}
|
||||
dismissErrors(){const alertContainer=document.getElementById('critical-errors-alert');if(alertContainer){alertContainer.classList.add('hidden');}}
|
||||
showNotification(message,type='info'){let notification=document.getElementById('admin-notification');if(!notification){notification=document.createElement('div');notification.id='admin-notification';notification.className='fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm transition-all duration-300';document.body.appendChild(notification);}
|
||||
const colors={success:'bg-green-500 text-white',error:'bg-red-500 text-white',info:'bg-blue-500 text-white',warning:'bg-yellow-500 text-white'};notification.className=`fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm transition-all duration-300 ${colors[type]}`;notification.textContent=message;notification.style.transform='translateX(0)';setTimeout(()=>{if(notification){notification.style.transform='translateX(100%)';setTimeout(()=>{if(notification&¬ification.parentNode){notification.parentNode.removeChild(notification);}},300);}},3000);}
|
||||
async loadLogs(level=null){const logsContainer=document.getElementById('logs-container');if(!logsContainer)return;logsContainer.innerHTML=`<div class="flex justify-center items-center py-12"><div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 dark:border-blue-400"></div><span class="ml-3 text-slate-600 dark:text-slate-400">Logs werden geladen...</span></div>`;try{const filter=level||document.getElementById('log-level-filter')?.value||'all';const url=`${this.apiBaseUrl}/api/admin/logs?level=${filter}&limit=100`;const response=await fetch(url,{headers:{'X-CSRFToken':this.csrfToken}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
|
||||
const data=await response.json();this.displayLogs(data.logs||[]);console.log('📋 Logs erfolgreich geladen');}catch(error){console.error('Fehler beim Laden der Logs:',error);logsContainer.innerHTML=`<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl p-6 text-center"><svg class="w-12 h-12 text-red-500 mx-auto mb-4"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg><h3 class="text-lg font-semibold text-red-800 dark:text-red-200 mb-2">Fehler beim Laden der Logs</h3><p class="text-red-600 dark:text-red-400 mb-4">${error.message}</p><button onclick="adminDashboard.loadLogs()"class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">🔄 Erneut versuchen</button></div>`;}}
|
||||
displayLogs(logs){const logsContainer=document.getElementById('logs-container');if(!logsContainer)return;if(!logs||logs.length===0){logsContainer.innerHTML=`<div class="text-center py-12"><svg class="w-16 h-16 mx-auto text-slate-400 dark:text-slate-500 mb-4"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg><h3 class="text-lg font-medium text-slate-900 dark:text-white mb-2">Keine Logs gefunden</h3><p class="text-slate-500 dark:text-slate-400">Es sind keine Logs für die ausgewählten Kriterien vorhanden.</p></div>`;return;}
|
||||
const logsHtml=logs.map(log=>{const levelColors={'error':'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-200','warning':'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-200','info':'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200','debug':'bg-gray-50 dark:bg-gray-900/20 border-gray-200 dark:border-gray-800 text-gray-800 dark:text-gray-200','critical':'bg-red-100 dark:bg-red-900/40 border-red-300 dark:border-red-700 text-red-900 dark:text-red-100'};const levelIcons={'error':'❌','warning':'⚠️','info':'ℹ️','debug':'🔍','critical':'🚨'};const levelClass=levelColors[log.level]||levelColors['info'];const levelIcon=levelIcons[log.level]||'ℹ️';return`<div class="border rounded-xl p-4 transition-all duration-200 hover:shadow-md ${levelClass}"><div class="flex items-start justify-between mb-2"><div class="flex items-center space-x-2"><span class="text-lg">${levelIcon}</span><span class="font-semibold text-sm uppercase tracking-wide">${log.level}</span><span class="text-xs opacity-75">${log.component||'System'}</span></div><div class="text-xs opacity-75">${this.formatLogTimestamp(log.timestamp)}</div></div><div class="mb-2"><p class="font-medium">${this.escapeHtml(log.message)}</p>${log.details?`<p class="text-sm opacity-75 mt-1">${this.escapeHtml(log.details)}</p>`:''}</div>${log.user?`<div class="text-xs opacity-75"><span class="font-medium">Benutzer:</span>${this.escapeHtml(log.user)}</div>`:''}
|
||||
${log.ip_address?`<div class="text-xs opacity-75"><span class="font-medium">IP:</span>${this.escapeHtml(log.ip_address)}</div>`:''}
|
||||
${log.request_id?`<div class="text-xs opacity-75 mt-2 font-mono"><span class="font-medium">Request-ID:</span>${this.escapeHtml(log.request_id)}</div>`:''}</div>`;}).join('');logsContainer.innerHTML=logsHtml;}
|
||||
formatLogTimestamp(timestamp){if(!timestamp)return'Unbekannt';try{const date=new Date(timestamp);return date.toLocaleString('de-DE',{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'});}catch(error){return timestamp;}}
|
||||
escapeHtml(text){if(!text)return'';const div=document.createElement('div');div.textContent=text;return div.innerHTML;}
|
||||
async exportLogs(){try{this.showNotification('📥 Logs werden exportiert...','info');const filter=document.getElementById('log-level-filter')?.value||'all';const url=`${this.apiBaseUrl}/api/admin/logs/export?level=${filter}`;const response=await fetch(url,{headers:{'X-CSRFToken':this.csrfToken}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
|
||||
const blob=await response.blob();const downloadUrl=window.URL.createObjectURL(blob);const a=document.createElement('a');a.href=downloadUrl;a.download=`system-logs-${new Date().toISOString().split('T')[0]}.csv`;document.body.appendChild(a);a.click();document.body.removeChild(a);window.URL.revokeObjectURL(downloadUrl);this.showNotification('✅ Logs erfolgreich exportiert!','success');}catch(error){console.error('Fehler beim Exportieren der Logs:',error);this.showNotification('❌ Fehler beim Exportieren der Logs: '+error.message,'error');}}
|
||||
testButtons(){console.log('🧪 Teste Button-Funktionalität...');const fixBtn=document.querySelector('#fix-errors-btn');if(fixBtn){console.log('✅ Fix-Errors Button gefunden:',fixBtn);console.log('🔗 Event-Listener-Status:',fixBtn.dataset.listenerAttached);fixBtn.addEventListener('click',(e)=>{console.log('🖱️ Fix-Errors Button wurde geklickt (manueller Listener)');e.preventDefault();e.stopPropagation();this.testFixErrors();});}else{console.error('❌ Fix-Errors Button NICHT gefunden!');}
|
||||
const viewBtn=document.querySelector('#view-error-details-btn');if(viewBtn){console.log('✅ View-Details Button gefunden:',viewBtn);console.log('🔗 Event-Listener-Status:',viewBtn.dataset.listenerAttached);viewBtn.addEventListener('click',(e)=>{console.log('🖱️ View-Details Button wurde geklickt (manueller Listener)');e.preventDefault();e.stopPropagation();console.log('🔄 Weiterleitung zu Logs-Tab...');window.location.href='/admin-dashboard?tab=logs';});}else{console.error('❌ View-Details Button NICHT gefunden!');}}
|
||||
async testFixErrors(){console.log('🧪 TEST: Fix-Errors wird ausgeführt...');console.log('🔒 Aktueller CSRF Token:',this.csrfToken);if(!this.csrfToken){console.error('❌ CSRF Token fehlt - versuche neu zu laden...');this.csrfToken=this.extractCSRFToken();console.log('🔒 Neu geladener Token:',this.csrfToken);}
|
||||
this.showNotification('🧪 TEST: Starte automatische Fehlerkorrektur...','info');try{const requestOptions={method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken,'X-Requested-With':'XMLHttpRequest'}};console.log('📡 TEST Request an:','/api/admin/fix-errors');console.log('📝 TEST Headers:',requestOptions.headers);const response=await fetch('/api/admin/fix-errors',requestOptions);console.log('📡 TEST Response Status:',response.status);console.log('📡 TEST Response Headers:',Object.fromEntries(response.headers.entries()));if(!response.ok){const errorText=await response.text();console.error('❌ TEST Response Error:',errorText);this.showNotification(`❌ TEST Fehler:${response.status}-${errorText}`,'error');return;}
|
||||
const data=await response.json();console.log('✅ TEST Response Data:',data);if(data.success){this.showNotification('✅ TEST: Automatische Reparatur erfolgreich!','success');}else{this.showNotification(`❌ TEST:Reparatur fehlgeschlagen-${data.message}`,'error');}}catch(error){console.error('❌ TEST Fehler:',error);this.showNotification(`❌ TEST Netzwerk-Fehler:${error.message}`,'error');}}}
|
||||
let adminDashboardInstance=null;document.addEventListener('DOMContentLoaded',function(){if(!adminDashboardInstance){adminDashboardInstance=new AdminDashboard();window.AdminDashboard=adminDashboardInstance;console.log('🎯 Admin Dashboard erfolgreich initialisiert (unified)');}});window.AdminDashboard=AdminDashboard;
|
||||
BIN
static/js/admin-unified.min.js.gz
Normal file
BIN
static/js/admin-unified.min.js.gz
Normal file
Binary file not shown.
752
static/js/advanced-components.js
Normal file
752
static/js/advanced-components.js
Normal 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');
|
||||
});
|
||||
|
||||
})();
|
||||
BIN
static/js/advanced-components.js.gz
Normal file
BIN
static/js/advanced-components.js.gz
Normal file
Binary file not shown.
79
static/js/advanced-components.min.js
vendored
Normal file
79
static/js/advanced-components.min.js
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
(function(){'use strict';window.MYP=window.MYP||{};window.MYP.Advanced=window.MYP.Advanced||{};class ProgressBar{constructor(container,options={}){this.container=typeof container==='string'?document.querySelector(container):container;this.options={value:0,max:100,showLabel:true,showPercentage:true,animated:true,color:'blue',size:'md',striped:false,...options};this.currentValue=this.options.value;this.init();}
|
||||
init(){if(!this.container){console.error('ProgressBar: Container nicht gefunden');return;}
|
||||
this.render();}
|
||||
render(){const percentage=Math.round((this.currentValue/this.options.max)*100);const sizeClass=this.getSizeClass();const colorClass=this.getColorClass();this.container.innerHTML=`<div class="progress-bar-container ${sizeClass}">${this.options.showLabel?`<div class="flex justify-between items-center mb-2"><span class="text-sm font-medium text-slate-700 dark:text-slate-300">${this.options.label||'Fortschritt'}</span>${this.options.showPercentage?`<span class="text-sm font-medium text-slate-700 dark:text-slate-300">${percentage}%</span>`:''}</div>`:''}<div class="progress-bar-track ${sizeClass}"><div class="progress-bar-fill ${colorClass} ${this.options.animated ? 'animated' : ''} ${this.options.striped ? 'striped' : ''}"
|
||||
style="width: ${percentage}%"
|
||||
role="progressbar"
|
||||
aria-valuenow="${this.currentValue}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="${this.options.max}"></div></div></div>`;}
|
||||
getSizeClass(){const sizes={'sm':'h-2','md':'h-3','lg':'h-4','xl':'h-6'};return sizes[this.options.size]||sizes.md;}
|
||||
getColorClass(){const colors={'blue':'bg-blue-500','green':'bg-green-500','red':'bg-red-500','yellow':'bg-yellow-500','purple':'bg-purple-500','indigo':'bg-indigo-500'};return colors[this.options.color]||colors.blue;}
|
||||
setValue(value,animate=true){const oldValue=this.currentValue;this.currentValue=Math.max(0,Math.min(this.options.max,value));if(animate){this.animateToValue(oldValue,this.currentValue);}else{this.render();}}
|
||||
animateToValue(from,to){const duration=500;const steps=60;const stepValue=(to-from)/steps;let currentStep=0;const animate=()=>{if(currentStep<steps){this.currentValue=from+(stepValue*currentStep);this.render();currentStep++;requestAnimationFrame(animate);}else{this.currentValue=to;this.render();}};animate();}
|
||||
increment(amount=1){this.setValue(this.currentValue+amount);}
|
||||
decrement(amount=1){this.setValue(this.currentValue-amount);}
|
||||
reset(){this.setValue(0);}
|
||||
complete(){this.setValue(this.options.max);}}
|
||||
class FileUpload{constructor(container,options={}){this.container=typeof container==='string'?document.querySelector(container):container;this.options={multiple:false,accept:'*/*',maxSize:50*1024*1024,maxFiles:10,dragDrop:true,showProgress:true,showPreview:true,uploadUrl:'/api/upload',chunkSize:1024*1024,...options};this.files=[];this.uploads=new Map();this.init();}
|
||||
init(){if(!this.container){console.error('FileUpload: Container nicht gefunden');return;}
|
||||
this.render();this.setupEventListeners();}
|
||||
render(){this.container.innerHTML=`<div class="file-upload-area"id="fileUploadArea"><div class="file-upload-dropzone ${this.options.dragDrop ? 'drag-enabled' : ''}"id="dropzone"><div class="text-center py-12"><svg class="mx-auto h-12 w-12 text-slate-400"stroke="currentColor"fill="none"viewBox="0 0 48 48"><path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"stroke-width="2"stroke-linecap="round"stroke-linejoin="round"/></svg><div class="mt-4"><label for="fileInput"class="cursor-pointer"><span class="mt-2 block text-sm font-medium text-slate-900 dark:text-white">${this.options.dragDrop?'Dateien hierher ziehen oder':''}<span class="text-blue-600 dark:text-blue-400 hover:text-blue-500">durchsuchen</span></span></label><input type="file"
|
||||
id="fileInput"
|
||||
name="files"
|
||||
${this.options.multiple?'multiple':''}
|
||||
accept="${this.options.accept}"
|
||||
class="sr-only"></div><p class="mt-1 text-xs text-slate-500 dark:text-slate-400">${this.getFileTypeText()}• Max.${this.formatFileSize(this.options.maxSize)}</p></div></div><div class="file-list mt-4"id="fileList"></div></div>`;}
|
||||
setupEventListeners(){const fileInput=this.container.querySelector('#fileInput');const dropzone=this.container.querySelector('#dropzone');fileInput.addEventListener('change',(e)=>{this.handleFiles(Array.from(e.target.files));});if(this.options.dragDrop){dropzone.addEventListener('dragover',(e)=>{e.preventDefault();dropzone.classList.add('drag-over');});dropzone.addEventListener('dragleave',(e)=>{e.preventDefault();dropzone.classList.remove('drag-over');});dropzone.addEventListener('drop',(e)=>{e.preventDefault();dropzone.classList.remove('drag-over');this.handleFiles(Array.from(e.dataTransfer.files));});}}
|
||||
handleFiles(fileList){for(const file of fileList){if(this.validateFile(file)){this.addFile(file);}}
|
||||
this.renderFileList();}
|
||||
validateFile(file){if(file.size>this.options.maxSize){this.showError(`Datei"${file.name}"ist zu groß.Maximum:${this.formatFileSize(this.options.maxSize)}`);return false;}
|
||||
if(!this.options.multiple&&this.files.length>0){this.files=[];}else if(this.files.length>=this.options.maxFiles){this.showError(`Maximal ${this.options.maxFiles}Dateien erlaubt`);return false;}
|
||||
return true;}
|
||||
addFile(file){const fileData={id:this.generateId(),file:file,name:file.name,size:file.size,type:file.type,status:'pending',progress:0,error:null};this.files.push(fileData);if(this.options.showPreview&&file.type.startsWith('image/')){this.generatePreview(fileData);}}
|
||||
generatePreview(fileData){const reader=new FileReader();reader.onload=(e)=>{fileData.preview=e.target.result;this.renderFileList();};reader.readAsDataURL(fileData.file);}
|
||||
renderFileList(){const fileListContainer=this.container.querySelector('#fileList');if(this.files.length===0){fileListContainer.innerHTML='';return;}
|
||||
fileListContainer.innerHTML=this.files.map(fileData=>`<div class="file-item"data-file-id="${fileData.id}"><div class="flex items-center space-x-4 p-4 bg-slate-50 dark:bg-slate-800 rounded-lg">${fileData.preview?`<img src="${fileData.preview}"class="w-12 h-12 object-cover rounded"alt="Preview">`:`<div class="w-12 h-12 bg-slate-200 dark:bg-slate-700 rounded flex items-center justify-center"><svg class="w-6 h-6 text-slate-400"fill="currentColor"viewBox="0 0 20 20"><path fill-rule="evenodd"d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"clip-rule="evenodd"/></svg></div>`}<div class="flex-1 min-w-0"><div class="flex items-center justify-between"><p class="text-sm font-medium text-slate-900 dark:text-white truncate">${fileData.name}</p><button class="remove-file text-slate-400 hover:text-red-500"data-file-id="${fileData.id}"><svg class="w-4 h-4"fill="currentColor"viewBox="0 0 20 20"><path fill-rule="evenodd"d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"clip-rule="evenodd"/></svg></button></div><p class="text-xs text-slate-500 dark:text-slate-400">${this.formatFileSize(fileData.size)}• ${this.getStatusText(fileData.status)}</p>${this.options.showProgress&&fileData.status==='uploading'?`<div class="mt-2"><div class="w-full bg-slate-200 dark:bg-slate-600 rounded-full h-2"><div class="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
||||
style="width: ${fileData.progress}%"></div></div></div>`:''}
|
||||
${fileData.error?`<p class="text-xs text-red-500 mt-1">${fileData.error}</p>`:''}</div></div></div>`).join('');fileListContainer.querySelectorAll('.remove-file').forEach(button=>{button.addEventListener('click',(e)=>{const fileId=e.target.closest('.remove-file').dataset.fileId;this.removeFile(fileId);});});}
|
||||
removeFile(fileId){this.files=this.files.filter(f=>f.id!==fileId);this.renderFileList();}
|
||||
async uploadFiles(){const pendingFiles=this.files.filter(f=>f.status==='pending');for(const fileData of pendingFiles){await this.uploadFile(fileData);}}
|
||||
async uploadFile(fileData){fileData.status='uploading';fileData.progress=0;this.renderFileList();try{const formData=new FormData();formData.append('file',fileData.file);const response=await fetch(this.options.uploadUrl,{method:'POST',body:formData,headers:{'X-CSRFToken':document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')||''}});if(response.ok){fileData.status='completed';fileData.progress=100;const result=await response.json();fileData.url=result.url;}else{throw new Error(`Upload fehlgeschlagen:${response.status}`);}}catch(error){fileData.status='error';fileData.error=error.message;}
|
||||
this.renderFileList();}
|
||||
getFileTypeText(){if(this.options.accept==='*/*')return'Alle Dateitypen';if(this.options.accept.includes('image/'))return'Bilder';if(this.options.accept.includes('.pdf'))return'PDF-Dateien';return'Spezifische Dateitypen';}
|
||||
getStatusText(status){const statusTexts={'pending':'Wartend','uploading':'Wird hochgeladen...','completed':'Abgeschlossen','error':'Fehler'};return statusTexts[status]||status;}
|
||||
formatFileSize(bytes){if(bytes===0)return'0 Bytes';const k=1024;const sizes=['Bytes','KB','MB','GB'];const i=Math.floor(Math.log(bytes)/Math.log(k));return parseFloat((bytes/Math.pow(k,i)).toFixed(2))+' '+sizes[i];}
|
||||
generateId(){return'file_'+Math.random().toString(36).substr(2,9);}
|
||||
showError(message){if(window.showToast){window.showToast(message,'error');}else{alert(message);}}
|
||||
getFiles(){return this.files;}
|
||||
getCompletedFiles(){return this.files.filter(f=>f.status==='completed');}
|
||||
clear(){this.files=[];this.renderFileList();}}
|
||||
class DatePicker{constructor(input,options={}){this.input=typeof input==='string'?document.querySelector(input):input;this.options={format:'dd.mm.yyyy',minDate:null,maxDate:null,disabledDates:[],language:'de',closeOnSelect:true,showWeekNumbers:false,...options};this.isOpen=false;this.currentDate=new Date();this.selectedDate=null;this.init();}
|
||||
init(){if(!this.input){console.error('DatePicker: Input-Element nicht gefunden');return;}
|
||||
this.setupInput();this.createCalendar();this.setupEventListeners();}
|
||||
setupInput(){this.input.setAttribute('readonly','true');this.input.classList.add('datepicker-input');this.container=document.createElement('div');this.container.className='datepicker-container relative';this.input.parentNode.insertBefore(this.container,this.input);this.container.appendChild(this.input);}
|
||||
createCalendar(){this.calendar=document.createElement('div');this.calendar.className='datepicker-calendar absolute top-full left-0 mt-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-lg shadow-lg z-50 hidden';this.calendar.innerHTML=this.renderCalendar();this.container.appendChild(this.calendar);}
|
||||
renderCalendar(){const year=this.currentDate.getFullYear();const month=this.currentDate.getMonth();const monthName=this.getMonthName(month);return`<div class="datepicker-header p-4 border-b border-slate-200 dark:border-slate-600"><div class="flex items-center justify-between"><button type="button"class="prev-month p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded"><svg class="w-4 h-4"fill="currentColor"viewBox="0 0 20 20"><path fill-rule="evenodd"d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"clip-rule="evenodd"/></svg></button><div class="text-sm font-medium text-slate-900 dark:text-white">${monthName}${year}</div><button type="button"class="next-month p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded"><svg class="w-4 h-4"fill="currentColor"viewBox="0 0 20 20"><path fill-rule="evenodd"d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"clip-rule="evenodd"/></svg></button></div></div><div class="datepicker-body p-4"><div class="grid grid-cols-7 gap-1 mb-2">${this.getWeekdayHeaders()}</div><div class="grid grid-cols-7 gap-1">${this.getDaysOfMonth(year,month)}</div></div>`;}
|
||||
getWeekdayHeaders(){const weekdays=['Mo','Di','Mi','Do','Fr','Sa','So'];return weekdays.map(day=>`<div class="text-xs font-medium text-slate-500 dark:text-slate-400 text-center p-1">${day}</div>`).join('');}
|
||||
getDaysOfMonth(year,month){const firstDay=new Date(year,month,1);const lastDay=new Date(year,month+1,0);const startDate=new Date(firstDay);startDate.setDate(startDate.getDate()-((firstDay.getDay()+6)%7));const days=[];const current=new Date(startDate);while(current<=lastDay||current.getMonth()===month){const isCurrentMonth=current.getMonth()===month;const isToday=this.isToday(current);const isSelected=this.isSelectedDate(current);const isDisabled=this.isDisabledDate(current);const classes=['w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center transition-colors',isCurrentMonth?'text-slate-900 dark:text-white':'text-slate-400 dark:text-slate-600',isToday?'bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100':'',isSelected?'bg-blue-500 text-white':'',!isDisabled&&isCurrentMonth?'hover:bg-slate-100 dark:hover:bg-slate-700':'',isDisabled?'cursor-not-allowed opacity-50':''].filter(Boolean);days.push(`<div class="${classes.join(' ')}"
|
||||
data-date="${this.formatDateForData(current)}"
|
||||
${isDisabled?'':'data-selectable="true"'}>${current.getDate()}</div>`);current.setDate(current.getDate()+1);if(days.length>=42)break;}
|
||||
return days.join('');}
|
||||
setupEventListeners(){this.input.addEventListener('click',()=>{this.toggle();});this.calendar.addEventListener('click',(e)=>{if(e.target.classList.contains('prev-month')){this.previousMonth();}else if(e.target.classList.contains('next-month')){this.nextMonth();}else if(e.target.dataset.selectable){this.selectDate(new Date(e.target.dataset.date));}});document.addEventListener('click',(e)=>{if(!this.container.contains(e.target)){this.close();}});}
|
||||
toggle(){if(this.isOpen){this.close();}else{this.open();}}
|
||||
open(){this.calendar.classList.remove('hidden');this.isOpen=true;this.updateCalendar();}
|
||||
close(){this.calendar.classList.add('hidden');this.isOpen=false;}
|
||||
selectDate(date){this.selectedDate=new Date(date);this.input.value=this.formatDate(date);this.input.dispatchEvent(new CustomEvent('dateselected',{detail:{date:new Date(date)}}));if(this.options.closeOnSelect){this.close();}}
|
||||
previousMonth(){this.currentDate.setMonth(this.currentDate.getMonth()-1);this.updateCalendar();}
|
||||
nextMonth(){this.currentDate.setMonth(this.currentDate.getMonth()+1);this.updateCalendar();}
|
||||
updateCalendar(){this.calendar.innerHTML=this.renderCalendar();}
|
||||
isToday(date){const today=new Date();return date.toDateString()===today.toDateString();}
|
||||
isSelectedDate(date){return this.selectedDate&&date.toDateString()===this.selectedDate.toDateString();}
|
||||
isDisabledDate(date){if(this.options.minDate&&date<this.options.minDate)return true;if(this.options.maxDate&&date>this.options.maxDate)return true;return this.options.disabledDates.some(disabled=>date.toDateString()===disabled.toDateString());}
|
||||
formatDate(date){const day=date.getDate().toString().padStart(2,'0');const month=(date.getMonth()+1).toString().padStart(2,'0');const year=date.getFullYear();return this.options.format.replace('dd',day).replace('mm',month).replace('yyyy',year);}
|
||||
formatDateForData(date){return date.toISOString().split('T')[0];}
|
||||
getMonthName(monthIndex){const months=['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];return months[monthIndex];}
|
||||
setValue(date){if(date){this.selectDate(new Date(date));}}
|
||||
getValue(){return this.selectedDate;}
|
||||
clear(){this.selectedDate=null;this.input.value='';}}
|
||||
window.MYP.Advanced={ProgressBar,FileUpload,DatePicker,createProgressBar:(container,options)=>new ProgressBar(container,options),createFileUpload:(container,options)=>new FileUpload(container,options),createDatePicker:(input,options)=>new DatePicker(input,options)};document.addEventListener('DOMContentLoaded',function(){document.querySelectorAll('[data-datepicker]').forEach(input=>{const options=JSON.parse(input.dataset.datepicker||'{}');new DatePicker(input,options);});document.querySelectorAll('[data-file-upload]').forEach(container=>{const options=JSON.parse(container.dataset.fileUpload||'{}');new FileUpload(container,options);});console.log('🚀 MYP Advanced Components geladen');});})();
|
||||
BIN
static/js/advanced-components.min.js.gz
Normal file
BIN
static/js/advanced-components.min.js.gz
Normal file
Binary file not shown.
143
static/js/auto-logout.js
Normal file
143
static/js/auto-logout.js
Normal 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
BIN
static/js/auto-logout.js.gz
Normal file
Binary file not shown.
14
static/js/auto-logout.min.js
vendored
Normal file
14
static/js/auto-logout.min.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
class AutoLogoutManager{constructor(){this.timer=null;this.warningTimer=null;this.timeout=60;this.warningTime=5;this.isWarningShown=false;this.init();}
|
||||
async init(){await this.loadSettings();this.setupActivityListeners();this.startTimer();}
|
||||
async loadSettings(){try{const response=await fetch('/api/user/settings');if(response.ok){const data=await response.json();if(data.success&&data.settings.privacy?.auto_logout){const timeout=parseInt(data.settings.privacy.auto_logout);if(timeout>0&&timeout!=='never'){this.timeout=timeout;}else{this.timeout=0;}}}}catch(error){console.warn('Auto-Logout-Einstellungen konnten nicht geladen werden:',error);}}
|
||||
setupActivityListeners(){const events=['mousedown','mousemove','keypress','scroll','touchstart','click'];events.forEach(event=>{document.addEventListener(event,()=>this.resetTimer(),{passive:true});});}
|
||||
startTimer(){if(this.timeout<=0)return;this.clearTimers();const timeoutMs=this.timeout*60*1000;const warningMs=this.warningTime*60*1000;this.warningTimer=setTimeout(()=>this.showWarning(),timeoutMs-warningMs);this.timer=setTimeout(()=>this.performLogout(),timeoutMs);}
|
||||
resetTimer(){if(this.isWarningShown){this.closeWarning();}
|
||||
this.startTimer();}
|
||||
clearTimers(){if(this.timer)clearTimeout(this.timer);if(this.warningTimer)clearTimeout(this.warningTimer);}
|
||||
showWarning(){if(this.isWarningShown)return;this.isWarningShown=true;const modal=document.createElement('div');modal.id='auto-logout-warning';modal.className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';modal.innerHTML=`<div class="bg-white dark:bg-slate-800 rounded-lg p-6 max-w-md mx-4 shadow-xl"><h3 class="text-lg font-medium text-slate-900 dark:text-white mb-4">Automatische Abmeldung</h3><p class="text-sm text-slate-600 dark:text-slate-300 mb-4">Sie werden in ${this.warningTime}Minuten aufgrund von Inaktivität abgemeldet.</p><div class="flex space-x-3"><button id="stay-logged-in"class="bg-blue-600 text-white px-4 py-2 rounded-lg">Angemeldet bleiben</button><button id="logout-now"class="bg-gray-300 text-slate-700 px-4 py-2 rounded-lg">Jetzt abmelden</button></div></div>`;document.body.appendChild(modal);document.getElementById('stay-logged-in').onclick=()=>{this.closeWarning();this.sendKeepAlive();this.resetTimer();};document.getElementById('logout-now').onclick=()=>{this.performLogout();};}
|
||||
closeWarning(){const modal=document.getElementById('auto-logout-warning');if(modal)modal.remove();this.isWarningShown=false;}
|
||||
async sendKeepAlive(){try{await fetch('/api/auth/keep-alive',{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()}});}catch(error){console.warn('Keep-Alive fehlgeschlagen:',error);}}
|
||||
getCSRFToken(){const metaTag=document.querySelector('meta[name="csrf-token"]');return metaTag?metaTag.getAttribute('content'):'';}
|
||||
async performLogout(){this.closeWarning();this.clearTimers();window.location.href='/auth/logout';}}
|
||||
document.addEventListener('DOMContentLoaded',function(){if(!window.location.pathname.includes('/login')){window.autoLogoutManager=new AutoLogoutManager();}});
|
||||
BIN
static/js/auto-logout.min.js.gz
Normal file
BIN
static/js/auto-logout.min.js.gz
Normal file
Binary file not shown.
413
static/js/charts.js
Normal file
413
static/js/charts.js
Normal 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
BIN
static/js/charts.js.gz
Normal file
Binary file not shown.
25
static/js/charts.min.js
vendored
Normal file
25
static/js/charts.min.js
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
window.statsCharts={};function getChartTheme(){const isDark=document.documentElement.classList.contains('dark');return{isDark:isDark,backgroundColor:isDark?'rgba(30, 41, 59, 0.8)':'rgba(255, 255, 255, 0.8)',textColor:isDark?'#e2e8f0':'#374151',gridColor:isDark?'rgba(148, 163, 184, 0.1)':'rgba(156, 163, 175, 0.2)',borderColor:isDark?'rgba(148, 163, 184, 0.3)':'rgba(156, 163, 175, 0.5)'};}
|
||||
function getDefaultChartOptions(){const theme=getChartTheme();return{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:theme.textColor,font:{family:'Inter, sans-serif'}}},tooltip:{backgroundColor:theme.backgroundColor,titleColor:theme.textColor,bodyColor:theme.textColor,borderColor:theme.borderColor,borderWidth:1}},scales:{x:{ticks:{color:theme.textColor,font:{family:'Inter, sans-serif'}},grid:{color:theme.gridColor}},y:{ticks:{color:theme.textColor,font:{family:'Inter, sans-serif'}},grid:{color:theme.gridColor}}}};}
|
||||
async function createJobStatusChart(){try{const response=await fetch('/api/stats/charts/job-status');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Job-Status-Daten');}
|
||||
const ctx=document.getElementById('job-status-chart');if(!ctx)return;if(window.statsCharts.jobStatus){window.statsCharts.jobStatus.destroy();}
|
||||
const theme=getChartTheme();window.statsCharts.jobStatus=new Chart(ctx,{type:'doughnut',data:data,options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{color:theme.textColor,font:{family:'Inter, sans-serif',size:12},padding:15}},tooltip:{backgroundColor:theme.backgroundColor,titleColor:theme.textColor,bodyColor:theme.textColor,borderColor:theme.borderColor,borderWidth:1,callbacks:{label:function(context){const label=context.label||'';const value=context.parsed;const total=context.dataset.data.reduce((a,b)=>a+b,0);const percentage=total>0?Math.round((value/total)*100):0;return`${label}:${value}(${percentage}%)`;}}}},cutout:'60%'}});}catch(error){console.error('Fehler beim Erstellen des Job-Status-Charts:',error);showChartError('job-status-chart','Fehler beim Laden der Job-Status-Daten');}}
|
||||
async function createPrinterUsageChart(){try{const response=await fetch('/api/stats/charts/printer-usage');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Drucker-Nutzung-Daten');}
|
||||
const ctx=document.getElementById('printer-usage-chart');if(!ctx)return;if(window.statsCharts.printerUsage){window.statsCharts.printerUsage.destroy();}
|
||||
const options=getDefaultChartOptions();options.scales.y.title={display:true,text:'Anzahl Jobs',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};window.statsCharts.printerUsage=new Chart(ctx,{type:'bar',data:data,options:options});}catch(error){console.error('Fehler beim Erstellen des Drucker-Nutzung-Charts:',error);showChartError('printer-usage-chart','Fehler beim Laden der Drucker-Nutzung-Daten');}}
|
||||
async function createJobsTimelineChart(){try{const response=await fetch('/api/stats/charts/jobs-timeline');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Jobs-Timeline-Daten');}
|
||||
const ctx=document.getElementById('jobs-timeline-chart');if(!ctx)return;if(window.statsCharts.jobsTimeline){window.statsCharts.jobsTimeline.destroy();}
|
||||
const options=getDefaultChartOptions();options.scales.y.title={display:true,text:'Jobs pro Tag',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};options.scales.x.title={display:true,text:'Datum (letzte 30 Tage)',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};window.statsCharts.jobsTimeline=new Chart(ctx,{type:'line',data:data,options:options});}catch(error){console.error('Fehler beim Erstellen des Jobs-Timeline-Charts:',error);showChartError('jobs-timeline-chart','Fehler beim Laden der Jobs-Timeline-Daten');}}
|
||||
async function createUserActivityChart(){try{const response=await fetch('/api/stats/charts/user-activity');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Benutzer-Aktivität-Daten');}
|
||||
const ctx=document.getElementById('user-activity-chart');if(!ctx)return;if(window.statsCharts.userActivity){window.statsCharts.userActivity.destroy();}
|
||||
const options=getDefaultChartOptions();options.indexAxis='y';options.scales.x.title={display:true,text:'Anzahl Jobs',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};options.scales.y.title={display:true,text:'Benutzer',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};window.statsCharts.userActivity=new Chart(ctx,{type:'bar',data:data,options:options});}catch(error){console.error('Fehler beim Erstellen des Benutzer-Aktivität-Charts:',error);showChartError('user-activity-chart','Fehler beim Laden der Benutzer-Aktivität-Daten');}}
|
||||
function showChartError(chartId,message){const container=document.getElementById(chartId);if(container){container.innerHTML=`<div class="flex items-center justify-center h-full"><div class="text-center"><svg class="h-12 w-12 mx-auto text-red-500 mb-4"fill="none"viewBox="0 0 24 24"stroke="currentColor"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg><p class="text-red-500 font-medium">${message}</p><button onclick="refreshAllCharts()"class="mt-2 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">Erneut versuchen</button></div></div>`;}}
|
||||
async function initializeAllCharts(){showChartLoading();await Promise.allSettled([createJobStatusChart(),createPrinterUsageChart(),createJobsTimelineChart(),createUserActivityChart()]);}
|
||||
function showChartLoading(){const chartIds=['job-status-chart','printer-usage-chart','jobs-timeline-chart','user-activity-chart'];chartIds.forEach(chartId=>{const container=document.getElementById(chartId);if(container){container.innerHTML=`<div class="flex items-center justify-center h-full"><div class="text-center"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div><p class="text-slate-500 dark:text-slate-400 text-sm">Diagramm wird geladen...</p></div></div>`;}});}
|
||||
async function refreshAllCharts(){console.log('Aktualisiere alle Diagramme...');Object.values(window.statsCharts).forEach(chart=>{if(chart&&typeof chart.destroy==='function'){chart.destroy();}});await initializeAllCharts();console.log('Alle Diagramme aktualisiert');}
|
||||
function updateChartsTheme(){refreshAllCharts();}
|
||||
let chartRefreshInterval;function startChartAutoRefresh(){if(chartRefreshInterval){clearInterval(chartRefreshInterval);}
|
||||
chartRefreshInterval=setInterval(()=>{refreshAllCharts();},5*60*1000);}
|
||||
function stopChartAutoRefresh(){if(chartRefreshInterval){clearInterval(chartRefreshInterval);chartRefreshInterval=null;}}
|
||||
function cleanup(){stopChartAutoRefresh();Object.values(window.statsCharts).forEach(chart=>{if(chart&&typeof chart.destroy==='function'){chart.destroy();}});window.statsCharts={};}
|
||||
window.refreshAllCharts=refreshAllCharts;window.updateChartsTheme=updateChartsTheme;window.startChartAutoRefresh=startChartAutoRefresh;window.stopChartAutoRefresh=stopChartAutoRefresh;window.cleanup=cleanup;document.addEventListener('DOMContentLoaded',function(){if(document.getElementById('job-status-chart')){initializeAllCharts();startChartAutoRefresh();}});if(typeof window.addEventListener!=='undefined'){window.addEventListener('darkModeChanged',function(e){updateChartsTheme();});}
|
||||
window.addEventListener('beforeunload',cleanup);
|
||||
BIN
static/js/charts.min.js.gz
Normal file
BIN
static/js/charts.min.js.gz
Normal file
Binary file not shown.
14
static/js/charts/apexcharts.min.js
vendored
Normal file
14
static/js/charts/apexcharts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/charts/apexcharts.min.js.gz
Normal file
BIN
static/js/charts/apexcharts.min.js.gz
Normal file
Binary file not shown.
291
static/js/charts/chart-adapter.js
Normal file
291
static/js/charts/chart-adapter.js
Normal 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>
|
||||
`;
|
||||
}
|
||||
BIN
static/js/charts/chart-adapter.js.gz
Normal file
BIN
static/js/charts/chart-adapter.js.gz
Normal file
Binary file not shown.
431
static/js/charts/chart-config.js
Normal file
431
static/js/charts/chart-config.js
Normal 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;
|
||||
}
|
||||
BIN
static/js/charts/chart-config.js.gz
Normal file
BIN
static/js/charts/chart-config.js.gz
Normal file
Binary file not shown.
400
static/js/charts/chart-renderer.js
Normal file
400
static/js/charts/chart-renderer.js
Normal 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();
|
||||
});
|
||||
BIN
static/js/charts/chart-renderer.js.gz
Normal file
BIN
static/js/charts/chart-renderer.js.gz
Normal file
Binary file not shown.
1
static/js/charts/chart.min.js
vendored
Normal file
1
static/js/charts/chart.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/charts/chart.min.js.gz
Normal file
BIN
static/js/charts/chart.min.js.gz
Normal file
Binary file not shown.
741
static/js/conflict-manager.js
Normal file
741
static/js/conflict-manager.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
BIN
static/js/conflict-manager.js.gz
Normal file
BIN
static/js/conflict-manager.js.gz
Normal file
Binary file not shown.
1
static/js/conflict-manager.min.js
vendored
Normal file
1
static/js/conflict-manager.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/conflict-manager.min.js.gz
Normal file
BIN
static/js/conflict-manager.min.js.gz
Normal file
Binary file not shown.
1
static/js/core-bundle.min.js
vendored
Normal file
1
static/js/core-bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/core-bundle.min.js.gz
Normal file
BIN
static/js/core-bundle.min.js.gz
Normal file
Binary file not shown.
468
static/js/core-utilities-optimized.js
Normal file
468
static/js/core-utilities-optimized.js
Normal 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);
|
||||
BIN
static/js/core-utilities-optimized.js.gz
Normal file
BIN
static/js/core-utilities-optimized.js.gz
Normal file
Binary file not shown.
1
static/js/core-utilities-optimized.min.js
vendored
Normal file
1
static/js/core-utilities-optimized.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/core-utilities-optimized.min.js.gz
Normal file
BIN
static/js/core-utilities-optimized.min.js.gz
Normal file
Binary file not shown.
493
static/js/core-utilities.js
Normal file
493
static/js/core-utilities.js
Normal 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 = '×';
|
||||
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);
|
||||
BIN
static/js/core-utilities.js.gz
Normal file
BIN
static/js/core-utilities.js.gz
Normal file
Binary file not shown.
1
static/js/core-utilities.min.js
vendored
Normal file
1
static/js/core-utilities.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/core-utilities.min.js.gz
Normal file
BIN
static/js/core-utilities.min.js.gz
Normal file
Binary file not shown.
1137
static/js/countdown-timer.js
Normal file
1137
static/js/countdown-timer.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/js/countdown-timer.js.gz
Normal file
BIN
static/js/countdown-timer.js.gz
Normal file
Binary file not shown.
86
static/js/countdown-timer.min.js
vendored
Normal file
86
static/js/countdown-timer.min.js
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
class CountdownTimer{constructor(options={}){this.config={name:options.name||'default_timer',duration:options.duration||1800,autoStart:options.autoStart||false,container:options.container||'countdown-timer',size:options.size||'large',theme:options.theme||'primary',showProgress:options.showProgress!==false,showControls:options.showControls!==false,warningThreshold:options.warningThreshold||30,showWarning:options.showWarning!==false,warningMessage:options.warningMessage||'Timer läuft ab!',forceQuitEnabled:options.forceQuitEnabled!==false,forceQuitAction:options.forceQuitAction||'logout',customEndpoint:options.customEndpoint||null,onTick:options.onTick||null,onWarning:options.onWarning||null,onExpired:options.onExpired||null,onForceQuit:options.onForceQuit||null,apiBase:options.apiBase||'/api/timers',updateInterval:options.updateInterval||1000,syncWithServer:options.syncWithServer!==false};this.state={remaining:this.config.duration,total:this.config.duration,status:'stopped',warningShown:false,lastServerSync:null};this.elements={};this.intervals={countdown:null,serverSync:null};this.listeners=new Map();this.init();}
|
||||
init(){this.createUI();this.attachEventListeners();if(this.config.syncWithServer){this.syncWithServer();this.startServerSync();}
|
||||
if(this.config.autoStart){this.start();}
|
||||
console.log(`Timer'${this.config.name}'initialisiert`);}
|
||||
createUI(){const container=document.getElementById(this.config.container);if(!container){console.error(`Container'${this.config.container}'nicht gefunden`);return;}
|
||||
const timerWrapper=document.createElement('div');timerWrapper.className=`countdown-timer-wrapper size-${this.config.size}theme-${this.config.theme}`;timerWrapper.innerHTML=this.getTimerHTML();container.appendChild(timerWrapper);this.elements={wrapper:timerWrapper,display:timerWrapper.querySelector('.timer-display'),timeText:timerWrapper.querySelector('.time-text'),progressBar:timerWrapper.querySelector('.progress-fill'),progressText:timerWrapper.querySelector('.progress-text'),statusIndicator:timerWrapper.querySelector('.status-indicator'),warningBox:timerWrapper.querySelector('.warning-box'),warningText:timerWrapper.querySelector('.warning-text'),controls:timerWrapper.querySelector('.timer-controls'),startBtn:timerWrapper.querySelector('.btn-start'),pauseBtn:timerWrapper.querySelector('.btn-pause'),stopBtn:timerWrapper.querySelector('.btn-stop'),resetBtn:timerWrapper.querySelector('.btn-reset'),extendBtn:timerWrapper.querySelector('.btn-extend')};this.updateDisplay();}
|
||||
getTimerHTML(){return`<div class="countdown-timer-container"><!--Timer-Display--><div class="timer-display"><div class="time-display"><span class="time-text">${this.formatTime(this.state.remaining)}</span><span class="time-label">verbleibend</span></div><div class="status-indicator"><i class="fas fa-circle"></i><span class="status-text">Gestoppt</span></div></div><!--Fortschrittsbalken-->${this.config.showProgress?`<div class="progress-container"><div class="progress-bar"><div class="progress-fill"style="width: 0%"></div></div><div class="progress-text">0%abgelaufen</div></div>`:''}<!--Warnungsbereich--><div class="warning-box"style="display: none;"><div class="warning-content"><i class="fas fa-exclamation-triangle"></i><span class="warning-text">${this.config.warningMessage}</span></div></div><!--Steuerungsbuttons-->${this.config.showControls?`<div class="timer-controls"><button class="btn btn-success btn-start"title="Timer starten"><i class="fas fa-play"></i><span>Start</span></button><button class="btn btn-warning btn-pause"title="Timer pausieren"style="display: none;"><i class="fas fa-pause"></i><span>Pause</span></button><button class="btn btn-danger btn-stop"title="Timer stoppen"><i class="fas fa-stop"></i><span>Stop</span></button><button class="btn btn-secondary btn-reset"title="Timer zurücksetzen"><i class="fas fa-redo"></i><span>Reset</span></button><button class="btn btn-info btn-extend"title="Timer verlängern"><i class="fas fa-plus"></i><span>+5min</span></button></div>`:''}</div>`;}
|
||||
attachEventListeners(){if(this.elements.startBtn){this.elements.startBtn.addEventListener('click',()=>this.start());}
|
||||
if(this.elements.pauseBtn){this.elements.pauseBtn.addEventListener('click',()=>this.pause());}
|
||||
if(this.elements.stopBtn){this.elements.stopBtn.addEventListener('click',()=>this.stop());}
|
||||
if(this.elements.resetBtn){this.elements.resetBtn.addEventListener('click',()=>this.reset());}
|
||||
if(this.elements.extendBtn){this.elements.extendBtn.addEventListener('click',()=>this.extend(300));}
|
||||
document.addEventListener('keydown',(e)=>this.handleKeyboardShortcuts(e));document.addEventListener('visibilitychange',()=>this.handleVisibilityChange());window.addEventListener('beforeunload',(e)=>this.handleBeforeUnload(e));}
|
||||
async start(){try{if(this.state.status==='running'){return true;}
|
||||
if(this.config.syncWithServer){const response=await this.apiCall('start','POST');if(!response.success){this.showError('Fehler beim Starten des Timers');return false;}}
|
||||
this.state.status='running';this.startCountdown();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'gestartet`);return true;}catch(error){console.error('Fehler beim Starten des Timers:',error);this.showError('Timer konnte nicht gestartet werden');return false;}}
|
||||
async pause(){try{if(this.state.status!=='running'){return true;}
|
||||
if(this.config.syncWithServer){const response=await this.apiCall('pause','POST');if(!response.success){this.showError('Fehler beim Pausieren des Timers');return false;}}
|
||||
this.state.status='paused';this.stopCountdown();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'pausiert`);return true;}catch(error){console.error('Fehler beim Pausieren des Timers:',error);this.showError('Timer konnte nicht pausiert werden');return false;}}
|
||||
async stop(){try{if(this.config.syncWithServer){const response=await this.apiCall('stop','POST');if(!response.success){this.showError('Fehler beim Stoppen des Timers');return false;}}
|
||||
this.state.status='stopped';this.state.remaining=this.state.total;this.state.warningShown=false;this.stopCountdown();this.hideWarning();this.updateDisplay();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'gestoppt`);return true;}catch(error){console.error('Fehler beim Stoppen des Timers:',error);this.showError('Timer konnte nicht gestoppt werden');return false;}}
|
||||
async reset(){try{if(this.config.syncWithServer){const response=await this.apiCall('reset','POST');if(!response.success){this.showError('Fehler beim Zurücksetzen des Timers');return false;}}
|
||||
this.stop();this.state.remaining=this.state.total;this.updateDisplay();console.log(`Timer'${this.config.name}'zurückgesetzt`);return true;}catch(error){console.error('Fehler beim Zurücksetzen des Timers:',error);this.showError('Timer konnte nicht zurückgesetzt werden');return false;}}
|
||||
async extend(seconds){try{if(this.config.syncWithServer){const response=await this.apiCall('extend','POST',{seconds});if(!response.success){this.showError('Fehler beim Verlängern des Timers');return false;}}
|
||||
this.state.remaining+=seconds;this.state.total+=seconds;this.state.warningShown=false;this.hideWarning();this.updateDisplay();this.showToast(`Timer um ${Math.floor(seconds/60)}Minuten verlängert`,'success');console.log(`Timer'${this.config.name}'um ${seconds}Sekunden verlängert`);return true;}catch(error){console.error('Fehler beim Verlängern des Timers:',error);this.showError('Timer konnte nicht verlängert werden');return false;}}
|
||||
startCountdown(){this.stopCountdown();this.intervals.countdown=setInterval(()=>{this.tick();},this.config.updateInterval);}
|
||||
stopCountdown(){if(this.intervals.countdown){clearInterval(this.intervals.countdown);this.intervals.countdown=null;}}
|
||||
tick(){if(this.state.status!=='running'){return;}
|
||||
this.state.remaining=Math.max(0,this.state.remaining-1);this.updateDisplay();if(this.config.onTick){this.config.onTick(this.state.remaining,this.state.total);}
|
||||
if(!this.state.warningShown&&this.state.remaining<=this.config.warningThreshold&&this.state.remaining>0){this.showWarning();}
|
||||
if(this.state.remaining<=0){this.handleExpired();}}
|
||||
async handleExpired(){console.warn(`Timer'${this.config.name}'ist abgelaufen`);this.state.status='expired';this.stopCountdown();this.updateDisplay();this.updateStatusIndicator();if(this.config.onExpired){this.config.onExpired();}
|
||||
if(this.config.forceQuitEnabled){await this.executeForceQuit();}}
|
||||
async executeForceQuit(){try{console.warn(`Force-Quit für Timer'${this.config.name}'wird ausgeführt...`);if(this.config.onForceQuit){const shouldContinue=this.config.onForceQuit(this.config.forceQuitAction);if(!shouldContinue){return;}}
|
||||
if(this.config.syncWithServer){const response=await this.apiCall('force-quit','POST');if(!response.success){console.error('Force-Quit-API-Aufruf fehlgeschlagen');}}
|
||||
switch(this.config.forceQuitAction){case'logout':this.performLogout();break;case'redirect':this.performRedirect();break;case'refresh':this.performRefresh();break;case'custom':this.performCustomAction();break;default:console.warn(`Unbekannte Force-Quit-Aktion:${this.config.forceQuitAction}`);}}catch(error){console.error('Fehler bei Force-Quit-Ausführung:',error);}}
|
||||
performLogout(){this.showModal('Session abgelaufen','Sie werden automatisch abgemeldet...','warning');setTimeout(()=>{window.location.href='/auth/logout';},2000);}
|
||||
performRedirect(){const redirectUrl=this.config.redirectUrl||'/';this.showModal('Umleitung','Sie werden weitergeleitet...','info');setTimeout(()=>{window.location.href=redirectUrl;},2000);}
|
||||
performRefresh(){this.showModal('Seite wird aktualisiert','Die Seite wird automatisch neu geladen...','info');setTimeout(()=>{window.location.reload();},2000);}
|
||||
performCustomAction(){if(this.config.customEndpoint){fetch(this.config.customEndpoint,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()},body:JSON.stringify({timer_name:this.config.name,action:'force_quit'})}).catch(error=>{console.error('Custom-Action-Request fehlgeschlagen:',error);});}}
|
||||
showWarning(){if(!this.config.showWarning||this.state.warningShown){return;}
|
||||
this.state.warningShown=true;if(this.elements.warningBox){this.elements.warningBox.style.display='block';this.elements.warningBox.classList.add('pulse');}
|
||||
if(this.config.onWarning){this.config.onWarning(this.state.remaining);}
|
||||
this.showNotification('Timer-Warnung',this.config.warningMessage);console.warn(`Timer-Warnung für'${this.config.name}':${this.state.remaining}Sekunden verbleiben`);}
|
||||
hideWarning(){this.state.warningShown=false;if(this.elements.warningBox){this.elements.warningBox.style.display='none';this.elements.warningBox.classList.remove('pulse');}}
|
||||
updateDisplay(){if(this.elements.timeText){this.elements.timeText.textContent=this.formatTime(this.state.remaining);}
|
||||
if(this.config.showProgress&&this.elements.progressBar){const progress=((this.state.total-this.state.remaining)/this.state.total)*100;this.elements.progressBar.style.width=`${progress}%`;if(this.elements.progressText){this.elements.progressText.textContent=`${Math.round(progress)}%abgelaufen`;}}
|
||||
this.updateTheme();}
|
||||
updateTheme(){if(!this.elements.wrapper)return;const progress=(this.state.total-this.state.remaining)/this.state.total;this.elements.wrapper.classList.remove('theme-primary','theme-warning','theme-danger');if(progress<0.7){this.elements.wrapper.classList.add('theme-primary');}else if(progress<0.9){this.elements.wrapper.classList.add('theme-warning');}else{this.elements.wrapper.classList.add('theme-danger');}}
|
||||
updateControls(){if(!this.config.showControls)return;const isRunning=this.state.status==='running';const isPaused=this.state.status==='paused';const isStopped=this.state.status==='stopped';if(this.elements.startBtn){this.elements.startBtn.style.display=(isStopped||isPaused)?'inline-flex':'none';}
|
||||
if(this.elements.pauseBtn){this.elements.pauseBtn.style.display=isRunning?'inline-flex':'none';}
|
||||
if(this.elements.stopBtn){this.elements.stopBtn.disabled=isStopped;}
|
||||
if(this.elements.resetBtn){this.elements.resetBtn.disabled=isRunning;}
|
||||
if(this.elements.extendBtn){this.elements.extendBtn.disabled=this.state.status==='expired';}}
|
||||
updateStatusIndicator(){if(!this.elements.statusIndicator)return;const statusText=this.elements.statusIndicator.querySelector('.status-text');const statusIcon=this.elements.statusIndicator.querySelector('i');if(statusText&&statusIcon){switch(this.state.status){case'running':statusText.textContent='Läuft';statusIcon.className='fas fa-circle text-success';break;case'paused':statusText.textContent='Pausiert';statusIcon.className='fas fa-circle text-warning';break;case'expired':statusText.textContent='Abgelaufen';statusIcon.className='fas fa-circle text-danger';break;default:statusText.textContent='Gestoppt';statusIcon.className='fas fa-circle text-secondary';}}}
|
||||
formatTime(seconds){const minutes=Math.floor(seconds/60);const secs=seconds%60;return`${minutes.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')}`;}
|
||||
async syncWithServer(){try{const response=await this.apiCall('status','GET');if(response.success&&response.data){const serverState=response.data;this.state.remaining=serverState.remaining_seconds||this.state.remaining;this.state.total=serverState.duration_seconds||this.state.total;this.state.status=serverState.status||this.state.status;this.updateDisplay();this.updateControls();this.updateStatusIndicator();this.state.lastServerSync=new Date();}}catch(error){console.error('Server-Synchronisation fehlgeschlagen:',error);}}
|
||||
startServerSync(){if(this.intervals.serverSync){clearInterval(this.intervals.serverSync);}
|
||||
this.intervals.serverSync=setInterval(()=>{this.syncWithServer();},30000);}
|
||||
async apiCall(action,method='GET',data=null){const url=`${this.config.apiBase}/${this.config.name}/${action}`;const options={method,headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()}};if(data&&(method==='POST'||method==='PUT')){options.body=JSON.stringify(data);}
|
||||
const response=await fetch(url,options);return await response.json();}
|
||||
getCSRFToken(){const token=document.querySelector('meta[name="csrf-token"]');return token?token.getAttribute('content'):'';}
|
||||
showNotification(title,message){if('Notification'in window&&Notification.permission==='granted'){new Notification(title,{body:message,icon:'/static/icons/timer-icon.png'});}}
|
||||
showToast(message,type='info'){console.log(`Toast[${type}]:${message}`);}
|
||||
showError(message){this.showModal('Fehler',message,'danger');}
|
||||
showModal(title,message,type='info'){console.log(`Modal[${type}]${title}:${message}`);}
|
||||
handleKeyboardShortcuts(e){if(e.ctrlKey||e.metaKey){switch(e.key){case' ':e.preventDefault();if(this.state.status==='running'){this.pause();}else{this.start();}
|
||||
break;case'r':e.preventDefault();this.reset();break;case's':e.preventDefault();this.stop();break;}}}
|
||||
handleVisibilityChange(){if(document.hidden){this.config._wasRunning=this.state.status==='running';}else{if(this.config.syncWithServer){this.syncWithServer();}}}
|
||||
handleBeforeUnload(e){if(this.state.status==='running'&&this.state.remaining>0){e.preventDefault();e.returnValue='Timer läuft noch. Möchten Sie die Seite wirklich verlassen?';return e.returnValue;}}
|
||||
destroy(){this.stopCountdown();if(this.intervals.serverSync){clearInterval(this.intervals.serverSync);}
|
||||
if(this.elements.wrapper){this.elements.wrapper.remove();}
|
||||
this.listeners.forEach((listener,element)=>{element.removeEventListener(listener.event,listener.handler);});console.log(`Timer'${this.config.name}'zerstört`);}}
|
||||
const timerStyles=`<style id="countdown-timer-styles">.countdown-timer-wrapper{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:400px;margin:0 auto;text-align:center;border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.15);background:white;overflow:hidden;}.countdown-timer-container{padding:24px;}
|
||||
.size-small{max-width:250px;font-size:0.9em;}.size-medium{max-width:350px;}.size-large{max-width:450px;font-size:1.1em;}
|
||||
.theme-primary{border-left:4px solid#007bff;}.theme-warning{border-left:4px solid#ffc107;}.theme-danger{border-left:4px solid#dc3545;}.theme-success{border-left:4px solid#28a745;}
|
||||
.timer-display{margin-bottom:20px;}.time-display{margin-bottom:8px;}.time-text{font-size:3em;font-weight:bold;color:#2c3e50;font-family:'Courier New',monospace;}.time-label{display:block;font-size:0.9em;color:#6c757d;margin-top:4px;}.status-indicator{display:flex;align-items:center;justify-content:center;gap:8px;font-size:0.9em;color:#6c757d;}
|
||||
.progress-container{margin-bottom:20px;}.progress-bar{height:8px;background-color:#e9ecef;border-radius:4px;overflow:hidden;margin-bottom:8px;}.progress-fill{height:100%;background:linear-gradient(90deg,#007bff,#0056b3);transition:width 0.5s ease-in-out;border-radius:4px;}.progress-text{font-size:0.85em;color:#6c757d;}
|
||||
.warning-box{background:linear-gradient(135deg,#fff3cd,#ffeaa7);border:1px solid#ffc107;border-radius:8px;padding:12px;margin-bottom:20px;animation:pulse 2s infinite;}.warning-content{display:flex;align-items:center;justify-content:center;gap:8px;color:#856404;font-weight:500;}.warning-content i{color:#ffc107;}
|
||||
.timer-controls{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;}.timer-controls.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border:none;border-radius:6px;font-size:0.9em;font-weight:500;cursor:pointer;transition:all 0.2s ease;text-decoration:none;}.timer-controls.btn:hover{transform:translateY(-1px);box-shadow:0 2px 8px rgba(0,0,0,0.2);}.timer-controls.btn:disabled{opacity:0.6;cursor:not-allowed;transform:none;}.btn-success{background:#28a745;color:white;}.btn-warning{background:#ffc107;color:#212529;}.btn-danger{background:#dc3545;color:white;}.btn-secondary{background:#6c757d;color:white;}.btn-info{background:#17a2b8;color:white;}
|
||||
@keyframes pulse{0%,100%{opacity:1;}
|
||||
50%{opacity:0.7;}}
|
||||
@media(max-width:480px){.countdown-timer-wrapper{margin:0 16px;}.time-text{font-size:2.5em;}.timer-controls{flex-direction:column;}.timer-controls.btn{width:100%;justify-content:center;}}
|
||||
@media(prefers-color-scheme:dark){.countdown-timer-wrapper{background:#2c3e50;color:white;}.time-text{color:#ecf0f1;}.progress-bar{background-color:#34495e;}}</style>`;if(!document.getElementById('countdown-timer-styles')){document.head.insertAdjacentHTML('beforeend',timerStyles);}
|
||||
window.CountdownTimer=CountdownTimer;window.TimerManager={timers:new Map(),create(name,options){if(this.timers.has(name)){console.warn(`Timer'${name}'existiert bereits`);return this.timers.get(name);}
|
||||
const timer=new CountdownTimer({...options,name:name});this.timers.set(name,timer);return timer;},get(name){return this.timers.get(name);},destroy(name){const timer=this.timers.get(name);if(timer){timer.destroy();this.timers.delete(name);}},destroyAll(){this.timers.forEach(timer=>timer.destroy());this.timers.clear();}};
|
||||
BIN
static/js/countdown-timer.min.js.gz
Normal file
BIN
static/js/countdown-timer.min.js.gz
Normal file
Binary file not shown.
283
static/js/csp-violation-handler.js
Normal file
283
static/js/csp-violation-handler.js
Normal 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;">×</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');
|
||||
BIN
static/js/csp-violation-handler.js.gz
Normal file
BIN
static/js/csp-violation-handler.js.gz
Normal file
Binary file not shown.
20
static/js/csp-violation-handler.min.js
vendored
Normal file
20
static/js/csp-violation-handler.min.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
class CSPViolationHandler{constructor(){this.violations=[];this.init();}
|
||||
init(){document.addEventListener('securitypolicyviolation',this.handleViolation.bind(this));if('ReportingObserver'in window){const observer=new ReportingObserver((reports,observer)=>{for(const report of reports){if(report.type==='csp-violation'){this.handleViolation(report.body);}}});observer.observe();}
|
||||
console.log('🛡️ CSP Violation Handler initialisiert');}
|
||||
handleViolation(violationEvent){const violation={timestamp:new Date().toISOString(),blockedURI:violationEvent.blockedURI||'unknown',violatedDirective:violationEvent.violatedDirective||'unknown',originalPolicy:violationEvent.originalPolicy||'unknown',documentURI:violationEvent.documentURI||window.location.href,sourceFile:violationEvent.sourceFile||'unknown',lineNumber:violationEvent.lineNumber||0,columnNumber:violationEvent.columnNumber||0,sample:violationEvent.sample||'',disposition:violationEvent.disposition||'enforce'};this.violations.push(violation);this.logViolation(violation);this.suggestFix(violation);this.reportViolation(violation);}
|
||||
logViolation(violation){console.group('🚨 CSP Violation detected');console.error('Blocked URI:',violation.blockedURI);console.error('Violated Directive:',violation.violatedDirective);console.error('Source:',`${violation.sourceFile}:${violation.lineNumber}:${violation.columnNumber}`);console.error('Sample:',violation.sample);console.error('Full Policy:',violation.originalPolicy);console.groupEnd();}
|
||||
suggestFix(violation){const directive=violation.violatedDirective;const blockedURI=violation.blockedURI;console.group('💡 Lösungsvorschlag');if(directive.includes('script-src')){if(blockedURI==='inline'){console.log('Problem: Inline-Script blockiert');console.log('Lösung 1: Script in externe .js-Datei auslagern');console.log('Lösung 2: data-action Attribute für Event-Handler verwenden');console.log('Lösung 3: Nonce verwenden (nicht empfohlen für Entwicklung)');console.log('Beispiel: <button data-action="refresh-dashboard">Aktualisieren</button>');}else if(blockedURI.includes('eval')){console.log('Problem: eval() oder ähnliche Funktionen blockiert');console.log('Lösung: Verwende sichere Alternativen zu eval()');}else{console.log(`Problem:Externes Script von ${blockedURI}blockiert`);console.log('Lösung: URL zur CSP script-src Richtlinie hinzufügen');}}else if(directive.includes('style-src')){console.log('Problem: Style blockiert');console.log('Lösung: CSS in externe .css-Datei auslagern oder CSP erweitern');}else if(directive.includes('connect-src')){console.log(`Problem:Verbindung zu ${blockedURI}blockiert`);console.log('Lösung: URL zur CSP connect-src Richtlinie hinzufügen');console.log('Tipp: Für API-Calls relative URLs verwenden');}else if(directive.includes('img-src')){console.log(`Problem:Bild von ${blockedURI}blockiert`);console.log('Lösung: URL zur CSP img-src Richtlinie hinzufügen');}
|
||||
console.groupEnd();}
|
||||
async reportViolation(violation){try{if(window.location.hostname!=='localhost'&&window.location.hostname!=='127.0.0.1'){await fetch('/api/security/csp-violation',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(violation)});}}catch(error){console.warn('Fehler beim Senden der CSP-Verletzung:',error);}}
|
||||
getViolations(){return this.violations;}
|
||||
getStats(){const stats={total:this.violations.length,byDirective:{},byURI:{},recent:this.violations.slice(-10)};this.violations.forEach(violation=>{const directive=violation.violatedDirective;stats.byDirective[directive]=(stats.byDirective[directive]||0)+1;const uri=violation.blockedURI;stats.byURI[uri]=(stats.byURI[uri]||0)+1;});return stats;}
|
||||
enableDebugMode(){this.createDebugPanel();console.log('🔧 CSP Debug Mode aktiviert');console.log('Verfügbare Befehle:');console.log('- cspHandler.getViolations() - Alle Verletzungen anzeigen');console.log('- cspHandler.getStats() - Statistiken anzeigen');console.log('- cspHandler.clearViolations() - Verletzungen löschen');console.log('- cspHandler.exportViolations() - Als JSON exportieren');}
|
||||
createDebugPanel(){const panel=document.createElement('div');panel.id='csp-debug-panel';panel.style.cssText=`position:fixed;top:10px;right:10px;width:300px;max-height:400px;background:rgba(0,0,0,0.9);color:white;font-family:monospace;font-size:12px;padding:10px;border-radius:5px;z-index:10000;overflow-y:auto;display:none;`;panel.innerHTML=`<div style="display: flex; justify-content: space-between; margin-bottom: 10px;"><strong>CSP Violations</strong><button onclick="this.parentElement.parentElement.style.display='none'"
|
||||
style="background: none; border: none; color: white; cursor: pointer;">×</button></div><div id="csp-violations-list"></div><div style="margin-top: 10px;"><button onclick="cspHandler.clearViolations()"
|
||||
style="background: #333; color: white; border: none; padding: 5px; margin-right: 5px; cursor: pointer;">Clear</button><button onclick="cspHandler.exportViolations()"
|
||||
style="background: #333; color: white; border: none; padding: 5px; cursor: pointer;">Export</button></div>`;document.body.appendChild(panel);document.addEventListener('keydown',(event)=>{if(event.ctrlKey&&event.shiftKey&&event.key==='C'){panel.style.display=panel.style.display==='none'?'block':'none';this.updateDebugPanel();}});}
|
||||
updateDebugPanel(){const list=document.getElementById('csp-violations-list');if(!list)return;const recent=this.violations.slice(-5);list.innerHTML=recent.map(v=>`<div style="margin-bottom: 5px; padding: 5px; background: rgba(255, 255, 255, 0.1);"><div><strong>${v.violatedDirective}</strong></div><div style="color: #ff6b6b;">${v.blockedURI}</div><div style="color: #ffd93d; font-size: 10px;">${v.timestamp}</div></div>`).join('');}
|
||||
clearViolations(){this.violations=[];this.updateDebugPanel();console.log('🗑️ CSP Violations gelöscht');}
|
||||
exportViolations(){const data={timestamp:new Date().toISOString(),stats:this.getStats(),violations:this.violations};const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=`csp-violations-${new Date().toISOString().split('T')[0]}.json`;a.click();URL.revokeObjectURL(url);console.log('📄 CSP Violations exportiert');}}
|
||||
const cspHandler=new CSPViolationHandler();if(window.location.hostname==='localhost'||window.location.hostname==='127.0.0.1'){cspHandler.enableDebugMode();console.log('🔍 CSP Debug Mode aktiv - Drücken Sie Ctrl+Shift+C für Debug-Panel');}
|
||||
window.cspHandler=cspHandler;console.log('🛡️ CSP Violation Handler geladen');
|
||||
BIN
static/js/csp-violation-handler.min.js.gz
Normal file
BIN
static/js/csp-violation-handler.min.js.gz
Normal file
Binary file not shown.
123
static/js/css-cache-manager.js
Normal file
123
static/js/css-cache-manager.js
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* MYP Platform - CSS Cache Manager
|
||||
* Integration und Management des CSS-Caching Service Workers
|
||||
*/
|
||||
|
||||
class CSSCacheManager {
|
||||
constructor() {
|
||||
this.serviceWorker = null;
|
||||
this.registration = null;
|
||||
this.isSupported = 'serviceWorker' in navigator;
|
||||
this.cacheStats = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.isSupported) {
|
||||
console.warn('[CSS-Cache] Service Worker wird nicht unterstützt');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.registration = await navigator.serviceWorker.register(
|
||||
'/static/js/css-cache-service-worker.js',
|
||||
{ scope: '/static/css/' }
|
||||
);
|
||||
|
||||
console.log('[CSS-Cache] Service Worker registriert');
|
||||
|
||||
if (this.registration.active) {
|
||||
this.serviceWorker = this.registration.active;
|
||||
}
|
||||
|
||||
this.startPerformanceMonitoring();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[CSS-Cache] Fehler bei Service Worker Registrierung:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async clearCache() {
|
||||
if (!this.serviceWorker) return false;
|
||||
|
||||
try {
|
||||
const messageChannel = new MessageChannel();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
resolve(event.data.success);
|
||||
};
|
||||
|
||||
this.serviceWorker.postMessage(
|
||||
{ type: 'CLEAR_CSS_CACHE' },
|
||||
[messageChannel.port2]
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[CSS-Cache] Fehler beim Cache leeren:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getCacheStats() {
|
||||
if (!this.serviceWorker) return null;
|
||||
|
||||
try {
|
||||
const messageChannel = new MessageChannel();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
this.cacheStats = event.data;
|
||||
resolve(event.data);
|
||||
};
|
||||
|
||||
this.serviceWorker.postMessage(
|
||||
{ type: 'GET_CACHE_STATS' },
|
||||
[messageChannel.port2]
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[CSS-Cache] Fehler beim Abrufen der Stats:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
startPerformanceMonitoring() {
|
||||
setInterval(async () => {
|
||||
const stats = await this.getCacheStats();
|
||||
if (stats) {
|
||||
console.log('[CSS-Cache] Performance-Stats:', stats);
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
setTimeout(async () => {
|
||||
await this.getCacheStats();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
debug() {
|
||||
console.group('[CSS-Cache] Debug-Informationen');
|
||||
console.log('Service Worker unterstützt:', this.isSupported);
|
||||
console.log('Service Worker aktiv:', !!this.serviceWorker);
|
||||
console.log('Registration:', this.registration);
|
||||
console.log('Cache-Stats:', this.cacheStats);
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Instanz erstellen
|
||||
window.cssCache = new CSSCacheManager();
|
||||
|
||||
// Entwicklungs-Hilfsfunktionen
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
window.clearCSSCache = () => window.cssCache.clearCache();
|
||||
window.getCSSStats = () => window.cssCache.getCacheStats();
|
||||
|
||||
console.log('[CSS-Cache] Entwicklungsmodus: Debug-Funktionen verfügbar');
|
||||
console.log('- cssCache.debug() - Debug-Informationen anzeigen');
|
||||
console.log('- clearCSSCache() - CSS-Cache leeren');
|
||||
console.log('- getCSSStats() - Cache-Statistiken abrufen');
|
||||
}
|
||||
|
||||
export default CSSCacheManager;
|
||||
BIN
static/js/css-cache-manager.js.gz
Normal file
BIN
static/js/css-cache-manager.js.gz
Normal file
Binary file not shown.
10
static/js/css-cache-manager.min.js
vendored
Normal file
10
static/js/css-cache-manager.min.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
class CSSCacheManager{constructor(){this.serviceWorker=null;this.registration=null;this.isSupported='serviceWorker'in navigator;this.cacheStats=null;this.init();}
|
||||
async init(){if(!this.isSupported){console.warn('[CSS-Cache] Service Worker wird nicht unterstützt');return;}
|
||||
try{this.registration=await navigator.serviceWorker.register('/static/js/css-cache-service-worker.js',{scope:'/static/css/'});console.log('[CSS-Cache] Service Worker registriert');if(this.registration.active){this.serviceWorker=this.registration.active;}
|
||||
this.startPerformanceMonitoring();}catch(error){console.error('[CSS-Cache] Fehler bei Service Worker Registrierung:',error);}}
|
||||
async clearCache(){if(!this.serviceWorker)return false;try{const messageChannel=new MessageChannel();return new Promise((resolve)=>{messageChannel.port1.onmessage=(event)=>{resolve(event.data.success);};this.serviceWorker.postMessage({type:'CLEAR_CSS_CACHE'},[messageChannel.port2]);});}catch(error){console.error('[CSS-Cache] Fehler beim Cache leeren:',error);return false;}}
|
||||
async getCacheStats(){if(!this.serviceWorker)return null;try{const messageChannel=new MessageChannel();return new Promise((resolve)=>{messageChannel.port1.onmessage=(event)=>{this.cacheStats=event.data;resolve(event.data);};this.serviceWorker.postMessage({type:'GET_CACHE_STATS'},[messageChannel.port2]);});}catch(error){console.error('[CSS-Cache] Fehler beim Abrufen der Stats:',error);return null;}}
|
||||
startPerformanceMonitoring(){setInterval(async()=>{const stats=await this.getCacheStats();if(stats){console.log('[CSS-Cache] Performance-Stats:',stats);}},5*60*1000);setTimeout(async()=>{await this.getCacheStats();},10000);}
|
||||
debug(){console.group('[CSS-Cache] Debug-Informationen');console.log('Service Worker unterstützt:',this.isSupported);console.log('Service Worker aktiv:',!!this.serviceWorker);console.log('Registration:',this.registration);console.log('Cache-Stats:',this.cacheStats);console.groupEnd();}}
|
||||
window.cssCache=new CSSCacheManager();if(window.location.hostname==='localhost'||window.location.hostname==='127.0.0.1'){window.clearCSSCache=()=>window.cssCache.clearCache();window.getCSSStats=()=>window.cssCache.getCacheStats();console.log('[CSS-Cache] Entwicklungsmodus: Debug-Funktionen verfügbar');console.log('- cssCache.debug() - Debug-Informationen anzeigen');console.log('- clearCSSCache() - CSS-Cache leeren');console.log('- getCSSStats() - Cache-Statistiken abrufen');}
|
||||
export default CSSCacheManager;
|
||||
BIN
static/js/css-cache-manager.min.js.gz
Normal file
BIN
static/js/css-cache-manager.min.js.gz
Normal file
Binary file not shown.
372
static/js/css-cache-service-worker.js
Normal file
372
static/js/css-cache-service-worker.js
Normal file
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* MYP Platform - CSS Caching Service Worker
|
||||
* Intelligentes Caching für optimierte CSS-Performance
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'myp-css-cache-v1.0';
|
||||
const CSS_CACHE_NAME = 'myp-css-resources-v1.0';
|
||||
|
||||
// Kritische CSS-Ressourcen für sofortiges Caching
|
||||
const CRITICAL_CSS_RESOURCES = [
|
||||
'/static/css/caching-optimizations.css',
|
||||
'/static/css/optimization-animations.css',
|
||||
'/static/css/glassmorphism.css',
|
||||
'/static/css/professional-theme.css',
|
||||
'/static/css/tailwind.min.css'
|
||||
];
|
||||
|
||||
// Nicht-kritische CSS-Ressourcen für Prefetching
|
||||
const NON_CRITICAL_CSS_RESOURCES = [
|
||||
'/static/css/components.css',
|
||||
'/static/css/printers.css',
|
||||
'/static/fontawesome/css/all.min.css'
|
||||
];
|
||||
|
||||
// CSS-spezifische Konfiguration
|
||||
const CSS_CACHE_CONFIG = {
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 Stunden
|
||||
maxEntries: 50,
|
||||
networkTimeoutSeconds: 5
|
||||
};
|
||||
|
||||
// Service Worker Installation
|
||||
self.addEventListener('install', event => {
|
||||
console.log('[CSS-SW] Service Worker wird installiert...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(CSS_CACHE_NAME)
|
||||
.then(cache => {
|
||||
console.log('[CSS-SW] Kritische CSS-Ressourcen werden gecacht...');
|
||||
return cache.addAll(CRITICAL_CSS_RESOURCES);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[CSS-SW] Installation abgeschlossen');
|
||||
return self.skipWaiting();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[CSS-SW] Fehler bei Installation:', error);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Service Worker Aktivierung
|
||||
self.addEventListener('activate', event => {
|
||||
console.log('[CSS-SW] Service Worker wird aktiviert...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
// Alte Caches löschen
|
||||
if (cacheName !== CACHE_NAME && cacheName !== CSS_CACHE_NAME) {
|
||||
console.log('[CSS-SW] Alter Cache wird gelöscht:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[CSS-SW] Aktivierung abgeschlossen');
|
||||
return self.clients.claim();
|
||||
})
|
||||
.then(() => {
|
||||
// Nicht-kritische Ressourcen im Hintergrund prefetchen
|
||||
return prefetchNonCriticalResources();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// CSS-Request-Behandlung mit Cache-First-Strategie
|
||||
self.addEventListener('fetch', event => {
|
||||
const { request } = event;
|
||||
|
||||
// Nur CSS-Requests verarbeiten
|
||||
if (!request.url.includes('.css') && !request.url.includes('/static/css/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
handleCSSRequest(request)
|
||||
);
|
||||
});
|
||||
|
||||
// CSS-Request-Handler mit intelligenter Cache-Strategie
|
||||
async function handleCSSRequest(request) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
try {
|
||||
// 1. Cache-First für CSS-Dateien
|
||||
const cachedResponse = await caches.match(request);
|
||||
|
||||
if (cachedResponse) {
|
||||
console.log('[CSS-SW] Cache-Hit für:', url.pathname);
|
||||
|
||||
// Hintergrund-Update für kritische Ressourcen
|
||||
if (CRITICAL_CSS_RESOURCES.some(resource => url.pathname.includes(resource))) {
|
||||
updateCacheInBackground(request);
|
||||
}
|
||||
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// 2. Network-Request mit Timeout
|
||||
const networkResponse = await fetchWithTimeout(request, CSS_CACHE_CONFIG.networkTimeoutSeconds * 1000);
|
||||
|
||||
if (networkResponse && networkResponse.ok) {
|
||||
// Response für Cache klonen
|
||||
const responseToCache = networkResponse.clone();
|
||||
|
||||
// Asynchron cachen
|
||||
cacheResponse(request, responseToCache);
|
||||
|
||||
console.log('[CSS-SW] Network-Response für:', url.pathname);
|
||||
return networkResponse;
|
||||
}
|
||||
|
||||
// 3. Fallback für kritische CSS
|
||||
return await getFallbackCSS(request);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[CSS-SW] Fehler bei CSS-Request:', error);
|
||||
return await getFallbackCSS(request);
|
||||
}
|
||||
}
|
||||
|
||||
// Network-Request mit Timeout
|
||||
function fetchWithTimeout(request, timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error('Network timeout'));
|
||||
}, timeout);
|
||||
|
||||
fetch(request)
|
||||
.then(response => {
|
||||
clearTimeout(timer);
|
||||
resolve(response);
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Response asynchron cachen
|
||||
async function cacheResponse(request, response) {
|
||||
try {
|
||||
const cache = await caches.open(CSS_CACHE_NAME);
|
||||
await cache.put(request, response);
|
||||
|
||||
// Cache-Größe prüfen und ggf. alte Einträge löschen
|
||||
await maintainCacheSize();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[CSS-SW] Fehler beim Cachen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache-Größe überwachen und bereinigen
|
||||
async function maintainCacheSize() {
|
||||
try {
|
||||
const cache = await caches.open(CSS_CACHE_NAME);
|
||||
const requests = await cache.keys();
|
||||
|
||||
if (requests.length > CSS_CACHE_CONFIG.maxEntries) {
|
||||
console.log('[CSS-SW] Cache-Bereinigung wird durchgeführt...');
|
||||
|
||||
// Älteste Einträge löschen (LRU-ähnlich)
|
||||
const excessCount = requests.length - CSS_CACHE_CONFIG.maxEntries;
|
||||
for (let i = 0; i < excessCount; i++) {
|
||||
await cache.delete(requests[i]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[CSS-SW] Fehler bei Cache-Wartung:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Hintergrund-Update für kritische Ressourcen
|
||||
async function updateCacheInBackground(request) {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response && response.ok) {
|
||||
const cache = await caches.open(CSS_CACHE_NAME);
|
||||
await cache.put(request, response.clone());
|
||||
console.log('[CSS-SW] Hintergrund-Update für:', request.url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[CSS-SW] Hintergrund-Update fehlgeschlagen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback-CSS für kritische Requests
|
||||
async function getFallbackCSS(request) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Minimales Fallback-CSS für kritische Komponenten
|
||||
const fallbackCSS = `
|
||||
/* Fallback CSS für Offline-Nutzung */
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f8fafc;
|
||||
color: #1f2937;
|
||||
}
|
||||
.offline-notice {
|
||||
background: #fef3c7;
|
||||
border: 1px solid #f59e0b;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.btn {
|
||||
background: #0073ce;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
return new Response(fallbackCSS, {
|
||||
headers: {
|
||||
'Content-Type': 'text/css',
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Nicht-kritische Ressourcen im Hintergrund prefetchen
|
||||
async function prefetchNonCriticalResources() {
|
||||
try {
|
||||
const cache = await caches.open(CSS_CACHE_NAME);
|
||||
|
||||
for (const resource of NON_CRITICAL_CSS_RESOURCES) {
|
||||
try {
|
||||
const request = new Request(resource);
|
||||
const cachedResponse = await cache.match(request);
|
||||
|
||||
if (!cachedResponse) {
|
||||
const response = await fetch(request);
|
||||
if (response && response.ok) {
|
||||
await cache.put(request, response);
|
||||
console.log('[CSS-SW] Prefetch erfolgreich für:', resource);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[CSS-SW] Prefetch fehlgeschlagen für:', resource);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[CSS-SW] Fehler beim Prefetching:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Message-Handler für Cache-Management
|
||||
self.addEventListener('message', event => {
|
||||
const { type, data } = event.data;
|
||||
|
||||
switch (type) {
|
||||
case 'SKIP_WAITING':
|
||||
self.skipWaiting();
|
||||
break;
|
||||
|
||||
case 'CLEAR_CSS_CACHE':
|
||||
clearCSSCache().then(() => {
|
||||
event.ports[0].postMessage({ success: true });
|
||||
});
|
||||
break;
|
||||
|
||||
case 'PREFETCH_CSS':
|
||||
prefetchSpecificCSS(data.urls).then(() => {
|
||||
event.ports[0].postMessage({ success: true });
|
||||
});
|
||||
break;
|
||||
|
||||
case 'GET_CACHE_STATS':
|
||||
getCacheStats().then(stats => {
|
||||
event.ports[0].postMessage(stats);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// CSS-Cache leeren
|
||||
async function clearCSSCache() {
|
||||
try {
|
||||
await caches.delete(CSS_CACHE_NAME);
|
||||
console.log('[CSS-SW] CSS-Cache wurde geleert');
|
||||
} catch (error) {
|
||||
console.error('[CSS-SW] Fehler beim Leeren des CSS-Cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Spezifische CSS-Dateien prefetchen
|
||||
async function prefetchSpecificCSS(urls) {
|
||||
try {
|
||||
const cache = await caches.open(CSS_CACHE_NAME);
|
||||
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response && response.ok) {
|
||||
await cache.put(url, response);
|
||||
console.log('[CSS-SW] Spezifisches Prefetch für:', url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[CSS-SW] Spezifisches Prefetch fehlgeschlagen für:', url);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[CSS-SW] Fehler beim spezifischen Prefetching:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache-Statistiken abrufen
|
||||
async function getCacheStats() {
|
||||
try {
|
||||
const cache = await caches.open(CSS_CACHE_NAME);
|
||||
const requests = await cache.keys();
|
||||
|
||||
return {
|
||||
cssEntries: requests.length,
|
||||
maxEntries: CSS_CACHE_CONFIG.maxEntries,
|
||||
cacheUtilization: (requests.length / CSS_CACHE_CONFIG.maxEntries) * 100,
|
||||
cachedUrls: requests.map(req => req.url)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[CSS-SW] Fehler beim Abrufen der Cache-Stats:', error);
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Performance-Monitoring
|
||||
self.addEventListener('install', () => {
|
||||
console.log('[CSS-SW] Installation gestartet um:', new Date().toISOString());
|
||||
});
|
||||
|
||||
self.addEventListener('activate', () => {
|
||||
console.log('[CSS-SW] Aktivierung abgeschlossen um:', new Date().toISOString());
|
||||
});
|
||||
|
||||
// Globaler Error-Handler
|
||||
self.addEventListener('error', event => {
|
||||
console.error('[CSS-SW] Globaler Fehler:', event.error);
|
||||
});
|
||||
|
||||
self.addEventListener('unhandledrejection', event => {
|
||||
console.error('[CSS-SW] Unbehandelte Promise-Rejection:', event.reason);
|
||||
});
|
||||
|
||||
console.log('[CSS-SW] CSS-Caching Service Worker geladen');
|
||||
BIN
static/js/css-cache-service-worker.js.gz
Normal file
BIN
static/js/css-cache-service-worker.js.gz
Normal file
Binary file not shown.
15
static/js/css-cache-service-worker.min.js
vendored
Normal file
15
static/js/css-cache-service-worker.min.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
const CACHE_NAME='myp-css-cache-v1.0';const CSS_CACHE_NAME='myp-css-resources-v1.0';const CRITICAL_CSS_RESOURCES=['/static/css/caching-optimizations.css','/static/css/optimization-animations.css','/static/css/glassmorphism.css','/static/css/professional-theme.css','/static/css/tailwind.min.css'];const NON_CRITICAL_CSS_RESOURCES=['/static/css/components.css','/static/css/printers.css','/static/fontawesome/css/all.min.css'];const CSS_CACHE_CONFIG={maxAge:24*60*60*1000,maxEntries:50,networkTimeoutSeconds:5};self.addEventListener('install',event=>{console.log('[CSS-SW] Service Worker wird installiert...');event.waitUntil(caches.open(CSS_CACHE_NAME).then(cache=>{console.log('[CSS-SW] Kritische CSS-Ressourcen werden gecacht...');return cache.addAll(CRITICAL_CSS_RESOURCES);}).then(()=>{console.log('[CSS-SW] Installation abgeschlossen');return self.skipWaiting();}).catch(error=>{console.error('[CSS-SW] Fehler bei Installation:',error);}));});self.addEventListener('activate',event=>{console.log('[CSS-SW] Service Worker wird aktiviert...');event.waitUntil(caches.keys().then(cacheNames=>{return Promise.all(cacheNames.map(cacheName=>{if(cacheName!==CACHE_NAME&&cacheName!==CSS_CACHE_NAME){console.log('[CSS-SW] Alter Cache wird gelöscht:',cacheName);return caches.delete(cacheName);}}));}).then(()=>{console.log('[CSS-SW] Aktivierung abgeschlossen');return self.clients.claim();}).then(()=>{return prefetchNonCriticalResources();}));});self.addEventListener('fetch',event=>{const{request}=event;if(!request.url.includes('.css')&&!request.url.includes('/static/css/')){return;}
|
||||
event.respondWith(handleCSSRequest(request));});async function handleCSSRequest(request){const url=new URL(request.url);try{const cachedResponse=await caches.match(request);if(cachedResponse){console.log('[CSS-SW] Cache-Hit für:',url.pathname);if(CRITICAL_CSS_RESOURCES.some(resource=>url.pathname.includes(resource))){updateCacheInBackground(request);}
|
||||
return cachedResponse;}
|
||||
const networkResponse=await fetchWithTimeout(request,CSS_CACHE_CONFIG.networkTimeoutSeconds*1000);if(networkResponse&&networkResponse.ok){const responseToCache=networkResponse.clone();cacheResponse(request,responseToCache);console.log('[CSS-SW] Network-Response für:',url.pathname);return networkResponse;}
|
||||
return await getFallbackCSS(request);}catch(error){console.error('[CSS-SW] Fehler bei CSS-Request:',error);return await getFallbackCSS(request);}}
|
||||
function fetchWithTimeout(request,timeout){return new Promise((resolve,reject)=>{const timer=setTimeout(()=>{reject(new Error('Network timeout'));},timeout);fetch(request).then(response=>{clearTimeout(timer);resolve(response);}).catch(error=>{clearTimeout(timer);reject(error);});});}
|
||||
async function cacheResponse(request,response){try{const cache=await caches.open(CSS_CACHE_NAME);await cache.put(request,response);await maintainCacheSize();}catch(error){console.error('[CSS-SW] Fehler beim Cachen:',error);}}
|
||||
async function maintainCacheSize(){try{const cache=await caches.open(CSS_CACHE_NAME);const requests=await cache.keys();if(requests.length>CSS_CACHE_CONFIG.maxEntries){console.log('[CSS-SW] Cache-Bereinigung wird durchgeführt...');const excessCount=requests.length-CSS_CACHE_CONFIG.maxEntries;for(let i=0;i<excessCount;i++){await cache.delete(requests[i]);}}}catch(error){console.error('[CSS-SW] Fehler bei Cache-Wartung:',error);}}
|
||||
async function updateCacheInBackground(request){try{const response=await fetch(request);if(response&&response.ok){const cache=await caches.open(CSS_CACHE_NAME);await cache.put(request,response.clone());console.log('[CSS-SW] Hintergrund-Update für:',request.url);}}catch(error){console.log('[CSS-SW] Hintergrund-Update fehlgeschlagen:',error);}}
|
||||
async function getFallbackCSS(request){const url=new URL(request.url);const fallbackCSS=`body{font-family:system-ui,sans-serif;margin:0;padding:20px;background:#f8fafc;color:#1f2937;}.offline-notice{background:#fef3c7;border:1px solid#f59e0b;border-radius:8px;padding:16px;margin-bottom:20px;text-align:center;}.btn{background:#0073ce;color:white;border:none;padding:8px 16px;border-radius:4px;cursor:pointer;}.card{background:white;border:1px solid#e5e7eb;border-radius:8px;padding:16px;margin-bottom:16px;}`;return new Response(fallbackCSS,{headers:{'Content-Type':'text/css','Cache-Control':'no-cache'}});}
|
||||
async function prefetchNonCriticalResources(){try{const cache=await caches.open(CSS_CACHE_NAME);for(const resource of NON_CRITICAL_CSS_RESOURCES){try{const request=new Request(resource);const cachedResponse=await cache.match(request);if(!cachedResponse){const response=await fetch(request);if(response&&response.ok){await cache.put(request,response);console.log('[CSS-SW] Prefetch erfolgreich für:',resource);}}}catch(error){console.log('[CSS-SW] Prefetch fehlgeschlagen für:',resource);}}}catch(error){console.error('[CSS-SW] Fehler beim Prefetching:',error);}}
|
||||
self.addEventListener('message',event=>{const{type,data}=event.data;switch(type){case'SKIP_WAITING':self.skipWaiting();break;case'CLEAR_CSS_CACHE':clearCSSCache().then(()=>{event.ports[0].postMessage({success:true});});break;case'PREFETCH_CSS':prefetchSpecificCSS(data.urls).then(()=>{event.ports[0].postMessage({success:true});});break;case'GET_CACHE_STATS':getCacheStats().then(stats=>{event.ports[0].postMessage(stats);});break;}});async function clearCSSCache(){try{await caches.delete(CSS_CACHE_NAME);console.log('[CSS-SW] CSS-Cache wurde geleert');}catch(error){console.error('[CSS-SW] Fehler beim Leeren des CSS-Cache:',error);}}
|
||||
async function prefetchSpecificCSS(urls){try{const cache=await caches.open(CSS_CACHE_NAME);for(const url of urls){try{const response=await fetch(url);if(response&&response.ok){await cache.put(url,response);console.log('[CSS-SW] Spezifisches Prefetch für:',url);}}catch(error){console.log('[CSS-SW] Spezifisches Prefetch fehlgeschlagen für:',url);}}}catch(error){console.error('[CSS-SW] Fehler beim spezifischen Prefetching:',error);}}
|
||||
async function getCacheStats(){try{const cache=await caches.open(CSS_CACHE_NAME);const requests=await cache.keys();return{cssEntries:requests.length,maxEntries:CSS_CACHE_CONFIG.maxEntries,cacheUtilization:(requests.length/CSS_CACHE_CONFIG.maxEntries)*100,cachedUrls:requests.map(req=>req.url)};}catch(error){console.error('[CSS-SW] Fehler beim Abrufen der Cache-Stats:',error);return{error:error.message};}}
|
||||
self.addEventListener('install',()=>{console.log('[CSS-SW] Installation gestartet um:',new Date().toISOString());});self.addEventListener('activate',()=>{console.log('[CSS-SW] Aktivierung abgeschlossen um:',new Date().toISOString());});self.addEventListener('error',event=>{console.error('[CSS-SW] Globaler Fehler:',event.error);});self.addEventListener('unhandledrejection',event=>{console.error('[CSS-SW] Unbehandelte Promise-Rejection:',event.reason);});console.log('[CSS-SW] CSS-Caching Service Worker geladen');
|
||||
BIN
static/js/css-cache-service-worker.min.js.gz
Normal file
BIN
static/js/css-cache-service-worker.min.js.gz
Normal file
Binary file not shown.
192
static/js/dark-mode-fix.js
Normal file
192
static/js/dark-mode-fix.js
Normal 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');
|
||||
});
|
||||
BIN
static/js/dark-mode-fix.js.gz
Normal file
BIN
static/js/dark-mode-fix.js.gz
Normal file
Binary file not shown.
1
static/js/dark-mode-fix.min.js
vendored
Normal file
1
static/js/dark-mode-fix.min.js
vendored
Normal 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")}));
|
||||
BIN
static/js/dark-mode-fix.min.js.gz
Normal file
BIN
static/js/dark-mode-fix.min.js.gz
Normal file
Binary file not shown.
306
static/js/dark-mode.js
Normal file
306
static/js/dark-mode.js
Normal 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
BIN
static/js/dark-mode.js.gz
Normal file
Binary file not shown.
15
static/js/dark-mode.min.js
vendored
Normal file
15
static/js/dark-mode.min.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
(function(){"use strict";const STORAGE_KEY='myp-dark-mode';let darkModeToggle;const html=document.documentElement;document.addEventListener('DOMContentLoaded',initialize);function shouldUseDarkMode(){const savedMode=localStorage.getItem(STORAGE_KEY);if(savedMode!==null){return savedMode==='true';}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;}
|
||||
function setDarkMode(enable){html.classList.add('disable-transitions');if(enable){html.classList.add('dark');html.setAttribute('data-theme','dark');html.style.colorScheme='dark';}else{html.classList.remove('dark');html.setAttribute('data-theme','light');html.style.colorScheme='light';}
|
||||
localStorage.setItem(STORAGE_KEY,enable);updateMetaThemeColor(enable);if(darkModeToggle){updateDarkModeToggle(enable);}
|
||||
window.dispatchEvent(new CustomEvent('darkModeChanged',{detail:{isDark:enable,source:'dark-mode-toggle',timestamp:new Date().toISOString()}}));const eventName=enable?'darkModeEnabled':'darkModeDisabled';window.dispatchEvent(new CustomEvent(eventName,{detail:{timestamp:new Date().toISOString()}}));setTimeout(function(){html.classList.remove('disable-transitions');},100);console.log(`${enable?'🌙':'☀️'}${enable?'Dark Mode aktiviert - Augenschonender Modus aktiv':'Light Mode aktiviert - Heller Modus aktiv'}`);}
|
||||
function updateMetaThemeColor(isDark){const metaTags=[document.getElementById('metaThemeColor'),document.querySelector('meta[name="theme-color"]'),document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: light)"]'),document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: dark)"]')];const darkColor=getComputedStyle(document.documentElement).getPropertyValue('--color-bg')||'#0f172a';const lightColor=getComputedStyle(document.documentElement).getPropertyValue('--color-bg')||'#ffffff';metaTags.forEach(tag=>{if(tag){if(tag.getAttribute('media')==='(prefers-color-scheme: dark)'){tag.setAttribute('content',darkColor);}else if(tag.getAttribute('media')==='(prefers-color-scheme: light)'){tag.setAttribute('content',lightColor);}else{tag.setAttribute('content',isDark?darkColor:lightColor);}}});}
|
||||
function updateDarkModeToggle(isDark){darkModeToggle.setAttribute('aria-pressed',isDark.toString());darkModeToggle.title=isDark?"Light Mode aktivieren":"Dark Mode aktivieren";const sunIcon=darkModeToggle.querySelector('.sun-icon');const moonIcon=darkModeToggle.querySelector('.moon-icon');if(sunIcon&&moonIcon){if(isDark){sunIcon.classList.add('hidden');moonIcon.classList.remove('hidden');}else{sunIcon.classList.remove('hidden');moonIcon.classList.add('hidden');}}else{const icon=darkModeToggle.querySelector('svg');if(icon){icon.classList.add('animate-spin-once');setTimeout(()=>{icon.classList.remove('animate-spin-once');},300);const pathElement=icon.querySelector('path');if(pathElement){if(isDark){pathElement.setAttribute("d","M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z");}else{pathElement.setAttribute("d","M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z");}}}}}
|
||||
function initialize(){darkModeToggle=document.getElementById('darkModeToggle');if(!darkModeToggle){console.log('🔧 Dark Mode Toggle nicht gefunden - erstelle automatisch einen neuen Button');createDarkModeToggle();}
|
||||
if(darkModeToggle){darkModeToggle.addEventListener('click',function(){const isDark=!shouldUseDarkMode();console.log(`👆 Dark Mode Toggle:Wechsel zu ${isDark?'🌙 dunkel':'☀️ hell'}angefordert`);setDarkMode(isDark);});}
|
||||
document.addEventListener('keydown',function(e){if(e.ctrlKey&&e.shiftKey&&e.key==='D'){const isDark=!shouldUseDarkMode();console.log(`⌨️ Tastenkombination STRG+SHIFT+D erkannt:Wechsel zu ${isDark?'🌙 dunkel':'☀️ hell'}`);setDarkMode(isDark);e.preventDefault();}});const darkModeMediaQuery=window.matchMedia('(prefers-color-scheme: dark)');try{darkModeMediaQuery.addEventListener('change',function(e){if(localStorage.getItem(STORAGE_KEY)===null){console.log(`🖥️ Systemeinstellung geändert:${e.matches?'🌙 dunkel':'☀️ hell'}`);setDarkMode(e.matches);}});}catch(error){darkModeMediaQuery.addListener(function(e){if(localStorage.getItem(STORAGE_KEY)===null){console.log(`🖥️ Systemeinstellung geändert(Legacy-Browser):${e.matches?'🌙 dunkel':'☀️ hell'}`);setDarkMode(e.matches);}});}
|
||||
const initialState=shouldUseDarkMode();console.log(`🔍 Ermittelter Ausgangszustand:${initialState?'🌙 Dark Mode':'☀️ Light Mode'}`);setDarkMode(initialState);const animClass=initialState?'dark-mode-transition':'light-mode-transition';document.body.classList.add(animClass);setTimeout(()=>{document.body.classList.remove(animClass);},300);console.log('🚀 Dark Mode Handler erfolgreich initialisiert');}
|
||||
function createDarkModeToggle(){const header=document.querySelector('header');const nav=document.querySelector('nav');const container=document.querySelector('.dark-mode-container')||header||nav;if(!container){console.error('⚠️ Kein geeigneter Container für Dark Mode Toggle gefunden');return;}
|
||||
darkModeToggle=document.createElement('button');darkModeToggle.id='darkModeToggle';darkModeToggle.className='dark-mode-toggle-new';darkModeToggle.setAttribute('aria-label','Dark Mode umschalten');darkModeToggle.setAttribute('title','Dark Mode aktivieren');darkModeToggle.setAttribute('data-action','toggle-dark-mode');const sunIcon=document.createElementNS("http://www.w3.org/2000/svg","svg");sunIcon.setAttribute("class","w-5 h-5 sm:w-5 sm:h-5 sun-icon");sunIcon.setAttribute("fill","none");sunIcon.setAttribute("stroke","currentColor");sunIcon.setAttribute("viewBox","0 0 24 24");sunIcon.setAttribute("aria-hidden","true");const sunPath=document.createElementNS("http://www.w3.org/2000/svg","path");sunPath.setAttribute("stroke-linecap","round");sunPath.setAttribute("stroke-linejoin","round");sunPath.setAttribute("stroke-width","2");sunPath.setAttribute("d","M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z");const moonIcon=document.createElementNS("http://www.w3.org/2000/svg","svg");moonIcon.setAttribute("class","w-5 h-5 sm:w-5 sm:h-5 moon-icon hidden");moonIcon.setAttribute("fill","none");moonIcon.setAttribute("stroke","currentColor");moonIcon.setAttribute("viewBox","0 0 24 24");moonIcon.setAttribute("aria-hidden","true");const moonPath=document.createElementNS("http://www.w3.org/2000/svg","path");moonPath.setAttribute("stroke-linecap","round");moonPath.setAttribute("stroke-linejoin","round");moonPath.setAttribute("stroke-width","2");moonPath.setAttribute("d","M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z");sunIcon.appendChild(sunPath);moonIcon.appendChild(moonPath);darkModeToggle.appendChild(sunIcon);darkModeToggle.appendChild(moonIcon);container.appendChild(darkModeToggle);console.log('✅ Dark Mode Toggle Button erfolgreich erstellt und zur Benutzeroberfläche hinzugefügt');}
|
||||
const isDark=shouldUseDarkMode();console.log(`🏃♂️ Sofortige Anwendung:${isDark?'🌙 Dark Mode':'☀️ Light Mode'}(vor DOM-Ladung)`);setDarkMode(isDark);})();if(!document.querySelector('style#dark-mode-animations')){const styleTag=document.createElement('style');styleTag.id='dark-mode-animations';styleTag.textContent=`@keyframes spin-once{from{transform:rotate(0deg);}
|
||||
to{transform:rotate(360deg);}}.animate-spin-once{animation:spin-once 0.3s ease-in-out;}`;document.head.appendChild(styleTag);console.log('💫 Animations-Styles für Dark Mode Toggle hinzugefügt');}
|
||||
BIN
static/js/dark-mode.min.js.gz
Normal file
BIN
static/js/dark-mode.min.js.gz
Normal file
Binary file not shown.
354
static/js/dashboard.js
Normal file
354
static/js/dashboard.js
Normal 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
BIN
static/js/dashboard.js.gz
Normal file
Binary file not shown.
32
static/js/dashboard.min.js
vendored
Normal file
32
static/js/dashboard.min.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
let dashboardData={};let updateInterval;const elements={activeJobs:null,scheduledJobs:null,availablePrinters:null,totalPrintTime:null,schedulerStatus:null,recentJobsList:null,recentActivitiesList:null,refreshBtn:null,schedulerToggleBtn:null};document.addEventListener('DOMContentLoaded',function(){initializeDashboard();});function initializeDashboard(){elements.activeJobs=document.getElementById('active-jobs');elements.scheduledJobs=document.getElementById('scheduled-jobs');elements.availablePrinters=document.getElementById('available-printers');elements.totalPrintTime=document.getElementById('total-print-time');elements.schedulerStatus=document.getElementById('scheduler-status');elements.recentJobsList=document.getElementById('recent-jobs-list');elements.recentActivitiesList=document.getElementById('recent-activities-list');elements.refreshBtn=document.getElementById('refresh-btn');elements.schedulerToggleBtn=document.getElementById('scheduler-toggle-btn');if(elements.refreshBtn){elements.refreshBtn.addEventListener('click',refreshDashboard);}
|
||||
if(elements.schedulerToggleBtn){elements.schedulerToggleBtn.addEventListener('click',toggleScheduler);}
|
||||
loadDashboardData();loadRecentJobs();loadRecentActivities();loadSchedulerStatus();updateInterval=setInterval(function(){loadDashboardData();loadRecentJobs();loadRecentActivities();loadSchedulerStatus();},30000);}
|
||||
async function loadDashboardData(){try{const response=await fetch('/api/dashboard');if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
|
||||
dashboardData=await response.json();updateDashboardUI();}catch(error){console.error('Fehler beim Laden der Dashboard-Daten:',error);showError('Fehler beim Laden der Dashboard-Daten');}}
|
||||
function updateDashboardUI(){if(elements.activeJobs){elements.activeJobs.textContent=dashboardData.active_jobs||0;}
|
||||
if(elements.scheduledJobs){elements.scheduledJobs.textContent=dashboardData.scheduled_jobs||0;}
|
||||
if(elements.availablePrinters){elements.availablePrinters.textContent=dashboardData.available_printers||0;}
|
||||
if(elements.totalPrintTime){const hours=Math.floor((dashboardData.total_print_time||0)/3600);const minutes=Math.floor(((dashboardData.total_print_time||0)%3600)/60);elements.totalPrintTime.textContent=`${hours}h ${minutes}m`;}}
|
||||
async function loadRecentJobs(){try{const response=await fetch('/api/jobs/recent');if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
|
||||
const data=await response.json();updateRecentJobsList(data.jobs);}catch(error){console.error('Fehler beim Laden der aktuellen Jobs:',error);if(elements.recentJobsList){elements.recentJobsList.innerHTML='<li class="list-group-item text-danger">Fehler beim Laden</li>';}}}
|
||||
function updateRecentJobsList(jobs){if(!elements.recentJobsList)return;if(!jobs||jobs.length===0){elements.recentJobsList.innerHTML='<li class="list-group-item text-muted">Keine aktuellen Jobs</li>';return;}
|
||||
const jobsHtml=jobs.map(job=>{const statusClass=getStatusClass(job.status);const timeAgo=formatTimeAgo(job.created_at);return`<li class="list-group-item d-flex justify-content-between align-items-center"><div><strong>${escapeHtml(job.name)}</strong><br><small class="text-muted">${escapeHtml(job.printer_name)}• ${timeAgo}</small></div><span class="badge ${statusClass}">${getStatusText(job.status)}</span></li>`;}).join('');elements.recentJobsList.innerHTML=jobsHtml;}
|
||||
async function loadRecentActivities(){try{const response=await fetch('/api/activity/recent');if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
|
||||
const data=await response.json();updateRecentActivitiesList(data.activities);}catch(error){console.error('Fehler beim Laden der Aktivitäten:',error);if(elements.recentActivitiesList){elements.recentActivitiesList.innerHTML='<li class="list-group-item text-danger">Fehler beim Laden</li>';}}}
|
||||
function updateRecentActivitiesList(activities){if(!elements.recentActivitiesList)return;if(!activities||activities.length===0){elements.recentActivitiesList.innerHTML='<li class="list-group-item text-muted">Keine aktuellen Aktivitäten</li>';return;}
|
||||
const activitiesHtml=activities.map(activity=>{const timeAgo=formatTimeAgo(activity.timestamp);return`<li class="list-group-item"><div>${escapeHtml(activity.description)}</div><small class="text-muted">${timeAgo}</small></li>`;}).join('');elements.recentActivitiesList.innerHTML=activitiesHtml;}
|
||||
async function loadSchedulerStatus(){try{const response=await fetch('/api/scheduler/status');if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
|
||||
const data=await response.json();updateSchedulerStatus(data.running);}catch(error){console.error('Fehler beim Laden des Scheduler-Status:',error);if(elements.schedulerStatus){elements.schedulerStatus.innerHTML='<span class="badge bg-secondary">Unbekannt</span>';}}}
|
||||
function updateSchedulerStatus(isRunning){if(!elements.schedulerStatus)return;const statusClass=isRunning?'bg-success':'bg-danger';const statusText=isRunning?'Aktiv':'Gestoppt';elements.schedulerStatus.innerHTML=`<span class="badge ${statusClass}">${statusText}</span>`;if(elements.schedulerToggleBtn){elements.schedulerToggleBtn.textContent=isRunning?'Scheduler stoppen':'Scheduler starten';elements.schedulerToggleBtn.className=isRunning?'btn btn-danger btn-sm':'btn btn-success btn-sm';}}
|
||||
async function toggleScheduler(){try{const isRunning=dashboardData.scheduler_running;const endpoint=isRunning?'/api/scheduler/stop':'/api/scheduler/start';const response=await fetch(endpoint,{method:'POST',headers:{'Content-Type':'application/json'}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);}
|
||||
const result=await response.json();if(result.success){showSuccess(result.message);setTimeout(loadSchedulerStatus,1000);}else{showError(result.error||'Unbekannter Fehler');}}catch(error){console.error('Fehler beim Umschalten des Schedulers:',error);showError('Fehler beim Umschalten des Schedulers');}}
|
||||
function refreshDashboard(){if(elements.refreshBtn){elements.refreshBtn.disabled=true;elements.refreshBtn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Aktualisiere...';}
|
||||
Promise.all([loadDashboardData(),loadRecentJobs(),loadRecentActivities(),loadSchedulerStatus()]).finally(()=>{if(elements.refreshBtn){elements.refreshBtn.disabled=false;elements.refreshBtn.innerHTML='<i class="fas fa-sync-alt"></i> Aktualisieren';}});}
|
||||
function getStatusClass(status){const statusClasses={'pending':'bg-warning','printing':'bg-primary','completed':'bg-success','failed':'bg-danger','cancelled':'bg-secondary','scheduled':'bg-info'};return statusClasses[status]||'bg-secondary';}
|
||||
function getStatusText(status){const statusTexts={'pending':'Wartend','printing':'Druckt','completed':'Abgeschlossen','failed':'Fehlgeschlagen','cancelled':'Abgebrochen','scheduled':'Geplant'};return statusTexts[status]||status;}
|
||||
function formatTimeAgo(timestamp){const now=new Date();const time=new Date(timestamp);const diffMs=now-time;const diffMins=Math.floor(diffMs/60000);const diffHours=Math.floor(diffMins/60);const diffDays=Math.floor(diffHours/24);if(diffMins<1)return'Gerade eben';if(diffMins<60)return`vor ${diffMins}Min`;if(diffHours<24)return`vor ${diffHours}Std`;return`vor ${diffDays}Tag${diffDays>1?'en':''}`;}
|
||||
function escapeHtml(text){const div=document.createElement('div');div.textContent=text;return div.innerHTML;}
|
||||
function showSuccess(message){showNotification(message,'success');}
|
||||
function showError(message){showNotification(message,'danger');}
|
||||
function showNotification(message,type){const alertDiv=document.createElement('div');alertDiv.className=`alert alert-${type}alert-dismissible fade show position-fixed`;alertDiv.style.top='20px';alertDiv.style.right='20px';alertDiv.style.zIndex='9999';alertDiv.innerHTML=`${escapeHtml(message)}<button type="button"class="btn-close"data-bs-dismiss="alert"></button>`;document.body.appendChild(alertDiv);setTimeout(()=>{if(alertDiv.parentNode){alertDiv.parentNode.removeChild(alertDiv);}},5000);}
|
||||
window.addEventListener('beforeunload',function(){if(updateInterval){clearInterval(updateInterval);}});
|
||||
BIN
static/js/dashboard.min.js.gz
Normal file
BIN
static/js/dashboard.min.js.gz
Normal file
Binary file not shown.
175
static/js/debug-fix.js
Normal file
175
static/js/debug-fix.js
Normal 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
BIN
static/js/debug-fix.js.gz
Normal file
Binary file not shown.
10
static/js/debug-fix.min.js
vendored
Normal file
10
static/js/debug-fix.min.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
(function(){'use strict';console.log('🔧 Debug Fix Script wird geladen...');window.MYP=window.MYP||{};window.MYP.UI=window.MYP.UI||{};window.MVP=window.MVP||{};window.MVP.UI=window.MVP.UI||{};window.MVP.UI.DarkModeManager=function(){console.log('⚠️ MVP.UI.DarkModeManager Konstruktor aufgerufen - verwende MYP.UI.darkMode stattdessen');if(window.MYP&&window.MYP.UI&&window.MYP.UI.darkMode){return window.MYP.UI.darkMode;}
|
||||
return{init:function(){console.log('DarkModeManager Fallback init');},setDarkMode:function(){console.log('DarkModeManager Fallback setDarkMode');},isDarkMode:function(){return false;}};};document.addEventListener('DOMContentLoaded',function(){console.log('🚀 Debug Fix: DOM Content geladen');setTimeout(()=>{try{if(window.MYP&&window.MYP.UI&&window.MYP.UI.darkMode){window.MVP.UI.DarkModeManager=function(){console.log('⚠️ MVP.UI.DarkModeManager Konstruktor aufgerufen - verwende MYP.UI.darkMode stattdessen');return window.MYP.UI.darkMode;};console.log('✅ MVP.UI.DarkModeManager Alias aktualisiert');}
|
||||
if(!window.jobManager&&window.JobManager){window.jobManager=new window.JobManager();console.log('✅ JobManager Instanz erstellt');}
|
||||
if(window.jobManager&&!window.jobManager.setupFormHandlers){window.jobManager.setupFormHandlers=function(){console.log('✅ setupFormHandlers Fallback aufgerufen');};}
|
||||
window.refreshJobs=function(){if(window.jobManager&&window.jobManager.loadJobs){return window.jobManager.loadJobs();}else{console.warn('⚠️ JobManager nicht verfügbar - Seite wird neu geladen');window.location.reload();}};window.startJob=function(jobId){if(window.jobManager&&window.jobManager.startJob){return window.jobManager.startJob(jobId);}};window.pauseJob=function(jobId){if(window.jobManager&&window.jobManager.pauseJob){return window.jobManager.pauseJob(jobId);}};window.resumeJob=function(jobId){if(window.jobManager&&window.jobManager.resumeJob){return window.jobManager.resumeJob(jobId);}};window.deleteJob=function(jobId){if(window.jobManager&&window.jobManager.deleteJob){return window.jobManager.deleteJob(jobId);}};console.log('✅ Debug Fix Script erfolgreich angewendet');}catch(error){console.error('❌ Debug Fix Fehler:',error);}},100);});window.addEventListener('error',function(e){const errorInfo={message:e.message||'Unbekannter Fehler',filename:e.filename||'Unbekannte Datei',lineno:e.lineno||0,colno:e.colno||0,stack:e.error?e.error.stack:'Stack nicht verfügbar',type:e.error?e.error.constructor.name:'Unbekannter Typ'};console.error('🐛 JavaScript Error abgefangen:',JSON.stringify(errorInfo,null,2));if(e.message.includes('MVP.UI.DarkModeManager is not a constructor')){console.log('🔧 DarkModeManager Fehler erkannt - verwende MYP.UI.darkMode');e.preventDefault();return false;}
|
||||
if(e.message.includes('setupFormHandlers is not a function')){console.log('🔧 setupFormHandlers Fehler erkannt - verwende Fallback');e.preventDefault();return false;}
|
||||
if(e.message.includes('refreshStats is not defined')){console.log('🔧 refreshStats Fehler erkannt - lade global-refresh-functions.js');const script=document.createElement('script');script.src='/static/js/global-refresh-functions.js';script.onload=function(){console.log('✅ global-refresh-functions.js nachgeladen');};document.head.appendChild(script);e.preventDefault();return false;}
|
||||
if(e.message.includes('Cannot read properties of undefined')){console.log('🔧 Undefined Properties Fehler erkannt - ignoriert für Stabilität');e.preventDefault();return false;}
|
||||
if(e.message.includes('jobManager')||e.message.includes('JobManager')){console.log('🔧 JobManager Fehler erkannt - verwende Fallback');e.preventDefault();return false;}
|
||||
if(e.message.includes('showToast is not defined')){console.log('🔧 showToast Fehler erkannt - verwende Fallback');e.preventDefault();return false;}});window.addEventListener('unhandledrejection',function(e){console.error('🐛 Promise Rejection abgefangen:',e.reason);if(e.reason&&e.reason.message&&e.reason.message.includes('Jobs')){console.log('🔧 Jobs-bezogener Promise Fehler - ignoriert');e.preventDefault();}});console.log('✅ Debug Fix Script bereit');})();
|
||||
BIN
static/js/debug-fix.min.js.gz
Normal file
BIN
static/js/debug-fix.min.js.gz
Normal file
Binary file not shown.
482
static/js/event-handlers.js
Normal file
482
static/js/event-handlers.js
Normal 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');
|
||||
BIN
static/js/event-handlers.js.gz
Normal file
BIN
static/js/event-handlers.js.gz
Normal file
Binary file not shown.
32
static/js/event-handlers.min.js
vendored
Normal file
32
static/js/event-handlers.min.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
class GlobalEventManager{constructor(){this.init();}
|
||||
init(){document.addEventListener('click',this.handleClick.bind(this));document.addEventListener('DOMContentLoaded',this.setupEventListeners.bind(this));if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',this.setupEventListeners.bind(this));}else{this.setupEventListeners();}}
|
||||
handleClick(event){const target=event.target.closest('[data-action]');if(!target)return;const action=target.getAttribute('data-action');const params=this.parseActionParams(target);event.preventDefault();this.executeAction(action,params,target);}
|
||||
parseActionParams(element){const params={};for(const attr of element.attributes){if(attr.name.startsWith('data-action-')){const key=attr.name.replace('data-action-','');params[key]=attr.value;}}
|
||||
return params;}
|
||||
executeAction(action,params,element){console.log(`🎯 Führe Action aus:${action}`,params);switch(action){case'refresh-dashboard':if(typeof refreshDashboard==='function')refreshDashboard();break;case'logout':if(typeof handleLogout==='function')handleLogout();break;case'go-back':window.history.back();break;case'reload-page':window.location.reload();break;case'print-page':window.print();break;case'refresh-jobs':if(typeof refreshJobs==='function')refreshJobs();break;case'toggle-batch-mode':if(typeof toggleBatchMode==='function')toggleBatchMode();break;case'start-job':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.startJob(params.id);}
|
||||
break;case'pause-job':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.pauseJob(params.id);}
|
||||
break;case'resume-job':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.resumeJob(params.id);}
|
||||
break;case'delete-job':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.deleteJob(params.id);}
|
||||
break;case'open-job-details':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.openJobDetails(params.id);}
|
||||
break;case'refresh-printers':if(typeof refreshPrinters==='function')refreshPrinters();break;case'toggle-maintenance-mode':if(typeof toggleMaintenanceMode==='function')toggleMaintenanceMode();break;case'open-add-printer-modal':if(typeof openAddPrinterModal==='function')openAddPrinterModal();break;case'toggle-auto-refresh':if(typeof toggleAutoRefresh==='function')toggleAutoRefresh();break;case'clear-all-filters':if(typeof clearAllFilters==='function')clearAllFilters();break;case'test-printer-connection':if(typeof testPrinterConnection==='function')testPrinterConnection();break;case'delete-printer':if(typeof deletePrinter==='function')deletePrinter();break;case'edit-printer':if(typeof printerManager!=='undefined'&¶ms.id){printerManager.editPrinter(params.id);}
|
||||
break;case'connect-printer':if(typeof printerManager!=='undefined'&¶ms.id){printerManager.connectPrinter(params.id);}
|
||||
break;case'refresh-calendar':if(typeof refreshCalendar==='function')refreshCalendar();break;case'toggle-auto-optimization':if(typeof toggleAutoOptimization==='function')toggleAutoOptimization();break;case'export-calendar':if(typeof exportCalendar==='function')exportCalendar();break;case'open-create-event-modal':if(typeof openCreateEventModal==='function')openCreateEventModal();break;case'close-modal':this.closeModal(params.target||element.closest('.fixed'));break;case'close-printer-modal':if(typeof closePrinterModal==='function')closePrinterModal();break;case'close-job-modal':if(typeof closeJobModal==='function')closeJobModal();break;case'close-event-modal':if(typeof closeEventModal==='function')closeEventModal();break;case'reset-form':if(typeof resetForm==='function')resetForm();else this.resetNearestForm(element);break;case'clear-file':if(typeof clearFile==='function')clearFile();break;case'check-status':if(typeof checkStatus==='function')checkStatus();break;case'copy-code':if(typeof copyCode==='function')copyCode();break;case'refresh-status':if(typeof refreshStatus==='function')refreshStatus();break;case'show-status-check':if(typeof showStatusCheck==='function')showStatusCheck();break;case'perform-bulk-action':if(typeof performBulkAction==='function'&¶ms.type){performBulkAction(params.type);}
|
||||
break;case'close-bulk-modal':if(typeof closeBulkModal==='function')closeBulkModal();break;case'clear-cache':if(typeof clearCache==='function')clearCache();break;case'optimize-database':if(typeof optimizeDatabase==='function')optimizeDatabase();break;case'create-backup':if(typeof createBackup==='function')createBackup();break;case'download-logs':if(typeof downloadLogs==='function')downloadLogs();break;case'run-maintenance':if(typeof runMaintenance==='function')runMaintenance();break;case'save-settings':if(typeof saveSettings==='function')saveSettings();break;case'toggle-edit-mode':if(typeof toggleEditMode==='function')toggleEditMode();break;case'trigger-avatar-upload':if(typeof triggerAvatarUpload==='function')triggerAvatarUpload();break;case'cancel-edit':if(typeof cancelEdit==='function')cancelEdit();break;case'download-user-data':if(typeof downloadUserData==='function')downloadUserData();break;case'refresh-stats':if(typeof refreshStats==='function')refreshStats();break;case'export-stats':if(typeof exportStats==='function')exportStats();break;case'remove-element':const targetElement=params.target?document.querySelector(params.target):element.closest(params.selector||'.removable');if(targetElement){targetElement.remove();}
|
||||
break;case'toggle-element':const toggleTarget=params.target?document.querySelector(params.target):element.nextElementSibling;if(toggleTarget){toggleTarget.classList.toggle('hidden');}
|
||||
break;case'show-element':const showTarget=document.querySelector(params.target);if(showTarget){showTarget.classList.remove('hidden');}
|
||||
break;case'hide-element':const hideTarget=document.querySelector(params.target);if(hideTarget){hideTarget.classList.add('hidden');}
|
||||
break;default:console.warn(`⚠️ Unbekannte Action:${action}`);if(typeof window[action]==='function'){window[action](params);}
|
||||
break;}}
|
||||
closeModal(modalElement){if(modalElement){modalElement.classList.add('hidden');modalElement.remove();}}
|
||||
resetNearestForm(element){const form=element.closest('form');if(form){form.reset();}}
|
||||
setupEventListeners(){this.setupAutoRefresh();this.setupKeyboardShortcuts();this.setupFormValidation();console.log('🔧 Globale Event-Handler initialisiert');}
|
||||
setupAutoRefresh(){const currentPath=window.location.pathname;if(currentPath.includes('/dashboard')){setInterval(()=>{if(typeof refreshDashboard==='function'){refreshDashboard();}},30000);}
|
||||
if(currentPath.includes('/jobs')){setInterval(()=>{if(typeof refreshJobs==='function'){refreshJobs();}},15000);}}
|
||||
setupKeyboardShortcuts(){document.addEventListener('keydown',(event)=>{if(event.key==='Escape'){const openModal=document.querySelector('.fixed:not(.hidden)');if(openModal){this.closeModal(openModal);}}
|
||||
if(event.ctrlKey&&event.key==='r'){event.preventDefault();const currentPath=window.location.pathname;if(currentPath.includes('/dashboard')&&typeof refreshDashboard==='function'){refreshDashboard();}else if(currentPath.includes('/jobs')&&typeof refreshJobs==='function'){refreshJobs();}else if(currentPath.includes('/printers')&&typeof refreshPrinters==='function'){refreshPrinters();}else{window.location.reload();}}});}
|
||||
setupFormValidation(){const forms=document.querySelectorAll('form[data-validate]');forms.forEach(form=>{form.addEventListener('submit',this.validateForm.bind(this));});}
|
||||
validateForm(event){const form=event.target;const requiredFields=form.querySelectorAll('[required]');let isValid=true;requiredFields.forEach(field=>{if(!field.value.trim()){isValid=false;field.classList.add('border-red-500');const errorId=`${field.id}-error`;let errorElement=document.getElementById(errorId);if(!errorElement){errorElement=document.createElement('div');errorElement.id=errorId;errorElement.className='text-red-500 text-sm mt-1';field.parentNode.appendChild(errorElement);}
|
||||
errorElement.textContent=`${field.getAttribute('data-label')||'Dieses Feld'}ist erforderlich.`;}else{field.classList.remove('border-red-500');const errorElement=document.getElementById(`${field.id}-error`);if(errorElement){errorElement.remove();}}});if(!isValid){event.preventDefault();}
|
||||
return isValid;}}
|
||||
const globalEventManager=new GlobalEventManager();if(typeof module!=='undefined'&&module.exports){module.exports=GlobalEventManager;}
|
||||
console.log('🌍 Globaler Event Manager geladen');
|
||||
BIN
static/js/event-handlers.min.js.gz
Normal file
BIN
static/js/event-handlers.min.js.gz
Normal file
Binary file not shown.
6
static/js/fullcalendar/core.min.js
vendored
Normal file
6
static/js/fullcalendar/core.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/fullcalendar/core.min.js.gz
Normal file
BIN
static/js/fullcalendar/core.min.js.gz
Normal file
Binary file not shown.
6
static/js/fullcalendar/daygrid.min.js
vendored
Normal file
6
static/js/fullcalendar/daygrid.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/fullcalendar/daygrid.min.js.gz
Normal file
BIN
static/js/fullcalendar/daygrid.min.js.gz
Normal file
Binary file not shown.
6
static/js/fullcalendar/interaction.min.js
vendored
Normal file
6
static/js/fullcalendar/interaction.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/fullcalendar/interaction.min.js.gz
Normal file
BIN
static/js/fullcalendar/interaction.min.js.gz
Normal file
Binary file not shown.
6
static/js/fullcalendar/list.min.js
vendored
Normal file
6
static/js/fullcalendar/list.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/fullcalendar/list.min.js.gz
Normal file
BIN
static/js/fullcalendar/list.min.js.gz
Normal file
Binary file not shown.
2
static/js/fullcalendar/main.min.css
vendored
Normal file
2
static/js/fullcalendar/main.min.css
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/* FullCalendar v6 CSS is embedded in the JavaScript bundle */
|
||||
/* This file is kept for template compatibility */
|
||||
BIN
static/js/fullcalendar/main.min.css.gz
Normal file
BIN
static/js/fullcalendar/main.min.css.gz
Normal file
Binary file not shown.
6
static/js/fullcalendar/timegrid.min.js
vendored
Normal file
6
static/js/fullcalendar/timegrid.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/fullcalendar/timegrid.min.js.gz
Normal file
BIN
static/js/fullcalendar/timegrid.min.js.gz
Normal file
Binary file not shown.
1591
static/js/glassmorphism-notifications.js
Normal file
1591
static/js/glassmorphism-notifications.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/js/glassmorphism-notifications.js.gz
Normal file
BIN
static/js/glassmorphism-notifications.js.gz
Normal file
Binary file not shown.
80
static/js/glassmorphism-notifications.min.js
vendored
Normal file
80
static/js/glassmorphism-notifications.min.js
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
class GlassmorphismNotificationSystem{constructor(){this.notifications=new Map();this.toastCounter=0;this.soundEnabled=localStorage.getItem('myp-notification-sound')!=='false';this.animationsEnabled=!window.matchMedia('(prefers-reduced-motion: reduce)').matches;this.actionCallbacks=new Map();this.callbackCounter=0;this.init();this.setupGlobalFunctions();this.injectStyles();}
|
||||
init(){this.createToastContainer();this.setupEventListeners();console.log('🎨 Glassmorphism Notification System initialisiert');}
|
||||
createToastContainer(){if(!document.getElementById('glassmorphism-toast-container')){const container=document.createElement('div');container.id='glassmorphism-toast-container';container.className='notifications-container';document.body.appendChild(container);}}
|
||||
setupGlobalFunctions(){window.showFlashMessage=this.showToast.bind(this);window.showToast=this.showToast.bind(this);window.showNotification=this.showToast.bind(this);window.showSuccessMessage=(msg,duration)=>this.showToast(msg,'success',duration);window.showErrorMessage=(msg,duration)=>this.showToast(msg,'error',duration);window.showWarningMessage=(msg,duration)=>this.showToast(msg,'warning',duration);window.showInfoMessage=(msg,duration)=>this.showToast(msg,'info',duration);window.showSuccessNotification=(msg)=>this.showToast(msg,'success');window.showPersistentAlert=this.showPersistentAlert.bind(this);window.showConfirmationToast=this.showConfirmationToast.bind(this);window.showProgressToast=this.showProgressToast.bind(this);window.executeNotificationCallback=this.executeCallback.bind(this);}
|
||||
setupEventListeners(){document.addEventListener('keydown',(e)=>{if(e.key==='Escape'){this.closeAllToasts();}
|
||||
if((e.ctrlKey||e.metaKey)&&e.shiftKey&&e.key==='N'){this.showNotificationSettings();}});window.addEventListener('orientationchange',()=>{setTimeout(()=>this.repositionAllToasts(),200);});}
|
||||
showToast(message,type='info',duration=5000,options={}){if(window.dndManager?.isEnabled){window.dndManager.suppressNotification(message,type);return null;}
|
||||
const toastId=`glass-toast-${++this.toastCounter}`;const toast=this.createToastElement(toastId,message,type,duration,options);this.addToContainer(toast);this.animateIn(toast);this.scheduleRemoval(toastId,duration,options.persistent);if(this.soundEnabled&&options.playSound!==false){this.playNotificationSound(type);}
|
||||
if(options.browserNotification&&'Notification'in window){this.showBrowserNotification(message,type,options);}
|
||||
return toastId;}
|
||||
createToastElement(toastId,message,type,duration,options){const toast=document.createElement('div');toast.id=toastId;toast.className=`glassmorphism-toast notification notification-${type}`;toast.setAttribute('role','alert');toast.setAttribute('aria-live','polite');toast.setAttribute('aria-atomic','true');const iconSvg=this.getIconSvg(type);const progressBar=duration>0&&!options.persistent?this.createProgressBar(duration):'';toast.innerHTML=`<div class="toast-content"><div class="toast-header"><div class="toast-icon">${iconSvg}</div><div class="toast-body">${options.title?`<div class="toast-title">${options.title}</div>`:''}<div class="toast-message">${message}</div></div><div class="toast-actions">${options.actions?this.createActionButtons(options.actions,toastId):''}<button class="toast-close"onclick="glassNotificationSystem.closeToast('${toastId}')"
|
||||
aria-label="Benachrichtigung schließen"title="Schließen"><svg class="w-4 h-4"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M6 18L18 6M6 6l12 12"/></svg></button></div></div>${progressBar}</div>`;if(duration>0&&!options.persistent){this.setupHoverEvents(toast,toastId);}
|
||||
this.notifications.set(toastId,{element:toast,type,message,timestamp:Date.now(),options});return toast;}
|
||||
createProgressBar(duration){return`<div class="toast-progress"><div class="toast-progress-bar"style="animation: toast-progress ${duration}ms linear forwards;"></div></div>`;}
|
||||
createActionButtons(actions,toastId){return actions.map(action=>{let callbackId='';if(action.callback&&typeof action.callback==='function'){callbackId=`callback-${++this.callbackCounter}`;this.actionCallbacks.set(callbackId,action.callback);}
|
||||
return`<button class="toast-action-btn toast-action-${action.type || 'secondary'}"
|
||||
onclick="glassNotificationSystem.handleActionClick('${callbackId}', '${toastId}', ${action.closeAfter !== false})"
|
||||
title="${action.title || action.text}">${action.icon?`<svg class="w-4 h-4">${action.icon}</svg>`:''}
|
||||
${action.text}</button>`;}).join('');}
|
||||
getIconSvg(type){const icons={success:`<svg class="w-5 h-5"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M5 13l4 4L19 7"/></svg>`,error:`<svg class="w-5 h-5"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>`,warning:`<svg class="w-5 h-5"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>`,info:`<svg class="w-5 h-5"fill="none"stroke="currentColor"viewBox="0 0 24 24"><path stroke-linecap="round"stroke-linejoin="round"stroke-width="2"d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`,loading:`<svg class="w-5 h-5 animate-spin"fill="none"viewBox="0 0 24 24"><circle cx="12"cy="12"r="10"stroke="currentColor"stroke-width="4"class="opacity-25"></circle><path fill="currentColor"d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"class="opacity-75"></path></svg>`};return icons[type]||icons.info;}
|
||||
addToContainer(toast){const container=document.getElementById('glassmorphism-toast-container');if(container){container.appendChild(toast);this.repositionAllToasts();}}
|
||||
animateIn(toast){if(!this.animationsEnabled){toast.classList.add('show');return;}
|
||||
toast.style.transform='translateX(120%) scale(0.8) rotateY(15deg)';toast.style.opacity='0';toast.style.filter='blur(8px)';requestAnimationFrame(()=>{toast.style.transition='all 0.8s cubic-bezier(0.34, 1.56, 0.64, 1)';toast.style.transform='translateX(0) scale(1) rotateY(0deg)';toast.style.opacity='1';toast.style.filter='blur(0px)';toast.classList.add('show');});}
|
||||
setupHoverEvents(toast,toastId){let timeoutId;let isPaused=false;const notification=this.notifications.get(toastId);if(!notification)return;const pauseTimer=()=>{isPaused=true;clearTimeout(timeoutId);const progressBar=toast.querySelector('.toast-progress-bar');if(progressBar){progressBar.style.animationPlayState='paused';}
|
||||
toast.style.transform='translateY(-4px) scale(1.03)';toast.style.filter='brightness(1.1) saturate(1.1)';};const resumeTimer=()=>{isPaused=false;const progressBar=toast.querySelector('.toast-progress-bar');if(progressBar){progressBar.style.animationPlayState='running';}
|
||||
toast.style.transform='translateY(0) scale(1)';toast.style.filter='brightness(1) saturate(1)';};toast.addEventListener('mouseenter',pauseTimer);toast.addEventListener('mouseleave',resumeTimer);toast.addEventListener('focus',pauseTimer);toast.addEventListener('blur',resumeTimer);}
|
||||
scheduleRemoval(toastId,duration,persistent=false){if(persistent||duration<=0)return;setTimeout(()=>{this.closeToast(toastId);},duration);}
|
||||
closeToast(toastId){const notification=this.notifications.get(toastId);if(!notification)return;const toast=notification.element;if(this.animationsEnabled){toast.style.transition='all 0.6s cubic-bezier(0.4, 0, 1, 1)';toast.style.transform='translateX(120%) scale(0.8) rotateY(-15deg)';toast.style.opacity='0';toast.style.filter='blur(4px)';setTimeout(()=>{this.removeToast(toastId);},600);}else{this.removeToast(toastId);}}
|
||||
removeToast(toastId){const notification=this.notifications.get(toastId);if(!notification)return;if(notification.element.parentNode){notification.element.parentNode.removeChild(notification.element);}
|
||||
this.notifications.delete(toastId);this.actionCallbacks.forEach((callback,callbackId)=>{if(callbackId.includes(toastId)){this.actionCallbacks.delete(callbackId);}});this.repositionAllToasts();}
|
||||
repositionAllToasts(){const container=document.getElementById('glassmorphism-toast-container');if(!container)return;const toasts=Array.from(container.children);toasts.forEach((toast,index)=>{toast.style.top=`${1+index*3.75}rem`;toast.style.zIndex=1000-index;if(index>0){toast.style.transform=`scale(${1-index*0.015})`;toast.style.opacity=`${1-index*0.08}`;}});}
|
||||
closeAllToasts(){const toastIds=Array.from(this.notifications.keys());toastIds.forEach((id,index)=>{setTimeout(()=>this.closeToast(id),index*100);});}
|
||||
showConfirmationToast(message,onConfirm,onCancel=null,options={}){const confirmCallback=()=>{if(typeof onConfirm==='function'){try{onConfirm();}catch(error){console.error('Fehler beim Ausführen der Bestätigungslogik:',error);}}};const cancelCallback=()=>{if(typeof onCancel==='function'){try{onCancel();}catch(error){console.error('Fehler beim Ausführen der Abbruchlogik:',error);}}};const actions=[{text:options.confirmText||'Bestätigen',type:'primary',callback:confirmCallback,closeAfter:true}];if(onCancel||options.cancelText){actions.push({text:options.cancelText||'Abbrechen',type:'secondary',callback:cancelCallback,closeAfter:true});}
|
||||
return this.showToast(message,'warning',0,{persistent:true,title:options.title||'Bestätigung erforderlich',actions:actions,...options});}
|
||||
showProgressToast(message,type='info',options={}){const toastId=this.showToast(message,'loading',0,{persistent:true,title:options.title||'Verarbeitung...',...options});return{id:toastId,updateProgress:(percent)=>this.updateProgressToast(toastId,percent),updateMessage:(newMessage)=>this.updateToastMessage(toastId,newMessage),complete:(finalMessage,finalType='success')=>{this.closeToast(toastId);if(finalMessage){this.showToast(finalMessage,finalType,3000);}}};}
|
||||
updateProgressToast(toastId,percent){const notification=this.notifications.get(toastId);if(!notification)return;let progressBar=notification.element.querySelector('.toast-progress-bar');if(!progressBar){const progressContainer=document.createElement('div');progressContainer.className='toast-progress';progressContainer.innerHTML=`<div class="toast-progress-bar"></div>`;notification.element.querySelector('.toast-content').appendChild(progressContainer);progressBar=progressContainer.querySelector('.toast-progress-bar');}
|
||||
progressBar.style.width=`${Math.min(100,Math.max(0,percent))}%`;}
|
||||
updateToastMessage(toastId,newMessage){const notification=this.notifications.get(toastId);if(!notification)return;const messageEl=notification.element.querySelector('.toast-message');if(messageEl){messageEl.textContent=newMessage;}}
|
||||
showPersistentAlert(message,type='warning',options={}){return this.showToast(message,type,0,{persistent:true,title:options.title||'Wichtiger Hinweis',actions:[{text:'Verstanden',type:'primary',onClick:''}],...options});}
|
||||
async showBrowserNotification(message,type,options={}){if(!('Notification'in window))return null;if(Notification.permission==='granted'){return new Notification(options.title||'MYP Platform',{body:message,icon:'/static/icons/notification-icon.png',badge:'/static/icons/badge-icon.png',tag:`myp-${type}`,...options.browserOptions});}else if(Notification.permission==='default'){const permission=await Notification.requestPermission();if(permission==='granted'){return this.showBrowserNotification(message,type,options);}}
|
||||
return null;}
|
||||
playNotificationSound(type){if(!this.soundEnabled)return;try{const audioContext=new(window.AudioContext||window.webkitAudioContext)();const frequencies={success:[523.25,659.25,783.99,880],error:[440,370,311],warning:[493.88,587.33,659.25],info:[523.25,659.25],loading:[392,440,493.88,523.25]};const freq=frequencies[type]||frequencies.info;freq.forEach((f,i)=>{setTimeout(()=>{const oscillator=audioContext.createOscillator();const gainNode=audioContext.createGain();const filterNode=audioContext.createBiquadFilter();oscillator.connect(filterNode);filterNode.connect(gainNode);gainNode.connect(audioContext.destination);filterNode.type='lowpass';filterNode.frequency.setValueAtTime(2000,audioContext.currentTime);filterNode.Q.setValueAtTime(1,audioContext.currentTime);oscillator.frequency.setValueAtTime(f,audioContext.currentTime);oscillator.type=type==='error'?'triangle':'sine';const baseVolume=type==='error'?0.06:0.08;gainNode.gain.setValueAtTime(0,audioContext.currentTime);gainNode.gain.linearRampToValueAtTime(baseVolume,audioContext.currentTime+0.1);gainNode.gain.exponentialRampToValueAtTime(0.001,audioContext.currentTime+0.3);oscillator.start(audioContext.currentTime);oscillator.stop(audioContext.currentTime+0.3);},i*120);});if(type==='success'){setTimeout(()=>{const reverb=audioContext.createConvolver();const impulse=audioContext.createBuffer(2,audioContext.sampleRate*0.5,audioContext.sampleRate);for(let channel=0;channel<impulse.numberOfChannels;channel++){const channelData=impulse.getChannelData(channel);for(let i=0;i<channelData.length;i++){channelData[i]=(Math.random()*2-1)*Math.pow(1-i/channelData.length,2);}}
|
||||
reverb.buffer=impulse;const finalOsc=audioContext.createOscillator();const finalGain=audioContext.createGain();finalOsc.connect(reverb);reverb.connect(finalGain);finalGain.connect(audioContext.destination);finalOsc.frequency.setValueAtTime(1046.5,audioContext.currentTime);finalOsc.type='sine';finalGain.gain.setValueAtTime(0,audioContext.currentTime);finalGain.gain.linearRampToValueAtTime(0.03,audioContext.currentTime+0.1);finalGain.gain.exponentialRampToValueAtTime(0.001,audioContext.currentTime+0.8);finalOsc.start(audioContext.currentTime);finalOsc.stop(audioContext.currentTime+0.8);},freq.length*120);}}catch(error){}}
|
||||
toggleSound(){this.soundEnabled=!this.soundEnabled;localStorage.setItem('myp-notification-sound',this.soundEnabled.toString());this.showToast(`Benachrichtigungstöne ${this.soundEnabled?'aktiviert':'deaktiviert'}`,'info',2000);}
|
||||
showNotificationSettings(){const settingsHTML=`<div class="notification-settings"><h3>Benachrichtigungseinstellungen</h3><label class="setting-item"><input type="checkbox"${this.soundEnabled?'checked':''}
|
||||
onchange="glassNotificationSystem.toggleSound()"><span>Benachrichtigungstöne</span></label><label class="setting-item"><input type="checkbox"${this.animationsEnabled?'checked':''}
|
||||
onchange="glassNotificationSystem.toggleAnimations()"><span>Animationen</span></label></div>`;this.showToast(settingsHTML,'info',0,{persistent:true,title:'Einstellungen',actions:[{text:'Schließen',type:'secondary',onClick:''}]});}
|
||||
toggleAnimations(){this.animationsEnabled=!this.animationsEnabled;this.showToast(`Animationen ${this.animationsEnabled?'aktiviert':'deaktiviert'}`,'info',2000);}
|
||||
injectStyles(){if(document.getElementById('glassmorphism-notification-styles'))return;const styles=document.createElement('style');styles.id='glassmorphism-notification-styles';styles.textContent=`.glassmorphism-toast{margin-bottom:0.625rem;transform:translateX(100%);opacity:0;transition:all 0.7s cubic-bezier(0.34,1.56,0.64,1);will-change:transform,opacity,filter;backdrop-filter:blur(50px)saturate(200%)brightness(120%)contrast(110%);-webkit-backdrop-filter:blur(50px)saturate(200%)brightness(120%)contrast(110%);box-shadow:0 32px 64px rgba(0,0,0,0.1),0 16px 32px rgba(0,0,0,0.06),0 6px 12px rgba(0,0,0,0.05),inset 0 2px 0 rgba(255,255,255,0.4),inset 0 1px 2px rgba(255,255,255,0.7),0 0 0 1px rgba(255,255,255,0.18);border-radius:1.5rem;overflow:hidden;position:relative;background:linear-gradient(145deg,rgba(255,255,255,0.12)0%,rgba(255,255,255,0.06)25%,rgba(255,255,255,0.1)50%,rgba(255,255,255,0.05)75%,rgba(255,255,255,0.08)100%);}
|
||||
.glassmorphism-toast::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at 25%25%,rgba(255,255,255,0.25)0%,transparent 35%),radial-gradient(circle at 75%75%,rgba(255,255,255,0.15)0%,transparent 35%),radial-gradient(circle at 50%10%,rgba(255,255,255,0.1)0%,transparent 40%),linear-gradient(135deg,rgba(255,255,255,0.15)0%,transparent 60%),conic-gradient(from 180deg at 50%50%,rgba(255,255,255,0.05)0deg,rgba(255,255,255,0.1)45deg,rgba(255,255,255,0.05)90deg,rgba(255,255,255,0.08)135deg,rgba(255,255,255,0.05)180deg,rgba(255,255,255,0.12)225deg,rgba(255,255,255,0.05)270deg,rgba(255,255,255,0.08)315deg,rgba(255,255,255,0.05)360deg);pointer-events:none;opacity:0;transition:opacity 0.4s cubic-bezier(0.4,0,0.2,1);animation:subtle-shimmer 8s ease-in-out infinite;}
|
||||
@keyframes subtle-shimmer{0%,100%{opacity:0;transform:scale(1)rotate(0deg);}
|
||||
25%{opacity:0.3;transform:scale(1.01)rotate(1deg);}
|
||||
50%{opacity:0.6;transform:scale(1.02)rotate(0deg);}
|
||||
75%{opacity:0.3;transform:scale(1.01)rotate(-1deg);}}.glassmorphism-toast:hover::before{opacity:1;animation-play-state:paused;}
|
||||
.glassmorphism-toast::after{content:'';position:absolute;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(circle at 20%20%,rgba(255,255,255,0.3)1px,transparent 1px),radial-gradient(circle at 60%80%,rgba(255,255,255,0.2)1px,transparent 1px),radial-gradient(circle at 80%30%,rgba(255,255,255,0.25)1px,transparent 1px),radial-gradient(circle at 30%70%,rgba(255,255,255,0.15)1px,transparent 1px);opacity:0;transition:opacity 0.6s ease;animation:floating-particles 12s linear infinite;pointer-events:none;}@keyframes floating-particles{0%{transform:translate(0,0)rotate(0deg);}
|
||||
25%{transform:translate(-10px,-10px)rotate(90deg);}
|
||||
50%{transform:translate(0,-20px)rotate(180deg);}
|
||||
75%{transform:translate(10px,-10px)rotate(270deg);}
|
||||
100%{transform:translate(0,0)rotate(360deg);}}.glassmorphism-toast:hover::after{opacity:1;}.glassmorphism-toast.show{transform:translateX(0);opacity:1;}
|
||||
.glassmorphism-toast.notification-success{background:linear-gradient(145deg,rgba(34,197,94,0.18)0%,rgba(134,239,172,0.12)20%,rgba(16,185,129,0.15)40%,rgba(34,197,94,0.08)60%,rgba(134,239,172,0.1)80%,rgba(16,185,129,0.06)100%);border:1px solid rgba(34,197,94,0.3);box-shadow:0 40px 80px rgba(34,197,94,0.15),0 20px 40px rgba(34,197,94,0.08),0 8px 16px rgba(16,185,129,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(134,239,172,0.4),0 0 0 1px rgba(34,197,94,0.2);color:rgba(5,95,70,0.95);}.dark.glassmorphism-toast.notification-success{color:rgba(134,239,172,0.95);background:linear-gradient(145deg,rgba(34,197,94,0.25)0%,rgba(134,239,172,0.15)30%,rgba(34,197,94,0.12)70%,rgba(16,185,129,0.08)100%);}.glassmorphism-toast.notification-error{background:linear-gradient(145deg,rgba(239,68,68,0.18)0%,rgba(252,165,165,0.12)20%,rgba(220,38,38,0.15)40%,rgba(239,68,68,0.08)60%,rgba(252,165,165,0.1)80%,rgba(220,38,38,0.06)100%);border:1px solid rgba(239,68,68,0.3);box-shadow:0 40px 80px rgba(239,68,68,0.15),0 20px 40px rgba(239,68,68,0.08),0 8px 16px rgba(220,38,38,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(252,165,165,0.4),0 0 0 1px rgba(239,68,68,0.2);color:rgba(153,27,27,0.95);}.dark.glassmorphism-toast.notification-error{color:rgba(252,165,165,0.95);background:linear-gradient(145deg,rgba(239,68,68,0.25)0%,rgba(252,165,165,0.15)30%,rgba(239,68,68,0.12)70%,rgba(220,38,38,0.08)100%);}.glassmorphism-toast.notification-warning{background:linear-gradient(145deg,rgba(245,158,11,0.18)0%,rgba(252,211,77,0.12)20%,rgba(217,119,6,0.15)40%,rgba(245,158,11,0.08)60%,rgba(252,211,77,0.1)80%,rgba(217,119,6,0.06)100%);border:1px solid rgba(245,158,11,0.3);box-shadow:0 40px 80px rgba(245,158,11,0.15),0 20px 40px rgba(245,158,11,0.08),0 8px 16px rgba(217,119,6,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(252,211,77,0.4),0 0 0 1px rgba(245,158,11,0.2);color:rgba(146,64,14,0.95);}.dark.glassmorphism-toast.notification-warning{color:rgba(252,211,77,0.95);background:linear-gradient(145deg,rgba(245,158,11,0.25)0%,rgba(252,211,77,0.15)30%,rgba(245,158,11,0.12)70%,rgba(217,119,6,0.08)100%);}.glassmorphism-toast.notification-info{background:linear-gradient(145deg,rgba(59,130,246,0.18)0%,rgba(147,197,253,0.12)20%,rgba(37,99,235,0.15)40%,rgba(59,130,246,0.08)60%,rgba(147,197,253,0.1)80%,rgba(37,99,235,0.06)100%);border:1px solid rgba(59,130,246,0.3);box-shadow:0 40px 80px rgba(59,130,246,0.15),0 20px 40px rgba(59,130,246,0.08),0 8px 16px rgba(37,99,235,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(147,197,253,0.4),0 0 0 1px rgba(59,130,246,0.2);color:rgba(30,64,175,0.95);}.dark.glassmorphism-toast.notification-info{color:rgba(147,197,253,0.95);background:linear-gradient(145deg,rgba(59,130,246,0.25)0%,rgba(147,197,253,0.15)30%,rgba(59,130,246,0.12)70%,rgba(37,99,235,0.08)100%);}.glassmorphism-toast.notification-loading{background:linear-gradient(145deg,rgba(99,102,241,0.18)0%,rgba(165,180,252,0.12)20%,rgba(79,70,229,0.15)40%,rgba(99,102,241,0.08)60%,rgba(165,180,252,0.1)80%,rgba(79,70,229,0.06)100%);border:1px solid rgba(99,102,241,0.3);box-shadow:0 40px 80px rgba(99,102,241,0.15),0 20px 40px rgba(99,102,241,0.08),0 8px 16px rgba(79,70,229,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(165,180,252,0.4),0 0 0 1px rgba(99,102,241,0.2);color:rgba(55,48,163,0.95);}.dark.glassmorphism-toast.notification-loading{color:rgba(165,180,252,0.95);background:linear-gradient(145deg,rgba(99,102,241,0.25)0%,rgba(165,180,252,0.15)30%,rgba(99,102,241,0.12)70%,rgba(79,70,229,0.08)100%);}
|
||||
.dark.glassmorphism-toast{backdrop-filter:blur(80px)saturate(200%)brightness(115%)contrast(125%);-webkit-backdrop-filter:blur(80px)saturate(200%)brightness(115%)contrast(125%);box-shadow:0 40px 80px rgba(0,0,0,0.4),0 20px 40px rgba(0,0,0,0.3),0 8px 16px rgba(0,0,0,0.2),inset 0 3px 0 rgba(255,255,255,0.15),inset 0 1px 2px rgba(255,255,255,0.25),0 0 0 1px rgba(255,255,255,0.08);background:linear-gradient(145deg,rgba(0,0,0,0.25)0%,rgba(15,15,15,0.18)25%,rgba(0,0,0,0.22)50%,rgba(10,10,10,0.15)75%,rgba(0,0,0,0.2)100%);}.dark.glassmorphism-toast::before{background:radial-gradient(circle at 25%25%,rgba(255,255,255,0.12)0%,transparent 35%),radial-gradient(circle at 75%75%,rgba(255,255,255,0.08)0%,transparent 35%),radial-gradient(circle at 50%10%,rgba(255,255,255,0.06)0%,transparent 40%),linear-gradient(135deg,rgba(255,255,255,0.08)0%,transparent 60%),conic-gradient(from 180deg at 50%50%,rgba(255,255,255,0.03)0deg,rgba(255,255,255,0.06)45deg,rgba(255,255,255,0.03)90deg,rgba(255,255,255,0.05)135deg,rgba(255,255,255,0.03)180deg,rgba(255,255,255,0.07)225deg,rgba(255,255,255,0.03)270deg,rgba(255,255,255,0.05)315deg,rgba(255,255,255,0.03)360deg);}.dark.glassmorphism-toast::after{background:radial-gradient(circle at 20%20%,rgba(255,255,255,0.15)1px,transparent 1px),radial-gradient(circle at 60%80%,rgba(255,255,255,0.1)1px,transparent 1px),radial-gradient(circle at 80%30%,rgba(255,255,255,0.12)1px,transparent 1px),radial-gradient(circle at 30%70%,rgba(255,255,255,0.08)1px,transparent 1px);}.toast-content{position:relative;overflow:hidden;padding:1rem;border-radius:inherit;}.toast-header{display:flex;align-items:flex-start;gap:0.875rem;}.toast-icon{flex-shrink:0;width:2.25rem;height:2.25rem;display:flex;align-items:center;justify-content:center;border-radius:50%;background:linear-gradient(145deg,rgba(255,255,255,0.35)0%,rgba(255,255,255,0.2)50%,rgba(255,255,255,0.3)100%);backdrop-filter:blur(16px)saturate(140%);-webkit-backdrop-filter:blur(16px)saturate(140%);border:1px solid rgba(255,255,255,0.4);box-shadow:0 8px 16px rgba(0,0,0,0.06),0 2px 4px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.6),inset 0-1px 0 rgba(0,0,0,0.03);transition:all 0.3s cubic-bezier(0.4,0,0.2,1);position:relative;overflow:hidden;}
|
||||
.toast-icon::before{content:'';position:absolute;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(circle,rgba(255,255,255,0.25)0%,transparent 70%);opacity:0;transition:opacity 0.3s ease;animation:icon-pulse 3s ease-in-out infinite;}@keyframes icon-pulse{0%,100%{opacity:0;transform:scale(0.8);}
|
||||
50%{opacity:0.4;transform:scale(1.1);}}.toast-icon:hover::before{opacity:1;animation-play-state:paused;}.toast-icon:hover{transform:scale(1.08)rotate(8deg);box-shadow:0 12px 24px rgba(0,0,0,0.08),0 4px 8px rgba(0,0,0,0.06),inset 0 1px 0 rgba(255,255,255,0.7),inset 0-1px 0 rgba(0,0,0,0.05);}.dark.toast-icon{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.3)100%);border:1px solid rgba(255,255,255,0.12);box-shadow:0 8px 16px rgba(0,0,0,0.25),0 2px 4px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.12),inset 0-1px 0 rgba(255,255,255,0.03);}.dark.toast-icon::before{background:radial-gradient(circle,rgba(255,255,255,0.12)0%,transparent 70%);}.toast-body{flex:1;min-width:0;}.toast-title{font-weight:600;font-size:0.875rem;margin-bottom:0.375rem;line-height:1.3;letter-spacing:0.01em;text-shadow:0 1px 2px rgba(0,0,0,0.06);background:linear-gradient(135deg,currentColor 0%,currentColor 100%);-webkit-background-clip:text;background-clip:text;}.toast-message{font-size:0.8125rem;line-height:1.4;opacity:0.9;font-weight:450;text-shadow:0 1px 2px rgba(0,0,0,0.04);}.toast-actions{display:flex;gap:0.5rem;align-items:flex-start;flex-shrink:0;}.toast-action-btn{padding:0.5rem 0.875rem;border-radius:0.75rem;font-size:0.75rem;font-weight:500;border:none;cursor:pointer;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);display:flex;align-items:center;gap:0.375rem;background:linear-gradient(145deg,rgba(255,255,255,0.25)0%,rgba(255,255,255,0.12)50%,rgba(255,255,255,0.2)100%);color:inherit;backdrop-filter:blur(16px)saturate(130%);-webkit-backdrop-filter:blur(16px)saturate(130%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 4px 8px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.5);text-shadow:0 1px 2px rgba(0,0,0,0.06);position:relative;overflow:hidden;}
|
||||
.toast-action-btn::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.3)50%,transparent 100%);transition:left 0.4s cubic-bezier(0.4,0,0.2,1);}.toast-action-btn:hover::before{left:100%;}.toast-action-btn:hover{background:linear-gradient(145deg,rgba(255,255,255,0.35)0%,rgba(255,255,255,0.2)50%,rgba(255,255,255,0.3)100%);transform:translateY(-2px)scale(1.04);box-shadow:0 8px 16px rgba(0,0,0,0.08),0 2px 4px rgba(0,0,0,0.06),inset 0 1px 0 rgba(255,255,255,0.6);border-color:rgba(255,255,255,0.5);}.toast-action-btn:active{transform:translateY(-1px)scale(1.02);transition:transform 0.1s ease;}.toast-action-primary{background:linear-gradient(145deg,rgba(59,130,246,0.8)0%,rgba(37,99,235,0.85)50%,rgba(59,130,246,0.75)100%);color:white;border-color:rgba(59,130,246,0.6);box-shadow:0 4px 12px rgba(59,130,246,0.2),0 1px 3px rgba(59,130,246,0.12),inset 0 1px 0 rgba(255,255,255,0.25);text-shadow:0 1px 2px rgba(0,0,0,0.15);}.toast-action-primary::before{background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.25)50%,transparent 100%);}.toast-action-primary:hover{background:linear-gradient(145deg,rgba(59,130,246,0.9)0%,rgba(37,99,235,0.95)50%,rgba(59,130,246,0.85)100%);box-shadow:0 8px 20px rgba(59,130,246,0.25),0 2px 6px rgba(59,130,246,0.18),inset 0 1px 0 rgba(255,255,255,0.3);}.dark.toast-action-btn{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.3)100%);border:1px solid rgba(255,255,255,0.12);box-shadow:0 4px 8px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.08);}.dark.toast-action-btn:hover{background:linear-gradient(145deg,rgba(0,0,0,0.45)0%,rgba(15,15,15,0.35)50%,rgba(0,0,0,0.4)100%);box-shadow:0 8px 16px rgba(0,0,0,0.2),0 2px 4px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.12);}.toast-close{padding:0.375rem;border-radius:0.625rem;border:none;background:linear-gradient(145deg,rgba(255,255,255,0.18)0%,rgba(255,255,255,0.08)50%,rgba(255,255,255,0.12)100%);color:inherit;cursor:pointer;opacity:0.75;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);display:flex;align-items:center;justify-content:center;backdrop-filter:blur(16px)saturate(110%);-webkit-backdrop-filter:blur(16px)saturate(110%);border:1px solid rgba(255,255,255,0.2);box-shadow:0 2px 4px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.3);position:relative;overflow:hidden;}
|
||||
.toast-close::after{content:'';position:absolute;top:50%;left:50%;width:0;height:0;border-radius:50%;background:rgba(255,255,255,0.3);transform:translate(-50%,-50%);transition:width 0.25s ease,height 0.25s ease;}.toast-close:hover::after{width:100%;height:100%;}.toast-close:hover{opacity:1;background:linear-gradient(145deg,rgba(255,255,255,0.28)0%,rgba(255,255,255,0.15)50%,rgba(255,255,255,0.22)100%);transform:scale(1.1)rotate(90deg);box-shadow:0 4px 8px rgba(0,0,0,0.08),0 1px 2px rgba(0,0,0,0.06),inset 0 1px 0 rgba(255,255,255,0.5);}.toast-close:active{transform:scale(1.05)rotate(90deg);transition:transform 0.1s ease;}.dark.toast-close{background:linear-gradient(145deg,rgba(0,0,0,0.25)0%,rgba(15,15,15,0.15)50%,rgba(0,0,0,0.2)100%);border:1px solid rgba(255,255,255,0.08);box-shadow:0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.06);}.dark.toast-close::after{background:rgba(255,255,255,0.15);}.dark.toast-close:hover{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.4)100%);box-shadow:0 4px 8px rgba(0,0,0,0.25),0 1px 2px rgba(0,0,0,0.18),inset 0 1px 0 rgba(255,255,255,0.1);}.toast-progress{position:absolute;bottom:0;left:0;right:0;height:3px;background:linear-gradient(90deg,rgba(255,255,255,0.08)0%,rgba(255,255,255,0.04)50%,rgba(255,255,255,0.08)100%);overflow:hidden;border-radius:0 0 1.75rem 1.75rem;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);}.toast-progress-bar{height:100%;background:linear-gradient(90deg,rgba(255,255,255,0.6)0%,rgba(255,255,255,0.8)25%,rgba(255,255,255,0.9)50%,rgba(255,255,255,0.8)75%,rgba(255,255,255,0.6)100%);width:0%;transition:width 0.25s ease;position:relative;border-radius:inherit;box-shadow:0 0 8px rgba(255,255,255,0.4),0 0 4px rgba(255,255,255,0.3),inset 0 1px 0 rgba(255,255,255,0.7);overflow:hidden;}
|
||||
.toast-progress-bar::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.4)25%,rgba(255,255,255,0.6)50%,rgba(255,255,255,0.4)75%,transparent 100%);animation:progress-shimmer 2s ease-in-out infinite;}.toast-progress-bar::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(45deg,transparent 25%,rgba(255,255,255,0.2)25%,rgba(255,255,255,0.2)50%,transparent 50%,transparent 75%,rgba(255,255,255,0.2)75%);background-size:12px 12px;animation:progress-stripes 0.8s linear infinite;}@keyframes progress-shimmer{0%{transform:translateX(-100%);}
|
||||
100%{transform:translateX(100%);}}@keyframes progress-stripes{0%{background-position:0 0;}
|
||||
100%{background-position:12px 0;}}@keyframes toast-progress{from{width:100%;}
|
||||
to{width:0%;}}.notification-settings{max-width:320px;padding:0.875rem;background:linear-gradient(145deg,rgba(255,255,255,0.12)0%,rgba(255,255,255,0.06)50%,rgba(255,255,255,0.1)100%);border-radius:1.25rem;backdrop-filter:blur(24px)saturate(140%);-webkit-backdrop-filter:blur(24px)saturate(140%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 16px 32px rgba(0,0,0,0.08),0 6px 12px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.4);}.dark.notification-settings{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.3)100%);border:1px solid rgba(255,255,255,0.08);box-shadow:0 16px 32px rgba(0,0,0,0.25),0 6px 12px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.06);}.notification-settings h3{margin-bottom:1rem;font-size:1rem;font-weight:600;color:inherit;text-align:center;background:linear-gradient(135deg,currentColor 0%,currentColor 100%);-webkit-background-clip:text;background-clip:text;text-shadow:0 1px 2px rgba(0,0,0,0.08);}.setting-item{display:flex;align-items:center;gap:0.75rem;margin:1rem 0;cursor:pointer;font-size:0.875rem;font-weight:450;padding:0.75rem;border-radius:0.875rem;transition:all 0.25s cubic-bezier(0.4,0,0.2,1);background:linear-gradient(145deg,rgba(255,255,255,0.08)0%,rgba(255,255,255,0.04)100%);border:1px solid rgba(255,255,255,0.12);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);position:relative;overflow:hidden;}
|
||||
.setting-item::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.08)50%,transparent 100%);transition:left 0.3s ease;}.setting-item:hover::before{left:100%;}.setting-item:hover{background:linear-gradient(145deg,rgba(255,255,255,0.16)0%,rgba(255,255,255,0.08)100%);transform:translateX(6px)scale(1.01);box-shadow:0 6px 12px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.25);}.dark.setting-item{background:linear-gradient(145deg,rgba(0,0,0,0.18)0%,rgba(15,15,15,0.12)100%);border:1px solid rgba(255,255,255,0.06);}.dark.setting-item:hover{background:linear-gradient(145deg,rgba(0,0,0,0.25)0%,rgba(15,15,15,0.18)100%);box-shadow:0 6px 12px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.08);}.setting-item input[type="checkbox"]{margin:0;width:1.25rem;height:1.25rem;accent-color:currentColor;cursor:pointer;border-radius:0.3rem;transition:all 0.2s ease;}.setting-item input[type="checkbox"]:checked{transform:scale(1.05);box-shadow:0 0 6px rgba(59,130,246,0.3);}
|
||||
@media(max-width:640px){.notifications-container{left:0.5rem;right:0.5rem;top:0.5rem;}.glassmorphism-toast{margin-bottom:0.5rem;border-radius:1.25rem;}.toast-content{padding:0.875rem;}.toast-header{gap:0.75rem;}.toast-icon{width:2rem;height:2rem;}.toast-title{font-size:0.8125rem;}.toast-message{font-size:0.75rem;}.toast-action-btn{padding:0.4rem 0.7rem;font-size:0.7rem;}.toast-close{padding:0.3rem;}}
|
||||
@media(prefers-contrast:high){.glassmorphism-toast{border:2px solid currentColor;backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(255,255,255,0.95);}.dark.glassmorphism-toast{background:rgba(0,0,0,0.95);}}
|
||||
@media(prefers-reduced-motion:reduce){.glassmorphism-toast,.toast-icon,.toast-action-btn,.toast-close{transition:none!important;animation:none!important;}.glassmorphism-toast:hover{transform:none!important;}}`;document.head.appendChild(styles);}
|
||||
handleActionClick(callbackId,toastId,shouldClose=true){if(callbackId&&this.actionCallbacks.has(callbackId)){const callback=this.actionCallbacks.get(callbackId);try{callback();}catch(error){console.error('Fehler beim Ausführen des Action-Callbacks:',error);}
|
||||
this.actionCallbacks.delete(callbackId);}
|
||||
if(shouldClose){this.closeToast(toastId);}}
|
||||
executeCallback(callbackId){if(this.actionCallbacks.has(callbackId)){const callback=this.actionCallbacks.get(callbackId);try{callback();}catch(error){console.error('Fehler beim Ausführen des Callbacks:',error);}
|
||||
this.actionCallbacks.delete(callbackId);}}}
|
||||
const glassNotificationSystem=new GlassmorphismNotificationSystem();if(typeof window!=='undefined'){window.glassNotificationSystem=glassNotificationSystem;window.GlassmorphismNotificationSystem=GlassmorphismNotificationSystem;}
|
||||
document.addEventListener('DOMContentLoaded',()=>{console.log('🎨 Glassmorphism Notification System bereit');});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user