📝 'feat': Renamed and moved various documentation files to '/docs/' directory

This commit is contained in:
2025-05-29 15:46:25 +02:00
parent 1c466b199a
commit 0e3f316a88
25 changed files with 990 additions and 165 deletions

View File

@@ -0,0 +1,746 @@
{% 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 %}
<script src="https://cdn.jsdelivr.net/npm/chart.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 (window.showToast) {
window.showToast(message, 'error');
} else {
alert(message);
}
}
showSuccess(message) {
if (window.showToast) {
window.showToast(message, 'success');
} else {
alert(message);
}
}
}
// Dashboard initialisieren
document.addEventListener('DOMContentLoaded', () => {
new AnalyticsDashboard();
});
</script>
{% endblock %}