"feat: Integrate new charting library in frontend"
This commit is contained in:
parent
919ebc312e
commit
aea600ee2e
@ -822,7 +822,7 @@ def user_update_profile():
|
|||||||
@login_required
|
@login_required
|
||||||
def user_api_update_settings():
|
def user_api_update_settings():
|
||||||
"""API-Endpunkt für Einstellungen-Updates (JSON)"""
|
"""API-Endpunkt für Einstellungen-Updates (JSON)"""
|
||||||
return user_update_settings()
|
return user_update_profile()
|
||||||
|
|
||||||
@app.route("/user/update-settings", methods=["POST"])
|
@app.route("/user/update-settings", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@ -2743,6 +2743,223 @@ def get_stats():
|
|||||||
app_logger.error(f"Fehler beim Abrufen der Statistiken: {str(e)}")
|
app_logger.error(f"Fehler beim Abrufen der Statistiken: {str(e)}")
|
||||||
return jsonify({"error": "Interner Serverfehler"}), 500
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
|
@app.route("/api/stats/charts/job-status", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def get_job_status_chart_data():
|
||||||
|
"""Gibt Diagrammdaten für Job-Status-Verteilung zurück."""
|
||||||
|
try:
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
# Job-Status zählen
|
||||||
|
job_status_counts = {
|
||||||
|
'completed': db_session.query(Job).filter(Job.status == 'completed').count(),
|
||||||
|
'failed': db_session.query(Job).filter(Job.status == 'failed').count(),
|
||||||
|
'cancelled': db_session.query(Job).filter(Job.status == 'cancelled').count(),
|
||||||
|
'running': db_session.query(Job).filter(Job.status == 'running').count(),
|
||||||
|
'scheduled': db_session.query(Job).filter(Job.status == 'scheduled').count()
|
||||||
|
}
|
||||||
|
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
chart_data = {
|
||||||
|
'labels': ['Abgeschlossen', 'Fehlgeschlagen', 'Abgebrochen', 'Läuft', 'Geplant'],
|
||||||
|
'datasets': [{
|
||||||
|
'label': 'Anzahl Jobs',
|
||||||
|
'data': [
|
||||||
|
job_status_counts['completed'],
|
||||||
|
job_status_counts['failed'],
|
||||||
|
job_status_counts['cancelled'],
|
||||||
|
job_status_counts['running'],
|
||||||
|
job_status_counts['scheduled']
|
||||||
|
],
|
||||||
|
'backgroundColor': [
|
||||||
|
'#10b981', # Grün für abgeschlossen
|
||||||
|
'#ef4444', # Rot für fehlgeschlagen
|
||||||
|
'#6b7280', # Grau für abgebrochen
|
||||||
|
'#3b82f6', # Blau für läuft
|
||||||
|
'#f59e0b' # Orange für geplant
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(chart_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"Fehler beim Abrufen der Job-Status-Diagrammdaten: {str(e)}")
|
||||||
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
|
@app.route("/api/stats/charts/printer-usage", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def get_printer_usage_chart_data():
|
||||||
|
"""Gibt Diagrammdaten für Drucker-Nutzung zurück."""
|
||||||
|
try:
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
# Drucker mit Job-Anzahl
|
||||||
|
printer_usage = db_session.query(
|
||||||
|
Printer.name,
|
||||||
|
func.count(Job.id).label('job_count')
|
||||||
|
).outerjoin(Job).group_by(Printer.id, Printer.name).all()
|
||||||
|
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
chart_data = {
|
||||||
|
'labels': [usage[0] for usage in printer_usage],
|
||||||
|
'datasets': [{
|
||||||
|
'label': 'Anzahl Jobs',
|
||||||
|
'data': [usage[1] for usage in printer_usage],
|
||||||
|
'backgroundColor': '#3b82f6',
|
||||||
|
'borderColor': '#1d4ed8',
|
||||||
|
'borderWidth': 1
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(chart_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"Fehler beim Abrufen der Drucker-Nutzung-Diagrammdaten: {str(e)}")
|
||||||
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
|
@app.route("/api/stats/charts/jobs-timeline", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def get_jobs_timeline_chart_data():
|
||||||
|
"""Gibt Diagrammdaten für Jobs-Timeline der letzten 30 Tage zurück."""
|
||||||
|
try:
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
# Letzte 30 Tage
|
||||||
|
end_date = datetime.now().date()
|
||||||
|
start_date = end_date - timedelta(days=30)
|
||||||
|
|
||||||
|
# Jobs pro Tag der letzten 30 Tage
|
||||||
|
daily_jobs = db_session.query(
|
||||||
|
func.date(Job.created_at).label('date'),
|
||||||
|
func.count(Job.id).label('count')
|
||||||
|
).filter(
|
||||||
|
func.date(Job.created_at) >= start_date,
|
||||||
|
func.date(Job.created_at) <= end_date
|
||||||
|
).group_by(func.date(Job.created_at)).all()
|
||||||
|
|
||||||
|
# Alle Tage füllen (auch ohne Jobs)
|
||||||
|
date_dict = {job_date: count for job_date, count in daily_jobs}
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
data = []
|
||||||
|
current_date = start_date
|
||||||
|
|
||||||
|
while current_date <= end_date:
|
||||||
|
labels.append(current_date.strftime('%d.%m'))
|
||||||
|
data.append(date_dict.get(current_date, 0))
|
||||||
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
chart_data = {
|
||||||
|
'labels': labels,
|
||||||
|
'datasets': [{
|
||||||
|
'label': 'Jobs pro Tag',
|
||||||
|
'data': data,
|
||||||
|
'fill': True,
|
||||||
|
'backgroundColor': 'rgba(59, 130, 246, 0.1)',
|
||||||
|
'borderColor': '#3b82f6',
|
||||||
|
'tension': 0.4
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(chart_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"Fehler beim Abrufen der Jobs-Timeline-Diagrammdaten: {str(e)}")
|
||||||
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
|
@app.route("/api/stats/charts/user-activity", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def get_user_activity_chart_data():
|
||||||
|
"""Gibt Diagrammdaten für Top-Benutzer-Aktivität zurück."""
|
||||||
|
try:
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
# Top 10 Benutzer nach Job-Anzahl
|
||||||
|
top_users = db_session.query(
|
||||||
|
User.username,
|
||||||
|
func.count(Job.id).label('job_count')
|
||||||
|
).join(Job).group_by(
|
||||||
|
User.id, User.username
|
||||||
|
).order_by(
|
||||||
|
func.count(Job.id).desc()
|
||||||
|
).limit(10).all()
|
||||||
|
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
chart_data = {
|
||||||
|
'labels': [user[0] for user in top_users],
|
||||||
|
'datasets': [{
|
||||||
|
'label': 'Anzahl Jobs',
|
||||||
|
'data': [user[1] for user in top_users],
|
||||||
|
'backgroundColor': '#8b5cf6',
|
||||||
|
'borderColor': '#7c3aed',
|
||||||
|
'borderWidth': 1
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(chart_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"Fehler beim Abrufen der Benutzer-Aktivität-Diagrammdaten: {str(e)}")
|
||||||
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
|
@app.route("/api/stats/export", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def export_stats():
|
||||||
|
"""Exportiert Statistiken als CSV."""
|
||||||
|
try:
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
# Basis-Statistiken sammeln
|
||||||
|
total_users = db_session.query(User).count()
|
||||||
|
total_printers = db_session.query(Printer).count()
|
||||||
|
total_jobs = db_session.query(Job).count()
|
||||||
|
completed_jobs = db_session.query(Job).filter(Job.status == "completed").count()
|
||||||
|
failed_jobs = db_session.query(Job).filter(Job.status == "failed").count()
|
||||||
|
|
||||||
|
# CSV-Inhalt erstellen
|
||||||
|
import io
|
||||||
|
import csv
|
||||||
|
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
writer.writerow(['Metrik', 'Wert'])
|
||||||
|
|
||||||
|
# Daten
|
||||||
|
writer.writerow(['Gesamte Benutzer', total_users])
|
||||||
|
writer.writerow(['Gesamte Drucker', total_printers])
|
||||||
|
writer.writerow(['Gesamte Jobs', total_jobs])
|
||||||
|
writer.writerow(['Abgeschlossene Jobs', completed_jobs])
|
||||||
|
writer.writerow(['Fehlgeschlagene Jobs', failed_jobs])
|
||||||
|
writer.writerow(['Erfolgsrate (%)', round((completed_jobs / total_jobs * 100), 2) if total_jobs > 0 else 0])
|
||||||
|
writer.writerow(['Exportiert am', datetime.now().strftime('%d.%m.%Y %H:%M:%S')])
|
||||||
|
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
# Response vorbereiten
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
response = Response(
|
||||||
|
output.getvalue(),
|
||||||
|
mimetype='text/csv',
|
||||||
|
headers={
|
||||||
|
'Content-Disposition': f'attachment; filename=statistiken_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"Fehler beim Exportieren der Statistiken: {str(e)}")
|
||||||
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
@app.route("/api/admin/users", methods=["GET"])
|
@app.route("/api/admin/users", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def get_users():
|
def get_users():
|
||||||
@ -3895,137 +4112,57 @@ def admin_update_user_form(user_id):
|
|||||||
def admin_update_printer_form(printer_id):
|
def admin_update_printer_form(printer_id):
|
||||||
"""Aktualisiert einen Drucker über HTML-Form (nur für Admins)."""
|
"""Aktualisiert einen Drucker über HTML-Form (nur für Admins)."""
|
||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
import os
|
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
|
||||||
import sys
|
return redirect(url_for("index"))
|
||||||
import logging
|
|
||||||
import atexit
|
|
||||||
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_login import LoginManager, login_user, logout_user, login_required, current_user
|
|
||||||
from flask_wtf import CSRFProtect
|
|
||||||
from flask_wtf.csrf import CSRFError
|
|
||||||
from werkzeug.utils import secure_filename
|
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
|
||||||
from sqlalchemy.orm import sessionmaker, joinedload
|
|
||||||
from sqlalchemy import func, text
|
|
||||||
from functools import wraps
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
||||||
from typing import List, Dict, Tuple
|
|
||||||
import time
|
|
||||||
import subprocess
|
|
||||||
import json
|
|
||||||
import signal
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
# Windows-spezifische Fixes früh importieren (sichere Version)
|
|
||||||
if os.name == 'nt':
|
|
||||||
try:
|
try:
|
||||||
from utils.windows_fixes import get_windows_thread_manager
|
# Form-Daten lesen
|
||||||
# apply_all_windows_fixes() wird automatisch beim Import ausgeführt
|
name = request.form.get("name", "").strip()
|
||||||
print("✅ Windows-Fixes (sichere Version) geladen")
|
ip_address = request.form.get("ip_address", "").strip()
|
||||||
except ImportError as e:
|
model = request.form.get("model", "").strip()
|
||||||
# Fallback falls windows_fixes nicht verfügbar
|
location = request.form.get("location", "").strip()
|
||||||
get_windows_thread_manager = None
|
description = request.form.get("description", "").strip()
|
||||||
print(f"⚠️ Windows-Fixes nicht verfügbar: {str(e)}")
|
status = request.form.get("status", "available").strip()
|
||||||
else:
|
|
||||||
get_windows_thread_manager = None
|
|
||||||
|
|
||||||
# Lokale Imports
|
# Pflichtfelder prüfen
|
||||||
from models import init_database, create_initial_admin, User, Printer, Job, Stats, SystemLog, get_db_session, GuestRequest, UserPermission, Notification
|
if not name or not ip_address:
|
||||||
from utils.logging_config import setup_logging, get_logger, measure_execution_time, log_startup_info, debug_request, debug_response
|
flash("Name und IP-Adresse sind erforderlich.", "error")
|
||||||
from utils.job_scheduler import JobScheduler, get_job_scheduler
|
return redirect(url_for("admin_edit_printer_page", printer_id=printer_id))
|
||||||
from utils.queue_manager import start_queue_manager, stop_queue_manager, get_queue_manager
|
|
||||||
from config.settings import SECRET_KEY, UPLOAD_FOLDER, ALLOWED_EXTENSIONS, ENVIRONMENT, SESSION_LIFETIME, SCHEDULER_ENABLED, SCHEDULER_INTERVAL, TAPO_USERNAME, TAPO_PASSWORD
|
|
||||||
from utils.file_manager import file_manager, save_job_file, save_guest_file, save_avatar_file, delete_file as delete_file_safe
|
|
||||||
|
|
||||||
# Blueprints importieren
|
# IP-Adresse validieren
|
||||||
from blueprints.guest import guest_blueprint
|
import re
|
||||||
from blueprints.calendar import calendar_blueprint
|
ip_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
|
||||||
from blueprints.users import users_blueprint
|
if not re.match(ip_pattern, ip_address):
|
||||||
|
flash("Ungültige IP-Adresse.", "error")
|
||||||
|
return redirect(url_for("admin_edit_printer_page", printer_id=printer_id))
|
||||||
|
|
||||||
# Scheduler importieren falls verfügbar
|
db_session = get_db_session()
|
||||||
try:
|
|
||||||
from utils.job_scheduler import scheduler
|
|
||||||
except ImportError:
|
|
||||||
scheduler = None
|
|
||||||
|
|
||||||
# SSL-Kontext importieren falls verfügbar
|
printer = db_session.query(Printer).get(printer_id)
|
||||||
try:
|
if not printer:
|
||||||
from utils.ssl_config import get_ssl_context
|
db_session.close()
|
||||||
except ImportError:
|
flash("Drucker nicht gefunden.", "error")
|
||||||
def get_ssl_context():
|
return redirect(url_for("admin_page", tab="printers"))
|
||||||
return None
|
|
||||||
|
|
||||||
# Template-Helfer importieren falls verfügbar
|
# Drucker aktualisieren
|
||||||
try:
|
printer.name = name
|
||||||
from utils.template_helpers import register_template_helpers
|
printer.model = model
|
||||||
except ImportError:
|
printer.location = location
|
||||||
def register_template_helpers(app):
|
printer.description = description
|
||||||
pass
|
printer.plug_ip = ip_address
|
||||||
|
printer.status = status
|
||||||
|
|
||||||
# Datenbank-Monitor und Backup-Manager importieren falls verfügbar
|
db_session.commit()
|
||||||
try:
|
db_session.close()
|
||||||
from utils.database_monitor import DatabaseMonitor
|
|
||||||
database_monitor = DatabaseMonitor()
|
|
||||||
except ImportError:
|
|
||||||
database_monitor = None
|
|
||||||
|
|
||||||
try:
|
printers_logger.info(f"Drucker '{printer.name}' (ID: {printer_id}) aktualisiert von Admin {current_user.id}")
|
||||||
from utils.backup_manager import BackupManager
|
flash(f"Drucker '{printer.name}' erfolgreich aktualisiert.", "success")
|
||||||
backup_manager = BackupManager()
|
return redirect(url_for("admin_page", tab="printers"))
|
||||||
except ImportError:
|
|
||||||
backup_manager = None
|
|
||||||
|
|
||||||
# Import neuer Systeme
|
except Exception as e:
|
||||||
from utils.rate_limiter import limit_requests, rate_limiter, cleanup_rate_limiter
|
printers_logger.error(f"Fehler beim Aktualisieren eines Druckers über Form: {str(e)}")
|
||||||
from utils.security import init_security, require_secure_headers, security_check
|
flash("Fehler beim Aktualisieren des Druckers.", "error")
|
||||||
from utils.permissions import init_permission_helpers, require_permission, Permission, check_permission
|
return redirect(url_for("admin_edit_printer_page", printer_id=printer_id))
|
||||||
from utils.analytics import analytics_engine, track_event, get_dashboard_stats
|
|
||||||
|
|
||||||
# Drucker-Monitor importieren
|
|
||||||
from utils.printer_monitor import printer_monitor
|
|
||||||
|
|
||||||
# Flask-App initialisieren
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.secret_key = SECRET_KEY
|
|
||||||
app.config["PERMANENT_SESSION_LIFETIME"] = SESSION_LIFETIME
|
|
||||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
|
||||||
app.config["WTF_CSRF_ENABLED"] = True
|
|
||||||
|
|
||||||
# CSRF-Schutz initialisieren
|
|
||||||
csrf = CSRFProtect(app)
|
|
||||||
|
|
||||||
# Security-System initialisieren
|
|
||||||
app = init_security(app)
|
|
||||||
|
|
||||||
# Permission Template Helpers registrieren
|
|
||||||
init_permission_helpers(app)
|
|
||||||
|
|
||||||
# Template-Helper registrieren
|
|
||||||
register_template_helpers(app)
|
|
||||||
|
|
||||||
# CSRF-Error-Handler - Korrigierte Version für Flask-WTF 1.2.1+
|
|
||||||
@app.errorhandler(CSRFError)
|
|
||||||
def csrf_error(error):
|
|
||||||
"""Behandelt CSRF-Fehler und gibt detaillierte Informationen zurück."""
|
|
||||||
app_logger.error(f"CSRF-Fehler für {request.path}: {error}")
|
|
||||||
|
|
||||||
if request.path.startswith('/api/'):
|
|
||||||
# Für API-Anfragen: JSON-Response
|
|
||||||
return jsonify({
|
|
||||||
"error": "CSRF-Token fehlt oder ungültig",
|
|
||||||
"reason": str(error),
|
|
||||||
"help": "Fügen Sie ein gültiges CSRF-Token zu Ihrer Anfrage hinzu"
|
|
||||||
}), 400
|
|
||||||
else:
|
|
||||||
# Für normale Anfragen: Weiterleitung zur Fehlerseite
|
|
||||||
flash("Sicherheitsfehler: Anfrage wurde abgelehnt. Bitte versuchen Sie es erneut.", "error")
|
|
||||||
return redirect(request.url)
|
|
||||||
|
|
||||||
# Blueprints registrieren
|
|
||||||
app.register_blueprint(guest_blueprint)
|
|
||||||
app.register_blueprint(calendar_blueprint)
|
|
||||||
app.register_blueprint(users_blueprint)
|
|
||||||
|
|
||||||
# Login-Manager initialisieren
|
# Login-Manager initialisieren
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
@ -4719,7 +4856,7 @@ def user_update_profile():
|
|||||||
@login_required
|
@login_required
|
||||||
def user_api_update_settings():
|
def user_api_update_settings():
|
||||||
"""API-Endpunkt für Einstellungen-Updates (JSON)"""
|
"""API-Endpunkt für Einstellungen-Updates (JSON)"""
|
||||||
return user_update_settings()
|
return user_update_profile()
|
||||||
|
|
||||||
@app.route("/user/update-settings", methods=["POST"])
|
@app.route("/user/update-settings", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@ -6640,6 +6777,223 @@ def get_stats():
|
|||||||
app_logger.error(f"Fehler beim Abrufen der Statistiken: {str(e)}")
|
app_logger.error(f"Fehler beim Abrufen der Statistiken: {str(e)}")
|
||||||
return jsonify({"error": "Interner Serverfehler"}), 500
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
|
@app.route("/api/stats/charts/job-status", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def get_job_status_chart_data():
|
||||||
|
"""Gibt Diagrammdaten für Job-Status-Verteilung zurück."""
|
||||||
|
try:
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
# Job-Status zählen
|
||||||
|
job_status_counts = {
|
||||||
|
'completed': db_session.query(Job).filter(Job.status == 'completed').count(),
|
||||||
|
'failed': db_session.query(Job).filter(Job.status == 'failed').count(),
|
||||||
|
'cancelled': db_session.query(Job).filter(Job.status == 'cancelled').count(),
|
||||||
|
'running': db_session.query(Job).filter(Job.status == 'running').count(),
|
||||||
|
'scheduled': db_session.query(Job).filter(Job.status == 'scheduled').count()
|
||||||
|
}
|
||||||
|
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
chart_data = {
|
||||||
|
'labels': ['Abgeschlossen', 'Fehlgeschlagen', 'Abgebrochen', 'Läuft', 'Geplant'],
|
||||||
|
'datasets': [{
|
||||||
|
'label': 'Anzahl Jobs',
|
||||||
|
'data': [
|
||||||
|
job_status_counts['completed'],
|
||||||
|
job_status_counts['failed'],
|
||||||
|
job_status_counts['cancelled'],
|
||||||
|
job_status_counts['running'],
|
||||||
|
job_status_counts['scheduled']
|
||||||
|
],
|
||||||
|
'backgroundColor': [
|
||||||
|
'#10b981', # Grün für abgeschlossen
|
||||||
|
'#ef4444', # Rot für fehlgeschlagen
|
||||||
|
'#6b7280', # Grau für abgebrochen
|
||||||
|
'#3b82f6', # Blau für läuft
|
||||||
|
'#f59e0b' # Orange für geplant
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(chart_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"Fehler beim Abrufen der Job-Status-Diagrammdaten: {str(e)}")
|
||||||
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
|
@app.route("/api/stats/charts/printer-usage", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def get_printer_usage_chart_data():
|
||||||
|
"""Gibt Diagrammdaten für Drucker-Nutzung zurück."""
|
||||||
|
try:
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
# Drucker mit Job-Anzahl
|
||||||
|
printer_usage = db_session.query(
|
||||||
|
Printer.name,
|
||||||
|
func.count(Job.id).label('job_count')
|
||||||
|
).outerjoin(Job).group_by(Printer.id, Printer.name).all()
|
||||||
|
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
chart_data = {
|
||||||
|
'labels': [usage[0] for usage in printer_usage],
|
||||||
|
'datasets': [{
|
||||||
|
'label': 'Anzahl Jobs',
|
||||||
|
'data': [usage[1] for usage in printer_usage],
|
||||||
|
'backgroundColor': '#3b82f6',
|
||||||
|
'borderColor': '#1d4ed8',
|
||||||
|
'borderWidth': 1
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(chart_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"Fehler beim Abrufen der Drucker-Nutzung-Diagrammdaten: {str(e)}")
|
||||||
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
|
@app.route("/api/stats/charts/jobs-timeline", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def get_jobs_timeline_chart_data():
|
||||||
|
"""Gibt Diagrammdaten für Jobs-Timeline der letzten 30 Tage zurück."""
|
||||||
|
try:
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
# Letzte 30 Tage
|
||||||
|
end_date = datetime.now().date()
|
||||||
|
start_date = end_date - timedelta(days=30)
|
||||||
|
|
||||||
|
# Jobs pro Tag der letzten 30 Tage
|
||||||
|
daily_jobs = db_session.query(
|
||||||
|
func.date(Job.created_at).label('date'),
|
||||||
|
func.count(Job.id).label('count')
|
||||||
|
).filter(
|
||||||
|
func.date(Job.created_at) >= start_date,
|
||||||
|
func.date(Job.created_at) <= end_date
|
||||||
|
).group_by(func.date(Job.created_at)).all()
|
||||||
|
|
||||||
|
# Alle Tage füllen (auch ohne Jobs)
|
||||||
|
date_dict = {job_date: count for job_date, count in daily_jobs}
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
data = []
|
||||||
|
current_date = start_date
|
||||||
|
|
||||||
|
while current_date <= end_date:
|
||||||
|
labels.append(current_date.strftime('%d.%m'))
|
||||||
|
data.append(date_dict.get(current_date, 0))
|
||||||
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
chart_data = {
|
||||||
|
'labels': labels,
|
||||||
|
'datasets': [{
|
||||||
|
'label': 'Jobs pro Tag',
|
||||||
|
'data': data,
|
||||||
|
'fill': True,
|
||||||
|
'backgroundColor': 'rgba(59, 130, 246, 0.1)',
|
||||||
|
'borderColor': '#3b82f6',
|
||||||
|
'tension': 0.4
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(chart_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"Fehler beim Abrufen der Jobs-Timeline-Diagrammdaten: {str(e)}")
|
||||||
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
|
@app.route("/api/stats/charts/user-activity", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def get_user_activity_chart_data():
|
||||||
|
"""Gibt Diagrammdaten für Top-Benutzer-Aktivität zurück."""
|
||||||
|
try:
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
# Top 10 Benutzer nach Job-Anzahl
|
||||||
|
top_users = db_session.query(
|
||||||
|
User.username,
|
||||||
|
func.count(Job.id).label('job_count')
|
||||||
|
).join(Job).group_by(
|
||||||
|
User.id, User.username
|
||||||
|
).order_by(
|
||||||
|
func.count(Job.id).desc()
|
||||||
|
).limit(10).all()
|
||||||
|
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
chart_data = {
|
||||||
|
'labels': [user[0] for user in top_users],
|
||||||
|
'datasets': [{
|
||||||
|
'label': 'Anzahl Jobs',
|
||||||
|
'data': [user[1] for user in top_users],
|
||||||
|
'backgroundColor': '#8b5cf6',
|
||||||
|
'borderColor': '#7c3aed',
|
||||||
|
'borderWidth': 1
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(chart_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"Fehler beim Abrufen der Benutzer-Aktivität-Diagrammdaten: {str(e)}")
|
||||||
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
|
@app.route("/api/stats/export", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def export_stats():
|
||||||
|
"""Exportiert Statistiken als CSV."""
|
||||||
|
try:
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
# Basis-Statistiken sammeln
|
||||||
|
total_users = db_session.query(User).count()
|
||||||
|
total_printers = db_session.query(Printer).count()
|
||||||
|
total_jobs = db_session.query(Job).count()
|
||||||
|
completed_jobs = db_session.query(Job).filter(Job.status == "completed").count()
|
||||||
|
failed_jobs = db_session.query(Job).filter(Job.status == "failed").count()
|
||||||
|
|
||||||
|
# CSV-Inhalt erstellen
|
||||||
|
import io
|
||||||
|
import csv
|
||||||
|
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
writer.writerow(['Metrik', 'Wert'])
|
||||||
|
|
||||||
|
# Daten
|
||||||
|
writer.writerow(['Gesamte Benutzer', total_users])
|
||||||
|
writer.writerow(['Gesamte Drucker', total_printers])
|
||||||
|
writer.writerow(['Gesamte Jobs', total_jobs])
|
||||||
|
writer.writerow(['Abgeschlossene Jobs', completed_jobs])
|
||||||
|
writer.writerow(['Fehlgeschlagene Jobs', failed_jobs])
|
||||||
|
writer.writerow(['Erfolgsrate (%)', round((completed_jobs / total_jobs * 100), 2) if total_jobs > 0 else 0])
|
||||||
|
writer.writerow(['Exportiert am', datetime.now().strftime('%d.%m.%Y %H:%M:%S')])
|
||||||
|
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
# Response vorbereiten
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
response = Response(
|
||||||
|
output.getvalue(),
|
||||||
|
mimetype='text/csv',
|
||||||
|
headers={
|
||||||
|
'Content-Disposition': f'attachment; filename=statistiken_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"Fehler beim Exportieren der Statistiken: {str(e)}")
|
||||||
|
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||||
|
|
||||||
@app.route("/api/admin/users", methods=["GET"])
|
@app.route("/api/admin/users", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def get_users():
|
def get_users():
|
||||||
@ -9158,132 +9512,6 @@ def auto_optimize_jobs():
|
|||||||
'error': f'Optimierung fehlgeschlagen: {str(e)}'
|
'error': f'Optimierung fehlgeschlagen: {str(e)}'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
@app.route('/api/jobs/batch-operation', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def perform_batch_operation():
|
|
||||||
"""Batch-Operationen auf mehrere Jobs anwenden"""
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
job_ids = data.get('job_ids', [])
|
|
||||||
operation = data.get('operation', '')
|
|
||||||
|
|
||||||
if not job_ids:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Keine Job-IDs für Batch-Operation angegeben'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
if not operation:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Keine Operation für Batch-Verarbeitung angegeben'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
db_session = get_db_session()
|
|
||||||
|
|
||||||
# Jobs abrufen (nur eigene oder Admin-Rechte prüfen)
|
|
||||||
if current_user.is_admin:
|
|
||||||
jobs = db_session.query(Job).filter(Job.id.in_(job_ids)).all()
|
|
||||||
else:
|
|
||||||
jobs = db_session.query(Job).filter(
|
|
||||||
Job.id.in_(job_ids),
|
|
||||||
Job.user_id == int(current_user.id)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if not jobs:
|
|
||||||
db_session.close()
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Keine berechtigten Jobs für Batch-Operation gefunden'
|
|
||||||
}), 403
|
|
||||||
|
|
||||||
processed_count = 0
|
|
||||||
error_count = 0
|
|
||||||
|
|
||||||
# Batch-Operation durchführen
|
|
||||||
for job in jobs:
|
|
||||||
try:
|
|
||||||
if operation == 'start':
|
|
||||||
if job.status in ['queued', 'pending']:
|
|
||||||
job.status = 'running'
|
|
||||||
job.start_time = datetime.now()
|
|
||||||
processed_count += 1
|
|
||||||
|
|
||||||
elif operation == 'pause':
|
|
||||||
if job.status == 'running':
|
|
||||||
job.status = 'paused'
|
|
||||||
processed_count += 1
|
|
||||||
|
|
||||||
elif operation == 'cancel':
|
|
||||||
if job.status in ['queued', 'pending', 'running', 'paused']:
|
|
||||||
job.status = 'cancelled'
|
|
||||||
job.end_time = datetime.now()
|
|
||||||
processed_count += 1
|
|
||||||
|
|
||||||
elif operation == 'delete':
|
|
||||||
if job.status in ['completed', 'cancelled', 'failed']:
|
|
||||||
db_session.delete(job)
|
|
||||||
processed_count += 1
|
|
||||||
|
|
||||||
elif operation == 'restart':
|
|
||||||
if job.status in ['failed', 'cancelled']:
|
|
||||||
job.status = 'queued'
|
|
||||||
job.start_time = None
|
|
||||||
job.end_time = None
|
|
||||||
processed_count += 1
|
|
||||||
|
|
||||||
elif operation == 'priority_high':
|
|
||||||
job.priority = 'high'
|
|
||||||
processed_count += 1
|
|
||||||
|
|
||||||
elif operation == 'priority_normal':
|
|
||||||
job.priority = 'normal'
|
|
||||||
processed_count += 1
|
|
||||||
|
|
||||||
else:
|
|
||||||
jobs_logger.warning(f"Unbekannte Batch-Operation: {operation}")
|
|
||||||
error_count += 1
|
|
||||||
|
|
||||||
except Exception as job_error:
|
|
||||||
jobs_logger.error(f"Fehler bei Job {job.id} in Batch-Operation {operation}: {str(job_error)}")
|
|
||||||
error_count += 1
|
|
||||||
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
# System-Log erstellen
|
|
||||||
log_entry = SystemLog(
|
|
||||||
level='INFO',
|
|
||||||
component='batch_operations',
|
|
||||||
message=f'Batch-Operation "{operation}" durchgeführt: {processed_count} Jobs verarbeitet',
|
|
||||||
user_id=current_user.id,
|
|
||||||
details=json.dumps({
|
|
||||||
'operation': operation,
|
|
||||||
'processed_jobs': processed_count,
|
|
||||||
'error_count': error_count,
|
|
||||||
'job_ids': job_ids
|
|
||||||
})
|
|
||||||
)
|
|
||||||
db_session.add(log_entry)
|
|
||||||
db_session.commit()
|
|
||||||
db_session.close()
|
|
||||||
|
|
||||||
jobs_logger.info(f"Batch-Operation {operation} durchgeführt: {processed_count} Jobs verarbeitet, {error_count} Fehler")
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'processed_jobs': processed_count,
|
|
||||||
'error_count': error_count,
|
|
||||||
'operation': operation,
|
|
||||||
'message': f'Batch-Operation erfolgreich: {processed_count} Jobs verarbeitet'
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
app_logger.error(f"Fehler bei Batch-Operation: {str(e)}")
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': f'Batch-Operation fehlgeschlagen: {str(e)}'
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
@app.route('/api/optimization/settings', methods=['GET', 'POST'])
|
@app.route('/api/optimization/settings', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def optimization_settings():
|
def optimization_settings():
|
||||||
|
413
backend/app/static/js/charts.js
Normal file
413
backend/app/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);
|
Loading…
x
Reference in New Issue
Block a user