510 lines
17 KiB
JavaScript
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);
|