2025-06-04 10:03:22 +02:00

413 lines
13 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 = {};
// 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);