748 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Erweiterte Analytik - MYP Platform{% endblock %}
{% block extra_css %}
<style>
.analytics-card {
transition: all 0.3s ease;
border: 1px solid #e2e8f0;
}
.analytics-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.kpi-metric {
font-size: 2.5rem;
font-weight: 700;
line-height: 1;
}
.kpi-trend-up {
color: #22c55e;
}
.kpi-trend-down {
color: #ef4444;
}
.kpi-trend-stable {
color: #6b7280;
}
.chart-container {
position: relative;
height: 400px;
width: 100%;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.loading-spinner {
border: 4px solid #f3f4f6;
border-top: 4px solid #3b82f6;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.filter-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
}
.export-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
transition: all 0.3s ease;
}
.export-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-slate-900 dark:text-white mb-2">
📈 Erweiterte Analytik
</h1>
<p class="text-slate-600 dark:text-slate-400">
Umfassende Statistiken und KPIs für die MYP 3D-Druck Platform
</p>
</div>
<!-- Filter Section -->
<div class="filter-section">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex flex-wrap gap-4">
<div class="flex flex-col">
<label class="text-sm font-medium mb-1">Zeitraum</label>
<select id="timeRangeSelect" class="bg-white/20 text-white border border-white/30 rounded-lg px-3 py-2 backdrop-blur-sm">
<option value="day">Letzter Tag</option>
<option value="week">Letzte Woche</option>
<option value="month" selected>Letzter Monat</option>
<option value="quarter">Letztes Quartal</option>
<option value="year">Letztes Jahr</option>
</select>
</div>
<div class="flex flex-col">
<label class="text-sm font-medium mb-1">Report-Typ</label>
<select id="reportTypeSelect" class="bg-white/20 text-white border border-white/30 rounded-lg px-3 py-2 backdrop-blur-sm">
<option value="comprehensive">Umfassend</option>
<option value="printer_usage">Drucker-Nutzung</option>
<option value="user_activity">Benutzer-Aktivität</option>
<option value="efficiency">Effizienz</option>
</select>
</div>
</div>
<div class="flex gap-2">
<button id="refreshData" class="export-button">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Aktualisieren
</button>
<button id="exportReport" class="export-button">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Exportieren
</button>
</div>
</div>
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="text-center py-8 hidden">
<div class="loading-spinner"></div>
<p class="mt-4 text-slate-600 dark:text-slate-400">Lade Analytik-Daten...</p>
</div>
<!-- KPI Dashboard -->
<div id="kpiDashboard" class="mb-8">
<h2 class="text-2xl font-bold text-slate-900 dark:text-white mb-6">🎯 Key Performance Indicators</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6">
<!-- KPI Cards werden dynamisch geladen -->
</div>
</div>
<!-- Analytics Grid -->
<div class="stats-grid">
<!-- Drucker-Statistiken -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
🖨️ Drucker-Statistiken
</h3>
<div class="text-2xl">📊</div>
</div>
<div id="printerStatsContainer">
<div class="loading-spinner"></div>
</div>
</div>
<!-- Job-Statistiken -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
⚙️ Job-Statistiken
</h3>
<div class="text-2xl">📈</div>
</div>
<div id="jobStatsContainer">
<div class="loading-spinner"></div>
</div>
</div>
<!-- Benutzer-Statistiken -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
👥 Benutzer-Statistiken
</h3>
<div class="text-2xl">👤</div>
</div>
<div id="userStatsContainer">
<div class="loading-spinner"></div>
</div>
</div>
<!-- Trend-Analyse -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg col-span-full">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
📊 Trend-Analyse
</h3>
<div class="text-2xl">📉</div>
</div>
<div class="chart-container" id="trendChart">
<canvas id="trendChartCanvas"></canvas>
</div>
</div>
<!-- Drucker-Auslastung -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
⚡ Drucker-Auslastung
</h3>
<div class="text-2xl">🔋</div>
</div>
<div class="chart-container" id="utilizationChart">
<canvas id="utilizationChartCanvas"></canvas>
</div>
</div>
<!-- Top-Benutzer -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
🏆 Top-Benutzer
</h3>
<div class="text-2xl">🥇</div>
</div>
<div id="topUsersContainer">
<div class="loading-spinner"></div>
</div>
</div>
<!-- System-Gesundheit -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
💚 System-Gesundheit
</h3>
<div class="text-2xl">❤️</div>
</div>
<div id="systemHealthContainer">
<div class="loading-spinner"></div>
</div>
</div>
</div>
<!-- Report Export Modal -->
<div id="exportModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 max-w-md w-full">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">
📊 Report exportieren
</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Format
</label>
<select id="exportFormat" class="w-full border border-slate-300 dark:border-slate-600 rounded-lg px-3 py-2 bg-white dark:bg-slate-700 text-slate-900 dark:text-white">
<option value="json">JSON</option>
<option value="csv">CSV</option>
<option value="pdf">PDF</option>
<option value="excel">Excel</option>
</select>
</div>
<div class="flex justify-end space-x-3">
<button id="cancelExport" class="px-4 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200">
Abbrechen
</button>
<button id="confirmExport" class="export-button">
Export starten
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- Chart.js - Lokale Version -->
<script src="{{ url_for('static', filename='js/charts/chart.min.js') }}"></script>
<script>
/**
* MYP Analytics Dashboard
* Erweiterte Analytik und Statistiken
*/
class AnalyticsDashboard {
constructor() {
this.currentTimeRange = 'month';
this.currentReportType = 'comprehensive';
this.charts = {};
this.data = {};
this.init();
}
init() {
this.setupEventListeners();
this.loadInitialData();
}
setupEventListeners() {
// Filter-Änderungen
document.getElementById('timeRangeSelect').addEventListener('change', (e) => {
this.currentTimeRange = e.target.value;
this.loadData();
});
document.getElementById('reportTypeSelect').addEventListener('change', (e) => {
this.currentReportType = e.target.value;
this.loadData();
});
// Aktionen
document.getElementById('refreshData').addEventListener('click', () => {
this.loadData();
});
document.getElementById('exportReport').addEventListener('click', () => {
this.showExportModal();
});
// Export Modal
document.getElementById('cancelExport').addEventListener('click', () => {
this.hideExportModal();
});
document.getElementById('confirmExport').addEventListener('click', () => {
this.exportReport();
});
// Modal schließen bei Klick außerhalb
document.getElementById('exportModal').addEventListener('click', (e) => {
if (e.target.id === 'exportModal') {
this.hideExportModal();
}
});
}
async loadInitialData() {
this.showLoading();
await this.loadData();
this.hideLoading();
}
async loadData() {
try {
this.showLoading();
// KPIs laden
await this.loadKPIs();
// Report-Daten laden
await this.loadReportData();
// Charts aktualisieren
this.updateCharts();
} catch (error) {
console.error('Fehler beim Laden der Analytics-Daten:', error);
this.showError('Fehler beim Laden der Daten');
} finally {
this.hideLoading();
}
}
async loadKPIs() {
try {
const response = await fetch('/api/analytics/dashboard');
if (!response.ok) throw new Error('Failed to load KPIs');
const data = await response.json();
this.renderKPIs(data.kpis || []);
} catch (error) {
console.error('Fehler beim Laden der KPIs:', error);
}
}
async loadReportData() {
try {
const response = await fetch(`/api/analytics/report/${this.currentReportType}?time_range=${this.currentTimeRange}`);
if (!response.ok) throw new Error('Failed to load report data');
this.data = await response.json();
this.renderStatistics();
} catch (error) {
console.error('Fehler beim Laden der Report-Daten:', error);
}
}
renderKPIs(kpis) {
const container = document.querySelector('#kpiDashboard .grid');
container.innerHTML = kpis.map(kpi => `
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-slate-600 dark:text-slate-400">${kpi.name}</h4>
<span class="kpi-trend-${kpi.trend}">
${kpi.trend === 'up' ? '↗️' : kpi.trend === 'down' ? '↘️' : '➡️'}
</span>
</div>
<div class="kpi-metric text-slate-900 dark:text-white mb-1">
${this.formatMetric(kpi.current_value, kpi.unit)}
</div>
<div class="flex items-center justify-between text-xs">
<span class="text-slate-500 dark:text-slate-400">
Ziel: ${this.formatMetric(kpi.target_value, kpi.unit)}
</span>
<span class="kpi-trend-${kpi.trend} font-medium">
${kpi.change_percent > 0 ? '+' : ''}${kpi.change_percent}%
</span>
</div>
</div>
`).join('');
}
renderStatistics() {
// Drucker-Statistiken
if (this.data.sections?.printers) {
this.renderPrinterStats(this.data.sections.printers);
}
// Job-Statistiken
if (this.data.sections?.jobs) {
this.renderJobStats(this.data.sections.jobs);
}
// Benutzer-Statistiken
if (this.data.sections?.users) {
this.renderUserStats(this.data.sections.users);
}
}
renderPrinterStats(printerData) {
const container = document.getElementById('printerStatsContainer');
const summary = printerData.summary;
container.innerHTML = `
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400">
${summary.total_printers}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Drucker gesamt</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600 dark:text-green-400">
${summary.online_printers}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Online</div>
</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-slate-900 dark:text-white">
${summary.availability_rate}% Verfügbarkeit
</div>
<div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2 mt-2">
<div class="bg-green-500 h-2 rounded-full" style="width: ${summary.availability_rate}%"></div>
</div>
</div>
</div>
`;
}
renderJobStats(jobData) {
const container = document.getElementById('jobStatsContainer');
const summary = jobData.summary;
container.innerHTML = `
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400">
${summary.total_jobs}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Jobs gesamt</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600 dark:text-green-400">
${summary.success_rate}%
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Erfolgsrate</div>
</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-slate-900 dark:text-white">
${summary.avg_duration_hours}h Durchschnitt
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">
🎯 ${summary.completed_jobs} abgeschlossen
</div>
</div>
</div>
`;
}
renderUserStats(userData) {
const container = document.getElementById('userStatsContainer');
const summary = userData.summary;
container.innerHTML = `
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400">
${summary.total_users}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Benutzer gesamt</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600 dark:text-green-400">
${summary.active_users}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Aktiv</div>
</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-slate-900 dark:text-white">
${summary.engagement_rate}% Engagement
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">
${summary.new_users} neue Benutzer
</div>
</div>
</div>
`;
// Top-Benutzer rendern
if (userData.top_users) {
this.renderTopUsers(userData.top_users);
}
}
renderTopUsers(topUsers) {
const container = document.getElementById('topUsersContainer');
container.innerHTML = `
<div class="space-y-3">
${topUsers.slice(0, 5).map((user, index) => `
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center text-white text-sm font-bold">
${index + 1}
</div>
<div>
<div class="font-medium text-slate-900 dark:text-white">${user.name || user.username}</div>
<div class="text-xs text-slate-600 dark:text-slate-400">${user.jobs} Jobs</div>
</div>
</div>
<div class="text-sm font-medium text-slate-600 dark:text-slate-400">
${user.total_hours}h
</div>
</div>
`).join('')}
</div>
`;
}
updateCharts() {
this.updateTrendChart();
this.updateUtilizationChart();
}
updateTrendChart() {
const canvas = document.getElementById('trendChartCanvas');
const ctx = canvas.getContext('2d');
// Destroy existing chart
if (this.charts.trend) {
this.charts.trend.destroy();
}
// Sample data - would be replaced with real data
const dailyTrend = this.data.sections?.jobs?.daily_trend || [];
this.charts.trend = new Chart(ctx, {
type: 'line',
data: {
labels: dailyTrend.map(d => new Date(d.date).toLocaleDateString('de-DE')),
datasets: [{
label: 'Jobs pro Tag',
data: dailyTrend.map(d => d.jobs),
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
updateUtilizationChart() {
const canvas = document.getElementById('utilizationChartCanvas');
const ctx = canvas.getContext('2d');
// Destroy existing chart
if (this.charts.utilization) {
this.charts.utilization.destroy();
}
const printerUsage = this.data.sections?.printers?.usage_by_printer || [];
this.charts.utilization = new Chart(ctx, {
type: 'doughnut',
data: {
labels: printerUsage.map(p => p.name),
datasets: [{
data: printerUsage.map(p => p.utilization_rate),
backgroundColor: [
'#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
formatMetric(value, unit) {
if (typeof value !== 'number') return value;
if (unit === '%') {
return `${value.toFixed(1)}%`;
} else if (unit === 'Stunden') {
return `${value.toFixed(1)}h`;
} else if (unit === 'g') {
return `${value.toLocaleString()}g`;
} else {
return `${value.toLocaleString()} ${unit}`;
}
}
showExportModal() {
document.getElementById('exportModal').classList.remove('hidden');
}
hideExportModal() {
document.getElementById('exportModal').classList.add('hidden');
}
async exportReport() {
try {
const format = document.getElementById('exportFormat').value;
const response = await fetch(`/api/analytics/report/${this.currentReportType}?time_range=${this.currentTimeRange}&format=${format}`);
if (!response.ok) throw new Error('Export fehlgeschlagen');
// Download starten
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `myp-analytics-${this.currentReportType}-${this.currentTimeRange}.${format}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
this.hideExportModal();
this.showSuccess('Report erfolgreich exportiert');
} catch (error) {
console.error('Export-Fehler:', error);
this.showError('Fehler beim Exportieren des Reports');
}
}
showLoading() {
document.getElementById('loadingIndicator').classList.remove('hidden');
}
hideLoading() {
document.getElementById('loadingIndicator').classList.add('hidden');
}
showError(message) {
if (typeof showFlashMessage === 'function') {
showFlashMessage(message, 'error');
} else {
alert(message);
}
}
showSuccess(message) {
if (typeof showFlashMessage === 'function') {
showFlashMessage(message, 'success');
} else {
alert(message);
}
}
}
// Dashboard initialisieren
document.addEventListener('DOMContentLoaded', () => {
new AnalyticsDashboard();
});
</script>
{% endblock %}