"Refactor template files for calendar and guest status"

This commit is contained in:
2025-05-29 15:44:20 +02:00
parent b916cdaca3
commit 1c466b199a
4 changed files with 996 additions and 226 deletions

View File

@@ -77,6 +77,12 @@ try:
except ImportError: except ImportError:
backup_manager = None backup_manager = None
# Import neuer Systeme
from utils.rate_limiter import limit_requests, rate_limiter, cleanup_rate_limiter
from utils.security import init_security, require_secure_headers, security_check
from utils.permissions import init_permission_helpers, require_permission, Permission, check_permission
from utils.analytics import analytics_engine, track_event, get_dashboard_stats
# Flask-App initialisieren # Flask-App initialisieren
app = Flask(__name__) app = Flask(__name__)
app.secret_key = SECRET_KEY app.secret_key = SECRET_KEY
@@ -87,6 +93,15 @@ app.config["WTF_CSRF_ENABLED"] = True
# CSRF-Schutz initialisieren # CSRF-Schutz initialisieren
csrf = CSRFProtect(app) 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+ # CSRF-Error-Handler - Korrigierte Version für Flask-WTF 1.2.1+
@app.errorhandler(CSRFError) @app.errorhandler(CSRFError)
def csrf_error(error): def csrf_error(error):

View File

