📝 Commit Details:
This commit is contained in:
413
backend/static/js/charts.js
Normal file
413
backend/static/js/charts.js
Normal file
@ -0,0 +1,413 @@
|
||||
/**
|
||||
* 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);
|
Reference in New Issue
Block a user