748 lines
27 KiB
HTML
748 lines
27 KiB
HTML
{% 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 %} |