"feat: Implement button functionality tests

This commit is contained in:
Till Tomczak 2025-05-29 17:43:52 +02:00
parent 06119be88b
commit 831b578a26
6 changed files with 596 additions and 722 deletions

View File

@ -0,0 +1 @@

View File

@ -4616,7 +4616,7 @@ def get_dashboard_active_jobs():
"jobs": jobs_data
})
except Exception as e:
} except Exception as e:
app_logger.error(f"Fehler beim Laden der aktiven Jobs: {str(e)}")
return jsonify({
"success": False,
@ -4713,156 +4713,23 @@ def analytics_page():
flash("Fehler beim Laden der Analytics", "error")
return redirect(url_for('dashboard'))
# ===== HAUPTPROGRAMM =====
if __name__ == "__main__":
import sys
# Debug-Modus aktivieren wenn Parameter übergeben
debug_mode = "--debug" in sys.argv
if debug_mode:
print("🚀 Debug-Modus aktiviert")
# Setze saubere Umgebung
os.environ['FLASK_ENV'] = 'development'
os.environ['PYTHONIOENCODING'] = 'utf-8'
os.environ['PYTHONUTF8'] = '1'
# Windows-spezifisches Signal-Handling für ordnungsgemäßes Shutdown
def signal_handler(sig, frame):
"""Signal-Handler für ordnungsgemäßes Shutdown."""
app_logger.warning(f"🛑 Signal {sig} empfangen - fahre System herunter...")
try:
# Queue Manager stoppen
app_logger.info("🔄 Beende Queue Manager...")
stop_queue_manager()
# Scheduler stoppen falls aktiviert
if SCHEDULER_ENABLED and scheduler:
try:
scheduler.stop()
app_logger.info("Job-Scheduler gestoppt")
except Exception as e:
app_logger.error(f"Fehler beim Stoppen des Schedulers: {str(e)}")
app_logger.info("✅ Shutdown abgeschlossen")
sys.exit(0)
except Exception as e:
app_logger.error(f"❌ Fehler beim Shutdown: {str(e)}")
sys.exit(1)
# Signal-Handler registrieren (Windows-kompatibel)
if os.name == 'nt': # Windows
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Zusätzlich für Flask-Development-Server
signal.signal(signal.SIGBREAK, signal_handler)
else: # Unix/Linux
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGHUP, signal_handler)
"success": True,
"status": {
"cpu_usage": 15.2,
"memory_usage": 42.8,
"disk_usage": 67.3,
@admin_required
def get_admin_system_status():
"""Liefert detaillierte System-Status-Informationen"""
try:
# Datenbank initialisieren
init_database()
create_initial_admin()
import psutil
import os
from datetime import datetime, timedelta
"active_jobs": active_jobs,
"queued_jobs": queued_jobs,
"success_rate": success_rate
}
})
# Template-Hilfsfunktionen registrieren
register_template_helpers(app)
# Queue-Manager für automatische Drucker-Überwachung starten
# Nur im Produktionsmodus starten (nicht im Debug-Modus)
if not debug_mode:
try:
queue_manager = start_queue_manager()
app_logger.info("✅ Printer Queue Manager erfolgreich gestartet")
# Verbesserte Shutdown-Handler registrieren
def cleanup_queue_manager():
try:
app_logger.info("🔄 Beende Queue Manager...")
stop_queue_manager()
except Exception as e:
app_logger.error(f"❌ Fehler beim Queue Manager Cleanup: {str(e)}")
atexit.register(cleanup_queue_manager)
except Exception as e:
app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}")
else:
app_logger.info("🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung")
# Scheduler starten (falls aktiviert)
if SCHEDULER_ENABLED:
try:
scheduler.start()
app_logger.info("Job-Scheduler gestartet")
except Exception as e:
app_logger.error(f"Fehler beim Starten des Schedulers: {str(e)}")
if debug_mode:
# Debug-Modus: HTTP auf Port 5000
app_logger.info("Starte Debug-Server auf 0.0.0.0:5000 (HTTP)")
# Windows-spezifische Flask-Konfiguration für bessere Thread-Behandlung
if os.name == 'nt':
try:
# Windows: Ohne use_reloader um WERKZEUG_SERVER_FD Fehler zu vermeiden
app.run(
host="0.0.0.0",
port=5000,
debug=True,
threaded=True,
use_reloader=False, # Deaktiviere Auto-Reload für Windows
passthrough_errors=False
)
except Exception as e:
app_logger.warning(f"Windows-Debug-Server Fehler: {str(e)}")
app_logger.info("Fallback: Starte ohne Debug-Modus")
# Fallback: Ohne Debug-Features
app.run(
host="0.0.0.0",
port=5000,
debug=False,
threaded=True
)
else:
app.run(
host="0.0.0.0",
port=5000,
debug=True,
threaded=True
)
else:
# Produktions-Modus: HTTPS auf Port 443
ssl_context = get_ssl_context()
if ssl_context:
app_logger.info("Starte HTTPS-Server auf 0.0.0.0:443")
app.run(
host="0.0.0.0",
port=443,
debug=False,
ssl_context=ssl_context,
threaded=True
)
else:
app_logger.info("Starte HTTP-Server auf 0.0.0.0:8080")
app.run(
host="0.0.0.0",
port=8080,
debug=False,
threaded=True
)
except KeyboardInterrupt:
app_logger.info("🔄 Tastatur-Unterbrechung empfangen - beende Anwendung...")
signal_handler(signal.SIGINT, None)
except Exception as e:
app_logger.error(f"Fehler beim Starten der Anwendung: {str(e)}")
# Cleanup bei Fehler
try:
stop_queue_manager()
except:
pass
sys.exit(1)
app_logger.error(f"Fehler beim Laden der Live-Admin-Statistiken: {str(e)}")

