/** * 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 = {}; // API Base URL Detection function detectApiBaseUrl() { const currentPort = window.location.port; const currentProtocol = window.location.protocol; const currentHost = window.location.hostname; // Development-Umgebung (Port 5000) if (currentPort === '5000') { return `${currentProtocol}//${currentHost}:${currentPort}`; } // Production-Umgebung (Port 443 oder kein Port) if (currentPort === '443' || currentPort === '') { return `${currentProtocol}//${currentHost}`; } // Fallback für andere Ports return window.location.origin; } const API_BASE_URL = detectApiBaseUrl(); /** * Zentrale API-Response-Validierung mit umfassendem Error-Handling * @param {Response} response - Fetch Response-Objekt * @param {string} context - Kontext der API-Anfrage für bessere Fehlermeldungen * @returns {Promise} - Validierte JSON-Daten * @throws {Error} - Bei Validierungsfehlern */ async function validateApiResponse(response, context = 'API-Anfrage') { try { // 1. HTTP Status Code prüfen if (!response.ok) { // Spezielle Behandlung für bekannte Fehler-Codes switch (response.status) { case 401: throw new Error(`Authentifizierung fehlgeschlagen (${context})`); case 403: throw new Error(`Zugriff verweigert (${context})`); case 404: throw new Error(`Ressource nicht gefunden (${context})`); case 429: throw new Error(`Zu viele Anfragen (${context})`); case 500: throw new Error(`Serverfehler (${context})`); case 503: throw new Error(`Service nicht verfügbar (${context})`); default: throw new Error(`HTTP ${response.status}: ${response.statusText} (${context})`); } } // 2. Content-Type prüfen (muss application/json enthalten) const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { // Versuche Response-Text zu lesen für bessere Fehlermeldung const responseText = await response.text(); // Prüfe auf HTML-Fehlerseiten (typisch für 404/500 Seiten) if (responseText.includes('') || responseText.includes(' 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_BASE_URL}/api/stats/charts/printer-usage`); const data = await validateApiResponse(response, 'Drucker-Nutzung-Chart-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_BASE_URL}/api/stats/charts/jobs-timeline`); const data = await validateApiResponse(response, 'Jobs-Timeline-Chart-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 validateApiResponse(response, 'Benutzer-Aktivität-Chart-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 = `

${message}

`; } } // 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 = `

Diagramm wird geladen...

`; } }); } // 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);