"Refactor stats template using Tailwind CSS"
This commit is contained in:
parent
aea600ee2e
commit
d1bee6f8bb
@ -3,7 +3,7 @@ import sys
|
|||||||
import logging
|
import logging
|
||||||
import atexit
|
import atexit
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, send_file, abort, session, make_response
|
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, send_file, abort, session, make_response, Response
|
||||||
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
||||||
from flask_wtf import CSRFProtect
|
from flask_wtf import CSRFProtect
|
||||||
from flask_wtf.csrf import CSRFError
|
from flask_wtf.csrf import CSRFError
|
||||||
|
2
backend/app/static/css/tailwind.min.css
vendored
2
backend/app/static/css/tailwind.min.css
vendored
File diff suppressed because one or more lines are too long
@ -31,123 +31,84 @@
|
|||||||
<!-- Overview Stats Cards -->
|
<!-- Overview Stats Cards -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
||||||
<!-- Total Jobs -->
|
<!-- Total Jobs -->
|
||||||
<div class="stats-card p-6" data-api-endpoint="/api/stats/total-jobs" data-counter>
|
<div class="stats-card p-6" id="stats-total-jobs">
|
||||||
<div class="absolute top-5 right-5 text-slate-900 dark:text-blue-400 text-3xl">
|
<div class="absolute top-5 right-5 text-slate-900 dark:text-blue-400 text-3xl">
|
||||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Gesamte Jobs</p>
|
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Gesamte Jobs</p>
|
||||||
{% if total_jobs is defined and total_jobs %}
|
<p id="total-jobs-count" class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">-</p>
|
||||||
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ total_jobs }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Completed Jobs -->
|
<!-- Completed Jobs -->
|
||||||
<div class="stats-card p-6" data-api-endpoint="/api/stats/completed-jobs" data-counter>
|
<div class="stats-card p-6" id="stats-completed-jobs">
|
||||||
<div class="absolute top-5 right-5 text-green-600 dark:text-green-400 text-3xl">
|
<div class="absolute top-5 right-5 text-green-600 dark:text-green-400 text-3xl">
|
||||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Abgeschlossene Jobs</p>
|
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Abgeschlossene Jobs</p>
|
||||||
{% if completed_jobs is defined and completed_jobs %}
|
<p id="completed-jobs-count" class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">-</p>
|
||||||
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ completed_jobs }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Active Printers -->
|
<!-- Active Printers -->
|
||||||
<div class="stats-card p-6" data-api-endpoint="/api/stats/active-printers" data-counter>
|
<div class="stats-card p-6" id="stats-active-printers">
|
||||||
<div class="absolute top-5 right-5 text-purple-600 dark:text-purple-400 text-3xl">
|
<div class="absolute top-5 right-5 text-purple-600 dark:text-purple-400 text-3xl">
|
||||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Aktive Drucker</p>
|
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Online Drucker</p>
|
||||||
{% if active_printers is defined and active_printers %}
|
<p id="online-printers-count" class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">-</p>
|
||||||
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ active_printers }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Total Print Time -->
|
<!-- Success Rate -->
|
||||||
<div class="stats-card p-6" data-api-endpoint="/api/stats/print-time" data-counter>
|
<div class="stats-card p-6" id="stats-success-rate">
|
||||||
<div class="absolute top-5 right-5 text-amber-600 dark:text-amber-400 text-3xl">
|
<div class="absolute top-5 right-5 text-amber-600 dark:text-amber-400 text-3xl">
|
||||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Gesamte Druckzeit</p>
|
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Erfolgsrate</p>
|
||||||
{% if total_print_time is defined and total_print_time %}
|
<p id="success-rate-percent" class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">-%</p>
|
||||||
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ total_print_time }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Charts and Detailed Stats -->
|
<!-- Charts Section -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<!-- Job Status Distribution -->
|
<!-- Job Status Distribution -->
|
||||||
<div class="stats-card p-6">
|
<div class="stats-card p-6">
|
||||||
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Job-Status Verteilung</h2>
|
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Job-Status Verteilung</h2>
|
||||||
<div id="job-status-chart" class="h-64" data-api-endpoint="/api/stats/job-status" data-chart>
|
<div class="h-64">
|
||||||
{% if job_status_data is defined and job_status_data and job_status_data|length > 0 %}
|
<canvas id="job-status-chart"></canvas>
|
||||||
<!-- Recharts wird hier dynamisch gerendert -->
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Printer Usage -->
|
<!-- Printer Usage -->
|
||||||
<div class="stats-card p-6">
|
<div class="stats-card p-6">
|
||||||
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Drucker-Nutzung</h2>
|
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Drucker-Nutzung</h2>
|
||||||
<div id="printer-usage-chart" class="h-64" data-api-endpoint="/api/stats/printer-usage" data-chart>
|
<div class="h-64">
|
||||||
{% if printer_usage_data is defined and printer_usage_data and printer_usage_data|length > 0 %}
|
<canvas id="printer-usage-chart"></canvas>
|
||||||
<!-- Recharts wird hier dynamisch gerendert -->
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detailed Statistics Tables -->
|
<!-- Timeline and User Activity Charts -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<!-- Recent Activity -->
|
<!-- Jobs Timeline -->
|
||||||
<div class="stats-card p-6">
|
<div class="stats-card p-6">
|
||||||
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Letzte Aktivitäten</h2>
|
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Jobs der letzten 30 Tage</h2>
|
||||||
<div id="recent-activity" class="space-y-4" data-api-endpoint="/api/stats/activity" data-list>
|
<div class="h-64">
|
||||||
{% if recent_activity is defined and recent_activity and recent_activity|length > 0 %}
|
<canvas id="jobs-timeline-chart"></canvas>
|
||||||
{% for activity in recent_activity %}
|
|
||||||
<div class="border-l-4 border-black dark:border-blue-500 pl-4 py-3 bg-slate-50 dark:bg-slate-700/30 rounded-r-xl">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex-1">
|
|
||||||
<p class="text-sm text-slate-900 dark:text-white">{{ activity.description }}</p>
|
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400">{{ activity.timestamp|format_datetime }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Top Users -->
|
<!-- User Activity -->
|
||||||
<div class="stats-card p-6">
|
<div class="stats-card p-6">
|
||||||
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Top Benutzer</h2>
|
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Top Benutzer-Aktivität</h2>
|
||||||
<div id="top-users" class="space-y-4" data-api-endpoint="/api/stats/users" data-list>
|
<div class="h-64">
|
||||||
{% if top_users is defined and top_users and top_users|length > 0 %}
|
<canvas id="user-activity-chart"></canvas>
|
||||||
{% for user in top_users %}
|
|
||||||
<div class="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="mr-3 text-lg font-bold text-slate-900 dark:text-blue-400">#{{ loop.index }}</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-slate-900 dark:text-white">{{ user.name }}</p>
|
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400">{{ user.email }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<p class="text-lg font-bold text-slate-900 dark:text-white">{{ user.job_count }}</p>
|
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400">Jobs</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -156,37 +117,31 @@
|
|||||||
<div class="stats-card p-6">
|
<div class="stats-card p-6">
|
||||||
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Systemleistung</h2>
|
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Systemleistung</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<!-- Average Job Duration -->
|
<!-- Active Jobs -->
|
||||||
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl" data-api-endpoint="/api/stats/job-duration" data-counter>
|
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
|
||||||
<svg class="h-8 w-8 mx-auto mb-3 text-slate-900 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-8 w-8 mx-auto mb-3 text-slate-900 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">Durchschnittliche Job-Dauer</p>
|
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">Aktive Jobs</p>
|
||||||
{% if avg_job_duration is defined and avg_job_duration %}
|
<p id="active-jobs-count" class="text-xl font-bold text-slate-900 dark:text-white">-</p>
|
||||||
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ avg_job_duration }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Success Rate -->
|
<!-- Failed Jobs -->
|
||||||
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl" data-api-endpoint="/api/stats/success-rate" data-counter>
|
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
|
||||||
<svg class="h-8 w-8 mx-auto mb-3 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-8 w-8 mx-auto mb-3 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<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>
|
</svg>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">Erfolgsrate</p>
|
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">Fehlgeschlagene Jobs</p>
|
||||||
{% if success_rate is defined and success_rate %}
|
<p id="failed-jobs-count" class="text-xl font-bold text-slate-900 dark:text-white">-</p>
|
||||||
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ success_rate }}%</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- System Uptime -->
|
<!-- Total Users -->
|
||||||
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl" data-api-endpoint="/api/stats/uptime" data-counter>
|
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
|
||||||
<svg class="h-8 w-8 mx-auto mb-3 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-8 w-8 mx-auto mb-3 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">System-Verfügbarkeit</p>
|
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">Registrierte Benutzer</p>
|
||||||
{% if system_uptime is defined and system_uptime %}
|
<p id="total-users-count" class="text-xl font-bold text-slate-900 dark:text-white">-</p>
|
||||||
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ system_uptime }}%</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -194,271 +149,134 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<!-- ApexCharts Integration für unsere Diagramme -->
|
<!-- Chart.js CDN -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Charts JavaScript -->
|
||||||
|
<script src="{{ url_for('static', filename='js/charts.js') }}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
loadStats();
|
// Basis-Statistiken laden
|
||||||
|
loadBasicStats();
|
||||||
|
|
||||||
// Theme wechsel Event-Listener
|
// Theme wechsel Event-Listener
|
||||||
window.addEventListener('darkModeChanged', function(e) {
|
window.addEventListener('darkModeChanged', function(e) {
|
||||||
// Charts neu rendern mit passendem Farbschema
|
if (window.updateChartsTheme) {
|
||||||
if (window.jobStatusChart) {
|
window.updateChartsTheme();
|
||||||
updateChartTheme(window.jobStatusChart, e.detail.isDark);
|
|
||||||
}
|
|
||||||
if (window.printerUsageChart) {
|
|
||||||
updateChartTheme(window.printerUsageChart, e.detail.isDark);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadStats() {
|
|
||||||
const counterElements = document.querySelectorAll('[data-counter]');
|
|
||||||
const chartElements = document.querySelectorAll('[data-chart]');
|
|
||||||
const listElements = document.querySelectorAll('[data-list]');
|
|
||||||
|
|
||||||
// Zähler laden
|
|
||||||
counterElements.forEach(el => {
|
|
||||||
const endpoint = el.getAttribute('data-api-endpoint');
|
|
||||||
if (endpoint) {
|
|
||||||
fetch(endpoint)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
el.querySelector('p:last-child').textContent = data.value || '0';
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error loading counter:', error));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Diagramme laden (vereinfachte Implementierung)
|
|
||||||
chartElements.forEach(el => {
|
|
||||||
const endpoint = el.getAttribute('data-api-endpoint');
|
|
||||||
if (endpoint) {
|
|
||||||
fetch(endpoint)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
const chartId = el.id;
|
|
||||||
if (chartId === 'job-status-chart') {
|
|
||||||
renderJobStatusChart(el, data);
|
|
||||||
} else if (chartId === 'printer-usage-chart') {
|
|
||||||
renderPrinterUsageChart(el, data);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(`Fehler beim Laden der Diagrammdaten von ${endpoint}:`, error);
|
|
||||||
el.innerHTML = '<div class="flex items-center justify-center h-full"><p class="text-red-500">Fehler beim Laden der Daten</p></div>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen laden
|
|
||||||
listElements.forEach(el => {
|
|
||||||
const endpoint = el.getAttribute('data-api-endpoint');
|
|
||||||
if (endpoint) {
|
|
||||||
fetch(endpoint)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
const listId = el.id;
|
|
||||||
|
|
||||||
if (listId === 'recent-activity') {
|
|
||||||
renderActivityList(el, data.activities || []);
|
|
||||||
} else if (listId === 'top-users') {
|
|
||||||
renderUsersList(el, data.users || []);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(`Fehler beim Laden der Listendaten von ${endpoint}:`, error);
|
|
||||||
el.innerHTML = '<div class="p-4"><p class="text-red-500">Fehler beim Laden der Daten</p></div>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderJobStatusChart(element, data) {
|
// Auto-refresh für Basis-Statistiken (alle 30 Sekunden)
|
||||||
const chartData = data.data || [];
|
setInterval(loadBasicStats, 30000);
|
||||||
const labels = data.labels || ['Abgeschlossen', 'In Bearbeitung', 'Fehler', 'Wartend', 'Abgebrochen'];
|
|
||||||
const series = chartData.length > 0 ? chartData : [44, 55, 13, 33, 22]; // Fallback-Daten
|
|
||||||
|
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
series: series,
|
|
||||||
chart: {
|
|
||||||
type: 'donut',
|
|
||||||
height: 240,
|
|
||||||
fontFamily: 'Inter, sans-serif',
|
|
||||||
foreColor: isDark ? '#94a3b8' : '#64748b',
|
|
||||||
},
|
|
||||||
labels: labels,
|
|
||||||
colors: isDark ? ['#10b981', '#3b82f6', '#ef4444', '#f59e0b', '#6b7280'] : ['#10b981', '#000000', '#ef4444', '#f59e0b', '#6b7280'],
|
|
||||||
plotOptions: {
|
|
||||||
pie: {
|
|
||||||
donut: {
|
|
||||||
size: '60%'
|
|
||||||
},
|
|
||||||
expandOnClick: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
position: 'bottom'
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
theme: isDark ? 'dark' : 'light'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Element leeren und Chart erstellen
|
|
||||||
element.innerHTML = '';
|
|
||||||
window.jobStatusChart = new ApexCharts(element, options);
|
|
||||||
window.jobStatusChart.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPrinterUsageChart(element, data) {
|
|
||||||
const printerNames = data.categories || ['Drucker 1', 'Drucker 2', 'Drucker 3', 'Drucker 4', 'Drucker 5'];
|
|
||||||
const usageData = data.series || [30, 40, 25, 50, 49]; // Fallback-Daten
|
|
||||||
|
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
series: [{
|
|
||||||
name: 'Druckzeit (Stunden)',
|
|
||||||
data: usageData
|
|
||||||
}],
|
|
||||||
chart: {
|
|
||||||
type: 'bar',
|
|
||||||
height: 240,
|
|
||||||
fontFamily: 'Inter, sans-serif',
|
|
||||||
toolbar: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
foreColor: isDark ? '#94a3b8' : '#64748b',
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
bar: {
|
|
||||||
borderRadius: 4,
|
|
||||||
horizontal: false,
|
|
||||||
columnWidth: '60%',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
colors: [isDark ? '#3b82f6' : '#000000'],
|
|
||||||
xaxis: {
|
|
||||||
categories: printerNames,
|
|
||||||
labels: {
|
|
||||||
style: {
|
|
||||||
fontSize: '12px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
title: {
|
|
||||||
text: 'Stunden'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
theme: isDark ? 'dark' : 'light'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Element leeren und Chart erstellen
|
|
||||||
element.innerHTML = '';
|
|
||||||
window.printerUsageChart = new ApexCharts(element, options);
|
|
||||||
window.printerUsageChart.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderActivityList(element, activities) {
|
|
||||||
if (!activities || activities.length === 0) {
|
|
||||||
element.innerHTML = '<div class="p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl"><p class="text-center text-slate-500 dark:text-slate-400">Keine Aktivitäten gefunden</p></div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
activities.forEach(activity => {
|
|
||||||
const timestamp = activity.timestamp ? new Date(activity.timestamp).toLocaleString('de-DE') : '';
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div class="border-l-4 border-black dark:border-blue-500 pl-4 py-3 bg-slate-50 dark:bg-slate-700/30 rounded-r-xl">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex-1">
|
|
||||||
<p class="text-sm text-slate-900 dark:text-white">${activity.description}</p>
|
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400">${timestamp}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
element.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderUsersList(element, users) {
|
|
||||||
if (!users || users.length === 0) {
|
|
||||||
element.innerHTML = '<div class="p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl"><p class="text-center text-slate-500 dark:text-slate-400">Keine Benutzer gefunden</p></div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
users.forEach((user, index) => {
|
|
||||||
html += `
|
|
||||||
<div class="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="mr-3 text-lg font-bold text-slate-900 dark:text-blue-400">#${index + 1}</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-slate-900 dark:text-white">${user.name}</p>
|
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400">${user.email}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<p class="text-lg font-bold text-slate-900 dark:text-white">${user.job_count || 0}</p>
|
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400">Jobs</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
element.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateChartTheme(chart, isDark) {
|
|
||||||
chart.updateOptions({
|
|
||||||
chart: {
|
|
||||||
foreColor: isDark ? '#94a3b8' : '#64748b'
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
theme: isDark ? 'dark' : 'light'
|
|
||||||
},
|
|
||||||
colors: isDark ? ['#10b981', '#3b82f6', '#ef4444', '#f59e0b', '#6b7280'] : ['#10b981', '#000000', '#ef4444', '#f59e0b', '#6b7280']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Statistiken neu laden
|
|
||||||
function refreshStats() {
|
|
||||||
// Feedback für den Benutzer
|
|
||||||
showToast('Statistiken werden aktualisiert...', 'info');
|
|
||||||
|
|
||||||
// Daten neu laden
|
|
||||||
loadStats();
|
|
||||||
|
|
||||||
// Erfolgsmeldung
|
|
||||||
setTimeout(() => {
|
|
||||||
showToast('Statistiken erfolgreich aktualisiert', 'success');
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Statistiken exportieren
|
|
||||||
function exportStats() {
|
|
||||||
// Direkter Download vom API-Endpunkt
|
|
||||||
window.location.href = '/api/stats/export';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper-Funktion für Toast-Benachrichtigungen
|
|
||||||
function showToast(message, type) {
|
|
||||||
if (window.showToast) {
|
|
||||||
window.showToast(message, type);
|
|
||||||
} else {
|
|
||||||
alert(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadBasicStats() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/stats');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Fehler beim Laden der Statistiken');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistiken aktualisieren
|
||||||
|
updateStatsCounter('total-jobs-count', data.total_jobs);
|
||||||
|
updateStatsCounter('completed-jobs-count', data.completed_jobs);
|
||||||
|
updateStatsCounter('online-printers-count', data.online_printers);
|
||||||
|
updateStatsCounter('success-rate-percent', data.success_rate + '%');
|
||||||
|
updateStatsCounter('active-jobs-count', data.active_jobs);
|
||||||
|
updateStatsCounter('failed-jobs-count', data.failed_jobs);
|
||||||
|
updateStatsCounter('total-users-count', data.total_users);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Basis-Statistiken:', error);
|
||||||
|
showToast('Fehler beim Laden der Statistiken', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatsCounter(elementId, value, animate = true) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
|
// Animierte Zählung
|
||||||
|
const currentValue = parseInt(element.textContent.replace(/[^\d]/g, '')) || 0;
|
||||||
|
const targetValue = parseInt(value.toString().replace(/[^\d]/g, '')) || 0;
|
||||||
|
|
||||||
|
if (currentValue !== targetValue) {
|
||||||
|
animateCounter(element, currentValue, targetValue, value.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
element.textContent = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateCounter(element, start, end, finalText) {
|
||||||
|
const duration = 1000; // 1 Sekunde
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
function updateCounter(currentTime) {
|
||||||
|
const elapsed = currentTime - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
|
||||||
|
// Easing-Funktion (ease-out)
|
||||||
|
const easeOut = 1 - Math.pow(1 - progress, 3);
|
||||||
|
const currentValue = Math.round(start + (end - start) * easeOut);
|
||||||
|
|
||||||
|
if (finalText.includes('%')) {
|
||||||
|
element.textContent = currentValue + '%';
|
||||||
|
} else {
|
||||||
|
element.textContent = currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(updateCounter);
|
||||||
|
} else {
|
||||||
|
element.textContent = finalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(updateCounter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistiken neu laden
|
||||||
|
function refreshStats() {
|
||||||
|
// Feedback für den Benutzer
|
||||||
|
showToast('Statistiken werden aktualisiert...', 'info');
|
||||||
|
|
||||||
|
// Basis-Statistiken laden
|
||||||
|
loadBasicStats();
|
||||||
|
|
||||||
|
// Charts aktualisieren
|
||||||
|
if (window.refreshAllCharts) {
|
||||||
|
window.refreshAllCharts();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erfolgsmeldung
|
||||||
|
setTimeout(() => {
|
||||||
|
showToast('Statistiken erfolgreich aktualisiert', 'success');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistiken exportieren
|
||||||
|
function exportStats() {
|
||||||
|
// Direkter Download vom API-Endpunkt
|
||||||
|
window.location.href = '/api/stats/export';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper-Funktion für Toast-Benachrichtigungen
|
||||||
|
function showToast(message, type) {
|
||||||
|
if (window.showToast) {
|
||||||
|
window.showToast(message, type);
|
||||||
|
} else {
|
||||||
|
// Fallback für einfache Alert
|
||||||
|
if (type === 'error') {
|
||||||
|
alert('Fehler: ' + message);
|
||||||
|
} else {
|
||||||
|
console.log(type + ': ' + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user