@@ -10,30 +10,90 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="bg-professional"> <div class="bg-professional" style="background: #f8fafc !important;">
<!-- Dark Mode Override -->
<style>
.dark .bg-professional {
background: #000000 !important;
}
.dark .professional-hero {
background: linear-gradient(135deg, #000000 0%, #1a1a1a 100%) !important;
border-color: #333333 !important;
}
.dark .professional-container {
background: #111111 !important;
border-color: #333333 !important;
}
.dark .mb-glass {
background: rgba(17, 17, 17, 0.95) !important;
border-color: rgba(255, 255, 255, 0.1) !important;
}
.dark .input-professional {
background: #1a1a1a !important;
border-color: #333333 !important;
color: #ffffff !important;
}
.dark .input-professional:focus {
border-color: #3b82f6 !important;
background: #222222 !important;
}
/* FullCalendar Dark Mode Anpassungen */
.dark .fc {
background: #111111 !important;
color: #ffffff !important;
}
.dark .fc-theme-standard td,
.dark .fc-theme-standard th {
border-color: #333333 !important;
}
.dark .fc-button {
background: #1a1a1a !important;
border-color: #333333 !important;
color: #ffffff !important;
}
.dark .fc-button:hover {
background: #333333 !important;
}
.dark .fc-button-primary:not(:disabled).fc-button-active {
background: #3b82f6 !important;
border-color: #3b82f6 !important;
}
.dark .fc-col-header-cell {
background: #1a1a1a !important;
}
.dark .fc-daygrid-day {
background: #111111 !important;
}
.dark .fc-timegrid-slot {
border-color: #333333 !important;
}
.dark .fc-timegrid-axis {
background: #1a1a1a !important;
}
</style>
<!-- Professional Hero Header --> <!-- Professional Hero Header -->
<div class="professional-hero hero-pattern animate-fade-in"> <div class="professional-hero hero-pattern animate-fade-in" style="margin: 2rem; margin-bottom: 3rem;">
<div class="absolute inset-0 bg-gradient-to-r from-black/10 to-black/20 dark:from-black/20 dark:to-black/40"></div> <div class="absolute inset-0 bg-gradient-to-r from-black/10 to-black/20 dark:from-black/40 dark:to-black/60"></div>
<!-- Status Indicator --> <!-- Status Indicator -->
<div class="absolute top-6 right-6 flex items-center space-x-3 z-10"> <div class="absolute top-6 right-6 flex items-center space-x-4 z-10">
<div class="mb-glass rounded-full px-4 py-2 animate-scale-in"> <div class="mb-glass rounded-full px-6 py-3 animate-scale-in">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-3">
<div class="status-dot status-online"></div> <div class="status-dot status-online"></div>
<span class="text-sm font-semibold text-professional-primary">Live</span> <span class="text-sm font-semibold text-professional-primary">Live</span>
</div> </div>
</div> </div>
<div class="mb-glass rounded-full px-4 py-2 animate-scale-in"> <div class="mb-glass rounded-full px-6 py-3 animate-scale-in">
<span id="live-time" class="text-sm font-semibold text-professional-primary"></span> <span id="live-time" class="text-sm font-semibold text-professional-primary"></span>
</div> </div>
</div> </div>
<div class="relative max-w-7xl mx-auto px-6 lg:px-8 py-16 z-10"> <div class="relative max-w-7xl mx-auto px-6 lg:px-8 py-20 z-10">
<div class="text-center animate-slide-up"> <div class="text-center animate-slide-up">
<!-- Mercedes-Benz Logo --> <!-- Mercedes-Benz Logo -->
<div class="inline-flex items-center justify-center w-24 h-24 mb-glass rounded-full mb-8 professional-shadow"> <div class="inline-flex items-center justify-center w-28 h-28 mb-glass rounded-full mb-10 professional-shadow">
<svg class="w-12 h-12 text-professional-primary" viewBox="0 0 80 80" fill="currentColor"> <svg class="w-14 h-14 text-professional-primary" 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 <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 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 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
@@ -43,18 +103,18 @@
</svg> </svg>
</div> </div>
<h1 class="title-professional text-5xl md:text-6xl font-bold mb-6 tracking-tight"> <h1 class="title-professional text-6xl md:text-7xl font-bold mb-8 tracking-tight">
Intelligenter Schichtplan Intelligenter Schichtplan
</h1> </h1>
<p class="subtitle-professional text-xl md:text-2xl max-w-4xl mx-auto leading-relaxed mb-8"> <p class="subtitle-professional text-2xl md:text-3xl max-w-5xl mx-auto leading-relaxed mb-12">
Fortschrittliche Planung und Überwachung Ihrer 3D-Druckprozesse mit Mercedes-Benz Präzision Fortschrittliche Planung und Überwachung Ihrer 3D-Druckprozesse mit Mercedes-Benz Präzision
</p> </p>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex flex-wrap justify-center gap-4"> <div class="flex flex-wrap justify-center gap-6">
{% if can_edit %} {% if can_edit %}
<button onclick="openCreateEventModal()" <button onclick="openCreateEventModal()"
class="btn-professional group"> class="btn-professional group px-8 py-4">
<svg class="w-6 h-6 mr-3 group-hover:rotate-90 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 mr-3 group-hover:rotate-90 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg> </svg>
@@ -62,14 +122,14 @@
</button> </button>
{% endif %} {% endif %}
<button onclick="refreshCalendar()" <button onclick="refreshCalendar()"
class="btn-secondary-professional group"> class="btn-professional group px-8 py-4">
<svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" 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"></path> <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"></path>
</svg> </svg>
<span>Aktualisieren</span> <span>Aktualisieren</span>
</button> </button>
<button onclick="exportCalendar()" <button onclick="exportCalendar()"
class="btn-secondary-professional group"> class="btn-professional group px-8 py-4">
<svg class="w-6 h-6 mr-3 group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 mr-3 group-hover:scale-110 transition-transform duration-300" 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"></path> <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"></path>
</svg> </svg>
@@ -80,27 +140,27 @@
</div> </div>
</div> </div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10"> <div class="max-w-7xl mx-auto px-6 lg:px-8 -mt-12 relative z-10" style="margin-bottom: 4rem;">
<!-- Filter Section --> <!-- Filter Section -->
<div class="professional-container p-8 mb-8 animate-slide-up"> <div class="professional-container animate-slide-up" style="padding: 3rem; border-radius: 2rem; margin-bottom: 3rem;">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-6 lg:space-y-0"> <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-8 lg:space-y-0">
<div class="flex flex-col sm:flex-row sm:items-center sm:space-x-6 space-y-4 sm:space-y-0"> <div class="flex flex-col sm:flex-row sm:items-center sm:space-x-8 space-y-6 sm:space-y-0">
<!-- Drucker Filter --> <!-- Drucker Filter -->
<div class="group"> <div class="group">
<label for="printerFilter" class="block text-sm font-semibold text-professional-primary mb-2"> <label for="printerFilter" class="block text-base font-bold text-professional-primary mb-4">
Drucker auswählen Drucker auswählen
</label> </label>
<div class="relative"> <div class="relative">
<select id="printerFilter" <select id="printerFilter"
class="input-professional appearance-none pr-12 cursor-pointer"> class="input-professional appearance-none pr-12 cursor-pointer w-full text-base py-4 px-5" style="border-radius: 1rem;">
<option value="">Alle Produktionseinheiten</option> <option value="">Alle Produktionseinheiten</option>
{% for printer in printers %} {% for printer in printers %}
<option value="{{ printer.id }}">{{ printer.name }} {% if printer.location %}({{ printer.location }}){% endif %}</option> <option value="{{ printer.id }}">{{ printer.name }} {% if printer.location %}({{ printer.location }}){% endif %}</option>
{% endfor %} {% endfor %}
</select> </select>
<div class="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none"> <div class="absolute inset-y-0 right-0 flex items-center pr-5 pointer-events-none">
<svg class="h-5 w-5 text-professional-muted group-hover:text-professional-primary transition-colors duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-6 w-6 text-professional-muted group-hover:text-professional-primary transition-colors duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg> </svg>
</div> </div>
@@ -109,20 +169,20 @@
<!-- Status Filter --> <!-- Status Filter -->
<div class="group"> <div class="group">
<label for="statusFilter" class="block text-sm font-semibold text-professional-primary mb-2"> <label for="statusFilter" class="block text-base font-bold text-professional-primary mb-4">
Status-Filter Status-Filter
</label> </label>
<div class="relative"> <div class="relative">
<select id="statusFilter" <select id="statusFilter"
class="input-professional appearance-none pr-12 cursor-pointer"> class="input-professional appearance-none pr-12 cursor-pointer w-full text-base py-4 px-5" style="border-radius: 1rem;">
<option value="">Alle Status</option> <option value="">Alle Status</option>
<option value="running">Aktiv</option> <option value="running">Aktiv</option>
<option value="queued">Warteschlange</option> <option value="queued">Warteschlange</option>
<option value="completed">Abgeschlossen</option> <option value="completed">Abgeschlossen</option>
<option value="cancelled">Abgebrochen</option> <option value="cancelled">Abgebrochen</option>
</select> </select>
<div class="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none"> <div class="absolute inset-y-0 right-0 flex items-center pr-5 pointer-events-none">
<svg class="h-5 w-5 text-professional-muted group-hover:text-professional-primary transition-colors duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-6 w-6 text-professional-muted group-hover:text-professional-primary transition-colors duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg> </svg>
</div> </div>
@@ -131,30 +191,30 @@
</div> </div>
<!-- Quick Stats --> <!-- Quick Stats -->
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-6">
<div class="status-professional status-approved"> <div class="status-professional status-approved" style="min-width: 160px; padding: 1.5rem;">
<div class="status-dot status-online"></div> <div class="status-dot status-online"></div>
<span id="active-jobs">0 Aktiv</span> <span id="active-jobs" class="text-lg font-bold">0 Aktiv</span>
</div> </div>
<div class="status-professional" style="background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.3); color: var(--mb-primary);"> <div class="status-professional" style="background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.3); color: var(--mb-primary); min-width: 160px; padding: 1.5rem;">
<div class="w-3 h-3 bg-blue-500 rounded-full"></div> <div class="w-4 h-4 bg-blue-500 rounded-full"></div>
<span id="queued-jobs">0 Wartend</span> <span id="queued-jobs" class="text-lg font-bold">0 Wartend</span>
</div> </div>
<div class="status-professional" style="background: rgba(251, 146, 60, 0.1); border-color: rgba(251, 146, 60, 0.3); color: #ea580c;"> <div class="status-professional" style="background: rgba(251, 146, 60, 0.1); border-color: rgba(251, 146, 60, 0.3); color: #ea580c; min-width: 160px; padding: 1.5rem;">
<div class="w-3 h-3 bg-orange-500 rounded-full"></div> <div class="w-4 h-4 bg-orange-500 rounded-full"></div>
<span id="total-time">0h Heute</span> <span id="total-time" class="text-lg font-bold">0h Heute</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Calendar Container --> <!-- Calendar Container -->
<div class="professional-container p-6 animate-slide-up"> <div class="professional-container animate-slide-up" style="padding: 3rem; border-radius: 2rem; margin-bottom: 3rem;">
<div id="calendar"></div> <div id="calendar"></div>
</div> </div>
<!-- Legend Section --> <!-- Legend Section -->
<div class="professional-container p-8 mt-8 animate-slide-up"> <div class="professional-container animate-slide-up" style="padding: 3rem; border-radius: 2rem;">
<div class="text-center mb-6"> <div class="text-center mb-6">
<h3 class="text-xl font-bold text-professional-primary mb-2"> <h3 class="text-xl font-bold text-professional-primary mb-2">
Status-Legende Status-Legende

View File

@@ -9,39 +9,91 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900"> <div class="bg-professional" style="background: #f8fafc !important;">
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <!-- Dark Mode Override -->
<style>
.dark .bg-professional {
background: #000000 !important;
}
.dark .professional-hero {
background: linear-gradient(135deg, #000000 0%, #1a1a1a 100%) !important;
border-color: #333333 !important;
}
.dark .professional-container {
background: #111111 !important;
border-color: #333333 !important;
}
.dark .mb-glass {
background: rgba(17, 17, 17, 0.95) !important;
border-color: rgba(255, 255, 255, 0.1) !important;
}
</style>
<!-- Header --> <!-- Professional Hero Header -->
<div class="mb-8 text-center"> <div class="professional-hero hero-pattern animate-fade-in" style="margin: 2rem; margin-bottom: 3rem;">
<h1 class="text-4xl font-bold text-slate-900 dark:text-white mb-4">Anfrage-Status</h1> <div class="absolute inset-0 bg-gradient-to-r from-black/10 to-black/20 dark:from-black/40 dark:to-black/60"></div>
<p class="text-slate-600 dark:text-slate-400 text-lg">
Status Ihrer Gastanfrage für 3D-Druck <!-- Status Indicator -->
</p> <div class="absolute top-6 right-6 flex items-center space-x-4 z-10">
<div class="mb-glass rounded-full px-6 py-3 animate-scale-in">
<div class="flex items-center space-x-3">
<div class="status-dot status-online"></div>
<span class="text-sm font-semibold text-professional-primary">Live Status</span>
</div>
</div>
<div class="mb-glass rounded-full px-6 py-3 animate-scale-in">
<span id="live-time" class="text-sm font-semibold text-professional-primary"></span>
</div>
</div> </div>
<div class="relative max-w-6xl mx-auto px-6 lg:px-8 py-20 z-10">
<div class="text-center animate-slide-up">
<!-- Mercedes-Benz Logo -->
<div class="inline-flex items-center justify-center w-28 h-28 mb-glass rounded-full mb-10 professional-shadow">
<svg class="w-14 h-14 text-professional-primary" 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="title-professional text-6xl md:text-7xl font-bold mb-8 tracking-tight">
Anfrage-Status
</h1>
<p class="subtitle-professional text-2xl md:text-3xl max-w-5xl mx-auto leading-relaxed mb-12">
Verfolgen Sie den Status Ihrer Gastanfrage für 3D-Druck in Echtzeit
</p>
</div>
</div>
</div>
<div class="max-w-4xl mx-auto px-6 lg:px-8 -mt-12 relative z-10" style="margin-bottom: 4rem;">
<!-- Status Card --> <!-- Status Card -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-8 mb-6"> <div class="professional-container animate-slide-up" style="padding: 3rem; border-radius: 2rem; margin-bottom: 2rem;">
<!-- Status Badge --> <!-- Status Badge -->
<div class="flex justify-center mb-6"> <div class="flex justify-center mb-8">
{% if request.status == 'pending' %} {% if request.status == 'pending' %}
<div class="inline-flex items-center px-6 py-3 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200"> <div class="inline-flex items-center px-8 py-4 rounded-3xl bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-lg font-bold">
<svg class="w-5 h-5 mr-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 mr-3 animate-spin" 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"></path> <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"></path>
</svg> </svg>
Wird geprüft Wird geprüft
</div> </div>
{% elif request.status == 'approved' %} {% elif request.status == 'approved' %}
<div class="inline-flex items-center px-6 py-3 rounded-full bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200"> <div class="inline-flex items-center px-8 py-4 rounded-3xl bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 text-lg font-bold">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg> </svg>
Genehmigt Genehmigt
</div> </div>
{% elif request.status == 'denied' %} {% elif request.status == 'denied' %}
<div class="inline-flex items-center px-6 py-3 rounded-full bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200"> <div class="inline-flex items-center px-8 py-4 rounded-3xl bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 text-lg font-bold">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg> </svg>
Abgelehnt Abgelehnt
@@ -50,94 +102,104 @@
</div> </div>
<!-- Anfrage-Details --> <!-- Anfrage-Details -->
<div class="space-y-4 mb-6"> <div class="space-y-8 mb-10">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="text-center mb-12">
<div> <h2 class="title-professional text-3xl font-bold mb-4">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400">Anfrage-ID</h3> Anfrage Details
<p class="text-lg font-semibold text-slate-900 dark:text-white">#{{ request.id }}</p> </h2>
<p class="subtitle-professional text-lg">
Übersicht Ihrer eingereichten Gastanfrage
</p>
</div> </div>
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400">Erstellt am</h3> <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<p class="text-lg font-semibold text-slate-900 dark:text-white">{{ request.created_at|format_datetime }}</p> <div class="mb-glass p-6 rounded-2xl">
<h3 class="text-base font-bold text-professional-primary mb-3">Anfrage-ID</h3>
<p class="text-2xl font-bold text-professional-accent">#{{ request.id }}</p>
</div> </div>
<div> <div class="mb-glass p-6 rounded-2xl">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400">Name</h3> <h3 class="text-base font-bold text-professional-primary mb-3">Erstellt am</h3>
<p class="text-lg font-semibold text-slate-900 dark:text-white">{{ request.name }}</p> <p class="text-2xl font-bold text-professional-primary">{{ request.created_at|format_datetime }}</p>
</div> </div>
<div> <div class="mb-glass p-6 rounded-2xl">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400">Gewünschte Dauer</h3> <h3 class="text-base font-bold text-professional-primary mb-3">Name</h3>
<p class="text-lg font-semibold text-slate-900 dark:text-white">{{ request.duration_min }} Minuten</p> <p class="text-2xl font-bold text-professional-primary">{{ request.name }}</p>
</div>
<div class="mb-glass p-6 rounded-2xl">
<h3 class="text-base font-bold text-professional-primary mb-3">Gewünschte Dauer</h3>
<p class="text-2xl font-bold text-professional-accent">{{ request.duration_min }} Minuten</p>
</div> </div>
</div> </div>
{% if request.reason %} {% if request.reason %}
<div> <div class="mb-glass p-6 rounded-2xl">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400">Begründung</h3> <h3 class="text-base font-bold text-professional-primary mb-4">Begründung</h3>
<p class="text-slate-900 dark:text-white mt-1">{{ request.reason }}</p> <p class="text-professional-secondary text-lg leading-relaxed">{{ request.reason }}</p>
</div> </div>
{% endif %} {% endif %}
{% if request.printer %} {% if request.printer %}
<div> <div class="mb-glass p-6 rounded-2xl">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400">Drucker</h3> <h3 class="text-base font-bold text-professional-primary mb-4">Drucker</h3>
<p class="text-slate-900 dark:text-white mt-1">{{ request.printer.name }} {% if request.printer.location %}({{ request.printer.location }}){% endif %}</p> <p class="text-professional-secondary text-lg">{{ request.printer.name }} {% if request.printer.location %}({{ request.printer.location }}){% endif %}</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Status-spezifische Inhalte --> <!-- Status-spezifische Inhalte -->
{% if request.status == 'pending' %} {% if request.status == 'pending' %}
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-xl p-4"> <div class="alert-professional alert-warning" style="border-radius: 2rem; padding: 2.5rem;">
<div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"> <div class="w-16 h-16 bg-gradient-to-br from-yellow-500 to-orange-600 rounded-3xl flex items-center justify-center">
<svg class="h-8 w-8 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path> <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg> </svg>
</div> </div>
<div class="ml-3"> </div>
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">Ihre Anfrage wird geprüft</h3> <div class="flex-1 ml-8">
<p class="text-sm text-yellow-700 dark:text-yellow-300 mt-1"> <h3 class="text-2xl font-bold text-professional-primary mb-4">Ihre Anfrage wird geprüft</h3>
Unser Team prüft Ihre Anfrage. Sie erhalten eine Benachrichtigung, sobald eine Entscheidung getroffen wurde. <p class="text-lg text-professional-secondary leading-relaxed">
Unser Team prüft Ihre Anfrage mit höchster Priorität. Sie erhalten eine sofortige Benachrichtigung, sobald eine Entscheidung getroffen wurde.
Diese Seite aktualisiert sich automatisch alle 30 Sekunden. Diese Seite aktualisiert sich automatisch alle 30 Sekunden.
</p> </p>
</div> </div>
</div> </div>
</div>
{% elif request.status == 'approved' %} {% elif request.status == 'approved' %}
<div class="mb-8 p-6 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border border-green-200 dark:border-green-700 rounded-2xl"> <div class="alert-professional alert-success" style="border-radius: 2rem; padding: 2.5rem; margin-bottom: 2rem;">
<div class="flex items-center mb-4"> <div class="flex-shrink-0">
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center mr-4"> <div class="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-3xl flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-8 w-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg> </svg>
</div> </div>
<div> </div>
<h3 class="text-lg font-semibold text-green-800 dark:text-green-200">Anfrage genehmigt!</h3> <div class="flex-1 ml-8">
<p class="text-green-700 dark:text-green-300">Ihr Druckauftrag wurde genehmigt und ist bereit zum Start.</p> <h3 class="text-2xl font-bold text-professional-primary mb-4">Anfrage genehmigt!</h3>
<p class="text-lg text-professional-secondary leading-relaxed">Ihr Druckauftrag wurde genehmigt und ist bereit zum Start.</p>
</div> </div>
</div> </div>
{% if otp_code %} {% if otp_code %}
<!-- Code-Anzeige --> <!-- Code-Anzeige -->
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 mb-6 border border-green-200 dark:border-green-700"> <div class="professional-container" style="padding: 2.5rem; border-radius: 2rem; margin-bottom: 2rem;">
<h4 class="text-lg font-semibold text-slate-900 dark:text-white mb-4 text-center">Ihr Zugangscode</h4> <h4 class="text-2xl font-bold text-professional-primary mb-8 text-center">Ihr Zugangscode</h4>
<!-- 6-stelliger Code in schönen Boxen --> <!-- 6-stelliger Code in schönen Boxen -->
<div class="flex justify-center space-x-3 mb-4"> <div class="flex justify-center space-x-4 mb-8">
{% for char in otp_code %} {% for char in otp_code %}
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 text-white rounded-xl flex items-center justify-center text-2xl font-bold shadow-lg"> <div class="w-20 h-20 bg-gradient-to-br from-blue-500 to-indigo-600 text-white rounded-2xl flex items-center justify-center text-3xl font-bold shadow-lg transform hover:scale-105 transition-transform duration-300">
{{ char }} {{ char }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<!-- Code zum Kopieren --> <!-- Code zum Kopieren -->
<div class="text-center mb-4"> <div class="text-center mb-8">
<div class="inline-flex items-center bg-slate-100 dark:bg-slate-700 rounded-lg px-4 py-2"> <div class="inline-flex items-center mb-glass rounded-2xl px-6 py-4">
<span class="text-lg font-mono font-bold text-slate-900 dark:text-white mr-3" id="otpCode">{{ otp_code }}</span> <span class="text-2xl font-mono font-bold text-professional-primary mr-4" id="otpCode">{{ otp_code }}</span>
<button onclick="copyCode()" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" title="Code kopieren"> <button onclick="copyCode()" class="text-professional-accent hover:text-professional-primary transition-colors duration-200" title="Code kopieren">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg> </svg>
</button> </button>
@@ -145,29 +207,29 @@
</div> </div>
<!-- Wichtige Hinweise --> <!-- Wichtige Hinweise -->
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg p-4 mb-4"> <div class="alert-professional alert-warning" style="border-radius: 1.5rem; padding: 2rem; margin-bottom: 2rem;">
<div class="flex items-start"> <div class="flex-shrink-0">
<div class="w-6 h-6 bg-amber-500 rounded-full flex items-center justify-center mr-3 mt-0.5"> <div class="w-12 h-12 bg-gradient-to-br from-amber-500 to-orange-600 rounded-2xl flex items-center justify-center">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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 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 0L3.734 16.5c-.77.833.192 2.5 1.732 2.5z"/> <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 0L3.734 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg> </svg>
</div> </div>
<div> </div>
<h5 class="font-semibold text-amber-800 dark:text-amber-200 mb-2">Wichtige Hinweise:</h5> <div class="flex-1 ml-6">
<ul class="text-sm text-amber-700 dark:text-amber-300 space-y-1"> <h5 class="text-xl font-bold text-professional-primary mb-4">Wichtige Hinweise:</h5>
<ul class="text-base text-professional-secondary space-y-2">
<li>• Dieser Code ist nur <strong>einmalig verwendbar</strong></li> <li>• Dieser Code ist nur <strong>einmalig verwendbar</strong></li>
<li>• Notieren Sie sich den Code oder speichern Sie diese Seite</li> <li>• Notieren Sie sich den Code oder speichern Sie diese Seite</li>
<li>• Bei Verlust des Codes kontaktieren Sie den Administrator</li> <li>• Bei Verlust des Codes kontaktieren Sie den Administrator</li>
</ul> </ul>
</div> </div>
</div> </div>
</div>
<!-- Start-Button --> <!-- Start-Button -->
<div class="text-center"> <div class="text-center">
<a href="{{ url_for('guest.guest_start_job_form') }}" <a href="{{ url_for('guest.guest_start_job_form') }}"
class="inline-flex items-center px-8 py-4 bg-gradient-to-r from-green-600 to-emerald-600 text-white font-semibold rounded-2xl hover:from-green-700 hover:to-emerald-700 transform transition-all duration-300 hover:scale-105 shadow-lg"> class="btn-success-professional group px-10 py-5 text-lg">
<svg class="w-6 h-6 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 mr-4 group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M12 5v.01M12 19v.01M12 12h.01M12 9a3 3 0 100-6 3 3 0 000 6zm0 0a3 3 0 100 6 3 3 0 000-6z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M12 5v.01M12 19v.01M12 12h.01M12 9a3 3 0 100-6 3 3 0 000 6zm0 0a3 3 0 100 6 3 3 0 000-6z"/>
</svg> </svg>
Job jetzt starten Job jetzt starten
@@ -176,19 +238,19 @@
</div> </div>
{% elif show_start_link %} {% elif show_start_link %}
<!-- Code bereits vorhanden, aber noch nicht verwendet --> <!-- Code bereits vorhanden, aber noch nicht verwendet -->
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 border border-green-200 dark:border-green-700"> <div class="professional-container" style="padding: 2.5rem; border-radius: 2rem;">
<div class="text-center"> <div class="text-center">
<div class="w-16 h-16 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full flex items-center justify-center mx-auto mb-4"> <div class="w-20 h-20 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/>
</svg> </svg>
</div> </div>
<h4 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Zugangscode bereits generiert</h4> <h4 class="text-2xl font-bold text-professional-primary mb-4">Zugangscode bereits generiert</h4>
<p class="text-slate-600 dark:text-slate-400 mb-6">Ihr persönlicher Code wurde bereits erstellt und ist bereit zur Verwendung.</p> <p class="text-lg text-professional-secondary mb-8">Ihr persönlicher Code wurde bereits erstellt und ist bereit zur Verwendung.</p>
<a href="{{ url_for('guest.guest_start_job_form') }}" <a href="{{ url_for('guest.guest_start_job_form') }}"
class="inline-flex items-center px-8 py-4 bg-gradient-to-r from-blue-600 to-indigo-600 text-white font-semibold rounded-2xl hover:from-blue-700 hover:to-indigo-700 transform transition-all duration-300 hover:scale-105 shadow-lg"> class="btn-professional group px-10 py-5 text-lg">
<svg class="w-6 h-6 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 mr-4 group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/>
</svg> </svg>
Code eingeben und starten Code eingeben und starten
@@ -196,89 +258,56 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div>
{% elif request.status == 'denied' %} {% elif request.status == 'denied' %}
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 rounded-xl p-4"> <div class="alert-professional alert-danger" style="border-radius: 2rem; padding: 2.5rem;">
<div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20"> <div class="w-16 h-16 bg-gradient-to-br from-red-500 to-red-600 rounded-3xl flex items-center justify-center">
<svg class="h-8 w-8 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
</svg> </svg>
</div> </div>
<div class="ml-3"> </div>
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">Anfrage abgelehnt</h3> <div class="flex-1 ml-8">
<p class="text-sm text-red-700 dark:text-red-300 mt-1"> <h3 class="text-2xl font-bold text-professional-primary mb-4">Anfrage abgelehnt</h3>
<p class="text-lg text-professional-secondary leading-relaxed">
Ihre Anfrage wurde leider abgelehnt. Bei Fragen wenden Sie sich bitte an unser Team. Ihre Anfrage wurde leider abgelehnt. Bei Fragen wenden Sie sich bitte an unser Team.
</p> </p>
</div> </div>
</div> </div>
</div>
{% endif %} {% endif %}
</div> </div>
<!-- Aktionen --> <!-- Aktionen -->
<div class="flex items-center justify-center space-x-4"> <div class="flex items-center justify-center space-x-6">
<a href="{{ url_for('guest.guest_request_form') }}" <a href="{{ url_for('guest.guest_request_form') }}"
class="px-6 py-3 border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-all duration-300"> class="btn-professional group px-8 py-4">
<svg class="w-6 h-6 mr-3 group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
Neue Anfrage stellen Neue Anfrage stellen
</a> </a>
<button onclick="location.reload()" <button onclick="location.reload()"
class="px-6 py-3 bg-blue-500 text-white rounded-xl hover:bg-blue-600 transition-all duration-300"> class="btn-professional group px-8 py-4">
<svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" 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 Aktualisieren
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{% if request.status == 'approved' and otp_code and job %}
<script> <script>
async function startJob() { // Live Time Update
const btn = document.getElementById('startJobBtn'); function updateLiveTime() {
const originalText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Wird gestartet...';
try {
const response = await fetch(`/api/jobs/start/{{ otp_code }}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (result.success) {
alert('3D-Druck erfolgreich gestartet!');
location.reload();
} else {
alert('Fehler beim Starten: ' + (result.error || 'Unbekannter Fehler'));
}
} catch (error) {
console.error('Fehler beim Starten des Jobs:', error);
alert('Fehler beim Starten des 3D-Drucks. Bitte versuchen Sie es später erneut.');
} finally {
btn.disabled = false;
btn.textContent = originalText;
}
}
</script>
{% endif %}
<script>
document.addEventListener('DOMContentLoaded', function() {
updateDateTime();
setInterval(updateDateTime, 1000);
});
function updateDateTime() {
const now = new Date(); const now = new Date();
document.getElementById('current-time').textContent = now.toLocaleTimeString('de-DE'); document.getElementById('live-time').textContent = now.toLocaleTimeString('de-DE');
} }
updateLiveTime();
setInterval(updateLiveTime, 1000);
// Code-Kopier-Funktion für genehmigte Anfragen // Code-Kopier-Funktion
function copyCode() { function copyCode() {
const codeElement = document.getElementById('otpCode'); const codeElement = document.getElementById('otpCode');
if (codeElement) { if (codeElement) {
@@ -326,7 +355,7 @@ function showCopySuccess() {
const originalHTML = button.innerHTML; const originalHTML = button.innerHTML;
button.innerHTML = ` button.innerHTML = `
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg> </svg>
`; `;

View File

@@ -1 +1,667 @@
#!/usr/bin/env python3
"""
Erweiterte Analytik und Statistiken für MYP Platform
Umfassende Datenanalyse, Berichte und KPI-Tracking
"""
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Any
from sqlalchemy import func, desc, and_, or_, extract
from sqlalchemy.orm import Session
from dataclasses import dataclass, asdict
from enum import Enum
from utils.logging_config import get_logger
logger = get_logger("analytics")
# ===== ANALYTICS ENUMS =====
class MetricType(Enum):
"""Typen von Metriken"""
COUNTER = "counter" # Zähler (erhöht sich)
GAUGE = "gauge" # Momentanwert
HISTOGRAM = "histogram" # Verteilung von Werten
RATE = "rate" # Rate über Zeit
class TimeRange(Enum):
"""Zeiträume für Analysen"""
HOUR = "hour"
DAY = "day"
WEEK = "week"
MONTH = "month"
QUARTER = "quarter"
YEAR = "year"
CUSTOM = "custom"
class ReportFormat(Enum):
"""Ausgabeformate für Berichte"""
JSON = "json"
CSV = "csv"
PDF = "pdf"
EXCEL = "excel"
# ===== DATA CLASSES =====
@dataclass
class Metric:
"""Einzelne Metrik"""
name: str
value: float
unit: str
timestamp: datetime
tags: Dict[str, str] = None
def to_dict(self) -> Dict:
result = asdict(self)
result['timestamp'] = self.timestamp.isoformat()
return result
@dataclass
class AnalyticsData:
"""Container für Analytik-Daten"""
metrics: List[Metric]
timerange: TimeRange
start_date: datetime
end_date: datetime
filters: Dict[str, Any] = None
def to_dict(self) -> Dict:
return {
'metrics': [m.to_dict() for m in self.metrics],
'timerange': self.timerange.value,
'start_date': self.start_date.isoformat(),
'end_date': self.end_date.isoformat(),
'filters': self.filters or {}
}
@dataclass
class KPI:
"""Key Performance Indicator"""
name: str
current_value: float
previous_value: float
target_value: float
unit: str
trend: str # "up", "down", "stable"
change_percent: float
def to_dict(self) -> Dict:
return asdict(self)
# ===== ANALYTICS ENGINE =====
class AnalyticsEngine:
"""Hauptklasse für Analytik und Statistiken"""
def __init__(self):
self.cache = {}
self.cache_timeout = timedelta(minutes=10)
def get_printer_statistics(self, time_range: TimeRange = TimeRange.MONTH,
start_date: datetime = None, end_date: datetime = None) -> Dict:
"""
Drucker-Statistiken abrufen
Args:
time_range: Zeitraum für Analyse
start_date: Startdatum (optional)
end_date: Enddatum (optional)
Returns:
Dict: Drucker-Statistiken
"""
try:
from models import get_db_session, Printer, Job
if not start_date or not end_date:
start_date, end_date = self._get_date_range(time_range)
db_session = get_db_session()
# Basis-Statistiken
total_printers = db_session.query(Printer).filter(Printer.active == True).count()
online_printers = db_session.query(Printer).filter(
and_(Printer.active == True, Printer.status.in_(["online", "idle"]))
).count()
# Auslastung nach Druckern
printer_usage = db_session.query(
Printer.name,
func.count(Job.id).label('job_count'),
func.sum(Job.duration_minutes).label('total_duration')
).outerjoin(Job, and_(
Job.printer_id == Printer.id,
Job.created_at.between(start_date, end_date)
)).group_by(Printer.id, Printer.name).all()
# Status-Verteilung
status_distribution = db_session.query(
Printer.status,
func.count(Printer.id).label('count')
).filter(Printer.active == True).group_by(Printer.status).all()
# Durchschnittliche Verfügbarkeit
availability_stats = self._calculate_printer_availability(db_session, start_date, end_date)
db_session.close()
return {
'summary': {
'total_printers': total_printers,
'online_printers': online_printers,
'availability_rate': round((online_printers / total_printers * 100) if total_printers > 0 else 0, 1)
},
'usage_by_printer': [
{
'name': usage.name,
'jobs': usage.job_count or 0,
'total_hours': round((usage.total_duration or 0) / 60, 1),
'utilization_rate': self._calculate_utilization_rate(usage.total_duration, start_date, end_date)
}
for usage in printer_usage
],
'status_distribution': [
{'status': status.status, 'count': status.count}
for status in status_distribution
],
'availability': availability_stats,
'time_range': {
'start': start_date.isoformat(),
'end': end_date.isoformat(),
'type': time_range.value
}
}
except Exception as e:
logger.error(f"Fehler beim Abrufen der Drucker-Statistiken: {e}")
return {'error': str(e)}
def get_job_statistics(self, time_range: TimeRange = TimeRange.MONTH,
start_date: datetime = None, end_date: datetime = None) -> Dict:
"""
Job-Statistiken abrufen
Args:
time_range: Zeitraum für Analyse
start_date: Startdatum (optional)
end_date: Enddatum (optional)
Returns:
Dict: Job-Statistiken
"""
try:
from models import get_db_session, Job, User
if not start_date or not end_date:
start_date, end_date = self._get_date_range(time_range)
db_session = get_db_session()
# Basis-Statistiken
base_query = db_session.query(Job).filter(
Job.created_at.between(start_date, end_date)
)
total_jobs = base_query.count()
completed_jobs = base_query.filter(Job.status == 'completed').count()
failed_jobs = base_query.filter(Job.status == 'failed').count()
cancelled_jobs = base_query.filter(Job.status == 'cancelled').count()
# Status-Verteilung
status_distribution = db_session.query(
Job.status,
func.count(Job.id).label('count')
).filter(
Job.created_at.between(start_date, end_date)
).group_by(Job.status).all()
# Durchschnittliche Job-Dauer
avg_duration = db_session.query(
func.avg(Job.duration_minutes)
).filter(
and_(
Job.created_at.between(start_date, end_date),
Job.status == 'completed'
)
).scalar() or 0
# Top-Benutzer
top_users = db_session.query(
User.username,
User.name,
func.count(Job.id).label('job_count'),
func.sum(Job.duration_minutes).label('total_duration')
).join(Job).filter(
Job.created_at.between(start_date, end_date)
).group_by(User.id, User.username, User.name).order_by(
desc('job_count')
).limit(10).all()
# Jobs über Zeit (täglich)
daily_jobs = self._get_daily_job_trend(db_session, start_date, end_date)
# Material-Verbrauch (falls verfügbar)
material_usage = db_session.query(
func.sum(Job.material_used)
).filter(
and_(
Job.created_at.between(start_date, end_date),
Job.material_used.isnot(None)
)
).scalar() or 0
db_session.close()
success_rate = round((completed_jobs / total_jobs * 100) if total_jobs > 0 else 0, 1)
return {
'summary': {
'total_jobs': total_jobs,
'completed_jobs': completed_jobs,
'failed_jobs': failed_jobs,
'cancelled_jobs': cancelled_jobs,
'success_rate': success_rate,
'avg_duration_hours': round(avg_duration / 60, 1),
'total_material_g': round(material_usage, 1)
},
'status_distribution': [
{'status': status.status, 'count': status.count}
for status in status_distribution
],
'top_users': [
{
'username': user.username,
'name': user.name,
'jobs': user.job_count,
'total_hours': round((user.total_duration or 0) / 60, 1)
}
for user in top_users
],
'daily_trend': daily_jobs,
'time_range': {
'start': start_date.isoformat(),
'end': end_date.isoformat(),
'type': time_range.value
}
}
except Exception as e:
logger.error(f"Fehler beim Abrufen der Job-Statistiken: {e}")
return {'error': str(e)}
def get_user_statistics(self, time_range: TimeRange = TimeRange.MONTH,
start_date: datetime = None, end_date: datetime = None) -> Dict:
"""
Benutzer-Statistiken abrufen
Args:
time_range: Zeitraum für Analyse
start_date: Startdatum (optional)
end_date: Enddatum (optional)
Returns:
Dict: Benutzer-Statistiken
"""
try:
from models import get_db_session, User, Job
if not start_date or not end_date:
start_date, end_date = self._get_date_range(time_range)
db_session = get_db_session()
# Basis-Statistiken
total_users = db_session.query(User).filter(User.active == True).count()
active_users = db_session.query(func.distinct(Job.user_id)).filter(
Job.created_at.between(start_date, end_date)
).count()
# Neue Benutzer im Zeitraum
new_users = db_session.query(User).filter(
and_(
User.created_at.between(start_date, end_date),
User.active == True
)
).count()
# Benutzer-Aktivität
user_activity = db_session.query(
User.username,
User.name,
func.count(Job.id).label('jobs'),
func.max(Job.created_at).label('last_activity'),
func.sum(Job.duration_minutes).label('total_duration')
).outerjoin(Job, and_(
Job.user_id == User.id,
Job.created_at.between(start_date, end_date)
)).filter(User.active == True).group_by(
User.id, User.username, User.name
).all()
# Rollenverteilung
role_distribution = db_session.query(
User.role,
func.count(User.id).label('count')
).filter(User.active == True).group_by(User.role).all()
db_session.close()
# Engagement-Rate berechnen
engagement_rate = round((active_users / total_users * 100) if total_users > 0 else 0, 1)
return {
'summary': {
'total_users': total_users,
'active_users': active_users,
'new_users': new_users,
'engagement_rate': engagement_rate
},
'role_distribution': [
{'role': role.role or 'user', 'count': role.count}
for role in role_distribution
],
'user_activity': [
{
'username': user.username,
'name': user.name,
'jobs': user.jobs or 0,
'last_activity': user.last_activity.isoformat() if user.last_activity else None,
'total_hours': round((user.total_duration or 0) / 60, 1)
}
for user in user_activity
],
'time_range': {
'start': start_date.isoformat(),
'end': end_date.isoformat(),
'type': time_range.value
}
}
except Exception as e:
logger.error(f"Fehler beim Abrufen der Benutzer-Statistiken: {e}")
return {'error': str(e)}
def get_system_kpis(self, time_range: TimeRange = TimeRange.MONTH) -> Dict:
"""
System-KPIs abrufen
Args:
time_range: Zeitraum für Vergleich
Returns:
Dict: KPI-Daten
"""
try:
current_start, current_end = self._get_date_range(time_range)
previous_start, previous_end = self._get_previous_period(current_start, current_end)
# Aktuelle Periode
current_printer_stats = self.get_printer_statistics(TimeRange.CUSTOM, current_start, current_end)
current_job_stats = self.get_job_statistics(TimeRange.CUSTOM, current_start, current_end)
current_user_stats = self.get_user_statistics(TimeRange.CUSTOM, current_start, current_end)
# Vorherige Periode
previous_printer_stats = self.get_printer_statistics(TimeRange.CUSTOM, previous_start, previous_end)
previous_job_stats = self.get_job_statistics(TimeRange.CUSTOM, previous_start, previous_end)
previous_user_stats = self.get_user_statistics(TimeRange.CUSTOM, previous_start, previous_end)
# KPIs berechnen
kpis = [
self._create_kpi(
name="Drucker-Verfügbarkeit",
current=current_printer_stats['summary']['availability_rate'],
previous=previous_printer_stats['summary']['availability_rate'],
target=95.0,
unit="%"
),
self._create_kpi(
name="Job-Erfolgsrate",
current=current_job_stats['summary']['success_rate'],
previous=previous_job_stats['summary']['success_rate'],
target=90.0,
unit="%"
),
self._create_kpi(
name="Aktive Benutzer",
current=current_user_stats['summary']['active_users'],
previous=previous_user_stats['summary']['active_users'],
target=50,
unit="Benutzer"
),
self._create_kpi(
name="Durchschnittliche Job-Dauer",
current=current_job_stats['summary']['avg_duration_hours'],
previous=previous_job_stats['summary']['avg_duration_hours'],
target=4.0,
unit="Stunden"
),
self._create_kpi(
name="Material-Verbrauch",
current=current_job_stats['summary']['total_material_g'],
previous=previous_job_stats['summary']['total_material_g'],
target=10000,
unit="g"
)
]
return {
'kpis': [kpi.to_dict() for kpi in kpis],
'period': {
'current': {
'start': current_start.isoformat(),
'end': current_end.isoformat()
},
'previous': {
'start': previous_start.isoformat(),
'end': previous_end.isoformat()
}
}
}
except Exception as e:
logger.error(f"Fehler beim Abrufen der System-KPIs: {e}")
return {'error': str(e)}
def generate_report(self, report_type: str, time_range: TimeRange = TimeRange.MONTH,
format: ReportFormat = ReportFormat.JSON, **kwargs) -> Dict:
"""
Bericht generieren
Args:
report_type: Art des Berichts
time_range: Zeitraum
format: Ausgabeformat
**kwargs: Zusätzliche Parameter
Returns:
Dict: Bericht-Daten
"""
try:
start_date = kwargs.get('start_date')
end_date = kwargs.get('end_date')
if not start_date or not end_date:
start_date, end_date = self._get_date_range(time_range)
if report_type == "comprehensive":
return self._generate_comprehensive_report(start_date, end_date, format)
elif report_type == "printer_usage":
return self._generate_printer_usage_report(start_date, end_date, format)
elif report_type == "user_activity":
return self._generate_user_activity_report(start_date, end_date, format)
elif report_type == "efficiency":
return self._generate_efficiency_report(start_date, end_date, format)
else:
raise ValueError(f"Unbekannter Berichtstyp: {report_type}")
except Exception as e:
logger.error(f"Fehler beim Generieren des Berichts: {e}")
return {'error': str(e)}
# ===== HELPER METHODS =====
def _get_date_range(self, time_range: TimeRange) -> Tuple[datetime, datetime]:
"""Berechnet Datumsbereich basierend auf TimeRange"""
end_date = datetime.now()
if time_range == TimeRange.HOUR:
start_date = end_date - timedelta(hours=1)
elif time_range == TimeRange.DAY:
start_date = end_date - timedelta(days=1)
elif time_range == TimeRange.WEEK:
start_date = end_date - timedelta(weeks=1)
elif time_range == TimeRange.MONTH:
start_date = end_date - timedelta(days=30)
elif time_range == TimeRange.QUARTER:
start_date = end_date - timedelta(days=90)
elif time_range == TimeRange.YEAR:
start_date = end_date - timedelta(days=365)
else:
start_date = end_date - timedelta(days=30) # Default
return start_date, end_date
def _get_previous_period(self, start_date: datetime, end_date: datetime) -> Tuple[datetime, datetime]:
"""Berechnet vorherige Periode für Vergleiche"""
duration = end_date - start_date
previous_end = start_date
previous_start = previous_end - duration
return previous_start, previous_end
def _create_kpi(self, name: str, current: float, previous: float,
target: float, unit: str) -> KPI:
"""Erstellt KPI-Objekt mit Berechnungen"""
if previous > 0:
change_percent = round(((current - previous) / previous) * 100, 1)
else:
change_percent = 0.0
if abs(change_percent) < 1:
trend = "stable"
elif change_percent > 0:
trend = "up"
else:
trend = "down"
return KPI(
name=name,
current_value=current,
previous_value=previous,
target_value=target,
unit=unit,
trend=trend,
change_percent=change_percent
)
def _calculate_printer_availability(self, db_session: Session,
start_date: datetime, end_date: datetime) -> Dict:
"""Berechnet Drucker-Verfügbarkeit"""
# Vereinfachte Berechnung - kann erweitert werden
from models import Printer
total_printers = db_session.query(Printer).filter(Printer.active == True).count()
online_printers = db_session.query(Printer).filter(
and_(Printer.active == True, Printer.status.in_(["online", "idle"]))
).count()
availability_rate = round((online_printers / total_printers * 100) if total_printers > 0 else 0, 1)
return {
'total_printers': total_printers,
'online_printers': online_printers,
'availability_rate': availability_rate,
'downtime_hours': 0 # Placeholder - kann mit detaillierter Logging berechnet werden
}
def _calculate_utilization_rate(self, total_minutes: int,
start_date: datetime, end_date: datetime) -> float:
"""Berechnet Auslastungsrate"""
if not total_minutes:
return 0.0
total_hours = (end_date - start_date).total_seconds() / 3600
utilization_rate = (total_minutes / 60) / total_hours * 100
return round(min(utilization_rate, 100), 1)
def _get_daily_job_trend(self, db_session: Session,
start_date: datetime, end_date: datetime) -> List[Dict]:
"""Holt tägliche Job-Trends"""
from models import Job
daily_jobs = db_session.query(
func.date(Job.created_at).label('date'),
func.count(Job.id).label('count')
).filter(
Job.created_at.between(start_date, end_date)
).group_by(
func.date(Job.created_at)
).order_by('date').all()
return [
{
'date': job.date.isoformat(),
'jobs': job.count
}
for job in daily_jobs
]
def _generate_comprehensive_report(self, start_date: datetime,
end_date: datetime, format: ReportFormat) -> Dict:
"""Generiert umfassenden Bericht"""
printer_stats = self.get_printer_statistics(TimeRange.CUSTOM, start_date, end_date)
job_stats = self.get_job_statistics(TimeRange.CUSTOM, start_date, end_date)
user_stats = self.get_user_statistics(TimeRange.CUSTOM, start_date, end_date)
kpis = self.get_system_kpis(TimeRange.CUSTOM)
report = {
'title': 'Umfassender System-Bericht',
'generated_at': datetime.now().isoformat(),
'period': {
'start': start_date.isoformat(),
'end': end_date.isoformat()
},
'summary': {
'total_jobs': job_stats['summary']['total_jobs'],
'success_rate': job_stats['summary']['success_rate'],
'active_users': user_stats['summary']['active_users'],
'printer_availability': printer_stats['summary']['availability_rate']
},
'sections': {
'printers': printer_stats,
'jobs': job_stats,
'users': user_stats,
'kpis': kpis
}
}
if format == ReportFormat.JSON:
return report
else:
# Für andere Formate würde hier die Konvertierung stattfinden
return {'error': f'Format {format.value} noch nicht implementiert'}
# ===== GLOBALE INSTANZ =====
analytics_engine = AnalyticsEngine()
# ===== UTILITY FUNCTIONS =====
def get_dashboard_stats() -> Dict:
"""Schnelle Dashboard-Statistiken"""
return analytics_engine.get_system_kpis(TimeRange.DAY)
def export_statistics(report_type: str, time_range: TimeRange, format: ReportFormat = ReportFormat.JSON) -> Dict:
"""Exportiert Statistiken in verschiedenen Formaten"""
return analytics_engine.generate_report(report_type, time_range, format)
def track_event(event_name: str, properties: Dict = None):
"""Verfolgt Events für Analytik"""
try:
logger.info(f"📊 Event tracked: {event_name} - {properties or {}}")
# Hier könnte Event-Tracking implementiert werden
except Exception as e:
logger.error(f"Fehler beim Event-Tracking: {e}")
# Logging für Analytics-System
logger.info("📈 Analytics Engine initialisiert")