Files
Projektarbeit-MYP/backend/static/js/charts.js

510 lines
17 KiB
JavaScript

/**
* 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<Object>} - 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('<!DOCTYPE html>') || responseText.includes('<html')) {
console.warn(`❌ HTML-Fehlerseite erhalten statt JSON (${context}):`, responseText.substring(0, 200));
throw new Error(`Server-Fehlerseite erhalten statt JSON-Response (${context})`);
}
console.warn(`❌ Ungültiger Content-Type (${context}):`, contentType);
console.warn(`❌ Response-Text (${context}):`, responseText.substring(0, 500));
throw new Error(`Ungültiger Content-Type: ${contentType || 'fehlt'} (${context})`);
}
// 3. JSON parsing mit detailliertem Error-Handling
let data;
try {
data = await response.json();
} catch (jsonError) {
// Versuche rohen Text zu lesen für Debugging
const rawText = await response.text();
console.error(`❌ JSON-Parsing-Fehler (${context}):`, jsonError);
console.error(`❌ Raw Response (${context}):`, rawText.substring(0, 1000));
throw new Error(`Ungültige JSON-Response: ${jsonError.message} (${context})`);
}
// 4. Prüfe auf null/undefined Response
if (data === null || data === undefined) {
throw new Error(`Leere Response erhalten (${context})`);
}
// 5. Validiere Response-Struktur (wenn success-Feld erwartet wird)
if (typeof data === 'object' && data.hasOwnProperty('success')) {
if (!data.success && data.error) {
console.warn(`❌ API-Fehler (${context}):`, data.error);
throw new Error(`API-Fehler: ${data.error} (${context})`);
}
}
// Erfolgreiche Validierung
console.log(`✅ API-Response validiert (${context}):`, data);
return data;
} catch (error) {
// Error-Logging mit Kontext
console.error(`❌ validateApiResponse fehlgeschlagen (${context}):`, error);
console.error(`❌ Response-Details (${context}):`, {
status: response.status,
statusText: response.statusText,
url: response.url,
headers: Object.fromEntries(response.headers.entries())
});
// Re-throw mit erweiterten Informationen
throw error;
}
}
// 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_BASE_URL}/api/stats/charts/job-status`);
const data = await validateApiResponse(response, 'Job-Status-Chart-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_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 = `
<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);