1167 lines
43 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Mercedes-Benz MYP Admin Dashboard JavaScript
* Moderne Administration mit Echtzeit-Updates und eleganter UI
*/
// Globale Variablen
let statsUpdateInterval;
let systemStatusInterval;
let realTimeChart;
let csrfToken;
// Dynamische API-Base-URL-Erkennung
function detectApiBaseUrl() {
const currentHost = window.location.hostname;
const currentPort = window.location.port;
const currentProtocol = window.location.protocol;
console.log('🔍 Admin API URL-Informationen:', {
host: currentHost,
port: currentPort,
protocol: currentProtocol,
fullUrl: window.location.href
});
// Wenn wir bereits auf dem richtigen Port sind, verwende relative URLs
if (currentPort === '5000' || !currentPort) {
console.log('✅ Verwende relative URLs (gleicher Port oder Standard)');
return '';
}
// Wenn wir auf 8443 sind, versuche 5000 (häufiger Fall)
if (currentPort === '8443') {
const fallbackUrl = `http://${currentHost}:5000`;
console.log('🔄 Admin Fallback von HTTPS:8443 zu HTTP:5000:', fallbackUrl);
return fallbackUrl;
}
// Für andere Ports, verwende Standard-Backend-Port
const defaultPort = currentProtocol === 'https:' ? '8443' : '5000';
const fallbackUrl = `${currentProtocol}//${currentHost}:${defaultPort}`;
console.log('🔄 Admin Standard-Fallback:', fallbackUrl);
return fallbackUrl;
}
// Globale API-Base-URL
const API_BASE_URL = detectApiBaseUrl();
console.log('🔗 Admin API Base URL erkannt:', API_BASE_URL);
// Initialisierung beim Laden der Seite
document.addEventListener('DOMContentLoaded', function() {
// CSRF Token für AJAX-Anfragen
csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// Basis-Initialisierung
initProgressBars();
initConfirmDialogs();
initFlashMessages();
// Erweiterte Admin-Funktionalitäten
initAdminButtons();
initRealTimeUpdates();
initSystemMonitoring();
initModernAnimations();
initSearchAndFilters();
initDataExport();
// Auto-Refresh für Statistiken
startStatsUpdate();
console.log('🚀 Mercedes-Benz MYP Admin Dashboard loaded successfully');
});
/**
* Initialisiert alle Admin-Button-Funktionalitäten
*/
function initAdminButtons() {
// Benutzer-Management Buttons
initUserManagement();
// Drucker-Management Buttons
initPrinterManagement();
// System-Management Buttons
initSystemManagement();
// Job-Management Buttons
initJobManagement();
}
/**
* Benutzer-Management Funktionalitäten
*/
function initUserManagement() {
// Neuer Benutzer Button
const addUserBtn = document.getElementById('add-user-btn');
if (addUserBtn) {
addUserBtn.addEventListener('click', () => showUserModal());
}
// Benutzer bearbeiten Buttons
document.querySelectorAll('.edit-user-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const userId = e.target.closest('button').dataset.userId;
editUser(userId);
});
});
// Benutzer löschen Buttons
document.querySelectorAll('.delete-user-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const userId = e.target.closest('button').dataset.userId;
const userName = e.target.closest('button').dataset.userName;
deleteUser(userId, userName);
});
});
}
/**
* Drucker-Management Funktionalitäten
*/
function initPrinterManagement() {
// Drucker hinzufügen Button
const addPrinterBtn = document.getElementById('add-printer-btn');
if (addPrinterBtn) {
addPrinterBtn.addEventListener('click', () => showPrinterModal());
}
// Drucker verwalten Buttons
document.querySelectorAll('.manage-printer-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const printerId = e.target.closest('button').dataset.printerId;
managePrinter(printerId);
});
});
// Drucker-Einstellungen Buttons
document.querySelectorAll('.settings-printer-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const printerId = e.target.closest('button').dataset.printerId;
showPrinterSettings(printerId);
});
});
}
/**
* System-Management Funktionalitäten
*/
function initSystemManagement() {
// System Status Buttons
const systemStatusBtn = document.querySelector('button[onclick*="System Status"]');
if (systemStatusBtn) {
systemStatusBtn.onclick = null;
systemStatusBtn.addEventListener('click', () => showSystemStatus());
}
const analyticsBtn = document.querySelector('button[onclick*="Analytics"]');
if (analyticsBtn) {
analyticsBtn.onclick = null;
analyticsBtn.addEventListener('click', () => showAnalytics());
}
// Cache leeren
const clearCacheBtn = document.getElementById('clear-cache-btn');
if (clearCacheBtn) {
clearCacheBtn.addEventListener('click', () => clearSystemCache());
}
// Datenbank optimieren
const optimizeDbBtn = document.getElementById('optimize-db-btn');
if (optimizeDbBtn) {
optimizeDbBtn.addEventListener('click', () => optimizeDatabase());
}
// Backup erstellen
const createBackupBtn = document.getElementById('create-backup-btn');
if (createBackupBtn) {
createBackupBtn.addEventListener('click', () => createSystemBackup());
}
// Einstellungen bearbeiten
const editSettingsBtn = document.getElementById('edit-settings-btn');
if (editSettingsBtn) {
editSettingsBtn.addEventListener('click', () => showSystemSettings());
}
// Drucker aktualisieren
const updatePrintersBtn = document.getElementById('update-printers-btn');
if (updatePrintersBtn) {
updatePrintersBtn.addEventListener('click', () => updateAllPrinters());
}
// System neustarten
const restartSystemBtn = document.getElementById('restart-system-btn');
if (restartSystemBtn) {
restartSystemBtn.addEventListener('click', () => restartSystem());
}
}
/**
* Job-Management Funktionalitäten
*/
function initJobManagement() {
// Job-Aktionen
document.querySelectorAll('.job-action-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const action = e.target.closest('button').dataset.action;
const jobId = e.target.closest('button').dataset.jobId;
handleJobAction(action, jobId);
});
});
}
/**
* Echtzeit-Updates für Dashboard
*/
function initRealTimeUpdates() {
// Statistiken alle 30 Sekunden aktualisieren
statsUpdateInterval = setInterval(() => {
updateDashboardStats();
}, 30000);
// System-Status alle 10 Sekunden aktualisieren
systemStatusInterval = setInterval(() => {
updateSystemStatus();
}, 10000);
}
/**
* System-Monitoring initialisieren
*/
function initSystemMonitoring() {
// CPU/RAM Monitoring in Echtzeit
if (document.querySelector('.server-status')) {
monitorSystemResources();
}
}
/**
* Moderne Animationen initialisieren
*/
function initModernAnimations() {
// Animierte Counters für Statistiken
animateCounters();
// Progress Bars mit Animation
animateProgressBars();
// Hover-Effekte
addHoverEffects();
}
/**
* Such- und Filter-Funktionalitäten
*/
function initSearchAndFilters() {
// Benutzer-Suche
const userSearch = document.querySelector('#user-search');
if (userSearch) {
userSearch.addEventListener('input', (e) => {
filterUsers(e.target.value);
});
}
// Job-Filter
const jobFilter = document.querySelector('select[onchange*="filter"]');
if (jobFilter) {
jobFilter.onchange = null;
jobFilter.addEventListener('change', (e) => {
filterJobs(e.target.value);
});
}
}
/**
* Daten-Export Funktionalitäten
*/
function initDataExport() {
// Export-Buttons
document.querySelectorAll('button[onclick*="export"]').forEach(btn => {
btn.onclick = null;
btn.addEventListener('click', (e) => {
const exportType = btn.textContent.includes('Export') ? 'csv' : 'json';
exportData(exportType);
});
});
}
/**
* Benutzer Modal anzeigen
*/
function showUserModal(userId = null) {
const modal = createModal('Benutzer ' + (userId ? 'bearbeiten' : 'hinzufügen'), `
<form id="user-form" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">E-Mail</label>
<input type="email" name="email" required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-transparent
dark:bg-gray-700 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Name</label>
<input type="text" name="name"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-transparent
dark:bg-gray-700 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Passwort</label>
<input type="password" name="password" ${userId ? '' : 'required'}
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-transparent
dark:bg-gray-700 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Rolle</label>
<select name="role"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-transparent
dark:bg-gray-700 dark:text-white">
<option value="user">Benutzer</option>
<option value="admin">Administrator</option>
</select>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button type="button" onclick="closeModal()"
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">
Abbrechen
</button>
<button type="submit"
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
${userId ? 'Aktualisieren' : 'Erstellen'}
</button>
</div>
</form>
`);
// Form-Handler mit preventDefault
const form = document.getElementById('user-form');
form.addEventListener('submit', (e) => {
e.preventDefault(); // Verhindert automatische Weiterleitung
const formData = new FormData(e.target);
if (userId) {
updateUser(userId, formData);
} else {
createUser(formData);
}
});
}
/**
* System Status anzeigen
*/
function showSystemStatus() {
showLoadingOverlay();
const url = `${API_BASE_URL}/api/admin/system/status`;
fetch(url)
.then(response => response.json())
.then(data => {
hideLoadingOverlay();
const modal = createModal('🚀 System Status - Live Monitoring', `
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Server Resources</h3>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">CPU:</span>
<div class="flex items-center space-x-2">
<div class="w-24 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div class="bg-gradient-to-r from-green-400 to-green-600 h-2 rounded-full transition-all duration-500"
style="width: ${data.cpu_usage || 0}%"></div>
</div>
<span class="text-sm font-medium text-gray-900 dark:text-white">${data.cpu_usage || 0}%</span>
</div>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">RAM:</span>
<div class="flex items-center space-x-2">
<div class="w-24 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div class="bg-gradient-to-r from-blue-400 to-blue-600 h-2 rounded-full transition-all duration-500"
style="width: ${data.memory_usage || 0}%"></div>
</div>
<span class="text-sm font-medium text-gray-900 dark:text-white">${data.memory_usage || 0}%</span>
</div>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Uptime:</span>
<span class="text-sm font-medium text-gray-900 dark:text-white">${data.uptime || 'Unbekannt'}</span>
</div>
</div>
</div>
<div class="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Services</h3>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Database:</span>
<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full
${data.database_status === 'connected' ? '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'}">
<span class="w-2 h-2 mr-1 rounded-full ${data.database_status === 'connected' ? 'bg-green-400' : 'bg-red-400'}"></span>
${data.database_status === 'connected' ? 'Connected' : 'Disconnected'}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Scheduler:</span>
<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full
${data.scheduler_running ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'}">
<span class="w-2 h-2 mr-1 rounded-full ${data.scheduler_running ? 'bg-blue-400' : 'bg-yellow-400'}"></span>
${data.scheduler_running ? 'Running' : 'Stopped'}
</span>
</div>
</div>
</div>
</div>
<div class="mt-6 text-center">
<button onclick="refreshSystemStatus()"
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
🔄 Aktualisieren
</button>
</div>
`);
})
.catch(error => {
hideLoadingOverlay();
showNotification('Fehler beim Laden des System Status', 'error');
console.error('System status error:', error);
});
}
/**
* Analytics anzeigen
*/
function showAnalytics() {
const modal = createModal('📊 Live Analytics Dashboard', `
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Drucker Auslastung</h3>
<canvas id="printerUsageChart" width="400" height="200"></canvas>
</div>
<div class="bg-gradient-to-br from-orange-50 to-red-50 dark:from-orange-900/20 dark:to-red-900/20 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Erfolgsrate</h3>
<canvas id="successRateChart" width="400" height="200"></canvas>
</div>
</div>
<div class="bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Echtzeit Monitoring</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400" id="live-jobs">-</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Aktive Jobs</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600 dark:text-green-400" id="live-printers">-</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Online Drucker</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-orange-600 dark:text-orange-400" id="live-queue">-</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Warteschlange</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-purple-600 dark:text-purple-400" id="live-success">-</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Erfolg %</div>
</div>
</div>
</div>
`);
// Analytics-Daten laden und Charts initialisieren
loadAnalyticsData();
startLiveAnalytics();
}
/**
* System Cache leeren
*/
async function clearSystemCache() {
if (!confirm('🗑️ Möchten Sie wirklich den System-Cache leeren?')) return;
showLoadingOverlay();
try {
const url = `${API_BASE_URL}/api/admin/cache/clear`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
hideLoadingOverlay();
if (data.success) {
showNotification('✅ Cache erfolgreich geleert!', 'success');
// Seite nach 2 Sekunden neu laden
setTimeout(() => window.location.reload(), 2000);
} else {
showNotification('❌ Fehler beim Leeren des Cache: ' + data.message, 'error');
}
} catch (error) {
hideLoadingOverlay();
showNotification('❌ Netzwerkfehler beim Leeren des Cache', 'error');
console.error('Cache clear error:', error);
}
}
/**
* Datenbank optimieren
*/
async function optimizeDatabase() {
if (!confirm('🔧 Möchten Sie die Datenbank optimieren? Dies kann einige Minuten dauern.')) return;
showLoadingOverlay();
try {
const url = `${API_BASE_URL}/api/admin/database/optimize`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
hideLoadingOverlay();
if (data.success) {
showNotification('✅ Datenbank erfolgreich optimiert!', 'success');
} else {
showNotification('❌ Fehler bei der Datenbank-Optimierung: ' + data.message, 'error');
}
} catch (error) {
hideLoadingOverlay();
showNotification('❌ Netzwerkfehler bei der Datenbank-Optimierung', 'error');
console.error('Database optimization error:', error);
}
}
/**
* System Backup erstellen
*/
async function createSystemBackup() {
if (!confirm('💾 Möchten Sie ein System-Backup erstellen?')) return;
showLoadingOverlay();
try {
const url = `${API_BASE_URL}/api/admin/backup/create`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `myp-backup-${new Date().toISOString().split('T')[0]}.zip`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
hideLoadingOverlay();
showNotification('✅ Backup erfolgreich erstellt und heruntergeladen!', 'success');
} else {
throw new Error('Backup failed');
}
} catch (error) {
hideLoadingOverlay();
showNotification('❌ Fehler beim Erstellen des Backups', 'error');
console.error('Backup error:', error);
}
}
/**
* Utility-Funktionen für UI
*/
function createModal(title, content) {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4';
modal.innerHTML = `
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center">
<h2 class="text-xl font-bold text-gray-900 dark:text-white">${title}</h2>
<button onclick="closeModal()" 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">
${content}
</div>
</div>
`;
document.body.appendChild(modal);
// ESC-Key zum Schließen
modal.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
});
return modal;
}
function closeModal() {
const modal = document.querySelector('.fixed.inset-0.bg-black\\/50');
if (modal) {
modal.remove();
}
}
function showLoadingOverlay() {
const overlay = document.getElementById('loading-overlay');
if (overlay) {
overlay.classList.remove('hidden');
}
}
function hideLoadingOverlay() {
const overlay = document.getElementById('loading-overlay');
if (overlay) {
overlay.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(), 500);
}, 5000);
}
/**
* Original Basis-Funktionen (erweitert)
*/
function initProgressBars() {
const progressBars = document.querySelectorAll('.progress-bar-fill, [style*="width:"]');
progressBars.forEach((bar, index) => {
const width = bar.style.width || bar.getAttribute('data-width') || '0%';
bar.style.width = '0%';
// Animierte Progress Bars
setTimeout(() => {
bar.style.transition = 'width 1.5s cubic-bezier(0.4, 0, 0.2, 1)';
bar.style.width = width;
}, index * 200);
});
}
function initConfirmDialogs() {
// Erweiterte Confirm-Dialoge mit modernem Design
document.querySelectorAll('[data-confirm]').forEach(element => {
element.addEventListener('click', (e) => {
e.preventDefault();
const message = element.dataset.confirm;
if (confirm(message)) {
// Ursprüngliche Aktion ausführen
if (element.tagName === 'FORM') {
element.submit();
} else if (element.href) {
window.location.href = element.href;
}
}
});
});
}
function initFlashMessages() {
const flashMessages = document.querySelectorAll('.flash-messages .alert, [class*="alert"]');
flashMessages.forEach((message, index) => {
// Einblende-Animation
message.style.opacity = '0';
message.style.transform = 'translateY(-20px)';
setTimeout(() => {
message.style.transition = 'all 0.5s ease';
message.style.opacity = '1';
message.style.transform = 'translateY(0)';
}, index * 100);
// Auto-hide nach 7 Sekunden
setTimeout(() => {
message.style.transition = 'all 0.5s ease';
message.style.opacity = '0';
message.style.transform = 'translateY(-20px)';
setTimeout(() => message.remove(), 500);
}, 7000);
});
}
/**
* Dashboard-Updates
*/
function startStatsUpdate() {
updateDashboardStats();
setInterval(updateDashboardStats, 30000); // Alle 30 Sekunden
}
async function updateDashboardStats() {
try {
const url = `${API_BASE_URL}/api/admin/stats/live`;
const response = await fetch(url);
const data = await response.json();
// Stats animiert aktualisieren
updateAnimatedCounter('total-users', data.total_users);
updateAnimatedCounter('total-printers', data.total_printers);
updateAnimatedCounter('active-jobs', data.active_jobs);
updateAnimatedCounter('success-rate', data.success_rate);
} catch (error) {
console.error('Stats update error:', error);
}
}
function updateAnimatedCounter(elementId, newValue) {
const element = document.getElementById(elementId) ||
document.querySelector(`[data-stat="${elementId}"]`);
if (!element) return;
const currentValue = parseInt(element.textContent) || 0;
const difference = newValue - 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 + (elementId === 'success-rate' ? '%' : '');
if (step >= steps) {
clearInterval(interval);
element.textContent = newValue + (elementId === 'success-rate' ? '%' : '');
}
}, 50);
}
// Global verfügbare Funktionen für Modal-Callbacks
window.closeModal = closeModal;
window.refreshSystemStatus = () => {
closeModal();
showSystemStatus();
};
/**
* Animierte Counters für Statistiken
*/
function animateCounters() {
const counters = document.querySelectorAll('.counter, .stat-number, [data-counter]');
counters.forEach(counter => {
const target = parseInt(counter.textContent.replace(/[^\d]/g, '')) || 0;
const duration = 2000; // 2 Sekunden
const increment = target / (duration / 16); // 60 FPS
let current = 0;
const updateCounter = () => {
if (current < target) {
current += increment;
counter.textContent = Math.floor(current).toLocaleString('de-DE');
requestAnimationFrame(updateCounter);
} else {
counter.textContent = target.toLocaleString('de-DE');
}
};
// Intersection Observer für bessere Performance
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
updateCounter();
observer.unobserve(entry.target);
}
});
});
observer.observe(counter);
});
}
/**
* Progress Bars mit Animation
*/
function animateProgressBars() {
const progressBars = document.querySelectorAll('.progress-bar, [data-progress]');
progressBars.forEach(bar => {
const targetWidth = bar.dataset.progress || bar.style.width || '0%';
const targetValue = parseInt(targetWidth);
// Reset width
bar.style.width = '0%';
// Animate to target
setTimeout(() => {
bar.style.transition = 'width 1.5s ease-out';
bar.style.width = targetWidth;
}, 100);
});
}
/**
* Hover-Effekte hinzufügen
*/
function addHoverEffects() {
// Card hover effects
const cards = document.querySelectorAll('.card, .stat-card, .dashboard-card');
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
card.style.transform = 'translateY(-2px)';
card.style.boxShadow = '0 10px 25px rgba(0,0,0,0.1)';
});
card.addEventListener('mouseleave', () => {
card.style.transform = 'translateY(0)';
card.style.boxShadow = '';
});
});
// Button hover effects
const buttons = document.querySelectorAll('.btn, button:not(.no-hover)');
buttons.forEach(button => {
button.addEventListener('mouseenter', () => {
button.style.transform = 'scale(1.02)';
});
button.addEventListener('mouseleave', () => {
button.style.transform = 'scale(1)';
});
});
}
/**
* Drucker Modal anzeigen
*/
function showPrinterModal(printerId = null) {
const modal = createModal('Drucker ' + (printerId ? 'bearbeiten' : 'hinzufügen'), `
<form id="printer-form" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Name</label>
<input type="text" name="name" required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-transparent
dark:bg-gray-700 dark:text-white"
placeholder="z.B. Prusa i3 MK3S+">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">IP-Adresse</label>
<input type="text" name="ip_address" required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-transparent
dark:bg-gray-700 dark:text-white"
placeholder="192.168.1.100"
pattern="^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Standort</label>
<input type="text" name="location"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-transparent
dark:bg-gray-700 dark:text-white"
placeholder="z.B. Werkstatt, Büro">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Beschreibung</label>
<textarea name="description" rows="3"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-transparent
dark:bg-gray-700 dark:text-white"
placeholder="Zusätzliche Informationen zum Drucker"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Typ</label>
<select name="printer_type"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-transparent
dark:bg-gray-700 dark:text-white">
<option value="FDM">FDM (Fused Deposition Modeling)</option>
<option value="SLA">SLA (Stereolithography)</option>
<option value="SLS">SLS (Selective Laser Sintering)</option>
<option value="Other">Sonstiges</option>
</select>
</div>
<div class="flex items-center">
<input type="checkbox" name="is_active" id="printer-active" checked
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="printer-active" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">
Drucker ist aktiv
</label>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button type="button" onclick="closeModal()"
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">
Abbrechen
</button>
<button type="submit"
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
${printerId ? 'Aktualisieren' : 'Hinzufügen'}
</button>
</div>
</form>
`);
// Form-Handler mit preventDefault
const form = document.getElementById('printer-form');
form.addEventListener('submit', (e) => {
e.preventDefault(); // Verhindert automatische Weiterleitung
const formData = new FormData(e.target);
if (printerId) {
updatePrinter(printerId, formData);
} else {
createPrinter(formData);
}
});
// Wenn Drucker bearbeitet wird, Daten laden
if (printerId) {
loadPrinterData(printerId);
}
}
/**
* Drucker erstellen
*/
async function createPrinter(formData) {
try {
showLoadingOverlay();
const url = `${API_BASE_URL}/api/admin/printers/create`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
},
body: JSON.stringify({
name: formData.get('name'),
ip_address: formData.get('ip_address'),
location: formData.get('location'),
description: formData.get('description'),
printer_type: formData.get('printer_type'),
is_active: formData.get('is_active') === 'on'
})
});
const result = await response.json();
if (response.ok) {
showNotification('Drucker erfolgreich hinzugefügt', 'success');
closeModal();
location.reload(); // Seite neu laden um neue Daten anzuzeigen
} else {
showNotification(result.error || 'Fehler beim Hinzufügen des Druckers', 'error');
}
} catch (error) {
console.error('Error creating printer:', error);
showNotification('Fehler beim Hinzufügen des Druckers', 'error');
} finally {
hideLoadingOverlay();
}
}
/**
* Drucker aktualisieren
*/
async function updatePrinter(printerId, formData) {
try {
showLoadingOverlay();
const url = `${API_BASE_URL}/api/admin/printers/${printerId}/edit`;
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
},
body: JSON.stringify({
name: formData.get('name'),
ip_address: formData.get('ip_address'),
location: formData.get('location'),
description: formData.get('description'),
printer_type: formData.get('printer_type'),
is_active: formData.get('is_active') === 'on'
})
});
const result = await response.json();
if (response.ok) {
showNotification('Drucker erfolgreich aktualisiert', 'success');
closeModal();
location.reload();
} else {
showNotification(result.error || 'Fehler beim Aktualisieren des Druckers', 'error');
}
} catch (error) {
console.error('Error updating printer:', error);
showNotification('Fehler beim Aktualisieren des Druckers', 'error');
} finally {
hideLoadingOverlay();
}
}
/**
* Drucker-Daten laden
*/
async function loadPrinterData(printerId) {
try {
const url = `${API_BASE_URL}/api/printers`;
const response = await fetch(url);
const printers = await response.json();
const printer = printers.find(p => p.id == printerId);
if (printer) {
const form = document.getElementById('printer-form');
form.name.value = printer.name || '';
form.ip_address.value = printer.ip_address || '';
form.location.value = printer.location || '';
form.description.value = printer.description || '';
form.printer_type.value = printer.printer_type || 'FDM';
form.is_active.checked = printer.is_active;
}
} catch (error) {
console.error('Error loading printer data:', error);
showNotification('Fehler beim Laden der Drucker-Daten', 'error');
}
}
/**
* CSRF Token abrufen
*/
function getCSRFToken() {
const token = document.querySelector('meta[name=csrf-token]');
return token ? token.getAttribute('content') : '';
}
/**
* Benutzer erstellen
*/
async function createUser(formData) {
try {
showLoadingOverlay();
const url = `${API_BASE_URL}/api/admin/users/create`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
},
body: JSON.stringify({
email: formData.get('email'),
name: formData.get('name'),
password: formData.get('password'),
role: formData.get('role'),
username: formData.get('email').split('@')[0] // Username aus E-Mail ableiten
})
});
const result = await response.json();
if (response.ok) {
showNotification('Benutzer erfolgreich erstellt', 'success');
closeModal();
location.reload(); // Seite neu laden um neue Daten anzuzeigen
} else {
showNotification(result.error || 'Fehler beim Erstellen des Benutzers', 'error');
}
} catch (error) {
console.error('Error creating user:', error);
showNotification('Fehler beim Erstellen des Benutzers', 'error');
} finally {
hideLoadingOverlay();
}
}
/**
* Benutzer aktualisieren
*/
async function updateUser(userId, formData) {
try {
showLoadingOverlay();
const url = `${API_BASE_URL}/api/admin/users/${userId}/edit`;
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
},
body: JSON.stringify({
email: formData.get('email'),
name: formData.get('name'),
password: formData.get('password'),
role: formData.get('role')
})
});
const result = await response.json();
if (response.ok) {
showNotification('Benutzer erfolgreich aktualisiert', 'success');
closeModal();
location.reload();
} else {
showNotification(result.error || 'Fehler beim Aktualisieren des Benutzers', 'error');
}
} catch (error) {
console.error('Error updating user:', error);
showNotification('Fehler beim Aktualisieren des Benutzers', 'error');
} finally {
hideLoadingOverlay();
}
}
console.log('🎉 Mercedes-Benz MYP Admin Dashboard JavaScript vollständig geladen!');