Binary file not shown.

View File

@ -1,609 +1,372 @@
{% extends "base.html" %}
{% block title %}Gastanfragen verwalten{% endblock %}
{% block title %}Gastaufträge verwalten - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<!-- CSRF Token für AJAX-Anfragen -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<script src="{{ url_for('static', filename='js/admin-guest-requests.js') }}" defer></script>
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center hidden">
<div class="bg-white dark:bg-slate-800 rounded-2xl p-8 shadow-2xl">
<div class="flex items-center space-x-4">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<span class="text-lg font-medium text-gray-900 dark:text-white">Wird geladen...</span>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="bg-white rounded-lg shadow-lg p-6">
<!-- Header -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-800">
<i class="fas fa-user-friends mr-3 text-blue-600"></i>
Gastanfragen verwalten
</h1>
<div class="flex items-center space-x-4">
<div class="stats-summary flex space-x-4" id="statsContainer">
<!-- Statistiken werden hier geladen -->
<!-- Moderne Gastaufträge-Verwaltung -->
<div class="min-h-screen">
<!-- Hero Header -->
<div class="relative overflow-hidden bg-gradient-to-r from-slate-900 via-blue-900 to-indigo-900 text-white rounded-3xl mx-4 mt-4">
<div class="absolute inset-0 bg-black/20"></div>
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent"></div>
<!-- Live Status Indicator -->
<div class="absolute top-4 right-4 flex items-center space-x-2">
<div class="flex items-center space-x-2 bg-white/10 backdrop-blur-sm border border-white/20 rounded-full px-3 py-1">
<div id="live-indicator" class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span class="text-sm font-medium">Live</span>
</div>
<div class="bg-white/10 backdrop-blur-sm border border-white/20 rounded-full px-3 py-1">
<span id="live-time" class="text-sm font-medium"></span>
</div>
</div>
<!-- Animated Background Pattern -->
<div class="absolute inset-0 opacity-10 rounded-3xl overflow-hidden">
<div class="absolute inset-0" style="background-image: radial-gradient(circle at 25% 25%, white 2px, transparent 2px), radial-gradient(circle at 75% 75%, white 2px, transparent 2px); background-size: 50px 50px;"></div>
</div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div class="flex justify-between items-center">
<div>
<!-- Mercedes-Benz Logo -->
<div class="inline-flex items-center justify-center w-20 h-20 bg-white/10 backdrop-blur-sm rounded-full mb-6 border border-white/20">
<svg class="w-10 h-10 text-white" viewBox="0 0 80 80" fill="currentColor">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>
</div>
<h1 class="text-4xl md:text-5xl font-bold mb-4 tracking-tight">
<span class="bg-gradient-to-r from-white to-blue-200 bg-clip-text text-transparent">
Gastaufträge Verwaltung
</span>
</h1>
<p class="text-xl text-blue-100 max-w-2xl leading-relaxed">
Verwalten Sie Gastdruckaufträge mit modernster Technologie und Mercedes-Benz Qualität
</p>
</div>
<!-- Back to Admin Button -->
<div>
<a href="{{ url_for('admin_page') }}"
class="inline-flex items-center px-6 py-3 bg-white/10 backdrop-blur-sm border border-white/20 rounded-xl text-white hover:bg-white/20 transition-all duration-300 hover:scale-105">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
Zurück zum Admin
</a>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10">
<!-- Quick Stats Dashboard -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-12">
<!-- Pending Requests -->
<div class="group relative bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl rounded-3xl border border-white/20 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-orange-500/10 to-red-500/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative">
<div class="flex items-center justify-between mb-4">
<div class="p-3 bg-gradient-to-br from-orange-500 to-orange-600 rounded-xl shadow-lg">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
</div>
<div class="text-right">
<div id="pending-count" class="text-2xl font-bold text-slate-900 dark:text-white">-</div>
<div class="text-sm text-slate-500 dark:text-slate-400">Wartend</div>
</div>
</div>
<div class="flex items-center space-x-2 mb-2">
<div class="w-2 h-2 bg-orange-400 rounded-full animate-pulse"></div>
<span class="text-xs text-orange-600 dark:text-orange-400 font-medium">Benötigt Aufmerksamkeit</span>
</div>
</div>
</div>
<!-- Approved Requests -->
<div class="group relative bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl rounded-3xl border border-white/20 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-green-500/10 to-emerald-500/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative">
<div class="flex items-center justify-between mb-4">
<div class="p-3 bg-gradient-to-br from-green-500 to-green-600 rounded-xl shadow-lg">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
</div>
<div class="text-right">
<div id="approved-count" class="text-2xl font-bold text-slate-900 dark:text-white">-</div>
<div class="text-sm text-slate-500 dark:text-slate-400">Genehmigt</div>
</div>
</div>
<div class="flex items-center space-x-2 mb-2">
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span class="text-xs text-green-600 dark:text-green-400 font-medium">Aktiv</span>
</div>
</div>
</div>
<!-- Rejected Requests -->
<div class="group relative bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl rounded-3xl border border-white/20 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-red-500/10 to-pink-500/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative">
<div class="flex items-center justify-between mb-4">
<div class="p-3 bg-gradient-to-br from-red-500 to-red-600 rounded-xl shadow-lg">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="text-right">
<div id="rejected-count" class="text-2xl font-bold text-slate-900 dark:text-white">-</div>
<div class="text-sm text-slate-500 dark:text-slate-400">Abgelehnt</div>
</div>
</div>
<div class="flex items-center space-x-2 mb-2">
<div class="w-2 h-2 bg-red-400 rounded-full"></div>
<span class="text-xs text-red-600 dark:text-red-400 font-medium">Archiviert</span>
</div>
</div>
</div>
<!-- Total Requests -->
<div class="group relative bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl rounded-3xl border border-white/20 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/10 to-indigo-500/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative">
<div class="flex items-center justify-between mb-4">
<div class="p-3 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl shadow-lg">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div>
<div class="text-right">
<div id="total-count" class="text-2xl font-bold text-slate-900 dark:text-white">-</div>
<div class="text-sm text-slate-500 dark:text-slate-400">Gesamt</div>
</div>
</div>
<div class="flex items-center space-x-2 mb-2">
<div class="w-2 h-2 bg-blue-400 rounded-full animate-pulse"></div>
<span class="text-xs text-blue-600 dark:text-blue-400 font-medium">Alle Zeit</span>
</div>
</div>
<button onclick="refreshRequests()" class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
<i class="fas fa-sync-alt mr-2"></i>Aktualisieren
</button>
</div>
</div>
<!-- Filter und Such-Bar -->
<div class="mb-6 flex flex-wrap gap-4 items-center">
<div class="flex items-center space-x-2">
<label for="statusFilter" class="text-sm font-medium text-gray-700">Status:</label>
<select id="statusFilter" class="form-select rounded-md border-gray-300" onchange="filterRequests()">
<option value="all">Alle</option>
<option value="pending" selected>Wartend</option>
<option value="approved">Genehmigt</option>
<option value="denied">Abgelehnt</option>
</select>
</div>
<div class="flex items-center space-x-2">
<label for="searchInput" class="text-sm font-medium text-gray-700">Suchen:</label>
<input type="text" id="searchInput" placeholder="Name, E-Mail..."
class="form-input rounded-md border-gray-300 w-64"
oninput="debounceSearch()">
<!-- Control Panel -->
<div class="bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl rounded-3xl border border-white/20 dark:border-slate-700/50 p-8 shadow-xl mb-8">
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center space-y-4 lg:space-y-0">
<!-- Search and Filters -->
<div class="flex flex-col sm:flex-row gap-4 flex-1">
<div class="relative flex-1 max-w-md">
<input type="text" id="search-requests" placeholder="Gastaufträge durchsuchen..."
class="w-full pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
bg-white/70 dark:bg-slate-700/70 text-slate-900 dark:text-white
focus:ring-2 focus:ring-blue-500 focus:border-transparent
placeholder-slate-500 dark:placeholder-slate-400">
<svg class="w-5 h-5 text-slate-400 absolute left-3 top-1/2 transform -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<select id="status-filter"
class="px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
bg-white/70 dark:bg-slate-700/70 text-slate-900 dark:text-white
focus:ring-2 focus:ring-blue-500 focus:border-transparent min-w-[150px]">
<option value="all">Alle Status</option>
<option value="pending">Wartend</option>
<option value="approved">Genehmigt</option>
<option value="rejected">Abgelehnt</option>
<option value="expired">Abgelaufen</option>
</select>
<select id="sort-order"
class="px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
bg-white/70 dark:bg-slate-700/70 text-slate-900 dark:text-white
focus:ring-2 focus:ring-blue-500 focus:border-transparent min-w-[150px]">
<option value="newest">Neueste zuerst</option>
<option value="oldest">Älteste zuerst</option>
<option value="priority">Nach Priorität</option>
</select>
</div>
<!-- Action Buttons -->
<div class="flex space-x-3">
<button id="refresh-btn"
class="inline-flex items-center px-4 py-3 bg-blue-500 text-white rounded-xl hover:bg-blue-600 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105">
<svg class="w-5 h-5 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="export-btn"
class="inline-flex items-center px-4 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105">
<svg class="w-5 h-5 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>
<button id="bulk-actions-btn"
class="inline-flex items-center px-4 py-3 bg-purple-500 text-white rounded-xl hover:bg-purple-600 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
Massenaktionen
</button>
</div>
</div>
</div>
<!-- Anfragen-Liste -->
<div id="requestsContainer" class="space-y-4">
<!-- Wird dynamisch geladen -->
</div>
<!-- Loading Spinner -->
<div id="loadingSpinner" class="text-center py-8 hidden">
<i class="fas fa-spinner fa-spin text-3xl text-blue-600"></i>
<p class="text-gray-600 mt-2">Lade Anfragen...</p>
<!-- Guest Requests Table -->
<div class="bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl rounded-3xl border border-white/20 dark:border-slate-700/50 shadow-xl overflow-hidden">
<div class="p-8 border-b border-slate-200 dark:border-slate-700">
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Gastaufträge</h2>
<div class="flex items-center space-x-2 text-sm text-slate-500 dark:text-slate-400">
<span>Automatische Aktualisierung:</span>
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span>Aktiv</span>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead class="bg-slate-50 dark:bg-slate-900/50">
<tr>
<th class="px-6 py-4 text-left">
<input type="checkbox" id="select-all"
class="rounded border-slate-300 text-blue-600 focus:ring-blue-500">
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Antragsteller
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Datei & Details
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Status
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Zeitstempel
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Priorität
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Aktionen
</th>
</tr>
</thead>
<tbody id="requests-table-body" class="bg-white dark:bg-slate-800/50 divide-y divide-slate-200 dark:divide-slate-700">
<!-- Dynamic content will be loaded here -->
</tbody>
</table>
</div>
<!-- Loading State -->
<div id="table-loading" class="flex items-center justify-center py-12 hidden">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mr-4"></div>
<span class="text-slate-600 dark:text-slate-400">Lade Gastaufträge...</span>
</div>
<!-- Empty State -->
<div id="empty-state" class="text-center py-12 hidden">
<svg class="mx-auto h-12 w-12 text-slate-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
</svg>
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-2">Keine Gastaufträge gefunden</h3>
<p class="text-slate-500 dark:text-slate-400">Es sind derzeit keine Gastaufträge vorhanden oder sie entsprechen nicht den Filterkriterien.</p>
</div>
</div>
<!-- Pagination -->
<div id="paginationContainer" class="mt-6 flex justify-center">
<!-- Wird dynamisch geladen -->
<div id="pagination" class="mt-8 flex justify-center">
<!-- Pagination controls will be dynamically generated -->
</div>
</div>
</div>
<!-- Detail-Modal -->
<div id="detailModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden">
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
<div class="mt-3">
<!-- Modal Header -->
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900" id="modalTitle">Anfrage Details</h3>
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- Modal Content -->
<div id="modalContent">
<!-- Wird dynamisch geladen -->
<!-- Guest Request Detail Modal -->
<div id="detail-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div id="modal-content">
<!-- Modal content will be dynamically loaded -->
</div>
</div>
</div>
</div>
<!-- Aktions-Modal (Genehmigen/Ablehnen) -->
<div id="actionModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden">
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-lg shadow-lg rounded-md bg-white">
<div class="mt-3">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900" id="actionModalTitle">Anfrage bearbeiten</h3>
<button onclick="closeActionModal()" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
<!-- Bulk Actions Modal -->
<div id="bulk-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-md w-full">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Massenaktionen</h3>
</div>
<form id="actionForm">
<input type="hidden" id="actionRequestId">
<input type="hidden" id="actionType">
<!-- Genehmigen-Sektion -->
<div id="approveSection" class="hidden">
<div class="mb-4">
<label for="printerSelect" class="block text-sm font-medium text-gray-700 mb-2">
Drucker zuweisen:
</label>
<select id="printerSelect" class="form-select w-full rounded-md border-gray-300">
<option value="">Drucker auswählen...</option>
</select>
</div>
<div class="mb-4">
<label for="approvalNotes" class="block text-sm font-medium text-gray-700 mb-2">
Notizen (optional):
</label>
<textarea id="approvalNotes" rows="3"
class="form-textarea w-full rounded-md border-gray-300"
placeholder="Zusätzliche Anweisungen oder Hinweise..."></textarea>
</div>
<div class="p-6">
<div class="space-y-4">
<button onclick="performBulkAction('approve')"
class="w-full px-4 py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
Ausgewählte genehmigen
</button>
<button onclick="performBulkAction('reject')"
class="w-full px-4 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
Ausgewählte ablehnen
</button>
<button onclick="performBulkAction('delete')"
class="w-full px-4 py-3 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors">
Ausgewählte löschen
</button>
</div>
<!-- Ablehnen-Sektion -->
<div id="denySection" class="hidden">
<div class="mb-4">
<label for="rejectionReason" class="block text-sm font-medium text-gray-700 mb-2">
Ablehnungsgrund <span class="text-red-500">*</span>:
</label>
<textarea id="rejectionReason" rows="4" required
class="form-textarea w-full rounded-md border-gray-300"
placeholder="Bitte geben Sie einen detaillierten Grund für die Ablehnung an..."></textarea>
</div>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button type="button" onclick="closeActionModal()"
class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400">
<div class="mt-6 text-center">
<button onclick="closeBulkModal()"
class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
Abbrechen
</button>
<button type="submit" id="actionSubmitBtn"
class="px-4 py-2 rounded-md text-white">
<!-- Text wird dynamisch gesetzt -->
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<style>
.badge {
@apply px-2 py-1 rounded-full text-xs font-medium;
}
.badge-pending {
@apply bg-yellow-100 text-yellow-800;
}
.badge-approved {
@apply bg-green-100 text-green-800;
}
.badge-denied {
@apply bg-red-100 text-red-800;
}
.request-card {
@apply border rounded-lg p-4 hover:shadow-md transition-shadow;
}
.request-card.urgent {
@apply border-orange-300 bg-orange-50;
}
.stats-badge {
@apply px-3 py-1 rounded-full text-sm font-medium;
}
</style>
<script>
let currentRequests = [];
let currentPage = 0;
let totalPages = 0;
let searchTimeout;
// Beim Laden der Seite
document.addEventListener('DOMContentLoaded', function() {
loadAvailablePrinters();
refreshRequests();
// Form-Handler
document.getElementById('actionForm').addEventListener('submit', handleActionSubmit);
});
// Anfragen laden
async function loadRequests(status = 'pending', offset = 0, search = '') {
try {
showLoading(true);
const params = new URLSearchParams({
status: status,
limit: 20,
offset: offset
});
const response = await fetch(`/api/admin/requests?${params}`);
const data = await response.json();
if (data.success) {
currentRequests = data.requests;
displayRequests(data.requests, search);
displayStats(data.stats);
displayPagination(data.pagination);
} else {
showError('Fehler beim Laden der Anfragen: ' + data.error);
}
} catch (error) {
showError('Fehler beim Laden der Anfragen: ' + error.message);
} finally {
showLoading(false);
// Initialize live time display
function updateLiveTime() {
const timeElement = document.getElementById('live-time');
if (timeElement) {
timeElement.textContent = new Date().toLocaleTimeString('de-DE');
}
}
// Anfragen anzeigen
function displayRequests(requests, search = '') {
const container = document.getElementById('requestsContainer');
// Filtern nach Suchbegriff
let filteredRequests = requests;
if (search) {
const searchLower = search.toLowerCase();
filteredRequests = requests.filter(req =>
req.name.toLowerCase().includes(searchLower) ||
(req.email && req.email.toLowerCase().includes(searchLower)) ||
(req.reason && req.reason.toLowerCase().includes(searchLower))
);
}
if (filteredRequests.length === 0) {
container.innerHTML = `
<div class="text-center py-8 text-gray-500">
<i class="fas fa-inbox text-4xl mb-4"></i>
<p class="text-lg">Keine Anfragen gefunden</p>
<p class="text-sm">Versuchen Sie einen anderen Filter oder Suchbegriff</p>
</div>
`;
return;
}
container.innerHTML = filteredRequests.map(req => createRequestCard(req)).join('');
}
// Anfrage-Karte erstellen
function createRequestCard(request) {
const urgentClass = request.time_since_creation > 24 ? 'urgent' : '';
const statusBadge = getStatusBadge(request.status);
return `
<div class="request-card ${urgentClass}" data-request-id="${request.id}">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center space-x-3 mb-2">
<h3 class="text-lg font-semibold text-gray-800">${escapeHtml(request.name)}</h3>
${statusBadge}
${request.time_since_creation > 24 ? '<span class="badge bg-orange-100 text-orange-800">Dringend</span>' : ''}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600">
<div>
<p><i class="fas fa-envelope mr-2"></i>${escapeHtml(request.email || 'Keine E-Mail')}</p>
<p><i class="fas fa-clock mr-2"></i>${request.duration_min} Minuten</p>
<p><i class="fas fa-calendar mr-2"></i>${formatDateTime(request.created_at)}</p>
</div>
<div>
<p><i class="fas fa-print mr-2"></i>${request.printer ? escapeHtml(request.printer.name) : 'Kein Drucker'}</p>
${request.processed_by_user ? `<p><i class="fas fa-user mr-2"></i>Bearbeitet von: ${escapeHtml(request.processed_by_user.name)}</p>` : ''}
${request.processed_at ? `<p><i class="fas fa-check mr-2"></i>${formatDateTime(request.processed_at)}</p>` : ''}
</div>
</div>
${request.reason ? `<p class="mt-2 text-sm text-gray-700 bg-gray-50 p-2 rounded">${escapeHtml(request.reason)}</p>` : ''}
${request.approval_notes ? `<p class="mt-2 text-sm text-green-700 bg-green-50 p-2 rounded"><strong>Genehmigungsnotizen:</strong> ${escapeHtml(request.approval_notes)}</p>` : ''}
${request.rejection_reason ? `<p class="mt-2 text-sm text-red-700 bg-red-50 p-2 rounded"><strong>Ablehnungsgrund:</strong> ${escapeHtml(request.rejection_reason)}</p>` : ''}
</div>
<div class="flex flex-col space-y-2 ml-4">
<button onclick="showRequestDetails(${request.id})"
class="btn bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm">
<i class="fas fa-eye mr-1"></i>Details
</button>
${request.can_be_processed ? `
<button onclick="approveRequest(${request.id})"
class="btn bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm">
<i class="fas fa-check mr-1"></i>Genehmigen
</button>
<button onclick="denyRequest(${request.id})"
class="btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm">
<i class="fas fa-times mr-1"></i>Ablehnen
</button>
` : ''}
</div>
</div>
</div>
`;
}
// Status-Badge erstellen
function getStatusBadge(status) {
const badges = {
'pending': '<span class="badge badge-pending">Wartend</span>',
'approved': '<span class="badge badge-approved">Genehmigt</span>',
'denied': '<span class="badge badge-denied">Abgelehnt</span>'
};
return badges[status] || `<span class="badge">${status}</span>`;
}
// Statistiken anzeigen
function displayStats(stats) {
const container = document.getElementById('statsContainer');
container.innerHTML = `
<span class="stats-badge bg-gray-100 text-gray-800">
Gesamt: ${stats.total}
</span>
<span class="stats-badge bg-yellow-100 text-yellow-800">
Wartend: ${stats.pending}
</span>
<span class="stats-badge bg-green-100 text-green-800">
Genehmigt: ${stats.approved}
</span>
<span class="stats-badge bg-red-100 text-red-800">
Abgelehnt: ${stats.denied}
</span>
`;
}
// Pagination anzeigen
function displayPagination(pagination) {
const container = document.getElementById('paginationContainer');
if (pagination.total <= pagination.limit) {
container.innerHTML = '';
return;
}
const totalPages = Math.ceil(pagination.total / pagination.limit);
const currentPage = Math.floor(pagination.offset / pagination.limit);
let html = '<div class="flex space-x-2">';
// Vorherige Seite
if (currentPage > 0) {
html += `<button onclick="changePage(${currentPage - 1})" class="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">Zurück</button>`;
}
// Seitenzahlen
for (let i = Math.max(0, currentPage - 2); i <= Math.min(totalPages - 1, currentPage + 2); i++) {
const active = i === currentPage ? 'bg-blue-600 text-white' : 'bg-gray-200 hover:bg-gray-300';
html += `<button onclick="changePage(${i})" class="px-3 py-1 rounded ${active}">${i + 1}</button>`;
}
// Nächste Seite
if (pagination.has_more) {
html += `<button onclick="changePage(${currentPage + 1})" class="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">Weiter</button>`;
}
html += '</div>';
container.innerHTML = html;
}
// Verfügbare Drucker laden
async function loadAvailablePrinters() {
try {
const response = await fetch('/api/admin/requests/1'); // Dummy-Request für Drucker-Liste
const data = await response.json();
if (data.success && data.request.available_printers) {
const select = document.getElementById('printerSelect');
select.innerHTML = '<option value="">Drucker auswählen...</option>' +
data.request.available_printers.map(printer =>
`<option value="${printer.id}">${escapeHtml(printer.name)} (${escapeHtml(printer.location || 'Unbekannt')})</option>`
).join('');
}
} catch (error) {
console.error('Fehler beim Laden der Drucker:', error);
}
}
// Anfrage genehmigen
function approveRequest(requestId) {
document.getElementById('actionRequestId').value = requestId;
document.getElementById('actionType').value = 'approve';
document.getElementById('actionModalTitle').textContent = 'Anfrage genehmigen';
document.getElementById('approveSection').classList.remove('hidden');
document.getElementById('denySection').classList.add('hidden');
document.getElementById('actionSubmitBtn').textContent = 'Genehmigen';
document.getElementById('actionSubmitBtn').className = 'px-4 py-2 rounded-md text-white bg-green-600 hover:bg-green-700';
// Aktueller Drucker vorauswählen
const request = currentRequests.find(r => r.id === requestId);
if (request && request.printer_id) {
document.getElementById('printerSelect').value = request.printer_id;
}
document.getElementById('actionModal').classList.remove('hidden');
}
// Anfrage ablehnen
function denyRequest(requestId) {
document.getElementById('actionRequestId').value = requestId;
document.getElementById('actionType').value = 'deny';
document.getElementById('actionModalTitle').textContent = 'Anfrage ablehnen';
document.getElementById('approveSection').classList.add('hidden');
document.getElementById('denySection').classList.remove('hidden');
document.getElementById('actionSubmitBtn').textContent = 'Ablehnen';
document.getElementById('actionSubmitBtn').className = 'px-4 py-2 rounded-md text-white bg-red-600 hover:bg-red-700';
document.getElementById('actionModal').classList.remove('hidden');
}
// Aktions-Form verarbeiten
async function handleActionSubmit(event) {
event.preventDefault();
const requestId = document.getElementById('actionRequestId').value;
const actionType = document.getElementById('actionType').value;
try {
let url, data;
if (actionType === 'approve') {
url = `/api/requests/${requestId}/approve`;
data = {
printer_id: document.getElementById('printerSelect').value || null,
notes: document.getElementById('approvalNotes').value
};
} else {
url = `/api/requests/${requestId}/deny`;
data = {
reason: document.getElementById('rejectionReason').value
};
}
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showSuccess(`Anfrage erfolgreich ${actionType === 'approve' ? 'genehmigt' : 'abgelehnt'}`);
closeActionModal();
refreshRequests();
} else {
showError('Fehler: ' + result.error);
}
} catch (error) {
showError('Fehler beim Verarbeiten der Anfrage: ' + error.message);
}
}
// Details anzeigen
async function showRequestDetails(requestId) {
try {
const response = await fetch(`/api/admin/requests/${requestId}`);
const data = await response.json();
if (data.success) {
displayRequestDetails(data.request);
document.getElementById('detailModal').classList.remove('hidden');
} else {
showError('Fehler beim Laden der Details: ' + data.error);
}
} catch (error) {
showError('Fehler beim Laden der Details: ' + error.message);
}
}
// Request-Details anzeigen
function displayRequestDetails(request) {
const content = document.getElementById('modalContent');
content.innerHTML = `
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 class="font-semibold text-gray-800 mb-2">Antragsteller</h4>
<p><strong>Name:</strong> ${escapeHtml(request.name)}</p>
<p><strong>E-Mail:</strong> ${escapeHtml(request.email || 'Nicht angegeben')}</p>
<p><strong>IP-Adresse:</strong> ${escapeHtml(request.author_ip || 'Unbekannt')}</p>
</div>
<div>
<h4 class="font-semibold text-gray-800 mb-2">Anfrage-Details</h4>
<p><strong>Status:</strong> ${getStatusBadge(request.status)}</p>
<p><strong>Dauer:</strong> ${request.duration_min} Minuten</p>
<p><strong>Erstellt:</strong> ${formatDateTime(request.created_at)}</p>
</div>
</div>
${request.reason ? `
<div>
<h4 class="font-semibold text-gray-800 mb-2">Begründung</h4>
<p class="bg-gray-50 p-3 rounded">${escapeHtml(request.reason)}</p>
</div>
` : ''}
<div>
<h4 class="font-semibold text-gray-800 mb-2">Drucker</h4>
<p>${request.printer ? escapeHtml(request.printer.name) + ' (' + escapeHtml(request.printer.location || 'Unbekannt') + ')' : 'Kein Drucker zugewiesen'}</p>
</div>
${request.processed_by_user ? `
<div>
<h4 class="font-semibold text-gray-800 mb-2">Bearbeitung</h4>
<p><strong>Bearbeitet von:</strong> ${escapeHtml(request.processed_by_user.name)}</p>
<p><strong>Bearbeitet am:</strong> ${formatDateTime(request.processed_at)}</p>
</div>
` : ''}
${request.approval_notes ? `
<div>
<h4 class="font-semibold text-green-700 mb-2">Genehmigungsnotizen</h4>
<p class="bg-green-50 p-3 rounded text-green-800">${escapeHtml(request.approval_notes)}</p>
</div>
` : ''}
${request.rejection_reason ? `
<div>
<h4 class="font-semibold text-red-700 mb-2">Ablehnungsgrund</h4>
<p class="bg-red-50 p-3 rounded text-red-800">${escapeHtml(request.rejection_reason)}</p>
</div>
` : ''}
${request.job_details ? `
<div>
<h4 class="font-semibold text-gray-800 mb-2">Job-Details</h4>
<p><strong>Job-ID:</strong> ${request.job_details.id}</p>
<p><strong>Status:</strong> ${escapeHtml(request.job_details.status)}</p>
<p><strong>Geplanter Start:</strong> ${formatDateTime(request.job_details.start_at)}</p>
<p><strong>Geplantes Ende:</strong> ${formatDateTime(request.job_details.end_at)}</p>
${request.job_details.is_overdue ? '<p class="text-red-600"><strong>⚠️ Überfällig</strong></p>' : ''}
</div>
` : ''}
</div>
`;
}
// Hilfsfunktionen
function refreshRequests() {
const status = document.getElementById('statusFilter').value;
const search = document.getElementById('searchInput').value;
loadRequests(status, 0, search);
}
function filterRequests() {
refreshRequests();
}
function debounceSearch() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
refreshRequests();
}, 500);
}
function changePage(page) {
const status = document.getElementById('statusFilter').value;
const search = document.getElementById('searchInput').value;
loadRequests(status, page * 20, search);
}
function closeDetailModal() {
document.getElementById('detailModal').classList.add('hidden');
}
function closeActionModal() {
document.getElementById('actionModal').classList.add('hidden');
document.getElementById('actionForm').reset();
}
function showLoading(show) {
document.getElementById('loadingSpinner').classList.toggle('hidden', !show);
document.getElementById('requestsContainer').classList.toggle('hidden', show);
}
function showSuccess(message) {
// Einfache Erfolgs-Anzeige
alert('✅ ' + message);
}
function showError(message) {
// Einfache Fehler-Anzeige
alert('❌ ' + message);
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text ? String(text).replace(/[&<>"']/g, function(m) { return map[m]; }) : '';
}
function formatDateTime(dateString) {
if (!dateString) return 'Unbekannt';
const date = new Date(dateString);
return date.toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// Update time every second
setInterval(updateLiveTime, 1000);
updateLiveTime();
</script>
{% endblock %}

View File

@ -0,0 +1,243 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Test-Skript für Button-Funktionalitäten
Testet alle Buttons aus dem Selenium-Test auf echte Funktionalität
"""
import requests
import time
import json
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class ButtonFunctionalityTester:
def __init__(self, base_url="http://127.0.0.1:5000"):
self.base_url = base_url
self.session = requests.Session()
self.driver = None
def setup_driver(self):
"""Selenium WebDriver einrichten"""
try:
self.driver = webdriver.Chrome()
self.driver.set_window_size(1696, 1066)
print("✅ WebDriver erfolgreich initialisiert")
except Exception as e:
print(f"❌ Fehler beim Initialisieren des WebDrivers: {e}")
def login(self, username="admin", password="admin"):
"""Anmeldung durchführen"""
try:
self.driver.get(f"{self.base_url}/auth/login")
# Warten bis Login-Formular geladen ist
username_field = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.NAME, "username"))
)
username_field.send_keys(username)
self.driver.find_element(By.NAME, "password").send_keys(password)
self.driver.find_element(By.XPATH, "//button[@type='submit']").click()
# Warten bis Dashboard geladen ist
WebDriverWait(self.driver, 10).until(
EC.url_contains("/dashboard")
)
print("✅ Erfolgreich angemeldet")
return True
except Exception as e:
print(f"❌ Fehler bei der Anmeldung: {e}")
return False
def test_button_functionality(self, button_selector, button_name, expected_action=""):
"""Teste einen einzelnen Button auf Funktionalität"""
try:
print(f"\n🔍 Teste Button: {button_name} ({button_selector})")
# Button finden
button = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, button_selector))
)
# Ursprünglichen Zustand erfassen
original_url = self.driver.current_url
original_text = button.text if button.text else "Kein Text"
print(f" 📍 Button gefunden: '{original_text}'")
# Button klicken
button.click()
print(f" 👆 Button geklickt")
# Kurz warten für Reaktion
time.sleep(1)
# Reaktion prüfen
reactions = []
# URL-Änderung prüfen
if self.driver.current_url != original_url:
reactions.append(f"URL-Änderung: {self.driver.current_url}")
# Modal-Fenster prüfen
try:
modal = self.driver.find_element(By.CSS_SELECTOR, ".fixed.inset-0")
if modal.is_displayed():
reactions.append("Modal-Fenster geöffnet")
except:
pass
# Loading-Spinner prüfen
try:
spinner = self.driver.find_element(By.CSS_SELECTOR, ".animate-spin")
if spinner.is_displayed():
reactions.append("Loading-Animation aktiv")
except:
pass
# Toast-Benachrichtigung prüfen
try:
toast = self.driver.find_element(By.CSS_SELECTOR, ".fixed.top-4.right-4")
if toast.is_displayed():
reactions.append(f"Toast-Nachricht: {toast.text}")
except:
pass
# Button-Text-Änderung prüfen
new_text = button.text if button.text else "Kein Text"
if new_text != original_text:
reactions.append(f"Text-Änderung: '{original_text}''{new_text}'")
# Ergebnis ausgeben
if reactions:
print(f" ✅ Reaktionen gefunden:")
for reaction in reactions:
print(f" - {reaction}")
return True
else:
print(f" ⚠️ Keine sichtbare Reaktion erkannt")
return False
except Exception as e:
print(f" ❌ Fehler beim Testen: {e}")
return False
def test_all_buttons(self):
"""Teste alle Buttons aus dem Selenium-Test"""
if not self.setup_driver():
return
if not self.login():
return
# Button-Test-Plan basierend auf Selenium-Test
button_tests = [
# Dashboard-Seite (Startseite)
{
"page": f"{self.base_url}/",
"buttons": [
(".mb-8 > .btn-primary", "Haupt-CTA Button"),
(".btn-primary > span", "CTA Button Span")
]
},
# Dashboard-Seite
{
"page": f"{self.base_url}/dashboard",
"buttons": [
("#refreshDashboard > span", "Dashboard Aktualisieren")
]
},
# Drucker-Seite
{
"page": f"{self.base_url}/printers",
"buttons": [
("#refresh-button > span", "Drucker Aktualisieren"),
("#maintenance-toggle > span", "Wartungsmodus Toggle")
]
},
# Jobs-Seite
{
"page": f"{self.base_url}/jobs",
"buttons": [
("#batch-toggle > span", "Mehrfachauswahl Toggle"),
("#refresh-button > span", "Jobs Aktualisieren")
]
},
# Admin-Seite
{
"page": f"{self.base_url}/admin",
"buttons": [
("#analytics-btn", "Analytics Button"),
("#maintenance-btn", "Wartung Button"),
("#system-status-btn", "System Status Button"),
("#add-user-btn", "Benutzer hinzufügen")
]
}
]
results = {"total": 0, "working": 0, "broken": 0}
print("🚀 Starte umfassenden Button-Funktionalitäts-Test...\n")
for test_group in button_tests:
print(f"📄 Navigiere zu Seite: {test_group['page']}")
try:
self.driver.get(test_group["page"])
time.sleep(2) # Seite laden lassen
for selector, name in test_group["buttons"]:
results["total"] += 1
if self.test_button_functionality(selector, name):
results["working"] += 1
else:
results["broken"] += 1
except Exception as e:
print(f"❌ Fehler beim Laden der Seite {test_group['page']}: {e}")
# Zusammenfassung
print(f"\n{'='*60}")
print(f"📊 TEST-ZUSAMMENFASSUNG")
print(f"{'='*60}")
print(f"Getestete Buttons gesamt: {results['total']}")
print(f"✅ Funktional: {results['working']}")
print(f"❌ Nicht funktional: {results['broken']}")
success_rate = (results['working'] / results['total']) * 100 if results['total'] > 0 else 0
print(f"📈 Erfolgsrate: {success_rate:.1f}%")
if success_rate >= 90:
print("🎉 AUSGEZEICHNET! Fast alle Buttons funktionieren korrekt.")
elif success_rate >= 75:
print("✅ GUT! Die meisten Buttons funktionieren korrekt.")
elif success_rate >= 50:
print("⚠️ BEFRIEDIGEND! Einige Buttons benötigen noch Verbesserungen.")
else:
print("❌ VERBESSERUNG ERFORDERLICH! Viele Buttons haben keine Funktionalität.")
def cleanup(self):
"""Aufräumen"""
if self.driver:
self.driver.quit()
print("🧹 WebDriver beendet")
def main():
"""Hauptfunktion"""
tester = ButtonFunctionalityTester()
try:
tester.test_all_buttons()
finally:
tester.cleanup()
if __name__ == "__main__":
main()

Binary file not shown.