📝 Commit Details:

This commit is contained in:
2025-05-31 22:40:29 +02:00
parent 91b1886dde
commit df8fb197c0
14061 changed files with 997277 additions and 103548 deletions

View File

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}404 - Seite nicht gefunden - Mercedes-Benz MYP Platform{% endblock %}
{% block content %}
<div class="min-h-[80vh] flex flex-col items-center justify-center p-4">
<!-- 404 Error Container -->
<div class="w-full max-w-md">
<div class="bg-white dark:bg-gray-800 backdrop-blur-xl bg-opacity-95 dark:bg-opacity-95 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 p-8 text-center transition-all duration-300">
<!-- Mercedes-Benz Logo -->
<div class="flex justify-center mb-6">
<div class="w-16 h-16 text-gray-300 dark:text-gray-600 transition-transform duration-500 hover:scale-110">
<svg class="w-full h-full" fill="currentColor" viewBox="0 0 80 80">
<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>
</div>
<!-- Error Message -->
<h1 class="text-6xl font-bold text-gray-300 dark:text-gray-600 mb-4">404</h1>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Seite nicht gefunden</h2>
<p class="text-gray-600 dark:text-gray-400 mb-6">Die von Ihnen gesuchte Seite existiert nicht oder wurde verschoben.</p>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row justify-center gap-4 mt-8">
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center justify-center px-5 py-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium rounded-lg transition-all duration-300 transform hover:-translate-y-0.5">
<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="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
<span>Zum Dashboard</span>
</a>
<button onclick="window.history.back()" class="inline-flex items-center justify-center px-5 py-3 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-all duration-300 transform hover:-translate-y-0.5">
<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>
<span>Zurück</span>
</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,66 @@
{% extends "base.html" %}
{% block title %}Interner Serverfehler - Mercedes-Benz MYP Platform{% endblock %}
{% block content %}
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-red-50 to-orange-50 dark:from-slate-900 dark:via-red-900/20 dark:to-orange-900/20 flex items-center justify-center px-4">
<div class="max-w-2xl w-full text-center">
<!-- Error Icon -->
<div class="mb-8">
<div class="inline-flex items-center justify-center w-24 h-24 bg-red-100 dark:bg-red-900/30 rounded-full mb-6">
<svg class="w-12 h-12 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
<!-- Error Message -->
<h1 class="text-6xl font-bold text-slate-900 dark:text-white mb-4">500</h1>
<h2 class="text-2xl font-semibold text-slate-700 dark:text-slate-300 mb-6">Interner Serverfehler</h2>
<p class="text-lg text-slate-600 dark:text-slate-400 mb-8 max-w-lg mx-auto">
Es ist ein unerwarteter Fehler aufgetreten. Unser Team wurde automatisch benachrichtigt und arbeitet an einer Lösung.
</p>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-xl transition-colors duration-200 shadow-lg hover:shadow-xl">
<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="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
Zurück zum Dashboard
</a>
<button onclick="window.location.reload()" class="inline-flex items-center px-6 py-3 bg-slate-200 hover:bg-slate-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 font-medium rounded-xl transition-colors duration-200">
<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>
Seite neu laden
</button>
</div>
<!-- Additional Info -->
<div class="mt-12 p-6 bg-white/60 dark:bg-slate-800/60 backdrop-blur-sm rounded-2xl border border-slate-200 dark:border-slate-700">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-3">Was können Sie tun?</h3>
<ul class="text-left text-slate-600 dark:text-slate-400 space-y-2">
<li class="flex items-start">
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" 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>
Versuchen Sie, die Seite neu zu laden
</li>
<li class="flex items-start">
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" 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>
Kehren Sie zum Dashboard zurück
</li>
<li class="flex items-start">
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" 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>
Kontaktieren Sie den Administrator, falls das Problem weiterhin besteht
</li>
</ul>
</div>
</div>
</div>
{% endblock %}

1080
backend/templates/admin.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,114 @@
{% extends "base.html" %}
{% block title %}Drucker hinzufügen - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
{% endblock %}
{% 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="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Neuen Drucker hinzufügen</h1>
<p class="text-slate-600 dark:text-slate-400 mt-2">Fügen Sie einen neuen 3D-Drucker zum MYP-System hinzu</p>
</div>
<a href="{{ url_for('admin_page', tab='printers') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<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 zur Druckerverwaltung
</a>
</div>
</div>
<!-- Form -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-8">
<form method="POST" action="{{ url_for('admin_create_printer_form') }}" class="space-y-6">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Drucker-Name
</label>
<input type="text" name="name" id="name" required
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Prusa i3 MK3S+">
</div>
<!-- IP-Adresse -->
<div>
<label for="ip_address" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
IP-Adresse
</label>
<input type="text" name="ip_address" id="ip_address" required
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="192.168.1.100"
pattern="^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
</div>
<!-- Modell -->
<div>
<label for="model" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Drucker-Modell
</label>
<input type="text" name="model" id="model"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Prusa i3 MK3S+">
</div>
<!-- Standort -->
<div>
<label for="location" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Standort
</label>
<input type="text" name="location" id="location"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Werkstatt A, Regal 3">
</div>
<!-- Beschreibung -->
<div>
<label for="description" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Beschreibung
</label>
<textarea name="description" id="description" rows="3"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Zusätzliche Informationen zum Drucker..."></textarea>
</div>
<!-- Status -->
<div>
<label for="status" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Status
</label>
<select name="status" id="status"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
<option value="available">Verfügbar</option>
<option value="maintenance">Wartung</option>
<option value="offline">Offline</option>
</select>
</div>
<!-- Buttons -->
<div class="flex items-center justify-end space-x-4 pt-4">
<a href="{{ url_for('admin_page', tab='printers') }}"
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">
Abbrechen
</a>
<button type="submit"
class="px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300 shadow-lg">
Drucker hinzufügen
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,92 @@
{% extends "base.html" %}
{% block title %}Benutzer hinzufügen - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
{% endblock %}
{% 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="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Neuen Benutzer hinzufügen</h1>
<p class="text-slate-600 dark:text-slate-400 mt-2">Erstellen Sie einen neuen Benutzer für das MYP-System</p>
</div>
<a href="{{ url_for('admin_page', tab='users') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<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 zur Benutzerverwaltung
</a>
</div>
</div>
<!-- Form -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-8">
<form method="POST" action="{{ url_for('admin_create_user_form') }}" class="space-y-6">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<!-- Benutzername -->
<div>
<label for="username" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Benutzername <span class="text-red-500">*</span>
</label>
<input type="text" name="username" id="username" required
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="max.mustermann">
</div>
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Vollständiger Name
</label>
<input type="text" name="name" id="name"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Max Mustermann">
</div>
<!-- Passwort -->
<div>
<label for="password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Passwort
</label>
<input type="password" name="password" id="password" required
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Sicheres Passwort">
</div>
<!-- Rolle -->
<div>
<label for="role" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Benutzerrolle
</label>
<select name="role" id="role"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
<option value="user">Benutzer</option>
<option value="admin">Administrator</option>
</select>
</div>
<!-- Buttons -->
<div class="flex items-center justify-end space-x-4 pt-4">
<a href="{{ url_for('admin_page', tab='users') }}"
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">
Abbrechen
</a>
<button type="submit"
class="px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300 shadow-lg">
Benutzer erstellen
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,168 @@
{% extends "base.html" %}
{% block title %}Benutzer bearbeiten - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
{% endblock %}
{% 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="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Benutzer bearbeiten</h1>
<p class="text-slate-600 dark:text-slate-400 mt-2">Bearbeiten Sie die Daten von {{ user.name or user.email }}</p>
</div>
<a href="{{ url_for('admin_page', tab='users') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<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 zur Benutzerverwaltung
</a>
</div>
</div>
<!-- Form -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-8">
<form method="POST" action="{{ url_for('admin_update_user_form', user_id=user.id) }}" class="space-y-6">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="_method" value="PUT"/>
<!-- Benutzername -->
<div>
<label for="username" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Benutzername
</label>
<input type="text" name="username" id="username" required
value="{{ user.username }}"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="max.mustermann">
</div>
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Vollständiger Name
</label>
<input type="text" name="name" id="name"
value="{{ user.name }}"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Max Mustermann">
</div>
<!-- Neues Passwort (optional) -->
<div>
<label for="password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Neues Passwort (leer lassen, um beizubehalten)
</label>
<input type="password" name="password" id="password"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Neues Passwort">
</div>
<!-- Rolle -->
<div>
<label for="role" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Benutzerrolle
</label>
<select name="role" id="role"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
<option value="user" {% if not user.is_admin %}selected{% endif %}>Benutzer</option>
<option value="admin" {% if user.is_admin %}selected{% endif %}>Administrator</option>
</select>
</div>
<!-- Status -->
<div>
<label for="is_active" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Benutzerstatus
</label>
<select name="is_active" id="is_active"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
<option value="true" {% if user.active %}selected{% endif %}>Aktiv</option>
<option value="false" {% if not user.active %}selected{% endif %}>Deaktiviert</option>
</select>
</div>
<!-- Berechtigungen -->
<div class="border-t border-slate-200 dark:border-slate-600 pt-6">
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-4">Benutzerberechtigungen</h3>
<div class="space-y-4">
<!-- Jobs ohne Genehmigung starten -->
<div class="flex items-center justify-between">
<div>
<label for="can_start_jobs" class="text-sm font-medium text-slate-700 dark:text-slate-300">
Jobs ohne Genehmigung starten
</label>
<p class="text-xs text-slate-500 dark:text-slate-400">
Benutzer kann eigene Druckjobs ohne Admin-Genehmigung starten
</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input type="checkbox" name="can_start_jobs" id="can_start_jobs"
{% if user.permissions and user.permissions.can_start_jobs %}checked{% endif %}
class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer">
<label for="can_start_jobs" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div>
<!-- Genehmigungspflicht -->
<div class="flex items-center justify-between">
<div>
<label for="needs_approval" class="text-sm font-medium text-slate-700 dark:text-slate-300">
Genehmigungspflicht für Jobs
</label>
<p class="text-xs text-slate-500 dark:text-slate-400">
Jobs des Benutzers müssen von einem Admin genehmigt werden
</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input type="checkbox" name="needs_approval" id="needs_approval"
{% if not user.permissions or user.permissions.needs_approval %}checked{% endif %}
class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer">
<label for="needs_approval" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div>
<!-- Jobs genehmigen -->
<div class="flex items-center justify-between">
<div>
<label for="can_approve_jobs" class="text-sm font-medium text-slate-700 dark:text-slate-300">
Jobs genehmigen
</label>
<p class="text-xs text-slate-500 dark:text-slate-400">
Benutzer kann Gastanfragen und fremde Jobs genehmigen
</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input type="checkbox" name="can_approve_jobs" id="can_approve_jobs"
{% if user.permissions and user.permissions.can_approve_jobs %}checked{% endif %}
class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer">
<label for="can_approve_jobs" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div>
</div>
</div>
<!-- Buttons -->
<div class="flex items-center justify-end space-x-4 pt-4">
<a href="{{ url_for('admin_page', tab='users') }}"
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">
Abbrechen
</a>
<button type="submit"
class="px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300 shadow-lg">
Änderungen speichern
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,372 @@
{% extends "base.html" %}
{% 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/60 backdrop-blur-sm z-50 flex items-center justify-center hidden">
<div class="bg-white dark:bg-slate-900 rounded-2xl p-8 shadow-2xl border border-gray-200 dark:border-slate-700">
<div class="flex items-center space-x-4">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 dark:border-blue-400"></div>
<span class="text-lg font-medium text-gray-900 dark:text-slate-100">Wird geladen...</span>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<!-- 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 dark:from-slate-950 dark:via-blue-950 dark:to-indigo-950 text-white rounded-3xl mx-4 mt-4">
<div class="absolute inset-0 bg-black/30 dark:bg-black/50"></div>
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 dark:via-white/3 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/15 dark:bg-white/10 backdrop-blur-sm border border-white/30 dark:border-white/20 rounded-full px-3 py-1">
<div id="live-indicator" class="w-2 h-2 bg-green-400 dark:bg-green-300 rounded-full animate-pulse"></div>
<span class="text-sm font-medium text-white">Live</span>
</div>
<div class="bg-white/15 dark:bg-white/10 backdrop-blur-sm border border-white/30 dark:border-white/20 rounded-full px-3 py-1">
<span id="live-time" class="text-sm font-medium text-white"></span>
</div>
</div>
<!-- Animated Background Pattern -->
<div class="absolute inset-0 opacity-10 dark:opacity-5 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/15 dark:bg-white/10 backdrop-blur-sm rounded-full mb-6 border border-white/30 dark: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 via-blue-100 to-slate-100 dark:from-white dark:via-blue-200 dark:to-slate-200 bg-clip-text text-transparent">
Gastaufträge Verwaltung
</span>
</h1>
<p class="text-xl text-blue-100 dark:text-blue-200 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/15 dark:bg-white/10 backdrop-blur-sm border border-white/30 dark:border-white/20 rounded-xl text-white hover:bg-white/25 dark:hover:bg-white/15 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/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-orange-500/10 dark:from-orange-400/20 to-red-500/10 dark:to-red-400/20 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 dark:from-orange-400 dark:to-orange-500 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-slate-100">-</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 dark:bg-orange-300 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/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-green-500/10 dark:from-green-400/20 to-emerald-500/10 dark:to-emerald-400/20 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 dark:from-green-400 dark:to-green-500 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-slate-100">-</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 dark:bg-green-300 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/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-red-500/10 dark:from-red-400/20 to-pink-500/10 dark:to-pink-400/20 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 dark:from-red-400 dark:to-red-500 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-slate-100">-</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 dark:bg-red-300 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/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/10 dark:from-blue-400/20 to-indigo-500/10 dark:to-indigo-400/20 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 dark:from-blue-400 dark:to-blue-500 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-slate-100">-</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 dark:bg-blue-300 rounded-full animate-pulse"></div>
<span class="text-xs text-blue-600 dark:text-blue-400 font-medium">Alle Zeit</span>
</div>
</div>
</div>
</div>
<!-- Control Panel -->
<div class="bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl dark:shadow-2xl 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/80 dark:bg-slate-800/80 text-slate-900 dark:text-slate-100
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent
placeholder-slate-500 dark:placeholder-slate-400">
<svg class="w-5 h-5 text-slate-400 dark:text-slate-500 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/80 dark:bg-slate-800/80 text-slate-900 dark:text-slate-100
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 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/80 dark:bg-slate-800/80 text-slate-900 dark:text-slate-100
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 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 dark:bg-blue-600 text-white rounded-xl hover:bg-blue-600 dark:hover:bg-blue-700 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 dark:bg-green-600 text-white rounded-xl hover:bg-green-600 dark:hover:bg-green-700 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 dark:bg-purple-600 text-white rounded-xl hover:bg-purple-600 dark:hover:bg-purple-700 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>
<!-- Guest Requests Table -->
<div class="bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 shadow-xl dark:shadow-2xl 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-slate-100">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 dark:bg-green-300 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/80">
<tr>
<th class="px-6 py-4 text-left">
<input type="checkbox" id="select-all"
class="rounded border-slate-300 dark:border-slate-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 bg-white dark:bg-slate-800">
</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-900/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 dark:border-blue-400 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 dark:text-slate-500 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-slate-100 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="pagination" class="mt-8 flex justify-center">
<!-- Pagination controls will be dynamically generated -->
</div>
</div>
</div>
<!-- Guest Request Detail Modal -->
<div id="detail-modal" class="fixed inset-0 bg-black/60 dark:bg-black/80 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-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-slate-700 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>
<!-- Bulk Actions Modal -->
<div id="bulk-modal" class="fixed inset-0 bg-black/60 dark:bg-black/80 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-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-slate-700 max-w-md w-full">
<div class="p-6 border-b border-gray-200 dark:border-slate-700">
<h3 class="text-xl font-bold text-gray-900 dark:text-slate-100">Massenaktionen</h3>
</div>
<div class="p-6">
<div class="space-y-4">
<button onclick="performBulkAction('approve')"
class="w-full px-4 py-3 bg-green-500 dark:bg-green-600 text-white rounded-lg hover:bg-green-600 dark:hover:bg-green-700 transition-colors">
Ausgewählte genehmigen
</button>
<button onclick="performBulkAction('reject')"
class="w-full px-4 py-3 bg-red-500 dark:bg-red-600 text-white rounded-lg hover:bg-red-600 dark:hover:bg-red-700 transition-colors">
Ausgewählte ablehnen
</button>
<button onclick="performBulkAction('delete')"
class="w-full px-4 py-3 bg-gray-500 dark:bg-gray-600 text-white rounded-lg hover:bg-gray-600 dark:hover:bg-gray-700 transition-colors">
Ausgewählte löschen
</button>
</div>
<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>
</div>
</div>
</div>
</div>
</div>
<script>
// Initialize live time display
function updateLiveTime() {
const timeElement = document.getElementById('live-time');
if (timeElement) {
timeElement.textContent = new Date().toLocaleTimeString('de-DE');
}
}
// Update time every second
setInterval(updateLiveTime, 1000);
updateLiveTime();
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,238 @@
{% extends "base.html" %}
{% block title %}Drucker verwalten - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
{% endblock %}
{% 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="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">{{ printer.name }} verwalten</h1>
<p class="text-slate-600 dark:text-slate-400 mt-2">Verwaltung und Überwachung des Druckers</p>
</div>
<a href="{{ url_for('admin_page', tab='printers') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<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 zur Druckerverwaltung
</a>
</div>
</div>
<!-- Drucker-Info -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- Status Card -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Status</h3>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-slate-600 dark:text-slate-400">Aktueller Status:</span>
<span class="px-3 py-1 rounded-full text-sm font-medium
{% if printer.status == 'available' %}bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200{% elif printer.status == 'busy' %}bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200{% elif printer.status == 'maintenance' %}bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200{% else %}bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200{% endif %}">
{{ printer.status|title }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-600 dark:text-slate-400">IP-Adresse:</span>
<span class="text-slate-900 dark:text-white font-mono">{{ printer.ip_address }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-600 dark:text-slate-400">Standort:</span>
<span class="text-slate-900 dark:text-white">{{ printer.location or 'Nicht angegeben' }}</span>
</div>
</div>
</div>
<!-- Aktionen Card -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Aktionen</h3>
<div class="space-y-3">
<button onclick="togglePrinter({{ printer.id }})"
class="w-full px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600 transition-all duration-300">
{% if printer.status == 'available' %}Deaktivieren{% else %}Aktivieren{% endif %}
</button>
<button onclick="testConnection({{ printer.id }})"
class="w-full px-4 py-2 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-all duration-300">
Verbindung testen
</button>
<a href="{{ url_for('admin_printer_settings_page', printer_id=printer.id) }}"
class="block w-full px-4 py-2 bg-slate-500 text-white rounded-xl hover:bg-slate-600 transition-all duration-300 text-center">
Einstellungen
</a>
</div>
</div>
<!-- Statistiken Card -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Statistiken</h3>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-slate-600 dark:text-slate-400">Gesamte Jobs:</span>
<span class="text-slate-900 dark:text-white font-semibold" id="total-jobs">-</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-600 dark:text-slate-400">Aktive Jobs:</span>
<span class="text-slate-900 dark:text-white font-semibold" id="active-jobs">-</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-600 dark:text-slate-400">Erfolgsrate:</span>
<span class="text-slate-900 dark:text-white font-semibold" id="success-rate">-</span>
</div>
</div>
</div>
</div>
<!-- Aktuelle Jobs -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Aktuelle Jobs</h3>
<div id="current-jobs" class="space-y-4">
<div class="text-center text-slate-500 dark:text-slate-400 py-8">
Lade Jobs...
</div>
</div>
</div>
</div>
</div>
<script>
// CSRF Token
function getCsrfToken() {
const token = document.querySelector('meta[name="csrf-token"]');
return token ? token.getAttribute('content') : '';
}
// Notification anzeigen
function showNotification(message, type = 'info') {
if (type === 'success') {
alert('✓ ' + message);
} else if (type === 'error') {
alert('✗ ' + message);
} else {
alert(' ' + message);
}
}
// Drucker aktivieren/deaktivieren
async function togglePrinter(printerId) {
try {
const response = await fetch(`/api/admin/printers/${printerId}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
});
const result = await response.json();
if (response.ok && result.success) {
showNotification('Drucker-Status erfolgreich geändert', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showNotification(result.error || 'Fehler beim Ändern des Drucker-Status', 'error');
}
} catch (error) {
showNotification('Netzwerkfehler: ' + error.message, 'error');
}
}
// Verbindung testen
async function testConnection(printerId) {
try {
const response = await fetch(`/api/printers/${printerId}/test`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
});
const result = await response.json();
if (response.ok) {
if (result.connected) {
showNotification('Verbindung erfolgreich', 'success');
} else {
showNotification('Verbindung fehlgeschlagen', 'error');
}
} else {
showNotification(result.error || 'Fehler beim Testen der Verbindung', 'error');
}
} catch (error) {
showNotification('Netzwerkfehler: ' + error.message, 'error');
}
}
// Statistiken laden
async function loadStats() {
try {
const response = await fetch(`/api/printers/{{ printer.id }}/stats`);
const stats = await response.json();
if (response.ok) {
document.getElementById('total-jobs').textContent = stats.total_jobs || 0;
document.getElementById('active-jobs').textContent = stats.active_jobs || 0;
document.getElementById('success-rate').textContent = (stats.success_rate || 0) + '%';
}
} catch (error) {
console.error('Fehler beim Laden der Statistiken:', error);
}
}
// Jobs laden
async function loadJobs() {
try {
const response = await fetch(`/api/printers/{{ printer.id }}/jobs`);
const jobs = await response.json();
const container = document.getElementById('current-jobs');
if (response.ok && jobs.length > 0) {
container.innerHTML = jobs.map(job => `
<div class="border border-slate-200 dark:border-slate-700 rounded-xl p-4">
<div class="flex items-center justify-between">
<div>
<h4 class="font-medium text-slate-900 dark:text-white">${job.name}</h4>
<p class="text-sm text-slate-600 dark:text-slate-400">von ${job.user_name}</p>
</div>
<div class="text-right">
<span class="px-3 py-1 rounded-full text-sm font-medium
${job.status === 'running' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
job.status === 'pending' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' :
'bg-slate-100 text-slate-800 dark:bg-slate-900 dark:text-slate-200'}">
${job.status}
</span>
</div>
</div>
</div>
`).join('');
} else {
container.innerHTML = '<div class="text-center text-slate-500 dark:text-slate-400 py-8">Keine aktiven Jobs</div>';
}
} catch (error) {
console.error('Fehler beim Laden der Jobs:', error);
document.getElementById('current-jobs').innerHTML = '<div class="text-center text-red-500 py-8">Fehler beim Laden der Jobs</div>';
}
}
// Beim Laden der Seite
document.addEventListener('DOMContentLoaded', function() {
loadStats();
loadJobs();
// Alle 30 Sekunden aktualisieren
setInterval(() => {
loadStats();
loadJobs();
}, 30000);
});
</script>
{% endblock %}

View File

@ -0,0 +1,119 @@
{% extends "base.html" %}
{% block title %}Drucker-Einstellungen - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
{% endblock %}
{% 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="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">{{ printer.name }} - Einstellungen</h1>
<p class="text-slate-600 dark:text-slate-400 mt-2">Konfiguration und Einstellungen des Druckers</p>
</div>
<a href="{{ url_for('admin_manage_printer_page', printer_id=printer.id) }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<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 zur Verwaltung
</a>
</div>
</div>
<!-- Form -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-8">
<form method="POST" action="{{ url_for('admin_update_printer_form', printer_id=printer.id) }}" class="space-y-6">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="_method" value="PUT"/>
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Drucker-Name
</label>
<input type="text" name="name" id="name" required
value="{{ printer.name }}"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Prusa i3 MK3S+">
</div>
<!-- IP-Adresse -->
<div>
<label for="ip_address" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
IP-Adresse
</label>
<input type="text" name="ip_address" id="ip_address" required
value="{{ printer.ip_address }}"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="192.168.1.100"
pattern="^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
</div>
<!-- Modell -->
<div>
<label for="model" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Drucker-Modell
</label>
<input type="text" name="model" id="model"
value="{{ printer.model or '' }}"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Prusa i3 MK3S+">
</div>
<!-- Standort -->
<div>
<label for="location" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Standort
</label>
<input type="text" name="location" id="location"
value="{{ printer.location or '' }}"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Werkstatt A, Regal 3">
</div>
<!-- Beschreibung -->
<div>
<label for="description" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Beschreibung
</label>
<textarea name="description" id="description" rows="3"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
placeholder="Zusätzliche Informationen zum Drucker...">{{ printer.description or '' }}</textarea>
</div>
<!-- Status -->
<div>
<label for="status" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Status
</label>
<select name="status" id="status"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
<option value="available" {% if printer.status == 'available' %}selected{% endif %}>Verfügbar</option>
<option value="maintenance" {% if printer.status == 'maintenance' %}selected{% endif %}>Wartung</option>
<option value="offline" {% if printer.status == 'offline' %}selected{% endif %}>Offline</option>
</select>
</div>
<!-- Buttons -->
<div class="flex items-center justify-end space-x-4 pt-4">
<a href="{{ url_for('admin_manage_printer_page', printer_id=printer.id) }}"
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">
Abbrechen
</a>
<button type="submit"
class="px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300 shadow-lg">
Einstellungen speichern
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,376 @@
{% extends "base.html" %}
{% block title %}Admin-Einstellungen - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
{% endblock %}
{% 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="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Admin-Einstellungen</h1>
<p class="text-slate-600 dark:text-slate-400 mt-2">Systemkonfiguration und Verwaltungsoptionen</p>
</div>
<a href="{{ url_for('admin_page') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<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 Dashboard
</a>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- System-Wartung -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">System-Wartung</h3>
<div class="space-y-4">
<button onclick="clearCache()"
class="w-full px-4 py-3 bg-blue-500 text-white rounded-xl hover:bg-blue-600 transition-all duration-300">
Cache leeren
</button>
<button onclick="optimizeDatabase()"
class="w-full px-4 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-all duration-300">
Datenbank optimieren
</button>
<button onclick="createBackup()"
class="w-full px-4 py-3 bg-purple-500 text-white rounded-xl hover:bg-purple-600 transition-all duration-300">
Backup erstellen
</button>
</div>
</div>
<!-- Drucker-Verwaltung -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Drucker-Verwaltung</h3>
<div class="space-y-4">
<button onclick="updatePrinters()"
class="w-full px-4 py-3 bg-orange-500 text-white rounded-xl hover:bg-orange-600 transition-all duration-300">
Drucker-Status aktualisieren
</button>
<button onclick="testAllPrinters()"
class="w-full px-4 py-3 bg-teal-500 text-white rounded-xl hover:bg-teal-600 transition-all duration-300">
Alle Drucker testen
</button>
</div>
</div>
<!-- System-Informationen -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">System-Informationen</h3>
<div class="space-y-3" id="system-info">
<div class="flex items-center justify-between">
<span class="text-slate-600 dark:text-slate-400">Server-Status:</span>
<span class="text-slate-900 dark:text-white font-semibold" id="server-status">Lade...</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-600 dark:text-slate-400">Datenbank:</span>
<span class="text-slate-900 dark:text-white font-semibold" id="db-status">Lade...</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-600 dark:text-slate-400">Uptime:</span>
<span class="text-slate-900 dark:text-white font-semibold" id="uptime">Lade...</span>
</div>
</div>
</div>
<!-- Logs und Überwachung -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Logs und Überwachung</h3>
<div class="space-y-4">
<button onclick="downloadLogs()"
class="w-full px-4 py-3 bg-slate-500 text-white rounded-xl hover:bg-slate-600 transition-all duration-300">
Logs herunterladen
</button>
<button onclick="runMaintenance()"
class="w-full px-4 py-3 bg-indigo-500 text-white rounded-xl hover:bg-indigo-600 transition-all duration-300">
Wartung ausführen
</button>
</div>
</div>
</div>
<!-- Erweiterte Einstellungen -->
<div class="mt-8 bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Erweiterte Einstellungen</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Automatische Backup-Intervall (Stunden)
</label>
<input type="number" id="backup-interval" min="1" max="168" value="24"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Maximale Job-Laufzeit (Stunden)
</label>
<input type="number" id="max-job-time" min="1" max="72" value="12"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
</div>
</div>
<div class="mt-6 flex justify-end">
<button onclick="saveSettings()"
class="px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300 shadow-lg">
Einstellungen speichern
</button>
</div>
</div>
</div>
</div>
<script>
// CSRF Token
function getCsrfToken() {
const token = document.querySelector('meta[name="csrf-token"]');
return token ? token.getAttribute('content') : '';
}
// Notification anzeigen
function showNotification(message, type = 'info') {
if (type === 'success') {
alert('✓ ' + message);
} else if (type === 'error') {
alert('✗ ' + message);
} else {
alert(' ' + message);
}
}
// Cache leeren
async function clearCache() {
if (!confirm('Möchten Sie den Cache wirklich leeren?')) return;
try {
const response = await fetch('/api/admin/cache/clear', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
});
const result = await response.json();
if (response.ok && result.success) {
showNotification('Cache erfolgreich geleert', 'success');
} else {
showNotification(result.error || 'Fehler beim Leeren des Cache', 'error');
}
} catch (error) {
showNotification('Netzwerkfehler: ' + error.message, 'error');
}
}
// Datenbank optimieren
async function optimizeDatabase() {
if (!confirm('Möchten Sie die Datenbank optimieren? Dies kann einige Minuten dauern.')) return;
try {
const response = await fetch('/api/admin/database/optimize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
});
const result = await response.json();
if (response.ok && result.success) {
showNotification('Datenbank erfolgreich optimiert', 'success');
} else {
showNotification(result.error || 'Fehler bei der Datenbankoptimierung', 'error');
}
} catch (error) {
showNotification('Netzwerkfehler: ' + error.message, 'error');
}
}
// Backup erstellen
async function createBackup() {
if (!confirm('Möchten Sie ein Backup erstellen?')) return;
try {
const response = await fetch('/api/admin/backup/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
});
const result = await response.json();
if (response.ok && result.success) {
showNotification('Backup erfolgreich erstellt', 'success');
} else {
showNotification(result.error || 'Fehler beim Erstellen des Backups', 'error');
}
} catch (error) {
showNotification('Netzwerkfehler: ' + error.message, 'error');
}
}
// Drucker aktualisieren
async function updatePrinters() {
try {
const response = await fetch('/api/admin/printers/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
});
const result = await response.json();
if (response.ok && result.success) {
showNotification('Drucker-Status erfolgreich aktualisiert', 'success');
} else {
showNotification(result.error || 'Fehler beim Aktualisieren der Drucker', 'error');
}
} catch (error) {
showNotification('Netzwerkfehler: ' + error.message, 'error');
}
}
// Alle Drucker testen
async function testAllPrinters() {
try {
const response = await fetch('/api/printers/status');
const printers = await response.json();
if (response.ok) {
const onlineCount = printers.filter(p => p.status === 'available').length;
showNotification(`${onlineCount} von ${printers.length} Druckern sind online`, 'info');
} else {
showNotification('Fehler beim Testen der Drucker', 'error');
}
} catch (error) {
showNotification('Netzwerkfehler: ' + error.message, 'error');
}
}
// Logs herunterladen
async function downloadLogs() {
try {
const response = await fetch('/api/admin/logs/download');
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'myp-logs.zip';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showNotification('Logs werden heruntergeladen', 'success');
} else {
showNotification('Fehler beim Herunterladen der Logs', 'error');
}
} catch (error) {
showNotification('Netzwerkfehler: ' + error.message, 'error');
}
}
// Wartung ausführen
async function runMaintenance() {
if (!confirm('Möchten Sie die Systemwartung ausführen?')) return;
try {
const response = await fetch('/api/admin/maintenance/run', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
});
const result = await response.json();
if (response.ok && result.success) {
showNotification('Wartung erfolgreich ausgeführt', 'success');
} else {
showNotification(result.error || 'Fehler bei der Wartung', 'error');
}
} catch (error) {
showNotification('Netzwerkfehler: ' + error.message, 'error');
}
}
// Einstellungen speichern
async function saveSettings() {
const backupInterval = document.getElementById('backup-interval').value;
const maxJobTime = document.getElementById('max-job-time').value;
try {
const response = await fetch('/api/admin/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
backup_interval: parseInt(backupInterval),
max_job_time: parseInt(maxJobTime)
})
});
const result = await response.json();
if (response.ok && result.success) {
showNotification('Einstellungen erfolgreich gespeichert', 'success');
} else {
showNotification(result.error || 'Fehler beim Speichern der Einstellungen', 'error');
}
} catch (error) {
showNotification('Netzwerkfehler: ' + error.message, 'error');
}
}
// System-Informationen laden
async function loadSystemInfo() {
try {
const [systemResponse, dbResponse] = await Promise.all([
fetch('/api/admin/system/status'),
fetch('/api/admin/database/status')
]);
const systemData = await systemResponse.json();
const dbData = await dbResponse.json();
if (systemResponse.ok) {
document.getElementById('server-status').textContent = systemData.status || 'Online';
document.getElementById('uptime').textContent = systemData.uptime || 'Unbekannt';
}
if (dbResponse.ok) {
document.getElementById('db-status').textContent = dbData.connected ? 'Verbunden' : 'Getrennt';
}
} catch (error) {
console.error('Fehler beim Laden der System-Informationen:', error);
document.getElementById('server-status').textContent = 'Fehler';
document.getElementById('db-status').textContent = 'Fehler';
document.getElementById('uptime').textContent = 'Fehler';
}
}
// Beim Laden der Seite
document.addEventListener('DOMContentLoaded', function() {
loadSystemInfo();
// Alle 30 Sekunden aktualisieren
setInterval(loadSystemInfo, 30000);
});
</script>
{% endblock %}

View File

@ -0,0 +1,746 @@
{% extends "base.html" %}
{% block title %}Erweiterte Analytik - MYP Platform{% endblock %}
{% block extra_css %}
<style>
.analytics-card {
transition: all 0.3s ease;
border: 1px solid #e2e8f0;
}
.analytics-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.kpi-metric {
font-size: 2.5rem;
font-weight: 700;
line-height: 1;
}
.kpi-trend-up {
color: #22c55e;
}
.kpi-trend-down {
color: #ef4444;
}
.kpi-trend-stable {
color: #6b7280;
}
.chart-container {
position: relative;
height: 400px;
width: 100%;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.loading-spinner {
border: 4px solid #f3f4f6;
border-top: 4px solid #3b82f6;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.filter-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
}
.export-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
transition: all 0.3s ease;
}
.export-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-slate-900 dark:text-white mb-2">
📈 Erweiterte Analytik
</h1>
<p class="text-slate-600 dark:text-slate-400">
Umfassende Statistiken und KPIs für die MYP 3D-Druck Platform
</p>
</div>
<!-- Filter Section -->
<div class="filter-section">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex flex-wrap gap-4">
<div class="flex flex-col">
<label class="text-sm font-medium mb-1">Zeitraum</label>
<select id="timeRangeSelect" class="bg-white/20 text-white border border-white/30 rounded-lg px-3 py-2 backdrop-blur-sm">
<option value="day">Letzter Tag</option>
<option value="week">Letzte Woche</option>
<option value="month" selected>Letzter Monat</option>
<option value="quarter">Letztes Quartal</option>
<option value="year">Letztes Jahr</option>
</select>
</div>
<div class="flex flex-col">
<label class="text-sm font-medium mb-1">Report-Typ</label>
<select id="reportTypeSelect" class="bg-white/20 text-white border border-white/30 rounded-lg px-3 py-2 backdrop-blur-sm">
<option value="comprehensive">Umfassend</option>
<option value="printer_usage">Drucker-Nutzung</option>
<option value="user_activity">Benutzer-Aktivität</option>
<option value="efficiency">Effizienz</option>
</select>
</div>
</div>
<div class="flex gap-2">
<button id="refreshData" class="export-button">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Aktualisieren
</button>
<button id="exportReport" class="export-button">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Exportieren
</button>
</div>
</div>
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="text-center py-8 hidden">
<div class="loading-spinner"></div>
<p class="mt-4 text-slate-600 dark:text-slate-400">Lade Analytik-Daten...</p>
</div>
<!-- KPI Dashboard -->
<div id="kpiDashboard" class="mb-8">
<h2 class="text-2xl font-bold text-slate-900 dark:text-white mb-6">🎯 Key Performance Indicators</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6">
<!-- KPI Cards werden dynamisch geladen -->
</div>
</div>
<!-- Analytics Grid -->
<div class="stats-grid">
<!-- Drucker-Statistiken -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
🖨️ Drucker-Statistiken
</h3>
<div class="text-2xl">📊</div>
</div>
<div id="printerStatsContainer">
<div class="loading-spinner"></div>
</div>
</div>
<!-- Job-Statistiken -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
⚙️ Job-Statistiken
</h3>
<div class="text-2xl">📈</div>
</div>
<div id="jobStatsContainer">
<div class="loading-spinner"></div>
</div>
</div>
<!-- Benutzer-Statistiken -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
👥 Benutzer-Statistiken
</h3>
<div class="text-2xl">👤</div>
</div>
<div id="userStatsContainer">
<div class="loading-spinner"></div>
</div>
</div>
<!-- Trend-Analyse -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg col-span-full">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
📊 Trend-Analyse
</h3>
<div class="text-2xl">📉</div>
</div>
<div class="chart-container" id="trendChart">
<canvas id="trendChartCanvas"></canvas>
</div>
</div>
<!-- Drucker-Auslastung -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
⚡ Drucker-Auslastung
</h3>
<div class="text-2xl">🔋</div>
</div>
<div class="chart-container" id="utilizationChart">
<canvas id="utilizationChartCanvas"></canvas>
</div>
</div>
<!-- Top-Benutzer -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
🏆 Top-Benutzer
</h3>
<div class="text-2xl">🥇</div>
</div>
<div id="topUsersContainer">
<div class="loading-spinner"></div>
</div>
</div>
<!-- System-Gesundheit -->
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
💚 System-Gesundheit
</h3>
<div class="text-2xl">❤️</div>
</div>
<div id="systemHealthContainer">
<div class="loading-spinner"></div>
</div>
</div>
</div>
<!-- Report Export Modal -->
<div id="exportModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 max-w-md w-full">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">
📊 Report exportieren
</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Format
</label>
<select id="exportFormat" class="w-full border border-slate-300 dark:border-slate-600 rounded-lg px-3 py-2 bg-white dark:bg-slate-700 text-slate-900 dark:text-white">
<option value="json">JSON</option>
<option value="csv">CSV</option>
<option value="pdf">PDF</option>
<option value="excel">Excel</option>
</select>
</div>
<div class="flex justify-end space-x-3">
<button id="cancelExport" class="px-4 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200">
Abbrechen
</button>
<button id="confirmExport" class="export-button">
Export starten
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
/**
* MYP Analytics Dashboard
* Erweiterte Analytik und Statistiken
*/
class AnalyticsDashboard {
constructor() {
this.currentTimeRange = 'month';
this.currentReportType = 'comprehensive';
this.charts = {};
this.data = {};
this.init();
}
init() {
this.setupEventListeners();
this.loadInitialData();
}
setupEventListeners() {
// Filter-Änderungen
document.getElementById('timeRangeSelect').addEventListener('change', (e) => {
this.currentTimeRange = e.target.value;
this.loadData();
});
document.getElementById('reportTypeSelect').addEventListener('change', (e) => {
this.currentReportType = e.target.value;
this.loadData();
});
// Aktionen
document.getElementById('refreshData').addEventListener('click', () => {
this.loadData();
});
document.getElementById('exportReport').addEventListener('click', () => {
this.showExportModal();
});
// Export Modal
document.getElementById('cancelExport').addEventListener('click', () => {
this.hideExportModal();
});
document.getElementById('confirmExport').addEventListener('click', () => {
this.exportReport();
});
// Modal schließen bei Klick außerhalb
document.getElementById('exportModal').addEventListener('click', (e) => {
if (e.target.id === 'exportModal') {
this.hideExportModal();
}
});
}
async loadInitialData() {
this.showLoading();
await this.loadData();
this.hideLoading();
}
async loadData() {
try {
this.showLoading();
// KPIs laden
await this.loadKPIs();
// Report-Daten laden
await this.loadReportData();
// Charts aktualisieren
this.updateCharts();
} catch (error) {
console.error('Fehler beim Laden der Analytics-Daten:', error);
this.showError('Fehler beim Laden der Daten');
} finally {
this.hideLoading();
}
}
async loadKPIs() {
try {
const response = await fetch('/api/analytics/dashboard');
if (!response.ok) throw new Error('Failed to load KPIs');
const data = await response.json();
this.renderKPIs(data.kpis || []);
} catch (error) {
console.error('Fehler beim Laden der KPIs:', error);
}
}
async loadReportData() {
try {
const response = await fetch(`/api/analytics/report/${this.currentReportType}?time_range=${this.currentTimeRange}`);
if (!response.ok) throw new Error('Failed to load report data');
this.data = await response.json();
this.renderStatistics();
} catch (error) {
console.error('Fehler beim Laden der Report-Daten:', error);
}
}
renderKPIs(kpis) {
const container = document.querySelector('#kpiDashboard .grid');
container.innerHTML = kpis.map(kpi => `
<div class="analytics-card bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-slate-600 dark:text-slate-400">${kpi.name}</h4>
<span class="kpi-trend-${kpi.trend}">
${kpi.trend === 'up' ? '↗️' : kpi.trend === 'down' ? '↘️' : '➡️'}
</span>
</div>
<div class="kpi-metric text-slate-900 dark:text-white mb-1">
${this.formatMetric(kpi.current_value, kpi.unit)}
</div>
<div class="flex items-center justify-between text-xs">
<span class="text-slate-500 dark:text-slate-400">
Ziel: ${this.formatMetric(kpi.target_value, kpi.unit)}
</span>
<span class="kpi-trend-${kpi.trend} font-medium">
${kpi.change_percent > 0 ? '+' : ''}${kpi.change_percent}%
</span>
</div>
</div>
`).join('');
}
renderStatistics() {
// Drucker-Statistiken
if (this.data.sections?.printers) {
this.renderPrinterStats(this.data.sections.printers);
}
// Job-Statistiken
if (this.data.sections?.jobs) {
this.renderJobStats(this.data.sections.jobs);
}
// Benutzer-Statistiken
if (this.data.sections?.users) {
this.renderUserStats(this.data.sections.users);
}
}
renderPrinterStats(printerData) {
const container = document.getElementById('printerStatsContainer');
const summary = printerData.summary;
container.innerHTML = `
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400">
${summary.total_printers}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Drucker gesamt</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600 dark:text-green-400">
${summary.online_printers}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Online</div>
</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-slate-900 dark:text-white">
${summary.availability_rate}% Verfügbarkeit
</div>
<div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2 mt-2">
<div class="bg-green-500 h-2 rounded-full" style="width: ${summary.availability_rate}%"></div>
</div>
</div>
</div>
`;
}
renderJobStats(jobData) {
const container = document.getElementById('jobStatsContainer');
const summary = jobData.summary;
container.innerHTML = `
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400">
${summary.total_jobs}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Jobs gesamt</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600 dark:text-green-400">
${summary.success_rate}%
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Erfolgsrate</div>
</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-slate-900 dark:text-white">
${summary.avg_duration_hours}h Durchschnitt
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">
🎯 ${summary.completed_jobs} abgeschlossen
</div>
</div>
</div>
`;
}
renderUserStats(userData) {
const container = document.getElementById('userStatsContainer');
const summary = userData.summary;
container.innerHTML = `
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400">
${summary.total_users}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Benutzer gesamt</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600 dark:text-green-400">
${summary.active_users}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Aktiv</div>
</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-slate-900 dark:text-white">
${summary.engagement_rate}% Engagement
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">
${summary.new_users} neue Benutzer
</div>
</div>
</div>
`;
// Top-Benutzer rendern
if (userData.top_users) {
this.renderTopUsers(userData.top_users);
}
}
renderTopUsers(topUsers) {
const container = document.getElementById('topUsersContainer');
container.innerHTML = `
<div class="space-y-3">
${topUsers.slice(0, 5).map((user, index) => `
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center text-white text-sm font-bold">
${index + 1}
</div>
<div>
<div class="font-medium text-slate-900 dark:text-white">${user.name || user.username}</div>
<div class="text-xs text-slate-600 dark:text-slate-400">${user.jobs} Jobs</div>
</div>
</div>
<div class="text-sm font-medium text-slate-600 dark:text-slate-400">
${user.total_hours}h
</div>
</div>
`).join('')}
</div>
`;
}
updateCharts() {
this.updateTrendChart();
this.updateUtilizationChart();
}
updateTrendChart() {
const canvas = document.getElementById('trendChartCanvas');
const ctx = canvas.getContext('2d');
// Destroy existing chart
if (this.charts.trend) {
this.charts.trend.destroy();
}
// Sample data - would be replaced with real data
const dailyTrend = this.data.sections?.jobs?.daily_trend || [];
this.charts.trend = new Chart(ctx, {
type: 'line',
data: {
labels: dailyTrend.map(d => new Date(d.date).toLocaleDateString('de-DE')),
datasets: [{
label: 'Jobs pro Tag',
data: dailyTrend.map(d => d.jobs),
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
updateUtilizationChart() {
const canvas = document.getElementById('utilizationChartCanvas');
const ctx = canvas.getContext('2d');
// Destroy existing chart
if (this.charts.utilization) {
this.charts.utilization.destroy();
}
const printerUsage = this.data.sections?.printers?.usage_by_printer || [];
this.charts.utilization = new Chart(ctx, {
type: 'doughnut',
data: {
labels: printerUsage.map(p => p.name),
datasets: [{
data: printerUsage.map(p => p.utilization_rate),
backgroundColor: [
'#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
formatMetric(value, unit) {
if (typeof value !== 'number') return value;
if (unit === '%') {
return `${value.toFixed(1)}%`;
} else if (unit === 'Stunden') {
return `${value.toFixed(1)}h`;
} else if (unit === 'g') {
return `${value.toLocaleString()}g`;
} else {
return `${value.toLocaleString()} ${unit}`;
}
}
showExportModal() {
document.getElementById('exportModal').classList.remove('hidden');
}
hideExportModal() {
document.getElementById('exportModal').classList.add('hidden');
}
async exportReport() {
try {
const format = document.getElementById('exportFormat').value;
const response = await fetch(`/api/analytics/report/${this.currentReportType}?time_range=${this.currentTimeRange}&format=${format}`);
if (!response.ok) throw new Error('Export fehlgeschlagen');
// Download starten
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `myp-analytics-${this.currentReportType}-${this.currentTimeRange}.${format}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
this.hideExportModal();
this.showSuccess('Report erfolgreich exportiert');
} catch (error) {
console.error('Export-Fehler:', error);
this.showError('Fehler beim Exportieren des Reports');
}
}
showLoading() {
document.getElementById('loadingIndicator').classList.remove('hidden');
}
hideLoading() {
document.getElementById('loadingIndicator').classList.add('hidden');
}
showError(message) {
if (window.showToast) {
window.showToast(message, 'error');
} else {
alert(message);
}
}
showSuccess(message) {
if (window.showToast) {
window.showToast(message, 'success');
} else {
alert(message);
}
}
}
// Dashboard initialisieren
document.addEventListener('DOMContentLoaded', () => {
new AnalyticsDashboard();
});
</script>
{% endblock %}

712
backend/templates/base.html Normal file
View File

@ -0,0 +1,712 @@
<!DOCTYPE html>
<html lang="de" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0">
<meta name="description" content="MYP Platform - Mercedes-Benz 3D Druck Management System">
<meta name="robots" content="noindex, nofollow">
<!-- Dynamic theme-color meta tags for browser UI -->
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000">
<meta name="theme-color" id="metaThemeColor" content="#000000">
<!-- CSRF-Token für Formulare -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Title -->
<title>{% block title %}MYP Platform - Mercedes-Benz{% endblock %}</title>
<!-- PWA Manifest -->
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='icons/favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='icons/favicon-16x16.png') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='icons/apple-touch-icon.png') }}">
<link rel="mask-icon" href="{{ url_for('static', filename='favicon.svg') }}" color="#000000">
<!-- CSS -->
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/components.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/professional-theme.css') }}" rel="stylesheet">
<!-- Preload critical resources -->
<link rel="preload" href="{{ url_for('static', filename='js/ui-components.js') }}" as="script">
<link rel="preload" href="{{ url_for('static', filename='js/offline-app.js') }}" as="script">
<link rel="preload" href="{{ url_for('static', filename='js/optimization-features.js') }}" as="script">
<!-- Additional CSS -->
{% block extra_css %}{% endblock %}
<!-- Dark Mode Script (must be in head to prevent flash) -->
<script>
// Temporär für sofortige Anwendung ohne Flackern
document.documentElement.classList.add('disable-transitions');
const STORAGE_KEY = 'myp-dark-mode';
const savedMode = localStorage.getItem(STORAGE_KEY);
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = savedMode === 'true' || (savedMode === null && prefersDark);
// Dark Mode sofort anwenden
if (isDark) {
document.documentElement.classList.add('dark');
document.documentElement.setAttribute('data-theme', 'dark');
document.documentElement.style.colorScheme = 'dark';
} else {
document.documentElement.classList.remove('dark');
document.documentElement.setAttribute('data-theme', 'light');
document.documentElement.style.colorScheme = 'light';
}
// ThemeColor Meta-Tag aktualisieren
const metaThemeColor = document.getElementById('metaThemeColor');
if (metaThemeColor) {
metaThemeColor.setAttribute('content', isDark ? '#000000' : '#ffffff');
}
// Übergänge nach kurzer Verzögerung wieder aktivieren
setTimeout(function() {
document.documentElement.classList.remove('disable-transitions');
}, 10);
// Diese Funktion wird nach dem DOM-Laden ausgeführt
document.addEventListener('DOMContentLoaded', function() {
// Dark Mode Toggle Button Icons aktualisieren
const darkModeToggle = document.getElementById('darkModeToggle');
if (darkModeToggle) {
const sunIcon = darkModeToggle.querySelector('.sun-icon');
const moonIcon = darkModeToggle.querySelector('.moon-icon');
if (isDark) {
darkModeToggle.setAttribute('aria-pressed', 'true');
darkModeToggle.setAttribute('title', 'Light Mode aktivieren');
if (sunIcon) sunIcon.classList.add('hidden');
if (moonIcon) moonIcon.classList.remove('hidden');
} else {
darkModeToggle.setAttribute('aria-pressed', 'false');
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
if (sunIcon) sunIcon.classList.remove('hidden');
if (moonIcon) moonIcon.classList.add('hidden');
}
}
});
</script>
<!-- Disable Transitions Styling -->
<style>
.disable-transitions,
.disable-transitions * {
transition: none !important;
}
/* ===== ULTRA-DEZENTE SCROLLBALKEN ===== */
/* Webkit-Browser (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 3px;
transition: all 0.3s ease;
}
/* Nur bei Hover über scrollbaren Container sichtbar */
*:hover::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.05);
}
*:hover::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.1);
}
/* Dark Mode - noch dezenter */
.dark *:hover::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.03);
}
.dark *:hover::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.08);
}
/* Firefox - ultra-thin */
* {
scrollbar-width: none; /* Komplett versteckt in Firefox */
}
/* Nur bei Hover sichtbar in Firefox */
*:hover {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.05) transparent;
}
.dark *:hover {
scrollbar-color: rgba(255, 255, 255, 0.03) transparent;
}
/* Spezielle Container die scrollbar brauchen */
.modal-content::-webkit-scrollbar,
.dropdown-menu::-webkit-scrollbar {
width: 4px;
}
.modal-content::-webkit-scrollbar-thumb,
.dropdown-menu::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
}
.dark .modal-content::-webkit-scrollbar-thumb,
.dark .dropdown-menu::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
}
</style>
{% block head %}{% endblock %}
</head>
<body class="min-h-screen flex flex-col bg-white text-slate-900 dark:bg-black dark:text-white transition-colors duration-300 text-base mercedes-background">
<!-- Navigation -->
<nav class="navbar">
<div class="max-w-7xl mx-auto px-3 sm:px-4 lg:px-6">
<div class="flex items-center justify-between h-16 sm:h-18 lg:h-20">
<!-- Brand Section -->
<div class="flex-shrink-0">
<a href="{{ url_for('dashboard') }}" class="navbar-brand group" aria-label="Zur Startseite">
<!-- Mercedes-Benz Logo -->
<div class="w-5 h-5 sm:w-6 sm:h-6 lg:w-7 lg:h-7 transition-all duration-300 group-hover:rotate-180">
<svg class="w-full h-full text-slate-900 dark:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 80 80" aria-hidden="true">
<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>
<!-- Brand Text -->
<div class="flex flex-col ml-3">
<span class="text-sm lg:text-base font-bold text-slate-900 dark:text-white transition-colors duration-300 tracking-tight">Mercedes-Benz</span>
<span class="text-xs font-medium text-slate-600 dark:text-slate-400 transition-colors duration-300">MYP 3D-Druck Platform -</span>
</div>
</a>
</div>
<!-- Desktop Navigation Menu - überarbeitetes Design -->
<div class="hidden lg:flex flex-1 justify-center">
<nav class="navbar-menu-new" role="navigation" aria-label="Hauptnavigation">
<a href="{{ url_for('dashboard') }}"
class="nav-item {{ 'active' if request.endpoint == 'dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"/>
</svg>
<span>Dashboard</span>
</a>
<a href="{{ url_for('printers_page') }}"
class="nav-item {{ 'active' if request.endpoint == 'printers_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
<span>3D-Drucker</span>
</a>
<a href="{{ url_for('jobs_page') }}"
class="nav-item {{ 'active' if request.endpoint == 'jobs_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
<span>Druckaufträge</span>
</a>
<a href="{{ url_for('stats_page') }}"
class="nav-item {{ 'active' if request.endpoint == 'stats_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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>
<span>Statistiken</span>
</a>
<a href="{{ url_for('calendar.calendar_view') }}"
class="nav-item {{ 'active' if request.endpoint == 'calendar.calendar_view' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>Schichtplan</span>
</a>
<a href="{{ url_for('guest.guest_request_form') }}"
class="nav-item {{ 'active' if request.endpoint == 'guest.guest_request_form' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
<span>Gastanfrage</span>
</a>
<a href="{{ url_for('guest.guest_requests_overview') }}"
class="nav-item {{ 'active' if request.endpoint == 'guest.guest_requests_overview' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
<span>Anträge Übersicht</span>
</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin_page') }}"
class="nav-item {{ 'active' if request.endpoint == 'admin_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span>Administration</span>
</a>
{% endif %}
</nav>
</div>
<!-- Rechte Seite der Navbar -->
<div class="flex items-center space-x-2 sm:space-x-3">
<!-- Mobile Menu Toggle (neu) -->
<button
id="mobileMenuToggle"
class="lg:hidden p-2 rounded-full text-slate-700 dark:text-slate-300 hover:bg-slate-100/80 dark:hover:bg-slate-800/50"
aria-label="Menü öffnen"
aria-expanded="false"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<!-- Dark Mode Toggle - Moderner, schöner Design -->
<div class="relative">
<button
id="darkModeToggle"
class="dark-mode-toggle-premium group"
aria-label="Dark Mode umschalten"
data-action="toggle-dark-mode"
title="Design wechseln"
>
<!-- Hintergrund-Ring für besseren visuellen Effekt -->
<div class="absolute inset-0 rounded-full bg-gradient-to-r from-amber-300 to-orange-400 dark:from-blue-400 dark:to-purple-500 opacity-0 group-hover:opacity-100 blur-sm transition-all duration-500"></div>
<!-- Haupt-Container -->
<div class="relative flex items-center justify-center w-11 h-11 rounded-full bg-white/80 dark:bg-slate-800/80 backdrop-blur-md border border-slate-200/50 dark:border-slate-600/50 shadow-lg dark:shadow-slate-900/20 transition-all duration-300 group-hover:scale-105 group-active:scale-95">
<!-- Sonnen-Icon (Light Mode) -->
<div class="sun-icon absolute inset-0 flex items-center justify-center transition-all duration-500 ease-in-out opacity-100 dark:opacity-0 scale-100 dark:scale-75 rotate-0 dark:rotate-90">
<svg class="w-5 h-5 text-amber-500 drop-shadow-sm" fill="currentColor" viewBox="0 0 24 24" style="margin: auto;">
<path d="M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z"/>
</svg>
</div>
<!-- Mond-Icon (Dark Mode) -->
<div class="moon-icon absolute inset-0 flex items-center justify-center transition-all duration-500 ease-in-out opacity-0 dark:opacity-100 scale-75 dark:scale-100 rotate-90 dark:rotate-0">
<svg class="w-5 h-5 text-blue-400 drop-shadow-sm" fill="currentColor" viewBox="0 0 24 24" style="margin: auto;">
<path fill-rule="evenodd" d="M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z" clip-rule="evenodd"/>
</svg>
</div>
<!-- Animierte Punkte für zusätzlichen visuellen Effekt -->
<div class="absolute -inset-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div class="animate-pulse absolute top-0 left-1/2 w-1 h-1 bg-amber-400 dark:bg-blue-400 rounded-full transform -translate-x-1/2 -translate-y-1"></div>
<div class="animate-pulse absolute bottom-0 left-1/2 w-1 h-1 bg-amber-400 dark:bg-blue-400 rounded-full transform -translate-x-1/2 translate-y-1 animation-delay-500"></div>
<div class="animate-pulse absolute top-1/2 left-0 w-1 h-1 bg-amber-400 dark:bg-blue-400 rounded-full transform -translate-y-1/2 -translate-x-1 animation-delay-1000"></div>
<div class="animate-pulse absolute top-1/2 right-0 w-1 h-1 bg-amber-400 dark:bg-blue-400 rounded-full transform -translate-y-1/2 translate-x-1 animation-delay-1500"></div>
</div>
</div>
</button>
<!-- Tooltip -->
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-slate-900 dark:bg-slate-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-50">
<span class="dark:hidden">Dark Mode aktivieren</span>
<span class="hidden dark:inline">Light Mode aktivieren</span>
<div class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900 dark:border-t-slate-700"></div>
</div>
</div>
{% if current_user.is_authenticated %}
<!-- Benachrichtigungen - kompakteres Design -->
<div class="relative">
<button
id="notificationToggle"
class="relative p-1.5 rounded-full text-slate-700 dark:text-slate-300 hover:bg-slate-100/80 dark:hover:bg-slate-800/50 transition-all duration-200"
aria-label="Benachrichtigungen anzeigen"
title="Benachrichtigungen"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
<!-- Badge für ungelesene Benachrichtigungen -->
<span id="notificationBadge" class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center font-medium hidden">
0
</span>
</button>
<!-- Benachrichtigungs-Dropdown -->
<div id="notificationDropdown" class="absolute right-0 mt-2 w-72 sm:w-80 bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-slate-200 dark:border-slate-600 z-50 hidden">
<div class="p-3 border-b border-slate-200 dark:border-slate-600">
<h3 class="text-base font-semibold text-slate-900 dark:text-white">Benachrichtigungen</h3>
</div>
<div id="notificationList" class="max-h-80 overflow-y-auto">
<div class="p-3 text-center text-slate-500 dark:text-slate-400 text-sm">
Keine neuen Benachrichtigungen
</div>
</div>
<div class="p-2 border-t border-slate-200 dark:border-slate-600">
<button id="markAllRead" class="w-full text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 transition-colors">
Alle als gelesen markieren
</button>
</div>
</div>
</div>
<!-- User Profile Dropdown - kompakteres Design -->
<div class="relative" id="user-menu-container">
<button
id="user-menu-button"
class="flex items-center space-x-1 rounded-full p-1 text-slate-700 dark:text-slate-300 hover:bg-slate-100/80 dark:hover:bg-slate-800/50 transition-all duration-200"
aria-expanded="false"
aria-haspopup="true"
aria-label="Benutzermenu öffnen"
>
<!-- Profile Avatar -->
<div class="w-6 h-6 rounded-full bg-blue-500 flex items-center justify-center text-white text-xs font-medium">
{{ current_user.email[0].upper() if current_user.email else 'U' }}
</div>
<!-- User Info (nur auf größeren Geräten) -->
<div class="hidden sm:block text-left ml-1">
<div class="text-xs font-medium text-slate-900 dark:text-white transition-colors duration-300">{{ current_user.email.split('@')[0] if current_user.email else 'Benutzer' }}</div>
</div>
</button>
<!-- User Menu Dropdown -->
<div id="user-dropdown" class="absolute right-0 mt-2 w-64 bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-slate-200 dark:border-slate-600 z-50 hidden origin-top-right">
<!-- User Info Header -->
<div class="px-4 py-3 border-b border-slate-200 dark:border-slate-600">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-medium">
{{ current_user.email[0].upper() if current_user.email else 'U' }}
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-slate-900 dark:text-white truncate">
{{ current_user.full_name if current_user.full_name else current_user.email.split('@')[0] if current_user.email else 'Benutzer' }}
</p>
<p class="text-xs text-slate-500 dark:text-slate-400 truncate">
{{ current_user.email if current_user.email else 'Keine E-Mail' }}
</p>
{% if current_user.is_admin %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200 mt-1">
Administrator
</span>
{% endif %}
</div>
</div>
</div>
<!-- Menu Items -->
<div class="py-1">
<a href="{{ url_for('user_profile') }}"
class="flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
Mein Profil
</a>
<a href="{{ url_for('user_settings') }}"
class="flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Einstellungen
</a>
<a href="{{ url_for('jobs_page') }}"
class="flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
Neuer Auftrag
</a>
<a href="{{ url_for('stats_page') }}"
class="flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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>
Analytik
</a>
</div>
<div class="border-t border-slate-200 dark:border-slate-600">
<a href="{{ url_for('privacy') }}"
class="flex items-center px-4 py-2 text-sm text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
Datenschutz
</a>
<a href="{{ url_for('terms') }}"
class="flex items-center px-4 py-2 text-sm text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
Nutzungsbedingungen
</a>
<button onclick="handleLogout()"
class="flex items-center w-full px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors duration-200">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>
Abmelden
</button>
</div>
</div>
</div>
{% else %}
<!-- Login Button - kompakteres Design -->
<a href="{{ url_for('login') }}"
class="flex items-center space-x-1 py-1 px-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white text-xs transition-colors duration-200">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>
<span class="hidden sm:inline">Anmelden</span>
</a>
{% endif %}
</div>
</div>
</div>
</nav>
<!-- Mobile Menu (neu) -->
<div id="mobileMenu" class="mobile-menu-new hidden lg:hidden">
<nav class="flex flex-col space-y-1 px-3 py-4" role="navigation" aria-label="Mobile Navigation">
<a href="{{ url_for('dashboard') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"/>
</svg>
<span>Dashboard</span>
</a>
<a href="{{ url_for('printers_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'printers_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
<span>3D-Drucker</span>
</a>
<a href="{{ url_for('jobs_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'jobs_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
<span>Druckaufträge</span>
</a>
<a href="{{ url_for('stats_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'stats_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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>
<span>Statistiken</span>
</a>
<a href="{{ url_for('calendar.calendar_view') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'calendar.calendar_view' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>Schichtplan</span>
</a>
<a href="{{ url_for('guest.guest_request_form') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'guest.guest_request_form' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
<span>Gastanfrage</span>
</a>
<a href="{{ url_for('guest.guest_requests_overview') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'guest.guest_requests_overview' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
<span>Anträge Übersicht</span>
</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'admin_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span>Administration</span>
</a>
{% endif %}
</nav>
</div>
<!-- Main Content -->
<main id="main-content" class="flex-grow max-w-7xl w-full mx-auto px-3 sm:px-6 lg:px-8 py-4 sm:py-8">
<div class="container mx-auto px-4 py-8">
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages mb-4">
{% for category, message in messages %}
<div class="alert alert-{{ category }} mb-2">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</main>
<!-- Mercedes-Benz Footer -->
<footer class="bg-white dark:bg-black border-t border-gray-200 dark:border-slate-700 mt-auto transition-colors duration-300">
<div class="max-w-screen-xl w-full mx-auto px-3 sm:px-6 lg:px-8">
<div class="py-4 sm:py-8 border-t border-gray-200 dark:border-slate-700 mt-8 sm:mt-12 pt-4 sm:pt-8">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6 sm:gap-8">
<!-- Brand Section - Stack on mobile -->
<div class="flex flex-col space-y-4">
<div class="flex items-center gap-3">
<div class="w-6 h-6">
<svg class="w-full h-full text-slate-900 dark:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 80 80">
<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>
<div>
<div class="text-base font-bold text-slate-900 dark:text-white transition-colors duration-300">Mercedes-Benz</div>
<div class="text-xs text-slate-600 dark:text-slate-400 transition-colors duration-300">MYP Platform</div>
</div>
</div>
<p class="text-xs text-slate-600 dark:text-slate-400 leading-relaxed transition-colors duration-300">
Das Beste oder nichts - Professionelles 3D-Druck Management für Mercedes-Benz.
</p>
</div>
<!-- System Info -->
<div class="flex flex-col space-y-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white transition-colors duration-300">System</h3>
<div class="space-y-2 text-xs text-slate-600 dark:text-slate-400 leading-relaxed transition-colors duration-300">
<div class="flex justify-between">
<span>Version:</span>
<span class="text-slate-900 dark:text-white font-medium transition-colors duration-300">3.0.0</span>
</div>
<div class="flex justify-between">
<span>Status:</span>
<div id="connection-status" class="flex items-center space-x-1">
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span class="text-green-500 dark:text-green-400 font-medium transition-colors duration-300">Online</span>
</div>
</div>
</div>
</div>
<!-- Copyright -->
<div class="flex flex-col space-y-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white transition-colors duration-300">Rechtliches</h3>
<div class="space-y-2 text-xs text-slate-600 dark:text-slate-400 leading-relaxed transition-colors duration-300">
<p>© 2024 Mercedes-Benz Group AG</p>
<p>Alle Rechte vorbehalten.</p>
<p class="text-xs text-slate-500 dark:text-slate-500 transition-colors duration-300">
Entwickelt für interne Mercedes-Benz Anwendungen
</p>
</div>
</div>
</div>
</div>
</div>
</footer>
<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/debug-fix.js') }}"></script>
<script src="{{ url_for('static', filename='js/ui-components.js') }}"></script>
<script src="{{ url_for('static', filename='js/job-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/dark-mode-fix.js') }}"></script>
<script src="{{ url_for('static', filename='js/optimization-features.js') }}"></script>
<script src="{{ url_for('static', filename='js/global-refresh-functions.js') }}"></script>
<script src="{{ url_for('static', filename='js/event-handlers.js') }}"></script>
<script src="{{ url_for('static', filename='js/csp-violation-handler.js') }}"></script>
<script src="{{ url_for('static', filename='js/printer_monitor.js') }}"></script>
{% if current_user.is_authenticated %}
<script src="{{ url_for('static', filename='js/notifications.js') }}"></script>
<script src="{{ url_for('static', filename='js/session-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/auto-logout.js') }}"></script>
{% endif %}
<!-- Additional JavaScript Functions -->
<script>
/**
* Logout-Handler für sicheres Abmelden
*/
function handleLogout() {
// Bestätigung abfragen
if (confirm('Möchten Sie sich wirklich abmelden?')) {
// Loading-Animation anzeigen
document.body.style.opacity = '0.7';
document.body.style.pointerEvents = 'none';
// CSRF-Token aus Meta-Tag holen
const csrfToken = document.querySelector('meta[name="csrf-token"]');
// Logout-Formular erstellen und absenden
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("auth_logout") }}';
form.style.display = 'none';
// CSRF-Token hinzufügen falls verfügbar
if (csrfToken) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'csrf_token';
input.value = csrfToken.getAttribute('content');
form.appendChild(input);
}
// Formular absenden
document.body.appendChild(form);
form.submit();
}
}
/**
* Initialisierung aller UI-Komponenten nach DOM-Load
*/
document.addEventListener('DOMContentLoaded', function() {
// MYP App für Offline-Funktionalität initialisieren
if (typeof MYPApp !== 'undefined') {
window.mypApp = new MYPApp();
}
console.log('🚀 MYP Platform UI erfolgreich initialisiert');
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,952 @@
{% extends "base.html" %}
{% block title %}Dashboard - Mercedes-Benz MYP Platform{% endblock %}
{% block extra_css %}
<style>
/* Professional Mercedes-Benz Dashboard Styles */
.mb-dashboard-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 16px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.dark .mb-dashboard-card {
background: var(--mb-black, #000000); /* Schwarzer Hintergrund im Dark Mode */
border-color: var(--border-primary, #334155);
}
.mb-dashboard-card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.dark .mb-dashboard-card:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
}
.mb-stat-card {
background: linear-gradient(135deg, #f0f9ff 0%, #e6f2ff 100%);
color: #0f172a;
position: relative;
overflow: hidden;
border: none;
}
.dark .mb-stat-card {
background: linear-gradient(135deg, #000000 0%, #111827 100%); /* Mercedes Schwarz */
color: var(--text-primary, #f8fafc);
}
.mb-stat-card::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(0, 114, 206, 0.1) 0%, transparent 70%);
animation: pulse-glow 4s ease-in-out infinite;
}
.dark .mb-stat-card::before {
background: radial-gradient(circle, rgba(255, 255, 255, 0.05) 0%, transparent 70%); /* Subtle glow for dark */
}
@keyframes pulse-glow {
0%, 100% { opacity: 0.5; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.05); }
}
.mb-stat-icon {
background: rgba(0, 0, 0, 0.1); /* Schwarz statt Blau */
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 10;
transition: all 0.3s ease;
}
.dark .mb-stat-icon {
background: rgba(255, 255, 255, 0.1); /* Lighter icon background for contrast */
}
.mb-stat-card:hover .mb-stat-icon {
transform: scale(1.1);
background: rgba(0, 0, 0, 0.2); /* Schwarz statt Blau */
}
.dark .mb-stat-card:hover .mb-stat-icon {
background: rgba(255, 255, 255, 0.15);
}
.mb-status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
position: relative;
display: inline-block;
}
.mb-status-indicator::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
animation: status-pulse 2s infinite;
}
.mb-status-online {
background: #10b981;
}
.mb-status-online::after {
background: #10b981;
}
.mb-status-busy {
background: #f59e0b;
}
.mb-status-busy::after {
background: #f59e0b;
}
.mb-status-offline {
background: #ef4444;
}
.mb-status-offline::after {
background: #ef4444;
}
.mb-status-idle {
background: #6b7280;
}
.mb-status-idle::after {
background: #6b7280;
}
@keyframes status-pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.5; }
100% { transform: scale(2); opacity: 0; }
}
.mb-progress-container {
background: #f3f4f6;
height: 8px;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.dark .mb-progress-container {
background: #374151;
}
.mb-progress-bar {
background: linear-gradient(90deg, #000000 0%, #333333 100%); /* Schwarz statt Blau */
height: 100%;
border-radius: 4px;
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.dark .mb-progress-bar {
background: linear-gradient(90deg, #f3f4f6 0%, #e5e7eb 100%);
}
.mb-progress-bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: progress-shine 2s infinite;
}
@keyframes progress-shine {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.mb-activity-item {
border-left: 4px solid #000000; /* Schwarz statt Blau */
background: linear-gradient(90deg, rgba(0, 0, 0, 0.05) 0%, transparent 100%); /* Schwarz statt Blau */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 0 8px 8px 0;
}
.dark .mb-activity-item {
border-left-color: #f3f4f6;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.02) 0%, transparent 100%);
}
/* Light/Dark Mode compatible cards */
.glass-card-light {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(229, 231, 235, 0.8);
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.dark .glass-card-light {
background: rgba(0, 0, 0, 0.8);
border-color: rgba(100, 116, 139, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
.section-title {
color: #1e293b;
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
}
.dark .section-title {
color: #ffffff;
}
.stat-value {
color: #0f172a;
font-weight: 700;
font-size: 1.5rem;
}
.dark .stat-value {
color: #ffffff;
}
.stat-label {
color: #64748b;
font-size: 0.875rem;
}
.dark .stat-label {
color: #94a3b8;
}
</style>
{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Dashboard Header Card -->
<div class="dashboard-card p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="flex items-center gap-6">
<div class="w-16 h-16 flex-shrink-0">
<svg class="w-full h-full text-slate-900 dark:text-white" fill="currentColor" viewBox="0 0 80 80" aria-hidden="true">
<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>
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Dashboard</h1>
<p class="text-slate-500 dark:text-slate-400 mt-1">Übersicht über Ihre 3D-Druck Aktivitäten</p>
</div>
</div>
<div class="flex flex-wrap gap-3">
<button id="refreshDashboard"
class="btn-secondary flex items-center gap-2">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<span>Aktualisieren</span>
</button>
<a href="{{ url_for('jobs_page') }}"
class="btn-primary flex items-center gap-2">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<span>Neuer Auftrag</span>
</a>
<a href="{{ url_for('guest.guest_requests_overview') }}"
class="btn-secondary flex items-center gap-2">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
<span>Anträge Übersicht</span>
</a>
</div>
</div>
</div>
<!-- Stats Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Active Jobs Card -->
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Aktive Aufträge</h3>
<div class="stat-value">{{ active_jobs_count }}</div>
</div>
<div class="mb-stat-icon text-slate-900 dark:text-white">
<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="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
</div>
</div>
</div>
<!-- Available Printers Card -->
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Verfügbare Drucker</h3>
<div class="stat-value">{{ available_printers_count }}</div>
</div>
<div class="mb-stat-icon text-green-600 dark:text-green-400">
<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="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
</div>
</div>
</div>
<!-- Total Jobs Card -->
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Aufträge (gesamt)</h3>
<div class="stat-value">{{ total_jobs_count }}</div>
</div>
<div class="mb-stat-icon text-purple-600 dark:text-purple-400">
<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="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 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>
</div>
</div>
</div>
<!-- Success Rate Card -->
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Erfolgsrate</h3>
<div class="stat-value">{{ success_rate }}%</div>
</div>
<div class="mb-stat-icon text-amber-600 dark:text-amber-400">
<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="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
</div>
</div>
</div>
</div>
<!-- Active Jobs Section -->
<div class="dashboard-card p-6">
<h2 class="section-title">Aktuelle Druckaufträge</h2>
<div class="overflow-hidden">
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
<thead>
<tr class="text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
<th class="px-6 py-3">Status</th>
<th class="px-6 py-3">Auftrag</th>
<th class="px-6 py-3">Drucker</th>
<th class="px-6 py-3">Startzeit</th>
<th class="px-6 py-3">Fortschritt</th>
<th class="px-6 py-3">Aktionen</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
{% if active_jobs and active_jobs|length > 0 %}
{% for job in active_jobs %}
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="mb-status-indicator {{ job.status_class }}"></div>
<span class="ml-2 text-sm font-medium text-slate-700 dark:text-slate-300">
{{ job.status_text }}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-slate-900 dark:text-white">{{ job.name }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">{{ job.file_name }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-slate-700 dark:text-slate-300">{{ job.printer }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-slate-700 dark:text-slate-300">{{ job.start_time }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="mb-progress-container">
<div class="mb-progress-bar" style="width: {{ job.progress }}%"></div>
</div>
<div class="text-xs text-right mt-1 text-slate-500 dark:text-slate-400">{{ job.progress }}%</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<a href="{{ url_for('job_detail', job_id=job.id) }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 font-medium">Details</a>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6" class="px-6 py-8 text-center">
<div class="text-slate-500 dark:text-slate-400">
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine aktiven Druckaufträge</p>
<p class="text-slate-500 dark:text-slate-400 text-sm">Starten Sie einen neuen Druckauftrag, um ihn hier zu sehen.</p>
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="mt-4 text-right">
<a href="{{ url_for('jobs_page') }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 text-sm font-medium">
Alle Druckaufträge anzeigen →
</a>
</div>
</div>
<!-- Last Row: Printer Status and Recent Activity -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Available Printers -->
<div class="dashboard-card p-6">
<h2 class="section-title">Druckerstatus</h2>
<div class="space-y-4">
{% if printers and printers|length > 0 %}
{% for printer in printers %}
<div class="flex items-center justify-between p-4 rounded-xl bg-gray-50 dark:bg-slate-700/30">
<div class="flex items-center">
<div class="mb-status-indicator {{ printer.status_class }} mr-3"></div>
<div>
<div class="text-sm font-medium text-slate-900 dark:text-white">{{ printer.name }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">{{ printer.model }}</div>
</div>
</div>
<div class="text-right">
<div class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ printer.status_text }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">{{ printer.location }}</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-8">
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine Drucker gefunden</p>
<p class="text-slate-500 dark:text-slate-400 text-sm">Prüfen Sie die Verbindung oder fügen Sie Drucker hinzu.</p>
</div>
{% endif %}
</div>
<div class="mt-4 text-right">
<a href="{{ url_for('printers_page') }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 text-sm font-medium">
Alle Drucker anzeigen →
</a>
</div>
</div>
<!-- Recent Activity -->
<div class="dashboard-card p-6">
<h2 class="section-title">Letzte Aktivitäten</h2>
<div class="space-y-3">
{% if activities and activities|length > 0 %}
{% for activity in activities %}
<div class="mb-activity-item pl-4 py-3">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-slate-700 dark:text-slate-300">{{ activity.description }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">{{ activity.time }}</p>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-8">
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" 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>
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine Aktivitäten</p>
<p class="text-slate-500 dark:text-slate-400 text-sm">Ihre Aktivitäten werden hier angezeigt.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
class DashboardManager {
constructor() {
this.updateInterval = 30000; // 30 Sekunden
this.autoUpdateTimer = null;
this.isUpdating = false;
this.wsConnection = null;
this.init();
}
init() {
this.setupEventListeners();
this.setupAutoUpdate();
this.setupWebSocket();
this.animateCounters();
console.log('🚀 Dashboard Manager initialisiert');
}
setupEventListeners() {
// Refresh Button
const refreshBtn = document.getElementById('refreshDashboard');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.refreshDashboard();
});
}
// Job-Zeilen klickbar machen für Details
this.setupJobRowClicks();
// Drucker-Karten klickbar machen
this.setupPrinterClicks();
// Dark Mode Updates
window.addEventListener('darkModeChanged', (e) => {
this.updateThemeElements(e.detail.isDark);
});
// Visibility Change Detection für intelligente Updates
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.pauseAutoUpdate();
} else {
this.resumeAutoUpdate();
this.refreshDashboard(); // Einmalige Aktualisierung bei Rückkehr
}
});
}
setupJobRowClicks() {
const jobRows = document.querySelectorAll('tbody tr[data-job-id]');
jobRows.forEach(row => {
row.style.cursor = 'pointer';
row.addEventListener('click', () => {
const jobId = row.dataset.jobId;
if (jobId) {
window.location.href = `/jobs/${jobId}`;
}
});
// Hover-Effekt verstärken
row.addEventListener('mouseenter', () => {
row.style.transform = 'scale(1.01)';
row.style.transition = 'transform 0.2s ease';
});
row.addEventListener('mouseleave', () => {
row.style.transform = 'scale(1)';
});
});
}
setupPrinterClicks() {
const printerCards = document.querySelectorAll('[data-printer-id]');
printerCards.forEach(card => {
card.style.cursor = 'pointer';
card.addEventListener('click', () => {
const printerId = card.dataset.printerId;
if (printerId) {
window.location.href = `/printers/${printerId}`;
}
});
});
}
setupAutoUpdate() {
this.autoUpdateTimer = setInterval(() => {
if (!document.hidden && !this.isUpdating) {
this.updateDashboardData();
}
}, this.updateInterval);
}
pauseAutoUpdate() {
if (this.autoUpdateTimer) {
clearInterval(this.autoUpdateTimer);
this.autoUpdateTimer = null;
}
}
resumeAutoUpdate() {
if (!this.autoUpdateTimer) {
this.setupAutoUpdate();
}
}
setupWebSocket() {
// WebSocket für Real-time Updates
try {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/dashboard`;
this.wsConnection = new WebSocket(wsUrl);
this.wsConnection.onopen = () => {
console.log('📡 WebSocket Verbindung zu Dashboard hergestellt');
};
this.wsConnection.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleWebSocketUpdate(data);
} catch (error) {
console.error('Fehler beim Verarbeiten der WebSocket-Nachricht:', error);
}
};
this.wsConnection.onclose = () => {
console.log('📡 WebSocket Verbindung geschlossen - versuche Wiederverbindung in 5s');
setTimeout(() => {
this.setupWebSocket();
}, 5000);
};
this.wsConnection.onerror = (error) => {
console.error('WebSocket Fehler:', error);
};
} catch (error) {
console.log('WebSocket nicht verfügbar, verwende Polling-Updates');
}
}
handleWebSocketUpdate(data) {
console.log('📡 Erhaltenes WebSocket-Update:', data);
switch (data.type) {
case 'job_status_update':
this.updateJobStatus(data.job_id, data.status, data.progress);
break;
case 'printer_status_update':
this.updatePrinterStatus(data.printer_id, data.status);
break;
case 'new_activity':
this.addNewActivity(data.activity);
break;
case 'stats_update':
this.updateStats(data.stats);
break;
default:
console.log('Unbekannter WebSocket-Update-Typ:', data.type);
}
}
async refreshDashboard() {
if (this.isUpdating) return;
this.isUpdating = true;
const refreshBtn = document.getElementById('refreshDashboard');
try {
// Button-Status aktualisieren
if (refreshBtn) {
refreshBtn.disabled = true;
refreshBtn.innerHTML = `
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Aktualisiert...</span>
`;
}
// Vollständige Seitenaktualisierung
window.location.reload();
} catch (error) {
console.error('Fehler beim Aktualisieren:', error);
this.showToast('Fehler beim Aktualisieren des Dashboards', 'error');
} finally {
this.isUpdating = false;
// Button zurücksetzen
if (refreshBtn) {
refreshBtn.disabled = false;
refreshBtn.innerHTML = `
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<span>Aktualisieren</span>
`;
}
}
}
async updateDashboardData() {
if (this.isUpdating) return;
this.isUpdating = true;
try {
// Stille Hintergrund-Updates ohne vollständige Neuladen
const responses = await Promise.all([
fetch('/api/dashboard/stats'),
fetch('/api/dashboard/active-jobs'),
fetch('/api/dashboard/printers'),
fetch('/api/dashboard/activities')
]);
const [statsData, jobsData, printersData, activitiesData] = await Promise.all(
responses.map(r => r.json())
);
// UI-Updates
this.updateStats(statsData);
this.updateActiveJobs(jobsData);
this.updatePrinters(printersData);
this.updateActivities(activitiesData);
console.log('🔄 Dashboard-Daten erfolgreich aktualisiert');
} catch (error) {
console.error('Fehler beim Laden der Dashboard-Daten:', error);
// Fehlschlag stumm - verwende WebSocket oder warte auf nächste Aktualisierung
} finally {
this.isUpdating = false;
}
}
updateJobStatus(jobId, status, progress) {
const jobRow = document.querySelector(`tr[data-job-id="${jobId}"]`);
if (jobRow) {
// Status-Indikator aktualisieren
const statusIndicator = jobRow.querySelector('.mb-status-indicator');
const statusText = jobRow.querySelector('.text-sm.font-medium');
const progressBar = jobRow.querySelector('.mb-progress-bar');
const progressText = jobRow.querySelector('.text-xs.text-right');
if (statusIndicator) {
statusIndicator.className = `mb-status-indicator ${this.getStatusClass(status)}`;
}
if (statusText) {
statusText.textContent = this.getStatusText(status);
}
if (progressBar && progress !== undefined) {
progressBar.style.width = `${progress}%`;
}
if (progressText && progress !== undefined) {
progressText.textContent = `${progress}%`;
}
// Animation für Updates
jobRow.style.backgroundColor = 'rgba(59, 130, 246, 0.1)';
setTimeout(() => {
jobRow.style.backgroundColor = '';
}, 1000);
}
}
updatePrinterStatus(printerId, status) {
const printerCard = document.querySelector(`[data-printer-id="${printerId}"]`);
if (printerCard) {
const statusIndicator = printerCard.querySelector('.mb-status-indicator');
const statusText = printerCard.querySelector('.text-sm.font-medium:last-child');
if (statusIndicator) {
statusIndicator.className = `mb-status-indicator ${this.getStatusClass(status)}`;
}
if (statusText) {
statusText.textContent = this.getStatusText(status);
}
// Animation für Updates
printerCard.style.transform = 'scale(1.02)';
setTimeout(() => {
printerCard.style.transform = 'scale(1)';
}, 300);
}
}
addNewActivity(activity) {
const activitiesContainer = document.querySelector('.space-y-3');
if (activitiesContainer) {
const newActivity = document.createElement('div');
newActivity.className = 'mb-activity-item pl-4 py-3 opacity-0';
newActivity.innerHTML = `
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-slate-700 dark:text-slate-300">${activity.description}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">${activity.time}</p>
</div>
</div>
`;
// Am Anfang einfügen
activitiesContainer.insertBefore(newActivity, activitiesContainer.firstChild);
// Animation
setTimeout(() => {
newActivity.style.opacity = '1';
newActivity.style.transition = 'opacity 0.5s ease';
}, 100);
// Alte Aktivitäten entfernen (max 10)
const activities = activitiesContainer.querySelectorAll('.mb-activity-item');
if (activities.length > 10) {
activities[activities.length - 1].remove();
}
}
}
updateStats(stats) {
// Statistik-Karten aktualisieren mit Animation
const statValues = document.querySelectorAll('.stat-value');
const statsMapping = [
{ element: statValues[0], value: stats.active_jobs_count },
{ element: statValues[1], value: stats.available_printers_count },
{ element: statValues[2], value: stats.total_jobs_count },
{ element: statValues[3], value: `${stats.success_rate}%` }
];
statsMapping.forEach(({ element, value }) => {
if (element && element.textContent !== value.toString()) {
this.animateValueChange(element, value);
}
});
}
animateValueChange(element, newValue) {
element.style.transform = 'scale(1.1)';
element.style.transition = 'transform 0.3s ease';
setTimeout(() => {
element.textContent = newValue;
element.style.transform = 'scale(1)';
}, 150);
}
animateCounters() {
// Initialanimation für Counter
const counters = document.querySelectorAll('.stat-value');
counters.forEach((counter, index) => {
const finalValue = parseInt(counter.textContent) || 0;
if (finalValue > 0) {
let currentValue = 0;
const increment = finalValue / 30; // 30 Schritte
const timer = setInterval(() => {
currentValue += increment;
if (currentValue >= finalValue) {
counter.textContent = finalValue + (counter.textContent.includes('%') ? '%' : '');
clearInterval(timer);
} else {
counter.textContent = Math.floor(currentValue) + (counter.textContent.includes('%') ? '%' : '');
}
}, 50 + (index * 100)); // Verzögerung zwischen Countern
}
});
}
getStatusClass(status) {
const statusClasses = {
'running': 'mb-status-busy',
'completed': 'mb-status-online',
'failed': 'mb-status-offline',
'paused': 'mb-status-idle',
'queued': 'mb-status-idle',
'online': 'mb-status-online',
'offline': 'mb-status-offline',
'busy': 'mb-status-busy',
'idle': 'mb-status-idle'
};
return statusClasses[status] || 'mb-status-idle';
}
getStatusText(status) {
const statusTexts = {
'running': 'Druckt',
'completed': 'Abgeschlossen',
'failed': 'Fehlgeschlagen',
'paused': 'Pausiert',
'queued': 'Warteschlange',
'online': 'Online',
'offline': 'Offline',
'busy': 'Beschäftigt',
'idle': 'Bereit'
};
return statusTexts[status] || 'Unbekannt';
}
updateThemeElements(isDark) {
// Theme-spezifische Updates
console.log(`🎨 Dashboard Theme aktualisiert: ${isDark ? 'Dark' : 'Light'} Mode`);
// Spezielle Animationen für Theme-Wechsel
const cards = document.querySelectorAll('.dashboard-card');
cards.forEach(card => {
card.style.transition = 'all 0.3s ease';
});
}
showToast(message, type = 'info') {
// Toast-Benachrichtigung anzeigen
if (window.MYP && window.MYP.UI && window.MYP.UI.ToastManager) {
const toast = new window.MYP.UI.ToastManager();
toast.show(message, type, 5000);
} else {
console.log(`Toast: ${message}`);
}
}
// Cleanup beim Verlassen der Seite
cleanup() {
if (this.autoUpdateTimer) {
clearInterval(this.autoUpdateTimer);
}
if (this.wsConnection) {
this.wsConnection.close();
}
}
}
// Dashboard Manager initialisieren
document.addEventListener('DOMContentLoaded', function() {
window.dashboardManager = new DashboardManager();
// Cleanup beim Verlassen
window.addEventListener('beforeunload', () => {
if (window.dashboardManager) {
window.dashboardManager.cleanup();
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}403 - Zugriff verweigert{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="max-w-md mx-auto bg-white rounded-lg shadow-lg p-8 text-center">
<div class="mb-6">
<div class="text-6xl text-red-500 mb-4">🚫</div>
<h1 class="text-3xl font-bold text-gray-800 mb-2">403</h1>
<h2 class="text-xl text-gray-600 mb-4">Zugriff verweigert</h2>
</div>
<div class="mb-6">
<p class="text-gray-600 mb-4">
Sie haben keine Berechtigung, auf diese Seite zuzugreifen.
</p>
<p class="text-sm text-gray-500">
Falls Sie glauben, dass dies ein Fehler ist, wenden Sie sich an einen Administrator.
</p>
</div>
<div class="space-y-3">
<a href="{{ url_for('index') }}"
class="block w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
Zur Startseite
</a>
<button onclick="history.back()"
class="block w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors">
Zurück
</button>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}404 - Seite nicht gefunden - Mercedes-Benz MYP Platform{% endblock %}
{% block content %}
<div class="min-h-[80vh] flex flex-col items-center justify-center p-4">
<!-- 404 Error Container -->
<div class="w-full max-w-md">
<div class="bg-white dark:bg-gray-800 backdrop-blur-xl bg-opacity-95 dark:bg-opacity-95 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 p-8 text-center transition-all duration-300">
<!-- Mercedes-Benz Logo -->
<div class="flex justify-center mb-6">
<div class="w-16 h-16 text-gray-300 dark:text-gray-600 transition-transform duration-500 hover:scale-110">
<svg class="w-full h-full" fill="currentColor" viewBox="0 0 80 80">
<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>
</div>
<!-- Error Message -->
<h1 class="text-6xl font-bold text-gray-300 dark:text-gray-600 mb-4">404</h1>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Seite nicht gefunden</h2>
<p class="text-gray-600 dark:text-gray-400 mb-6">Die von Ihnen gesuchte Seite existiert nicht oder wurde verschoben.</p>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row justify-center gap-4 mt-8">
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center justify-center px-5 py-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium rounded-lg transition-all duration-300 transform hover:-translate-y-0.5">
<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="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
<span>Zum Dashboard</span>
</a>
<button onclick="window.history.back()" class="inline-flex items-center justify-center px-5 py-3 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-all duration-300 transform hover:-translate-y-0.5">
<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>
<span>Zurück</span>
</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,66 @@
{% extends "base.html" %}
{% block title %}Interner Serverfehler - Mercedes-Benz MYP Platform{% endblock %}
{% block content %}
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-red-50 to-orange-50 dark:from-slate-900 dark:via-red-900/20 dark:to-orange-900/20 flex items-center justify-center px-4">
<div class="max-w-2xl w-full text-center">
<!-- Error Icon -->
<div class="mb-8">
<div class="inline-flex items-center justify-center w-24 h-24 bg-red-100 dark:bg-red-900/30 rounded-full mb-6">
<svg class="w-12 h-12 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
<!-- Error Message -->
<h1 class="text-6xl font-bold text-slate-900 dark:text-white mb-4">500</h1>
<h2 class="text-2xl font-semibold text-slate-700 dark:text-slate-300 mb-6">Interner Serverfehler</h2>
<p class="text-lg text-slate-600 dark:text-slate-400 mb-8 max-w-lg mx-auto">
Es ist ein unerwarteter Fehler aufgetreten. Unser Team wurde automatisch benachrichtigt und arbeitet an einer Lösung.
</p>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-xl transition-colors duration-200 shadow-lg hover:shadow-xl">
<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="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
Zurück zum Dashboard
</a>
<button onclick="window.location.reload()" class="inline-flex items-center px-6 py-3 bg-slate-200 hover:bg-slate-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 font-medium rounded-xl transition-colors duration-200">
<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>
Seite neu laden
</button>
</div>
<!-- Additional Info -->
<div class="mt-12 p-6 bg-white/60 dark:bg-slate-800/60 backdrop-blur-sm rounded-2xl border border-slate-200 dark:border-slate-700">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-3">Was können Sie tun?</h3>
<ul class="text-left text-slate-600 dark:text-slate-400 space-y-2">
<li class="flex items-start">
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" 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>
Versuchen Sie, die Seite neu zu laden
</li>
<li class="flex items-start">
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" 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>
Kehren Sie zum Dashboard zurück
</li>
<li class="flex items-start">
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" 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>
Kontaktieren Sie den Administrator, falls das Problem weiterhin besteht
</li>
</ul>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,410 @@
{% extends "base.html" %}
{% block title %}Job-Status - {{ job.name }}{% endblock %}
{% block content %}
<div class="min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
<!-- Header Section -->
<div class="relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-r from-blue-600/10 to-purple-600/10"></div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
<h1 class="text-4xl lg:text-6xl font-bold text-slate-900 dark:text-white mb-6">
<span class="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Job-Status
</span>
</h1>
<p class="text-xl text-slate-600 dark:text-slate-400 max-w-3xl mx-auto leading-relaxed">
Überwachen Sie den aktuellen Status Ihres Druckauftrags in Echtzeit.
</p>
</div>
</div>
<!-- Main Content -->
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10">
<!-- Status Container -->
<div class="form-container professional-shadow p-8 lg:p-12">
<!-- Job-Header -->
<div class="text-center mb-8">
<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-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="M9 12h6m-6 4h6m2 5H7a2 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>
</div>
<h2 class="text-3xl font-bold text-slate-900 dark:text-white mb-4">
{{ job.name }}
</h2>
<div id="statusBadge" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium">
<!-- Wird von JavaScript aktualisiert -->
</div>
</div>
<!-- Live-Status-Updates -->
<div id="liveStatusContainer" class="mb-8">
<!-- Wird von JavaScript gefüllt -->
</div>
<!-- Fortschrittsbalken -->
<div class="mb-8">
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">Fortschritt</span>
<span id="progressText" class="text-sm text-slate-500 dark:text-slate-400">0%</span>
</div>
<div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-3">
<div id="progressBar" class="bg-gradient-to-r from-blue-500 to-indigo-500 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<!-- Job-Details -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="space-y-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Job-Details</h3>
<div class="flex justify-between items-center">
<span class="text-sm text-slate-500 dark:text-slate-400">Job-ID:</span>
<span class="text-sm font-medium text-slate-900 dark:text-white">#{{ job.id }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-slate-500 dark:text-slate-400">Geplante Dauer:</span>
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ job.duration_minutes }} Minuten</span>
</div>
{% if job.start_at %}
<div class="flex justify-between items-center">
<span class="text-sm text-slate-500 dark:text-slate-400">Gestartet:</span>
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ job.start_at|format_datetime('%H:%M') }}</span>
</div>
{% endif %}
{% if job.end_at %}
<div class="flex justify-between items-center">
<span class="text-sm text-slate-500 dark:text-slate-400">Geplantes Ende:</span>
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ job.end_at|format_datetime('%H:%M') }}</span>
</div>
{% endif %}
</div>
<div class="space-y-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Drucker-Details</h3>
{% if job.printer %}
<div class="flex justify-between items-center">
<span class="text-sm text-slate-500 dark:text-slate-400">Drucker:</span>
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ job.printer.name }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-slate-500 dark:text-slate-400">Standort:</span>
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ job.printer.location or 'Unbekannt' }}</span>
</div>
{% else %}
<p class="text-sm text-slate-500 dark:text-slate-400">Kein Drucker zugewiesen</p>
{% endif %}
{% if guest_request %}
<div class="flex justify-between items-center">
<span class="text-sm text-slate-500 dark:text-slate-400">Antragsteller:</span>
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ guest_request.name }}</span>
</div>
{% endif %}
</div>
</div>
<!-- Zeitstatus -->
<div class="bg-slate-50 dark:bg-slate-800 rounded-xl p-6 mb-8">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-center">
<div>
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400" id="elapsedTime">--</div>
<div class="text-sm text-slate-500 dark:text-slate-400">Verstrichene Zeit</div>
</div>
<div>
<div class="text-2xl font-bold text-green-600 dark:text-green-400" id="remainingTime">--</div>
<div class="text-sm text-slate-500 dark:text-slate-400">Verbleibende Zeit</div>
</div>
<div>
<div class="text-2xl font-bold text-purple-600 dark:text-purple-400" id="currentTime">--</div>
<div class="text-sm text-slate-500 dark:text-slate-400">Aktuelle Zeit</div>
</div>
</div>
</div>
<!-- Aktionen -->
<div class="text-center">
<div class="space-y-4 sm:space-y-0 sm:space-x-4 sm:flex sm:justify-center">
<button onclick="refreshStatus()"
class="w-full sm:w-auto px-6 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-colors duration-200">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Status aktualisieren
</button>
<a href="{{ url_for('guest.guest_request_form') }}"
class="w-full sm:w-auto inline-flex items-center justify-center px-6 py-3 bg-gray-600 text-white rounded-xl hover:bg-gray-700 transition-colors duration-200">
<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 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
Neue Anfrage
</a>
</div>
</div>
</div>
</div>
</div>
<style>
.form-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.dark .form-container {
background: rgba(30, 41, 59, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.professional-shadow {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.status-scheduled {
background-color: #fef3c7;
color: #92400e;
}
.status-running {
background-color: #d1fae5;
color: #065f46;
}
.status-completed, .status-finished {
background-color: #dbeafe;
color: #1e40af;
}
.status-failed, .status-cancelled {
background-color: #fee2e2;
color: #dc2626;
}
.dark .status-scheduled {
background-color: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.dark .status-running {
background-color: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.dark .status-completed, .dark .status-finished {
background-color: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.dark .status-failed, .dark .status-cancelled {
background-color: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
</style>
<script>
var jobId = parseInt('{{ job.id }}');
var refreshInterval;
var jobData = {};
// Initial load
document.addEventListener('DOMContentLoaded', function() {
refreshStatus();
startAutoRefresh();
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
});
async function refreshStatus() {
try {
const response = await fetch(`/api/guest/job/${jobId}/status`);
const result = await response.json();
if (result.success) {
jobData = result.job;
updateDisplay(jobData);
} else {
console.error('Fehler beim Laden des Job-Status:', result.error);
}
} catch (error) {
console.error('Fehler beim Status-Update:', error);
}
}
function updateDisplay(job) {
// Status-Badge aktualisieren
const statusBadge = document.getElementById('statusBadge');
statusBadge.className = `inline-flex items-center px-4 py-2 rounded-full text-sm font-medium status-${job.status}`;
statusBadge.innerHTML = getStatusText(job.status);
// Fortschrittsbalken
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
progressBar.style.width = job.progress_percent + '%';
progressText.textContent = job.progress_percent + '%';
// Fortschrittsbalken-Farbe je nach Status
if (job.status === 'completed' || job.status === 'finished') {
progressBar.className = 'bg-gradient-to-r from-green-500 to-emerald-500 h-3 rounded-full transition-all duration-300';
} else if (job.status === 'running') {
progressBar.className = 'bg-gradient-to-r from-blue-500 to-indigo-500 h-3 rounded-full transition-all duration-300';
} else if (job.status === 'failed' || job.status === 'cancelled') {
progressBar.className = 'bg-gradient-to-r from-red-500 to-rose-500 h-3 rounded-full transition-all duration-300';
}
// Zeit-Updates
updateTimeDisplays(job);
// Live-Status-Container
updateLiveStatus(job);
}
function updateTimeDisplays(job) {
const now = new Date();
if (job.start_at && job.status === 'running') {
const startTime = new Date(job.start_at);
const elapsed = Math.floor((now - startTime) / 1000 / 60);
document.getElementById('elapsedTime').textContent = elapsed + ' Min';
if (job.remaining_minutes !== undefined) {
document.getElementById('remainingTime').textContent = job.remaining_minutes + ' Min';
}
} else {
document.getElementById('elapsedTime').textContent = '--';
document.getElementById('remainingTime').textContent = '--';
}
}
function updateCurrentTime() {
const now = new Date();
document.getElementById('currentTime').textContent = now.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function updateLiveStatus(job) {
const container = document.getElementById('liveStatusContainer');
let statusHtml = '';
if (job.status === 'scheduled') {
statusHtml = `
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-xl p-4">
<div class="flex items-center">
<div class="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center mr-4">
<svg class="w-5 h-5 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>
<h3 class="text-lg font-semibold text-yellow-800 dark:text-yellow-200">Job ist geplant</h3>
<p class="text-yellow-700 dark:text-yellow-300">Der Job wartet auf den Start.</p>
</div>
</div>
</div>
`;
} else if (job.status === 'running') {
statusHtml = `
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-700 rounded-xl p-4">
<div class="flex items-center">
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center mr-4">
<svg class="w-5 h-5 text-white 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"/>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-green-800 dark:text-green-200">Job läuft</h3>
<p class="text-green-700 dark:text-green-300">Ihr Druckauftrag wird gerade ausgeführt.</p>
</div>
</div>
</div>
`;
} else if (job.status === 'completed' || job.status === 'finished') {
statusHtml = `
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-xl p-4">
<div class="flex items-center">
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center mr-4">
<svg class="w-5 h-5 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"/>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-blue-800 dark:text-blue-200">Job abgeschlossen</h3>
<p class="text-blue-700 dark:text-blue-300">Ihr Druckauftrag wurde erfolgreich abgeschlossen.</p>
</div>
</div>
</div>
`;
} else if (job.status === 'failed' || job.status === 'cancelled') {
const statusText = job.status === 'failed' ? 'fehlgeschlagen' : 'abgebrochen';
statusHtml = `
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 rounded-xl p-4">
<div class="flex items-center">
<div class="w-8 h-8 bg-red-500 rounded-full flex items-center justify-center mr-4">
<svg class="w-5 h-5 text-white" 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"/>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-red-800 dark:text-red-200">Job ${statusText}</h3>
<p class="text-red-700 dark:text-red-300">Der Druckauftrag konnte nicht abgeschlossen werden.</p>
</div>
</div>
</div>
`;
}
container.innerHTML = statusHtml;
}
function getStatusText(status) {
const statusTexts = {
'scheduled': 'Geplant',
'running': 'Läuft',
'completed': 'Abgeschlossen',
'finished': 'Beendet',
'failed': 'Fehlgeschlagen',
'cancelled': 'Abgebrochen'
};
return statusTexts[status] || status;
}
function startAutoRefresh() {
// Nur bei aktiven Jobs automatisch aktualisieren
if (jobData.is_active) {
refreshInterval = setInterval(refreshStatus, 5000); // Alle 5 Sekunden
} else if (refreshInterval) {
clearInterval(refreshInterval);
}
}
// Auto-Refresh stoppen wenn Job abgeschlossen ist
function checkAutoRefresh(job) {
if (!job.is_active && refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
} else if (job.is_active && !refreshInterval) {
refreshInterval = setInterval(refreshStatus, 5000);
}
}
// Cleanup beim Verlassen der Seite
window.addEventListener('beforeunload', function() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,270 @@
{% extends "base.html" %}
{% block title %}Druckanträge Übersicht - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Page Header -->
<div class="dashboard-card p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 flex-shrink-0">
<svg class="w-full h-full text-slate-900 dark:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
</div>
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Anträge Übersicht</h1>
<p class="text-slate-500 dark:text-slate-400 mt-1">Transparente Übersicht aller eingereichten Druckanträge</p>
</div>
</div>
<div class="flex flex-wrap gap-3">
<a href="{{ url_for('guest.guest_request_form') }}"
class="btn-primary flex items-center gap-2">
<svg class="w-5 h-5" 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>
<span>Neuen Antrag stellen</span>
</a>
<button onclick="location.reload()"
class="btn-secondary flex items-center gap-2">
<svg class="w-5 h-5" 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>
<span>Aktualisieren</span>
</button>
<a href="{{ url_for('index') }}"
class="btn-secondary flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
<span>Startseite</span>
</a>
</div>
</div>
</div>
<!-- Info Banner -->
<div class="dashboard-card p-6">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
<svg class="h-6 w-6 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
</svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">
Datenschutz & Transparenz
</h3>
<p class="text-slate-600 dark:text-slate-400 mb-4">
Diese Übersicht zeigt alle eingereichten Druckanträge in anonymisierter Form. Persönliche Daten sind durch "***" zensiert, um die Privatsphäre zu schützen und gleichzeitig Transparenz über den Bearbeitungsstand zu gewährleisten.
</p>
<div class="flex flex-wrap gap-2">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
Datenkonform
</span>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
Anonymisiert
</span>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400">
Transparent
</span>
</div>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
{% set total_requests = requests|length %}
{% set pending_requests = requests|selectattr("status", "equalto", "pending")|list|length %}
{% set approved_requests = requests|selectattr("status", "equalto", "approved")|list|length %}
{% set denied_requests = requests|selectattr("status", "equalto", "denied")|list|length %}
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Gesamt</h3>
<div class="stat-value">{{ total_requests }}</div>
</div>
<div class="mb-stat-icon text-slate-600 dark:text-slate-400">
<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="M9 12h6m-6 4h6m2 5H7a2 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>
</div>
</div>
</div>
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Prüfung</h3>
<div class="stat-value text-yellow-600 dark:text-yellow-400">{{ pending_requests }}</div>
</div>
<div class="mb-stat-icon text-yellow-600 dark:text-yellow-400">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
</div>
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Genehmigt</h3>
<div class="stat-value text-green-600 dark:text-green-400">{{ approved_requests }}</div>
</div>
<div class="mb-stat-icon text-green-600 dark:text-green-400">
<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="M5 13l4 4L19 7"/>
</svg>
</div>
</div>
</div>
<div class="dashboard-card p-6">
<div class="flex justify-between">
<div>
<h3 class="stat-label">Abgelehnt</h3>
<div class="stat-value text-red-600 dark:text-red-400">{{ denied_requests }}</div>
</div>
<div class="mb-stat-icon text-red-600 dark:text-red-400">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</div>
</div>
</div>
</div>
<!-- Requests List -->
{% if error %}
<div class="dashboard-card p-8 text-center">
<div class="text-red-500 dark:text-red-400 mb-4">
<svg class="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Fehler beim Laden</h3>
<p class="text-slate-500 dark:text-slate-400">{{ error }}</p>
</div>
{% elif requests|length == 0 %}
<div class="dashboard-card p-8 text-center">
<div class="text-slate-400 dark:text-slate-500 mb-4">
<svg class="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
</div>
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Keine Druckanträge</h3>
<p class="text-slate-500 dark:text-slate-400 mb-4">
Derzeit sind keine Druckanträge vorhanden.
</p>
<a href="{{ url_for('guest.guest_request_form') }}"
class="btn-primary">
Ersten Antrag stellen
</a>
</div>
{% else %}
<div class="space-y-4">
{% for request in requests %}
<div class="dashboard-card p-6 border-l-4 {% if request.status == 'pending' %}border-yellow-400{% elif request.status == 'approved' %}border-green-400{% elif request.status == 'denied' %}border-red-400{% endif %}">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<!-- Left Section: Request Info -->
<div class="flex-1">
<div class="flex items-center gap-4 mb-4">
<span class="text-lg font-bold text-slate-900 dark:text-white">
#{{ request.id }}
</span>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {% if request.status == 'pending' %}bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400{% elif request.status == 'approved' %}bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400{% elif request.status == 'denied' %}bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400{% endif %}">
{% if request.status == 'pending' %}
Wird geprüft
{% elif request.status == 'approved' %}
Genehmigt
{% elif request.status == 'denied' %}
Abgelehnt
{% endif %}
</span>
{% if request.job_status %}
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400">
Druckstatus: {{ request.job_status|title }}
</span>
{% endif %}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
<div class="bg-gray-50 dark:bg-slate-700/30 p-3 rounded-lg">
<div class="text-slate-500 dark:text-slate-400 font-medium mb-1">Antragsteller</div>
<div class="font-mono text-slate-600 dark:text-slate-300">{{ request.name }}</div>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 p-3 rounded-lg">
<div class="text-slate-500 dark:text-slate-400 font-medium mb-1">E-Mail</div>
<div class="font-mono text-slate-600 dark:text-slate-300">{{ request.email }}</div>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 p-3 rounded-lg">
<div class="text-slate-500 dark:text-slate-400 font-medium mb-1">Drucker</div>
<div class="text-slate-600 dark:text-slate-300">{{ request.printer_name if request.printer_name else 'Automatisch' }}</div>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 p-3 rounded-lg">
<div class="text-slate-500 dark:text-slate-400 font-medium mb-1">Dauer</div>
<div class="text-slate-600 dark:text-slate-300">{{ request.duration_min }} Min</div>
</div>
</div>
{% if request.reason %}
<div class="mt-4 bg-gray-50 dark:bg-slate-700/30 p-3 rounded-lg">
<div class="text-slate-500 dark:text-slate-400 font-medium mb-2">Projektbeschreibung</div>
<div class="font-mono text-sm text-slate-600 dark:text-slate-300">{{ request.reason }}</div>
</div>
{% endif %}
</div>
<!-- Right Section: Timestamp and Actions -->
<div class="lg:text-right">
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg text-center lg:text-right">
<div class="text-sm font-medium text-slate-900 dark:text-white mb-1">
{{ request.created_at.strftime('%d.%m.%Y') }}
</div>
<div class="text-xs text-slate-500 dark:text-slate-400">
{{ request.created_at.strftime('%H:%M') }} Uhr
</div>
{% if request.status == 'approved' %}
<div class="mt-3">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
Startbereit
</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Live-Zeitanzeige im Browser-Tab
function updateTabTitle() {
const now = new Date();
const timeString = now.toLocaleTimeString('de-DE');
document.title = `Druckanträge (${timeString}) - Mercedes-Benz MYP Platform`;
}
updateTabTitle();
setInterval(updateTabTitle, 1000);
// Auto-Refresh alle 30 Sekunden
setInterval(function() {
if (!document.hidden) {
window.location.reload();
}
}, 30000);
});
</script>
{% endblock %}

View File

@ -0,0 +1,347 @@
{% extends "base.html" %}
{% block title %}Job mit Code starten - Mercedes-Benz MYP Platform{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Page Header -->
<div class="dashboard-card p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 flex-shrink-0">
<svg class="w-full h-full text-slate-900 dark: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"/>
</svg>
</div>
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Job starten</h1>
<p class="text-slate-500 dark:text-slate-400 mt-1">Geben Sie Ihren 6-stelligen Zugangscode ein</p>
</div>
</div>
<div class="flex flex-wrap gap-3">
<a href="{{ url_for('guest.guest_request_form') }}"
class="btn-secondary flex items-center gap-2">
<svg class="w-5 h-5" 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>
<span>Neue Anfrage</span>
</a>
<a href="{{ url_for('index') }}"
class="btn-secondary flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
<span>Startseite</span>
</a>
</div>
</div>
</div>
<!-- Success Message (versteckt) -->
<div id="successMessage" class="hidden">
<div class="dashboard-card p-6 border-l-4 border-green-400">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-green-900 dark:text-green-100 mb-2">Job erfolgreich gestartet!</h3>
<p class="text-green-700 dark:text-green-300" id="successDetails"></p>
</div>
</div>
</div>
</div>
<!-- Error Message (versteckt) -->
<div id="errorMessage" class="hidden">
<div class="dashboard-card p-6 border-l-4 border-red-400">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-red-600 dark:text-red-400" 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"/>
</svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-red-900 dark:text-red-100 mb-2">Fehler beim Starten</h3>
<p class="text-red-700 dark:text-red-300" id="errorDetails"></p>
</div>
</div>
</div>
</div>
<!-- Code-Eingabe Container -->
<div class="dashboard-card p-8">
<div class="text-center mb-8">
<div class="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-xl flex items-center justify-center mx-auto mb-6">
<svg class="w-8 h-8 text-green-600 dark:text-green-400" 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"/>
</svg>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white mb-3">
Zugangscode eingeben
</h2>
<p class="text-slate-500 dark:text-slate-400">
Ihr persönlicher 6-stelliger Code wurde Ihnen nach der Genehmigung mitgeteilt.
</p>
</div>
<form id="codeForm" class="max-w-lg mx-auto">
<!-- Code-Eingabe -->
<div class="mb-8">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-6 text-center">
6-stelliger Zugangscode <span class="text-red-500">*</span>
</label>
<!-- Code-Input mit einzelnen Feldern -->
<div class="flex justify-center gap-3 mb-6">
<input type="text" id="code1" maxlength="1"
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
oninput="moveToNext(this, 'code2')" onkeydown="handleBackspace(event, this, null)">
<input type="text" id="code2" maxlength="1"
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
oninput="moveToNext(this, 'code3')" onkeydown="handleBackspace(event, this, 'code1')">
<input type="text" id="code3" maxlength="1"
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
oninput="moveToNext(this, 'code4')" onkeydown="handleBackspace(event, this, 'code2')">
<input type="text" id="code4" maxlength="1"
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
oninput="moveToNext(this, 'code5')" onkeydown="handleBackspace(event, this, 'code3')">
<input type="text" id="code5" maxlength="1"
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
oninput="moveToNext(this, 'code6')" onkeydown="handleBackspace(event, this, 'code4')">
<input type="text" id="code6" maxlength="1"
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
oninput="moveToNext(this, null)" onkeydown="handleBackspace(event, this, 'code5')">
</div>
<div class="text-center">
<p class="text-slate-500 dark:text-slate-400 text-sm">
Der Code besteht aus 6 Zeichen (Großbuchstaben und Zahlen)
</p>
</div>
</div>
<!-- Submit Button -->
<div class="text-center">
<button type="submit" id="submitBtn"
class="btn-primary disabled:opacity-50 disabled:cursor-not-allowed px-8 py-3 flex items-center gap-2 mx-auto">
<svg class="w-5 h-5" 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"/>
</svg>
Job jetzt starten
</button>
</div>
</form>
</div>
<!-- Hilfe-Sektion -->
<div class="dashboard-card p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6 text-center">
Brauchen Sie Hilfe?
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center p-4 bg-gray-50 dark:bg-slate-700/30 rounded-lg">
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/30 rounded-lg mx-auto mb-3 flex items-center justify-center">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
</div>
<p class="text-sm text-slate-600 dark:text-slate-400">Ihr Zugangscode wurde Ihnen nach der Genehmigung mitgeteilt</p>
</div>
<div class="text-center p-4 bg-gray-50 dark:bg-slate-700/30 rounded-lg">
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/30 rounded-lg mx-auto mb-3 flex items-center justify-center">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
<p class="text-sm text-slate-600 dark:text-slate-400">Der Code ist nur einmalig verwendbar und hat eine begrenzte Gültigkeit</p>
</div>
<div class="text-center p-4 bg-gray-50 dark:bg-slate-700/30 rounded-lg">
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/30 rounded-lg mx-auto mb-3 flex items-center justify-center">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M12 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<p class="text-sm text-slate-600 dark:text-slate-400">Bei Problemen wenden Sie sich an den Administrator</p>
</div>
</div>
</div>
</div>
<script>
// Code-Eingabe-Logik
function moveToNext(current, nextId) {
const value = current.value.toUpperCase();
// Nur alphanumerische Zeichen erlauben
if (!/^[A-Z0-9]$/.test(value)) {
current.value = '';
return;
}
current.value = value;
current.classList.add('border-green-500', 'bg-green-50', 'dark:bg-green-900/20');
// Zum nächsten Feld wechseln
if (nextId && value) {
document.getElementById(nextId).focus();
}
// Prüfen ob alle Felder ausgefüllt sind
checkFormComplete();
}
function handleBackspace(event, current, prevId) {
if (event.key === 'Backspace') {
if (current.value === '' && prevId) {
event.preventDefault();
const prevField = document.getElementById(prevId);
prevField.focus();
prevField.value = '';
prevField.classList.remove('border-green-500', 'bg-green-50', 'dark:bg-green-900/20');
} else if (current.value !== '') {
current.classList.remove('border-green-500', 'bg-green-50', 'dark:bg-green-900/20');
}
}
}
function checkFormComplete() {
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
const allFilled = inputs.every(id => document.getElementById(id).value !== '');
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = !allFilled;
}
function getCodeValue() {
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
return inputs.map(id => document.getElementById(id).value).join('');
}
function clearCode() {
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
inputs.forEach(id => {
const input = document.getElementById(id);
input.value = '';
input.classList.remove('border-green-500', 'bg-green-50', 'dark:bg-green-900/20');
});
checkFormComplete();
document.getElementById('code1').focus();
}
function showSuccess(message, details) {
document.getElementById('successMessage').classList.remove('hidden');
document.getElementById('successDetails').textContent = details;
document.getElementById('errorMessage').classList.add('hidden');
// Scroll zur Nachricht
document.getElementById('successMessage').scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function showError(message, details) {
document.getElementById('errorMessage').classList.remove('hidden');
document.getElementById('errorDetails').textContent = details;
document.getElementById('successMessage').classList.add('hidden');
// Scroll zur Nachricht
document.getElementById('errorMessage').scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// Form-Submit-Handler
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('codeForm');
const submitBtn = document.getElementById('submitBtn');
// Erstes Feld fokussieren
document.getElementById('code1').focus();
form.addEventListener('submit', async function(e) {
e.preventDefault();
const code = getCodeValue();
if (code.length !== 6) {
showError('Ungültiger Code', 'Bitte geben Sie alle 6 Zeichen ein.');
return;
}
// Button-Animation
submitBtn.disabled = true;
const originalContent = submitBtn.innerHTML;
submitBtn.innerHTML = `
<svg class="animate-spin w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Wird überprüft...
`;
try {
const response = await fetch('/api/guest/start-job', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ code: code })
});
const result = await response.json();
if (result.success) {
showSuccess(
'Job erfolgreich gestartet!',
`Ihr Job "${result.job_name}" wurde gestartet und läuft bis ${result.end_time}.`
);
// Form deaktivieren
form.style.opacity = '0.5';
form.style.pointerEvents = 'none';
// Nach 3 Sekunden zur Job-Status-Seite weiterleiten
setTimeout(() => {
if (result.job_id) {
window.location.href = `/guest/job/${result.job_id}/status`;
}
}, 3000);
} else {
showError('Code ungültig', result.error || 'Der eingegebene Code ist ungültig oder bereits verwendet.');
clearCode();
}
} catch (error) {
showError('Verbindungsfehler', 'Es gab ein Problem bei der Verbindung. Bitte versuchen Sie es erneut.');
clearCode();
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalContent;
checkFormComplete();
}
});
// Paste-Handler für kompletten Code
document.addEventListener('paste', function(e) {
const target = e.target;
if (target.classList.contains('code-input')) {
e.preventDefault();
const paste = (e.clipboardData || window.clipboardData).getData('text').toUpperCase();
if (paste.length === 6 && /^[A-Z0-9]+$/.test(paste)) {
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
inputs.forEach((id, index) => {
const input = document.getElementById(id);
input.value = paste[index] || '';
if (input.value) {
input.classList.add('border-green-500', 'bg-green-50', 'dark:bg-green-900/20');
}
});
checkFormComplete();
document.getElementById('code6').focus();
}
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,334 @@
{% extends "base.html" %}
{% block title %}Anfrage-Status - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta http-equiv="refresh" content="30">
{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Page Header -->
<div class="dashboard-card p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 flex-shrink-0">
<svg class="w-full h-full text-slate-900 dark: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>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Anfrage-Status</h1>
<p class="text-slate-500 dark:text-slate-400 mt-1">Verfolgen Sie den Status Ihrer Gastanfrage für 3D-Druck</p>
</div>
</div>
<div class="flex flex-wrap gap-3">
<button onclick="location.reload()"
class="btn-secondary flex items-center gap-2">
<svg class="w-5 h-5" 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>
<span>Aktualisieren</span>
</button>
<a href="{{ url_for('guest.guest_request_form') }}"
class="btn-primary flex items-center gap-2">
<svg class="w-5 h-5" 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>
<span>Neue Anfrage</span>
</a>
</div>
</div>
</div>
<!-- Status Badge -->
<div class="dashboard-card p-6 text-center">
{% 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 text-lg font-semibold">
<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"/>
</svg>
Wird geprüft
</div>
{% 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 text-lg font-semibold">
<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"/>
</svg>
Genehmigt
</div>
{% 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 text-lg font-semibold">
<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"/>
</svg>
Abgelehnt
</div>
{% endif %}
</div>
<!-- Anfrage-Details -->
<div class="dashboard-card p-6">
<div class="text-center mb-8">
<h2 class="text-2xl font-bold text-slate-900 dark:text-white mb-2">
Anfrage Details
</h2>
<p class="text-slate-500 dark:text-slate-400">
Übersicht Ihrer eingereichten Gastanfrage
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Anfrage-ID</h3>
<p class="text-xl font-bold text-slate-900 dark:text-white">#{{ request.id }}</p>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Erstellt am</h3>
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ request.created_at|format_datetime }}</p>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Name</h3>
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ request.name }}</p>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Gewünschte Dauer</h3>
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ request.duration_min }} Minuten</p>
</div>
</div>
{% if request.reason %}
<div class="mt-6 bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Begründung</h3>
<p class="text-slate-700 dark:text-slate-300">{{ request.reason }}</p>
</div>
{% endif %}
{% if request.printer %}
<div class="mt-6 bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Drucker</h3>
<p class="text-slate-700 dark:text-slate-300">{{ request.printer.name }} {% if request.printer.location %}({{ request.printer.location }}){% endif %}</p>
</div>
{% endif %}
</div>
<!-- Status-spezifische Inhalte -->
{% if request.status == 'pending' %}
<div class="dashboard-card p-6 border-l-4 border-yellow-400">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-yellow-100 dark:bg-yellow-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" 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"/>
</svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Ihre Anfrage wird geprüft</h3>
<p class="text-slate-600 dark:text-slate-400">
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.
</p>
</div>
</div>
</div>
{% elif request.status == 'approved' %}
<div class="dashboard-card p-6 border-l-4 border-green-400">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Anfrage genehmigt!</h3>
<p class="text-slate-600 dark:text-slate-400">Ihr Druckauftrag wurde genehmigt und ist bereit zum Start.</p>
</div>
</div>
</div>
{% if otp_code %}
<!-- Code-Anzeige -->
<div class="dashboard-card p-6">
<h4 class="text-xl font-bold text-slate-900 dark:text-white mb-6 text-center">Ihr Zugangscode</h4>
<!-- 6-stelliger Code in schönen Boxen -->
<div class="flex justify-center gap-3 mb-6">
{% for char in otp_code %}
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 text-blue-900 dark:text-blue-100 rounded-lg flex items-center justify-center text-2xl font-bold">
{{ char }}
</div>
{% endfor %}
</div>
<!-- Code zum Kopieren -->
<div class="text-center mb-6">
<div class="inline-flex items-center bg-gray-50 dark:bg-slate-700/30 rounded-lg px-4 py-3">
<span class="text-xl font-mono font-bold text-slate-900 dark:text-white mr-3" id="otpCode">{{ otp_code }}</span>
<button onclick="copyCode()" class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors" title="Code kopieren">
<svg class="w-5 h-5" 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"/>
</svg>
</button>
</div>
</div>
<!-- Wichtige Hinweise -->
<div class="bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-orange-100 dark:bg-orange-900/50 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-orange-600 dark:text-orange-400" 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"/>
</svg>
</div>
<div class="flex-1">
<h5 class="text-sm font-semibold text-orange-800 dark:text-orange-300 mb-2">Wichtige Hinweise:</h5>
<ul class="text-sm text-orange-700 dark:text-orange-400 space-y-1">
<li>• Dieser Code ist nur <strong>einmalig verwendbar</strong></li>
<li>• Notieren Sie sich den Code oder speichern Sie diese Seite</li>
<li>• Bei Verlust des Codes kontaktieren Sie den Administrator</li>
</ul>
</div>
</div>
</div>
<!-- Start-Button -->
<div class="text-center">
<a href="{{ url_for('guest.guest_start_job_form') }}"
class="btn-primary flex items-center gap-2 mx-auto">
<svg class="w-5 h-5" 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"/>
</svg>
Job jetzt starten
</a>
</div>
</div>
{% elif show_start_link %}
<!-- Code bereits vorhanden, aber noch nicht verwendet -->
<div class="dashboard-card p-6 text-center">
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-xl flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-blue-600 dark:text-blue-400" 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"/>
</svg>
</div>
<h4 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Zugangscode bereits generiert</h4>
<p class="text-slate-500 dark:text-slate-400 mb-6">Ihr persönlicher Code wurde bereits erstellt und ist bereit zur Verwendung.</p>
<a href="{{ url_for('guest.guest_start_job_form') }}"
class="btn-primary flex items-center gap-2 mx-auto">
<svg class="w-5 h-5" 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"/>
</svg>
Code eingeben und starten
</a>
</div>
{% endif %}
{% elif request.status == 'denied' %}
<div class="dashboard-card p-6 border-l-4 border-red-400">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-red-600 dark:text-red-400" 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"/>
</svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Anfrage abgelehnt</h3>
<p class="text-slate-600 dark:text-slate-400">
Ihre Anfrage wurde leider abgelehnt. Bei Fragen wenden Sie sich bitte an unser Team.
</p>
</div>
</div>
</div>
{% endif %}
</div>
<script>
// Code-Kopier-Funktion
function copyCode() {
const codeElement = document.getElementById('otpCode');
if (codeElement) {
const code = codeElement.textContent;
// Versuche den Code in die Zwischenablage zu kopieren
if (navigator.clipboard) {
navigator.clipboard.writeText(code).then(function() {
showCopySuccess();
}).catch(function(err) {
console.error('Fehler beim Kopieren:', err);
fallbackCopyText(code);
});
} else {
fallbackCopyText(code);
}
}
}
// Fallback für ältere Browser
function fallbackCopyText(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showCopySuccess();
} catch (err) {
console.error('Fallback-Kopieren fehlgeschlagen:', err);
alert('Code konnte nicht kopiert werden. Bitte manuell markieren und kopieren.');
}
document.body.removeChild(textArea);
}
// Erfolgs-Animation für das Kopieren
function showCopySuccess() {
const button = event.target.closest('button');
const originalHTML = button.innerHTML;
button.innerHTML = `
<svg class="w-5 h-5 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"/>
</svg>
`;
// Nach 2 Sekunden zurück zum Original
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
// Optional: Toast-Benachrichtigung
showToast('Code wurde in die Zwischenablage kopiert!');
}
// Toast-Benachrichtigung
function showToast(message) {
// Toast-Element erstellen, falls nicht vorhanden
let toast = document.getElementById('copyToast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'copyToast';
toast.className = 'fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 translate-x-full opacity-0 z-50';
document.body.appendChild(toast);
}
toast.textContent = message;
// Animation einblenden
setTimeout(() => {
toast.classList.remove('translate-x-full', 'opacity-0');
}, 100);
// Nach 3 Sekunden ausblenden
setTimeout(() => {
toast.classList.add('translate-x-full', 'opacity-0');
}, 3000);
}
</script>
{% endblock %}

1302
backend/templates/index.html Normal file

File diff suppressed because it is too large Load Diff

2146
backend/templates/jobs.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,116 @@
{% extends "base.html" %}
{% block title %}Neuer Druckauftrag - Mercedes-Benz MYP Platform{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Page Header -->
<div class="dashboard-card p-6">
<div class="flex items-center gap-4 mb-4">
<div class="w-12 h-12 bg-mercedes-blue text-white rounded-xl flex items-center justify-center">
<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="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
</div>
<div>
<h1 class="text-3xl font-bold text-mercedes-black dark:text-white">Neuer Druckauftrag</h1>
<p class="text-mercedes-gray dark:text-slate-400">Erstellen Sie einen neuen 3D-Druckauftrag</p>
</div>
</div>
</div>
<!-- Job Creation Form -->
<div class="dashboard-card p-8">
<form id="newJobForm" action="{{ url_for('create_job') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Job Details -->
<div>
<label for="job_title" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
Job-Titel <span class="text-red-500">*</span>
</label>
<input type="text" id="job_title" name="job_title" required
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg"
placeholder="Beschreibender Titel für den Druckauftrag">
</div>
<!-- Printer Selection -->
<div>
<label for="printer_id" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
Drucker <span class="text-red-500">*</span>
</label>
<select id="printer_id" name="printer_id" required
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg">
<option value="">Drucker auswählen...</option>
{% for printer in printers %}
<option value="{{ printer.id }}">{{ printer.name }} ({{ printer.location }})</option>
{% endfor %}
</select>
</div>
<!-- Start Time -->
<div>
<label for="start_time" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
Startzeit <span class="text-red-500">*</span>
</label>
<input type="datetime-local" id="start_time" name="start_time" required
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg">
</div>
<!-- Duration -->
<div>
<label for="duration" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
Geschätzte Dauer (Minuten) <span class="text-red-500">*</span>
</label>
<input type="number" id="duration" name="duration" required min="1" max="7200"
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg"
placeholder="60">
</div>
</div>
<!-- Description -->
<div>
<label for="description" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
Beschreibung
</label>
<textarea id="description" name="description" rows="3"
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg resize-none"
placeholder="Beschreibung des Druckauftrags..."></textarea>
</div>
<!-- File Upload -->
<div>
<label for="stl_file" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
3D-Datei hochladen (optional)
</label>
<input type="file" id="stl_file" name="stl_file" accept=".stl,.obj,.3mf,.gcode"
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg">
<p class="text-sm text-mercedes-gray dark:text-slate-400 mt-1">
Unterstützte Formate: STL, OBJ, 3MF, GCODE (max. 100MB)
</p>
</div>
<!-- Submit Button -->
<div class="flex items-center justify-end pt-6 border-t border-gray-200 dark:border-slate-600">
<a href="{{ url_for('jobs_page') }}" class="btn-secondary mr-3">
Abbrechen
</a>
<button type="submit" class="btn-primary">
Auftrag erstellen
</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Set default start time to now + 1 hour
const now = new Date();
now.setHours(now.getHours() + 1);
const formatted = now.toISOString().slice(0, 16);
document.getElementById('start_time').value = formatted;
});
</script>
{% endblock %}

1058
backend/templates/login.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,850 @@
{% extends "base.html" %}
{% block title %}Neuer Druckauftrag - MYP Platform{% endblock %}
{% block extra_css %}
<style>
/* Mercedes-Benz Corporate Design */
.text-mercedes-black { color: #000000; }
.text-mercedes-gray { color: #6b7280; }
.text-mercedes-silver { color: #9ca3af; }
.text-mercedes-blue { color: #0073ce; }
.text-mercedes-green { color: #008c32; }
.text-mercedes-red { color: #dc2626; }
.bg-mercedes-black { background-color: #000000; }
.bg-mercedes-silver { background-color: #e5e7eb; }
.bg-mercedes-blue { background-color: #0073ce; }
.bg-mercedes-green { background-color: #008c32; }
.border-mercedes-silver { border-color: #d1d5db; }
.border-mercedes-blue { border-color: #0073ce; }
.hover\:border-mercedes-blue:hover { border-color: #0073ce; }
.focus\:ring-mercedes-blue:focus {
--tw-ring-color: #0073ce;
--tw-ring-opacity: 0.5;
}
.focus\:border-mercedes-blue:focus { border-color: #0073ce; }
/* Mercedes Card Effect */
.mercedes-card {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
}
.dark .mercedes-card {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-color: #334155;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
}
.mercedes-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Mercedes Button */
.mercedes-button {
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.mercedes-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.mercedes-button:hover::before {
left: 100%;
}
/* Enhanced File Upload Area - KRITISCHER FIX */
#file-upload-area {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border: 2px dashed #cbd5e1;
position: relative;
overflow: hidden;
cursor: pointer !important;
user-select: none;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.dark #file-upload-area {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-color: #475569;
}
#file-upload-area:hover {
border-color: #0073ce;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
transform: scale(1.01);
}
#file-upload-area.drag-over {
border-color: #0073ce;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
transform: scale(1.02);
}
.dark #file-upload-area.drag-over {
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
}
/* WICHTIG: Sicherstellen dass alle Child-Elemente klickbar sind */
#file-upload-area * {
pointer-events: none;
}
/* File Preview Animation */
#file-preview {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Progress Ring for Upload */
.progress-ring {
width: 60px;
height: 60px;
transform: rotate(-90deg);
}
.progress-ring__circle {
stroke: #0073ce;
stroke-linecap: round;
stroke-width: 4;
fill: transparent;
r: 26;
cx: 30;
cy: 30;
stroke-dasharray: 163.36;
stroke-dashoffset: 163.36;
transition: stroke-dashoffset 0.3s ease;
}
/* Form Enhancements */
.form-input {
transition: all 0.2s ease;
border: 1px solid #d1d5db;
}
.form-input:focus {
border-color: #0073ce;
box-shadow: 0 0 0 3px rgba(0, 115, 206, 0.1);
transform: translateY(-1px);
}
.dark .form-input {
background-color: #1e293b;
border-color: #475569;
color: #f8fafc;
}
.dark .form-input:focus {
border-color: #0ea5e9;
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
}
/* Loading Overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
background: white;
padding: 2rem;
border-radius: 12px;
text-align: center;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.dark .loading-content {
background: #1e293b;
color: #f8fafc;
}
/* Upload Area Debug */
.upload-debug {
border: 2px solid red !important;
background: rgba(255, 0, 0, 0.1) !important;
}
</style>
{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-mercedes-black">Neuer Druckauftrag</h1>
<p class="mt-2 text-mercedes-gray">Erstellen Sie einen neuen 3D-Druckauftrag</p>
</div>
<a href="/jobs" class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Zurück zu Aufträgen
</a>
</div>
</div>
<!-- Debug Info -->
<div id="debug-info" class="mb-4 p-3 bg-blue-100 border border-blue-200 rounded-lg hidden">
<h3 class="font-bold text-blue-800">Debug Information:</h3>
<p id="debug-text" class="text-blue-700"></p>
</div>
<!-- Job Creation Form -->
<div class="mercedes-card rounded-xl p-8">
<form id="jobForm" class="space-y-8">
<!-- File Upload Section -->
<div>
<h2 class="text-xl font-bold text-mercedes-black mb-4">
3D-Datei hochladen
<span class="text-sm font-normal text-mercedes-gray">(STL, GCODE, 3MF, OBJ)</span>
</h2>
<!-- Hidden File Input -->
<input type="file" id="file-input" name="file" accept=".stl,.gcode,.3mf,.obj" class="hidden">
<!-- Klickbarer Upload-Bereich -->
<div id="file-upload-area" class="border-2 border-dashed border-mercedes-silver rounded-xl p-8 text-center hover:border-mercedes-blue transition-all duration-200">
<div id="upload-placeholder" class="space-y-4">
<svg class="h-16 w-16 text-mercedes-silver mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<div>
<p class="text-lg font-medium text-mercedes-black">Datei hier ablegen oder klicken zum Auswählen</p>
<p class="text-sm text-mercedes-gray mt-2">Unterstützte Formate: STL, GCODE, 3MF, OBJ (max. 100MB)</p>
<button type="button" id="upload-trigger" class="mt-4 bg-mercedes-blue hover:bg-blue-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
📁 Datei auswählen
</button>
</div>
</div>
<div id="file-preview" class="hidden space-y-4">
<svg class="h-16 w-16 text-mercedes-green mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p id="file-name" class="text-lg font-medium text-mercedes-black"></p>
<p id="file-size" class="text-sm text-mercedes-gray"></p>
<p id="file-type" class="text-xs text-mercedes-blue font-medium mt-1"></p>
</div>
<button type="button" id="clear-file-btn" class="text-mercedes-red hover:text-red-700 text-sm transition-colors duration-200">
🗑️ Datei entfernen
</button>
</div>
</div>
<!-- Status-Anzeige -->
<div id="upload-status" class="mt-4 hidden">
<div class="bg-green-100 border border-green-200 rounded-lg p-3">
<p class="text-green-800 text-sm">✅ Datei erfolgreich ausgewählt</p>
</div>
</div>
</div>
<!-- Job Settings Section -->
<div>
<h2 class="text-xl font-bold text-mercedes-black mb-4">Druckeinstellungen</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Printer Selection -->
<div>
<label for="printer-select" class="block text-sm font-medium text-mercedes-black mb-2">
Drucker auswählen *
</label>
<select id="printer-select" name="printer_id" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="">Drucker auswählen...</option>
</select>
<p class="mt-1 text-xs text-mercedes-gray">Nur verfügbare Drucker werden angezeigt</p>
</div>
<!-- Priority -->
<div>
<label for="priority-select" class="block text-sm font-medium text-mercedes-black mb-2">
Priorität
</label>
<select id="priority-select" name="priority"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="normal">Normal</option>
<option value="high">Hoch</option>
<option value="urgent">Dringend</option>
</select>
</div>
<!-- Material -->
<div>
<label for="material-input" class="block text-sm font-medium text-mercedes-black mb-2">
Material
</label>
<input type="text" id="material-input" name="material" placeholder="z.B. PLA, ABS, PETG"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<!-- Color -->
<div>
<label for="color-input" class="block text-sm font-medium text-mercedes-black mb-2">
Farbe
</label>
<input type="text" id="color-input" name="color" placeholder="z.B. Weiß, Schwarz, Rot"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<!-- Layer Height -->
<div>
<label for="layer-height-select" class="block text-sm font-medium text-mercedes-black mb-2">
Schichthöhe
</label>
<select id="layer-height-select" name="layer_height"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="">Standard</option>
<option value="0.1">0.1mm (Hoch)</option>
<option value="0.2">0.2mm (Normal)</option>
<option value="0.3">0.3mm (Schnell)</option>
</select>
</div>
<!-- Infill -->
<div>
<label for="infill-select" class="block text-sm font-medium text-mercedes-black mb-2">
Füllung
</label>
<select id="infill-select" name="infill"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="">Standard</option>
<option value="10">10% (Leicht)</option>
<option value="20">20% (Normal)</option>
<option value="50">50% (Stabil)</option>
<option value="100">100% (Massiv)</option>
</select>
</div>
</div>
</div>
<!-- Notes Section -->
<div>
<h2 class="text-xl font-bold text-mercedes-black mb-4">Zusätzliche Informationen</h2>
<div>
<label for="notes-textarea" class="block text-sm font-medium text-mercedes-black mb-2">
Notizen
</label>
<textarea id="notes-textarea" name="notes" rows="4"
placeholder="Besondere Anweisungen oder Hinweise für den Druck..."
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue"></textarea>
</div>
</div>
<!-- Submit Section -->
<div class="flex items-center justify-between pt-6 border-t border-mercedes-silver">
<div class="text-sm text-mercedes-gray">
<p>* Pflichtfelder</p>
</div>
<div class="flex space-x-4">
<button type="button" id="reset-btn"
class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
Zurücksetzen
</button>
<button type="submit" id="submit-button" disabled
class="bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed">
<span id="submit-text">Auftrag erstellen</span>
<svg id="submit-spinner" class="hidden animate-spin h-5 w-5 inline ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Globale Variablen
let selectedFile = null;
let printers = [];
let isUploading = false;
// Debug-Funktion
function debugLog(message) {
console.log('🔧 DEBUG:', message);
const debugInfo = document.getElementById('debug-info');
const debugText = document.getElementById('debug-text');
if (debugInfo && debugText) {
debugText.textContent = message;
debugInfo.classList.remove('hidden');
setTimeout(() => debugInfo.classList.add('hidden'), 3000);
}
}
// API-Hilfsfunktion mit besserer Fehlerbehandlung
async function apiCall(url, options = {}) {
try {
const response = await fetch(url, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
...options.headers
},
...options
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('API-Fehler:', error);
throw error;
}
}
// Toast-Benachrichtigung (Fallback falls nicht verfügbar)
function showToast(message, type = 'info') {
if (window.showToast) {
window.showToast(message, type);
} else {
// Fallback: Einfache Alert
const colors = {
'success': '✅',
'error': '❌',
'warning': '⚠️',
'info': ''
};
console.log(`${colors[type] || ''} ${message}`);
// Temporäre Anzeige in der Seite
const toast = document.createElement('div');
toast.className = `fixed top-4 right-4 p-4 rounded-lg text-white z-50 ${type === 'error' ? 'bg-red-500' : type === 'success' ? 'bg-green-500' : type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
}
// Dokumentbereitschaft
document.addEventListener('DOMContentLoaded', function() {
debugLog('Seite geladen - Initialisierung startet...');
try {
initializeFileUpload();
loadPrinters();
setupFormValidation();
setupEventListeners();
debugLog('Initialisierung erfolgreich abgeschlossen');
showToast('Upload-Interface bereit', 'success');
} catch (error) {
debugLog('Fehler bei Initialisierung: ' + error.message);
showToast('Fehler beim Laden der Seite', 'error');
}
});
// Datei-Upload initialisieren - HAUPTFUNKTION
function initializeFileUpload() {
const uploadArea = document.getElementById('file-upload-area');
const fileInput = document.getElementById('file-input');
const uploadTrigger = document.getElementById('upload-trigger');
const clearFileBtn = document.getElementById('clear-file-btn');
if (!uploadArea || !fileInput) {
throw new Error('Upload-Elemente nicht gefunden');
}
debugLog('Upload-Elemente gefunden, Event-Listener werden registriert...');
// HAUPTKLICK-EVENT - Direkt auf Upload-Area
uploadArea.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
debugLog('Upload-Area geklickt');
if (!selectedFile && !isUploading) {
debugLog('Datei-Dialog wird geöffnet...');
fileInput.click();
}
});
// Zusätzlicher Klick-Handler für Upload-Button
if (uploadTrigger) {
uploadTrigger.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
debugLog('Upload-Button geklickt');
fileInput.click();
});
}
// Datei-Input Change-Event
fileInput.addEventListener('change', function(e) {
debugLog('Datei-Input geändert');
handleFileSelection(e.target.files[0]);
});
// Drag & Drop
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
uploadArea.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', function(e) {
e.preventDefault();
uploadArea.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
uploadArea.classList.remove('drag-over');
debugLog('Datei dropped');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileSelection(files[0]);
}
});
// Clear-Button
if (clearFileBtn) {
clearFileBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
clearSelectedFile();
});
}
debugLog('Alle Upload-Event-Listener registriert');
}
// Datei-Auswahl behandeln
function handleFileSelection(file) {
if (!file) {
debugLog('Keine Datei ausgewählt');
return;
}
debugLog(`Datei ausgewählt: ${file.name} (${formatFileSize(file.size)})`);
// Datei validieren
const validation = validateFile(file);
if (!validation.valid) {
showToast(validation.message, 'error');
return;
}
selectedFile = file;
showFilePreview(file);
updateFormValidation();
showToast(`Datei "${file.name}" erfolgreich ausgewählt`, 'success');
}
// Datei validieren
function validateFile(file) {
const allowedExtensions = ['.stl', '.gcode', '.3mf', '.obj'];
const maxSize = 100 * 1024 * 1024; // 100MB
const extension = '.' + file.name.split('.').pop().toLowerCase();
if (!allowedExtensions.includes(extension)) {
return {
valid: false,
message: `Ungültiger Dateityp "${extension}". Erlaubt: ${allowedExtensions.join(', ')}`
};
}
if (file.size > maxSize) {
return {
valid: false,
message: `Datei zu groß (${formatFileSize(file.size)}). Maximum: 100MB`
};
}
if (file.size < 100) {
return {
valid: false,
message: 'Datei ist zu klein (weniger als 100 Bytes)'
};
}
return { valid: true };
}
// Datei-Vorschau anzeigen
function showFilePreview(file) {
const placeholder = document.getElementById('upload-placeholder');
const preview = document.getElementById('file-preview');
const fileName = document.getElementById('file-name');
const fileSize = document.getElementById('file-size');
const fileType = document.getElementById('file-type');
const uploadStatus = document.getElementById('upload-status');
if (placeholder) placeholder.classList.add('hidden');
if (preview) preview.classList.remove('hidden');
if (fileName) fileName.textContent = file.name;
if (fileSize) fileSize.textContent = formatFileSize(file.size);
if (fileType) {
const extension = '.' + file.name.split('.').pop().toLowerCase();
fileType.textContent = getFileTypeDescription(extension);
}
if (uploadStatus) uploadStatus.classList.remove('hidden');
debugLog('Datei-Vorschau aktualisiert');
}
// Ausgewählte Datei löschen
function clearSelectedFile() {
selectedFile = null;
const placeholder = document.getElementById('upload-placeholder');
const preview = document.getElementById('file-preview');
const fileInput = document.getElementById('file-input');
const uploadStatus = document.getElementById('upload-status');
if (placeholder) placeholder.classList.remove('hidden');
if (preview) preview.classList.add('hidden');
if (fileInput) fileInput.value = '';
if (uploadStatus) uploadStatus.classList.add('hidden');
updateFormValidation();
showToast('Datei entfernt', 'info');
debugLog('Datei entfernt');
}
// Drucker laden
async function loadPrinters() {
const select = document.getElementById('printer-select');
try {
debugLog('Lade Drucker...');
select.innerHTML = '<option value="">Lade Drucker...</option>';
select.disabled = true;
const response = await apiCall('/api/printers');
printers = response.printers || [];
debugLog(`${printers.length} Drucker geladen`);
select.innerHTML = '<option value="">Drucker auswählen...</option>';
select.disabled = false;
if (printers.length === 0) {
select.innerHTML = '<option value="">Keine Drucker verfügbar</option>';
select.disabled = true;
showToast('Keine Drucker verfügbar', 'warning');
return;
}
printers.forEach(printer => {
const option = document.createElement('option');
option.value = printer.id;
option.textContent = `${printer.name} (${printer.model || 'Unbekannt'})`;
select.appendChild(option);
});
showToast(`${printers.length} Drucker geladen`, 'success');
} catch (error) {
debugLog('Fehler beim Laden der Drucker: ' + error.message);
select.innerHTML = '<option value="">Fehler beim Laden</option>';
select.disabled = true;
showToast('Fehler beim Laden der Drucker', 'error');
}
}
// Event-Listener einrichten
function setupEventListeners() {
const resetBtn = document.getElementById('reset-btn');
const form = document.getElementById('jobForm');
if (resetBtn) {
resetBtn.addEventListener('click', function() {
if (confirm('Möchten Sie alle Eingaben zurücksetzen?')) {
form.reset();
clearSelectedFile();
showToast('Formular zurückgesetzt', 'info');
}
});
}
if (form) {
form.addEventListener('submit', handleFormSubmission);
}
// Drucker-Auswahl Change-Event
const printerSelect = document.getElementById('printer-select');
if (printerSelect) {
printerSelect.addEventListener('change', updateFormValidation);
}
}
// Formular-Validierung
function setupFormValidation() {
updateFormValidation();
}
function updateFormValidation() {
const printerSelected = document.getElementById('printer-select').value;
const submitButton = document.getElementById('submit-button');
const isValid = selectedFile && printerSelected && !isUploading;
if (submitButton) {
submitButton.disabled = !isValid;
if (isValid) {
submitButton.classList.remove('opacity-50', 'cursor-not-allowed');
submitButton.classList.add('hover:bg-green-700');
} else {
submitButton.classList.add('opacity-50', 'cursor-not-allowed');
submitButton.classList.remove('hover:bg-green-700');
}
}
}
// Formular-Übermittlung
async function handleFormSubmission(e) {
e.preventDefault();
if (!selectedFile) {
showToast('Bitte wählen Sie eine Datei aus', 'error');
return;
}
if (isUploading) {
showToast('Upload bereits in Bearbeitung', 'warning');
return;
}
isUploading = true;
updateFormValidation();
try {
debugLog('Formular wird übermittelt...');
showLoadingOverlay('Druckauftrag wird erstellt...');
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('printer_id', document.getElementById('printer-select').value);
formData.append('priority', document.getElementById('priority-select').value);
formData.append('material', document.getElementById('material-input').value);
formData.append('color', document.getElementById('color-input').value);
formData.append('layer_height', document.getElementById('layer-height-select').value);
formData.append('infill', document.getElementById('infill-select').value);
formData.append('notes', document.getElementById('notes-textarea').value);
const response = await fetch('/api/jobs', {
method: 'POST',
body: formData
});
const result = await response.json();
hideLoadingOverlay();
if (response.ok && result.success) {
showToast('Druckauftrag erfolgreich erstellt!', 'success');
debugLog(`Job erstellt: ID ${result.job_id}`);
setTimeout(() => {
window.location.href = '/jobs';
}, 1500);
} else {
throw new Error(result.message || 'Unbekannter Fehler');
}
} catch (error) {
hideLoadingOverlay();
debugLog('Fehler beim Erstellen des Jobs: ' + error.message);
showToast('Fehler beim Erstellen des Auftrags: ' + error.message, 'error');
} finally {
isUploading = false;
updateFormValidation();
}
}
// Hilfsfunktionen
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function getFileTypeDescription(extension) {
const descriptions = {
'.stl': 'STL - 3D Mesh Datei',
'.gcode': 'G-Code - Druckfertiger Code',
'.3mf': '3MF - 3D Manufacturing Format',
'.obj': 'OBJ - Wavefront 3D Object'
};
return descriptions[extension] || 'Unbekannter Dateityp';
}
function showLoadingOverlay(message) {
const overlay = document.createElement('div');
overlay.id = 'loading-overlay';
overlay.className = 'loading-overlay';
overlay.innerHTML = `
<div class="loading-content">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
<h3 class="text-lg font-semibold mt-4">${message}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-2">Bitte warten Sie...</p>
</div>
`;
document.body.appendChild(overlay);
}
function hideLoadingOverlay() {
const overlay = document.getElementById('loading-overlay');
if (overlay) {
overlay.remove();
}
}
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,546 @@
{% extends "base.html" %}
{% block title %}Datenschutzerklärung - MYP Platform{% endblock %}
{% block content %}
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Hero Header mit Gradient und Animation -->
<div class="relative overflow-hidden rounded-3xl mb-12 p-8 md:p-12 bg-gradient-to-br from-emerald-50 via-green-50 to-teal-50 dark:from-slate-900 dark:via-emerald-900/20 dark:to-green-900/20 border border-emerald-200/50 dark:border-emerald-700/30">
<!-- Animated Background Pattern -->
<div class="absolute inset-0 opacity-10 dark:opacity-5">
<div class="absolute top-0 left-0 w-full h-full">
<svg class="animate-pulse" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<pattern id="privacy-grid" width="8" height="8" patternUnits="userSpaceOnUse">
<circle cx="4" cy="4" r="1" fill="currentColor" opacity="0.3"/>
</pattern>
</defs>
<rect width="100" height="100" fill="url(#privacy-grid)" />
</svg>
</div>
</div>
<!-- Content -->
<div class="relative z-10 text-center">
<!-- Icon -->
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-emerald-100 dark:bg-emerald-900/50 mb-6">
<svg class="w-10 h-10 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
<h1 class="text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-4 tracking-tight">
Datenschutzerklärung
</h1>
<p class="text-xl text-slate-600 dark:text-slate-300 mb-6 max-w-2xl mx-auto">
Transparenz über die Verarbeitung Ihrer personenbezogenen Daten
</p>
<!-- Meta Information -->
<div class="inline-flex items-center gap-4 text-sm text-slate-500 dark:text-slate-400">
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>Gültig ab 15. Juni 2024</span>
</div>
<div class="flex items-center gap-2">
<svg class="w-4 h-4" 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>
<span>Lesezeit: ~8 Minuten</span>
</div>
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
<span>DSGVO-konform</span>
</div>
</div>
</div>
</div>
<!-- Table of Contents -->
<div class="mb-12">
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white mb-4 flex items-center gap-3">
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16"/>
</svg>
Inhaltsverzeichnis
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<a href="#section-1" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">1</span>
<span class="text-sm">Verantwortliche Stelle</span>
</a>
<a href="#section-2" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">2</span>
<span class="text-sm">Erhobene Daten</span>
</a>
<a href="#section-3" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">3</span>
<span class="text-sm">Verarbeitungszweck</span>
</a>
<a href="#section-4" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">4</span>
<span class="text-sm">Rechtsgrundlage</span>
</a>
<a href="#section-5" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">5</span>
<span class="text-sm">Speicherdauer</span>
</a>
<a href="#section-6" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">6</span>
<span class="text-sm">Ihre Rechte</span>
</a>
</div>
</div>
</div>
<!-- Privacy Content -->
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Sidebar Navigation (hidden on mobile) -->
<div class="hidden lg:block">
<div class="sticky top-8">
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-4 uppercase tracking-wider">Navigation</h3>
<nav class="space-y-2">
<a href="#section-1" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">1. Verantwortliche Stelle</a>
<a href="#section-2" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">2. Erhobene Daten</a>
<a href="#section-3" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">3. Verarbeitungszweck</a>
<a href="#section-4" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">4. Rechtsgrundlage</a>
<a href="#section-5" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">5. Speicherdauer</a>
<a href="#section-6" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">6. Ihre Rechte</a>
</nav>
</div>
</div>
</div>
<!-- Main Content -->
<div class="lg:col-span-3">
<div class="space-y-8">
<!-- Section 1 - Verantwortliche Stelle -->
<section id="section-1" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-emerald-100 dark:bg-emerald-900/50 flex items-center justify-center">
<span class="text-emerald-600 dark:text-emerald-400 font-bold">1</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Verantwortliche Stelle</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">
Verantwortlich für die Datenverarbeitung im Rahmen der MYP-Plattform ist:
</p>
<div class="bg-slate-50/50 dark:bg-slate-800/50 rounded-lg p-4 border-l-4 border-emerald-500">
<p class="text-slate-700 dark:text-slate-300 mb-0">
<strong>Mercedes-Benz Group AG</strong><br>
Mercedesstraße 120<br>
70372 Stuttgart<br>
Deutschland
</p>
</div>
</div>
</section>
<!-- Section 2 - Art der erhobenen Daten -->
<section id="section-2" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
<span class="text-blue-600 dark:text-blue-400 font-bold">2</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Art der erhobenen Daten</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">
Im Rahmen der Nutzung der MYP-Plattform werden folgende personenbezogene Daten erhoben und verarbeitet:
</p>
<div class="grid gap-3">
<div class="flex items-start gap-3 p-4 rounded-lg bg-blue-50/50 dark:bg-blue-900/10 border border-blue-200/50 dark:border-blue-800/50">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
<div>
<span class="font-medium text-slate-700 dark:text-slate-300">Personenstammdaten</span>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">Name, E-Mail-Adresse, Abteilung</p>
</div>
</div>
<div class="flex items-start gap-3 p-4 rounded-lg bg-purple-50/50 dark:bg-purple-900/10 border border-purple-200/50 dark:border-purple-800/50">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5 flex-shrink-0" 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-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
</svg>
<div>
<span class="font-medium text-slate-700 dark:text-slate-300">Benutzerkonto-Informationen</span>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">Benutzername, verschlüsseltes Passwort</p>
</div>
</div>
<div class="flex items-start gap-3 p-4 rounded-lg bg-green-50/50 dark:bg-green-900/10 border border-green-200/50 dark:border-green-800/50">
<svg class="w-5 h-5 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0" 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>
<span class="font-medium text-slate-700 dark:text-slate-300">Nutzungsdaten</span>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">Druckaufträge, Zeitstempel, verwendete Geräte</p>
</div>
</div>
<div class="flex items-start gap-3 p-4 rounded-lg bg-orange-50/50 dark:bg-orange-900/10 border border-orange-200/50 dark:border-orange-800/50">
<svg class="w-5 h-5 text-orange-600 dark:text-orange-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
<div>
<span class="font-medium text-slate-700 dark:text-slate-300">Protokolldaten</span>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">IP-Adresse, Zugriffszeiten, Aktivitäten</p>
</div>
</div>
<div class="flex items-start gap-3 p-4 rounded-lg bg-indigo-50/50 dark:bg-indigo-900/10 border border-indigo-200/50 dark:border-indigo-800/50">
<svg class="w-5 h-5 text-indigo-600 dark:text-indigo-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<div>
<span class="font-medium text-slate-700 dark:text-slate-300">Einstellungen</span>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">Präferenzen zur Personalisierung der Plattform</p>
</div>
</div>
</div>
</div>
</section>
<!-- Section 3 - Zweck der Datenverarbeitung -->
<section id="section-3" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center">
<span class="text-purple-600 dark:text-purple-400 font-bold">3</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Zweck der Datenverarbeitung</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">Die Verarbeitung der Daten erfolgt zu folgenden Zwecken:</p>
<div class="grid gap-3">
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Bereitstellung und Verwaltung der MYP-Plattform</span>
</div>
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Authentifizierung und Autorisierung der Nutzer</span>
</div>
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Verwaltung und Optimierung von Druckaufträgen</span>
</div>
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Ressourcenplanung und -optimierung</span>
</div>
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Gewährleistung der Systemsicherheit</span>
</div>
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Erstellung anonymisierter Statistiken zur Systemnutzung</span>
</div>
</div>
</div>
</section>
<!-- Section 4 - Rechtsgrundlage -->
<section id="section-4" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-orange-100 dark:bg-orange-900/50 flex items-center justify-center">
<span class="text-orange-600 dark:text-orange-400 font-bold">4</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Rechtsgrundlage der Verarbeitung</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">Die Datenverarbeitung erfolgt auf Grundlage:</p>
<div class="space-y-3">
<div class="p-4 rounded-lg bg-blue-50/50 dark:bg-blue-900/10 border border-blue-200/50 dark:border-blue-800/50">
<div class="flex items-start gap-3">
<span class="text-blue-600 dark:text-blue-400 font-bold text-sm">Art. 6 Abs. 1 lit. b DSGVO</span>
</div>
<p class="text-slate-700 dark:text-slate-300 text-sm mt-2">Der Erfüllung des Arbeitsverhältnisses</p>
</div>
<div class="p-4 rounded-lg bg-green-50/50 dark:bg-green-900/10 border border-green-200/50 dark:border-green-800/50">
<div class="flex items-start gap-3">
<span class="text-green-600 dark:text-green-400 font-bold text-sm">Art. 6 Abs. 1 lit. f DSGVO</span>
</div>
<p class="text-slate-700 dark:text-slate-300 text-sm mt-2">Der Wahrung berechtigter Interessen (effiziente Ressourcenverwaltung, Systemsicherheit)</p>
</div>
<div class="p-4 rounded-lg bg-purple-50/50 dark:bg-purple-900/10 border border-purple-200/50 dark:border-purple-800/50">
<div class="flex items-start gap-3">
<span class="text-purple-600 dark:text-purple-400 font-bold text-sm">Art. 6 Abs. 1 lit. a DSGVO</span>
</div>
<p class="text-slate-700 dark:text-slate-300 text-sm mt-2">Gegebenenfalls einer Einwilligung für optionale Funktionen</p>
</div>
</div>
</div>
</section>
<!-- Section 5 - Speicherdauer -->
<section id="section-5" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center">
<span class="text-indigo-600 dark:text-indigo-400 font-bold">5</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Speicherdauer</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">
Die personenbezogenen Daten werden nur so lange gespeichert, wie es für die genannten Zwecke erforderlich ist oder gesetzliche Aufbewahrungspflichten bestehen:
</p>
<div class="grid gap-4">
<div class="flex items-center gap-4 p-4 rounded-lg bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/10 dark:to-indigo-900/10 border border-blue-200/50 dark:border-blue-800/50">
<div class="w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<div>
<h4 class="font-semibold text-slate-900 dark:text-white">Benutzerkontodaten</h4>
<p class="text-sm text-slate-600 dark:text-slate-400">Für die Dauer der Unternehmenszugehörigkeit</p>
</div>
</div>
<div class="flex items-center gap-4 p-4 rounded-lg bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/10 dark:to-emerald-900/10 border border-green-200/50 dark:border-green-800/50">
<div class="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/50 flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
</div>
<div>
<h4 class="font-semibold text-slate-900 dark:text-white">Druckauftragsdaten</h4>
<p class="text-sm text-slate-600 dark:text-slate-400">12 Monate nach Abschluss des Auftrags</p>
</div>
</div>
<div class="flex items-center gap-4 p-4 rounded-lg bg-gradient-to-r from-orange-50 to-red-50 dark:from-orange-900/10 dark:to-red-900/10 border border-orange-200/50 dark:border-orange-800/50">
<div class="w-12 h-12 rounded-full bg-orange-100 dark:bg-orange-900/50 flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
</div>
<div>
<h4 class="font-semibold text-slate-900 dark:text-white">Protokolldaten</h4>
<p class="text-sm text-slate-600 dark:text-slate-400">90 Tage</p>
</div>
</div>
</div>
</div>
</section>
<!-- Section 6 - Ihre Rechte -->
<section id="section-6" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-emerald-100 dark:bg-emerald-900/50 flex items-center justify-center">
<span class="text-emerald-600 dark:text-emerald-400 font-bold">6</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Rechte der betroffenen Personen</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-6">
Als Nutzer der MYP-Plattform haben Sie folgende Rechte:
</p>
<div class="grid md:grid-cols-2 gap-4">
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div>
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Recht auf Auskunft</h4>
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 15 DSGVO - über die gespeicherten Daten</p>
</div>
</div>
</div>
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2V7a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/>
</svg>
<div>
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Recht auf Berichtigung</h4>
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 16 DSGVO - unrichtiger Daten</p>
</div>
</div>
</div>
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
<div>
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Recht auf Löschung</h4>
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 17 DSGVO - der Daten</p>
</div>
</div>
</div>
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18.364 5.636M5.636 18.364l12.728-12.728"/>
</svg>
<div>
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Recht auf Einschränkung</h4>
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 18 DSGVO - der Verarbeitung</p>
</div>
</div>
</div>
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-indigo-600 dark:text-indigo-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/>
</svg>
<div>
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Recht auf Datenübertragbarkeit</h4>
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 20 DSGVO</p>
</div>
</div>
</div>
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-orange-600 dark:text-orange-400 mt-1 flex-shrink-0" 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-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<div>
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Widerspruchsrecht</h4>
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 21 DSGVO - gegen die Verarbeitung</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Kompakte weitere Abschnitte -->
<div class="grid gap-6">
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
<span class="w-6 h-6 rounded-full bg-teal-100 dark:bg-teal-900/50 text-teal-600 dark:text-teal-400 text-sm font-bold flex items-center justify-center">7</span>
Datensicherheit
</h3>
<p class="text-slate-700 dark:text-slate-300 text-sm mb-3">Mercedes-Benz trifft angemessene technische und organisatorische Maßnahmen, um die Sicherheit der personenbezogenen Daten zu gewährleisten:</p>
<div class="grid sm:grid-cols-2 gap-2 text-sm text-slate-600 dark:text-slate-400">
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span>Verschlüsselung</span>
</div>
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span>Zugriffskontrollen</span>
</div>
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span>Sicherheitsüberprüfungen</span>
</div>
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span>Mitarbeiterschulungen</span>
</div>
</div>
</div>
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-sm font-bold flex items-center justify-center">8</span>
Datenschutzbeauftragter
</h3>
<p class="text-slate-700 dark:text-slate-300 text-sm mb-3">Bei Fragen zum Datenschutz oder zur Ausübung Ihrer Rechte wenden Sie sich an:</p>
<div class="bg-slate-50/50 dark:bg-slate-800/50 rounded-lg p-3 border-l-4 border-blue-500">
<p class="text-slate-700 dark:text-slate-300 text-sm mb-0">
<strong>Datenschutzbeauftragter</strong><br>
Mercedes-Benz Group AG<br>
HPC G353<br>
70546 Stuttgart<br>
<a href="mailto:data.protection@mercedes-benz.com" class="text-blue-600 dark:text-blue-400 hover:underline">data.protection@mercedes-benz.com</a>
</p>
</div>
</div>
</div>
<!-- Kontakt Section -->
<div class="glass-card bg-gradient-to-br from-emerald-50 to-green-50 dark:from-emerald-900/20 dark:to-green-900/20 border border-emerald-200/50 dark:border-emerald-700/30 rounded-2xl p-8">
<div class="text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-emerald-100 dark:bg-emerald-900/50 mb-6">
<svg class="w-8 h-8 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</div>
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-4">Fragen zum Datenschutz?</h3>
<p class="text-slate-600 dark:text-slate-400 mb-6">Bei Fragen zur Datenverarbeitung in der MYP-Plattform wenden Sie sich gerne an unser Team.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="mailto:till.tomczak@mercedes-benz.com" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Till Tomczak
</a>
<a href="mailto:torben.haack@mercedes-benz.com" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Torben Haack
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-4 justify-end mt-12 pt-8 border-t border-slate-200 dark:border-slate-700">
<button onclick="window.print()" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-lg transition-all duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
Drucken
</button>
<a href="javascript:history.back()" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
<svg class="w-4 h-4" 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
</a>
</div>
</div>
<!-- Smooth Scrolling Script -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,803 @@
{% extends "base.html" %}
{% block title %}Profil - MYP Platform{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Mein Profil</h1>
<p class="mt-2 text-slate-500 dark:text-slate-400">Verwalten Sie Ihre Kontoinformationen und Einstellungen</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Profile Information -->
<div class="lg:col-span-2">
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50 mb-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900 dark:text-white">Persönliche Informationen</h2>
<button onclick="toggleEditMode()" id="edit-button"
class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-4 py-2 rounded-lg transition-all duration-200">
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Bearbeiten
</button>
</div>
<form id="profile-form" onsubmit="handleProfileUpdate(event)">
<!-- Avatar Section -->
<div class="flex items-center space-x-6 mb-8 pb-6 border-b border-gray-200 dark:border-slate-600">
<div class="relative">
<div class="w-24 h-24 rounded-full bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center text-white text-2xl font-bold shadow-lg">
<span id="avatar-text">{{ (current_user.first_name[0] if current_user.first_name else current_user.email[0]) | upper }}</span>
<img id="avatar-image" src="{{ current_user.avatar_url if current_user.avatar_url else '' }}"
alt="Profilbild" class="w-24 h-24 rounded-full object-cover {{ 'hidden' if not current_user.avatar_url else '' }}">
</div>
<button type="button" onclick="triggerAvatarUpload()"
class="absolute bottom-0 right-0 bg-blue-600 hover:bg-blue-700 text-white rounded-full p-2 shadow-lg transition-all duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</button>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-1">
{{ current_user.first_name + ' ' + current_user.last_name if current_user.first_name and current_user.last_name else current_user.email.split('@')[0] }}
</h3>
<p class="text-slate-500 dark:text-slate-400">{{ current_user.email }}</p>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
{{ 'Administrator' if current_user.is_admin else 'Benutzer' }} •
{{ current_user.department or 'Keine Abteilung' }}
</p>
</div>
<input type="file" id="avatar-upload" name="avatar" accept="image/*" class="hidden" onchange="handleAvatarUpload(event)">
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="first-name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Vorname
</label>
<input type="text" id="first-name" name="first_name" value="{{ current_user.first_name or '' }}" disabled
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
</div>
<div>
<label for="last-name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Nachname
</label>
<input type="text" id="last-name" name="last_name" value="{{ current_user.last_name or '' }}" disabled
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
</div>
<div>
<label for="email" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
E-Mail-Adresse
</label>
<input type="email" id="email" name="email" value="{{ current_user.email }}" disabled
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
</div>
<div>
<label for="phone" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Telefonnummer
</label>
<input type="tel" id="phone" name="phone" value="{{ current_user.phone or '' }}" disabled
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
</div>
<div>
<label for="department" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Abteilung
</label>
<input type="text" id="department" name="department" value="{{ current_user.department or '' }}" disabled
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
</div>
<div>
<label for="role" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Rolle
</label>
<input type="text" id="role" value="{{ 'Administrator' if current_user.is_admin else 'Benutzer' }}" disabled
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-gray-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 cursor-not-allowed">
</div>
</div>
<div id="form-actions" class="hidden mt-6 flex space-x-4">
<button type="submit"
class="bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-500 text-white px-6 py-2 rounded-lg transition-all duration-200">
Änderungen speichern
</button>
<button type="button" onclick="cancelEdit()"
class="bg-gray-200 hover:bg-gray-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-800 dark:text-white px-6 py-2 rounded-lg transition-all duration-200">
Abbrechen
</button>
</div>
</form>
</div>
<!-- Password Change -->
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-6">Passwort ändern</h2>
<form id="password-form" onsubmit="handlePasswordChange(event)" class="space-y-4">
<div>
<label for="current-password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Aktuelles Passwort
</label>
<input type="password" id="current-password" name="current_password" required
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white">
</div>
<div>
<label for="new-password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Neues Passwort
</label>
<input type="password" id="new-password" name="new_password" required minlength="8"
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white">
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">Mindestens 8 Zeichen</p>
</div>
<div>
<label for="confirm-password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Passwort bestätigen
</label>
<input type="password" id="confirm-password" name="confirm_password" required
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white">
</div>
<button type="submit"
class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-6 py-2 rounded-lg transition-all duration-200">
Passwort ändern
</button>
</form>
</div>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Profile Stats -->
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Statistiken</h3>
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-sm text-slate-600 dark:text-slate-400">Gesamte Aufträge</span>
<span id="total-jobs" class="text-lg font-bold text-slate-900 dark:text-white">-</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-slate-600 dark:text-slate-400">Abgeschlossene Aufträge</span>
<span id="completed-jobs" class="text-lg font-bold text-green-600 dark:text-green-400">-</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-slate-600 dark:text-slate-400">Aktive Aufträge</span>
<span id="active-jobs" class="text-lg font-bold text-blue-600 dark:text-blue-400">-</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-slate-600 dark:text-slate-400">Fehlgeschlagene Aufträge</span>
<span id="failed-jobs" class="text-lg font-bold text-red-600 dark:text-red-400">-</span>
</div>
</div>
</div>
<!-- Account Info -->
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Kontoinformationen</h3>
<div class="space-y-3 text-sm">
<div>
<span class="text-slate-600 dark:text-slate-400">Mitglied seit:</span>
<div class="font-medium text-slate-900 dark:text-white">{{ current_user.created_at | format_datetime('%d.%m.%Y') if current_user.created_at else 'Unbekannt' }}</div>
</div>
<div>
<span class="text-slate-600 dark:text-slate-400">Letzte Anmeldung:</span>
<div class="font-medium text-slate-900 dark:text-white">{{ current_user.last_login | format_datetime if current_user.last_login else 'Nie' }}</div>
</div>
<div>
<span class="text-slate-600 dark:text-slate-400">Konto-Status:</span>
<div class="font-medium">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-600 dark:bg-green-700 text-white">
Aktiv
</span>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Schnellaktionen</h3>
<div class="space-y-3">
<a href="/new-job"
class="block w-full bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-500 text-white text-center py-2 px-4 rounded-lg transition-all duration-200">
Neuer Auftrag
</a>
<a href="/my/jobs"
class="block w-full bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white text-center py-2 px-4 rounded-lg transition-all duration-200">
Meine Aufträge
</a>
<button onclick="downloadUserData()"
class="block w-full bg-gray-200 hover:bg-gray-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-800 dark:text-white text-center py-2 px-4 rounded-lg transition-all duration-200">
Daten exportieren
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let isEditMode = false;
let originalFormData = {};
let avatarFile = null;
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadUserStats();
storeOriginalFormData();
setupPasswordStrengthMeter();
setupFormValidation();
});
// Store original form data
function storeOriginalFormData() {
const form = document.getElementById('profile-form');
const formData = new FormData(form);
originalFormData = {};
for (let [key, value] of formData.entries()) {
if (key !== 'avatar') { // Don't store file data
originalFormData[key] = value;
}
}
}
// Avatar Upload Functions
function triggerAvatarUpload() {
document.getElementById('avatar-upload').click();
}
async function handleAvatarUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Validate file
if (!file.type.startsWith('image/')) {
showFlashMessage('Bitte wählen Sie eine gültige Bilddatei aus', 'error');
return;
}
if (file.size > 5 * 1024 * 1024) { // 5MB limit
showFlashMessage('Die Datei ist zu groß. Maximale Größe: 5MB', 'error');
return;
}
try {
// Show loading state
const uploadButton = event.target.parentElement.querySelector('button');
const originalButtonContent = uploadButton.innerHTML;
uploadButton.innerHTML = `
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
`;
uploadButton.disabled = true;
// Preview image
const reader = new FileReader();
reader.onload = function(e) {
const avatarImage = document.getElementById('avatar-image');
const avatarText = document.getElementById('avatar-text');
avatarImage.src = e.target.result;
avatarImage.classList.remove('hidden');
avatarText.style.display = 'none';
};
reader.readAsDataURL(file);
// Upload avatar
const formData = new FormData();
formData.append('avatar', file);
const response = await fetch('/api/user/avatar', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
});
const result = await response.json();
if (result.success) {
showFlashMessage('Profilbild erfolgreich aktualisiert', 'success');
avatarFile = file;
} else {
throw new Error(result.message || 'Upload fehlgeschlagen');
}
} catch (error) {
console.error('Avatar upload error:', error);
showFlashMessage('Fehler beim Hochladen des Profilbilds: ' + error.message, 'error');
// Reset preview on error
const avatarImage = document.getElementById('avatar-image');
const avatarText = document.getElementById('avatar-text');
avatarImage.classList.add('hidden');
avatarText.style.display = 'flex';
} finally {
// Restore button
const uploadButton = event.target.parentElement.querySelector('button');
uploadButton.innerHTML = originalButtonContent;
uploadButton.disabled = false;
}
}
// Password Strength Meter
function setupPasswordStrengthMeter() {
const newPasswordInput = document.getElementById('new-password');
// Create password strength indicator
const strengthMeter = document.createElement('div');
strengthMeter.id = 'password-strength-meter';
strengthMeter.className = 'mt-2';
const strengthBar = document.createElement('div');
strengthBar.className = 'w-full bg-gray-200 dark:bg-slate-600 rounded-full h-2';
const strengthIndicator = document.createElement('div');
strengthIndicator.id = 'strength-indicator';
strengthIndicator.className = 'h-2 rounded-full transition-all duration-300';
const strengthText = document.createElement('div');
strengthText.id = 'strength-text';
strengthText.className = 'text-xs mt-1';
strengthBar.appendChild(strengthIndicator);
strengthMeter.appendChild(strengthBar);
strengthMeter.appendChild(strengthText);
newPasswordInput.parentElement.appendChild(strengthMeter);
newPasswordInput.addEventListener('input', function() {
const password = this.value;
const strength = calculatePasswordStrength(password);
updatePasswordStrengthMeter(strength);
});
}
function calculatePasswordStrength(password) {
let score = 0;
const checks = {
length: password.length >= 8,
lowercase: /[a-z]/.test(password),
uppercase: /[A-Z]/.test(password),
numbers: /\d/.test(password),
special: /[!@#$%^&*(),.?":{}|<>]/.test(password)
};
score = Object.values(checks).filter(Boolean).length;
let strength = 'weak';
if (score >= 4) strength = 'strong';
else if (score >= 3) strength = 'medium';
return { score, strength, checks };
}
function updatePasswordStrengthMeter(strengthData) {
const indicator = document.getElementById('strength-indicator');
const text = document.getElementById('strength-text');
const { strength, score } = strengthData;
const percentage = (score / 5) * 100;
// Update bar
indicator.style.width = `${percentage}%`;
// Update colors and text
switch (strength) {
case 'weak':
indicator.className = 'h-2 rounded-full transition-all duration-300 bg-red-500';
text.textContent = 'Schwach';
text.className = 'text-xs mt-1 text-red-500';
break;
case 'medium':
indicator.className = 'h-2 rounded-full transition-all duration-300 bg-yellow-500';
text.textContent = 'Mittel';
text.className = 'text-xs mt-1 text-yellow-500';
break;
case 'strong':
indicator.className = 'h-2 rounded-full transition-all duration-300 bg-green-500';
text.textContent = 'Stark';
text.className = 'text-xs mt-1 text-green-500';
break;
}
}
// Enhanced Form Validation
function setupFormValidation() {
// Real-time email validation
const emailInput = document.getElementById('email');
emailInput.addEventListener('blur', validateEmail);
// Phone number formatting
const phoneInput = document.getElementById('phone');
phoneInput.addEventListener('input', formatPhoneNumber);
// Name validation
['first-name', 'last-name'].forEach(id => {
const input = document.getElementById(id);
input.addEventListener('input', function() {
validateName(this);
});
});
}
function validateEmail(event) {
const email = event.target.value;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (email && !emailRegex.test(email)) {
event.target.setCustomValidity('Bitte geben Sie eine gültige E-Mail-Adresse ein');
event.target.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
} else {
event.target.setCustomValidity('');
event.target.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
}
}
function formatPhoneNumber(event) {
let value = event.target.value.replace(/\D/g, '');
if (value.length >= 10) {
value = value.replace(/(\d{2})(\d{3})(\d{3})(\d{4})/, '+49 $2 $3 $4');
}
event.target.value = value;
}
function validateName(input) {
const nameRegex = /^[a-zA-ZäöüÄÖÜß\s-]+$/;
const value = input.value.trim();
if (value && !nameRegex.test(value)) {
input.setCustomValidity('Namen dürfen nur Buchstaben, Leerzeichen und Bindestriche enthalten');
input.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
} else {
input.setCustomValidity('');
input.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
}
}
// Toggle edit mode with enhanced UX
function toggleEditMode() {
isEditMode = !isEditMode;
const inputs = document.querySelectorAll('#profile-form input:not(#role)');
const editButton = document.getElementById('edit-button');
const formActions = document.getElementById('form-actions');
inputs.forEach(input => {
input.disabled = !isEditMode;
if (isEditMode) {
input.classList.add('focus:ring-2', 'focus:ring-blue-600', 'focus:border-blue-600');
} else {
input.classList.remove('focus:ring-2', 'focus:ring-blue-600', 'focus:border-blue-600');
}
});
if (isEditMode) {
editButton.innerHTML = `
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
Abbrechen
`;
editButton.className = 'bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-500 text-white px-4 py-2 rounded-lg transition-all duration-200';
editButton.onclick = cancelEdit;
formActions.classList.remove('hidden');
} else {
editButton.innerHTML = `
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Bearbeiten
`;
editButton.className = 'bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-4 py-2 rounded-lg transition-all duration-200';
editButton.onclick = toggleEditMode;
formActions.classList.add('hidden');
}
}
// Cancel edit with confirmation
function cancelEdit() {
// Check if there are unsaved changes
const hasChanges = checkForUnsavedChanges();
if (hasChanges) {
if (!confirm('Sie haben ungespeicherte Änderungen. Möchten Sie wirklich abbrechen?')) {
return;
}
}
// Restore original values
Object.keys(originalFormData).forEach(key => {
const input = document.querySelector(`[name="${key}"]`);
if (input) {
input.value = originalFormData[key];
}
});
// Reset avatar if changed
if (avatarFile) {
const avatarImage = document.getElementById('avatar-image');
const avatarText = document.getElementById('avatar-text');
avatarImage.classList.add('hidden');
avatarText.style.display = 'flex';
avatarFile = null;
}
toggleEditMode();
}
function checkForUnsavedChanges() {
return Object.keys(originalFormData).some(key => {
const input = document.querySelector(`[name="${key}"]`);
return input && input.value !== originalFormData[key];
}) || avatarFile !== null;
}
// Enhanced profile update with loading state
async function handleProfileUpdate(event) {
event.preventDefault();
const submitButton = event.target.querySelector('button[type="submit"]');
const originalButtonText = submitButton.innerHTML;
// Show loading state
submitButton.disabled = true;
submitButton.innerHTML = `
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Speichern...
`;
const formData = new FormData(event.target);
const profileData = {};
for (let [key, value] of formData.entries()) {
if (key !== 'avatar') {
profileData[key] = value;
}
}
try {
const response = await apiCall('/api/user/profile', {
method: 'PUT',
body: JSON.stringify(profileData)
});
if (response.success) {
showFlashMessage('Profil erfolgreich aktualisiert', 'success');
storeOriginalFormData();
toggleEditMode();
// Update avatar text if name changed
const newInitial = (profileData.first_name?.[0] || profileData.email?.[0] || 'U').toUpperCase();
document.getElementById('avatar-text').textContent = newInitial;
} else {
throw new Error(response.message || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Error updating profile:', error);
showFlashMessage('Fehler beim Aktualisieren des Profils: ' + error.message, 'error');
} finally {
// Restore button
submitButton.disabled = false;
submitButton.innerHTML = originalButtonText;
}
}
// Enhanced password change with better validation
async function handlePasswordChange(event) {
event.preventDefault();
const submitButton = event.target.querySelector('button[type="submit"]');
const originalButtonText = submitButton.innerHTML;
const formData = new FormData(event.target);
const newPassword = formData.get('new_password');
const confirmPassword = formData.get('confirm_password');
// Enhanced password validation
const strength = calculatePasswordStrength(newPassword);
if (strength.score < 3) {
showFlashMessage('Das Passwort ist zu schwach. Verwenden Sie eine Kombination aus Groß- und Kleinbuchstaben, Zahlen und Sonderzeichen.', 'error');
return;
}
if (newPassword !== confirmPassword) {
showFlashMessage('Die Passwörter stimmen nicht überein', 'error');
return;
}
// Show loading state
submitButton.disabled = true;
submitButton.innerHTML = `
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Ändern...
`;
const passwordData = {
current_password: formData.get('current_password'),
new_password: newPassword
};
try {
const response = await apiCall('/api/user/password', {
method: 'PUT',
body: JSON.stringify(passwordData)
});
if (response.success) {
showFlashMessage('Passwort erfolgreich geändert', 'success');
document.getElementById('password-form').reset();
// Reset password strength meter
const strengthIndicator = document.getElementById('strength-indicator');
const strengthText = document.getElementById('strength-text');
if (strengthIndicator) {
strengthIndicator.style.width = '0%';
strengthText.textContent = '';
}
} else {
throw new Error(response.message || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Error changing password:', error);
showFlashMessage('Fehler beim Ändern des Passworts: ' + error.message, 'error');
} finally {
// Restore button
submitButton.disabled = false;
submitButton.innerHTML = originalButtonText;
}
}
// Load user statistics with enhanced error handling
async function loadUserStats() {
try {
const response = await apiCall('/api/user/stats');
if (response.success) {
const stats = response.stats;
// Animate counter updates
animateCounter('total-jobs', stats.total_jobs || 0);
animateCounter('completed-jobs', stats.completed_jobs || 0);
animateCounter('active-jobs', stats.active_jobs || 0);
animateCounter('failed-jobs', stats.failed_jobs || 0);
}
} catch (error) {
console.error('Error loading user stats:', error);
// Keep default values (-) if loading fails
}
}
function animateCounter(elementId, targetValue) {
const element = document.getElementById(elementId);
const startValue = parseInt(element.textContent) || 0;
const duration = 1500;
const increment = (targetValue - startValue) / (duration / 16);
let currentValue = startValue;
const timer = setInterval(() => {
currentValue += increment;
if (increment > 0 ? currentValue >= targetValue : currentValue <= targetValue) {
element.textContent = targetValue;
clearInterval(timer);
} else {
element.textContent = Math.floor(currentValue);
}
}, 16);
}
// Enhanced data export with progress tracking
async function downloadUserData() {
try {
// Show loading state
const button = event.target;
const originalButtonText = button.innerHTML;
button.disabled = true;
button.innerHTML = `
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 inline" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Exportiere...
`;
const response = await fetch('/api/user/export', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `meine_daten_${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showFlashMessage('Daten erfolgreich exportiert', 'success');
} catch (error) {
console.error('Error downloading user data:', error);
showFlashMessage('Fehler beim Exportieren der Daten: ' + error.message, 'error');
} finally {
// Restore button
button.disabled = false;
button.innerHTML = originalButtonText;
}
}
// Enhanced password confirmation validation
document.getElementById('confirm-password').addEventListener('input', function() {
const newPassword = document.getElementById('new-password').value;
const confirmPassword = this.value;
if (confirmPassword && newPassword !== confirmPassword) {
this.setCustomValidity('Die Passwörter stimmen nicht überein');
this.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
} else {
this.setCustomValidity('');
this.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
}
});
// Warn user about unsaved changes before leaving
window.addEventListener('beforeunload', function(e) {
if (isEditMode && checkForUnsavedChanges()) {
e.preventDefault();
e.returnValue = '';
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,961 @@
{% extends "base.html" %}
{% block title %}Einstellungen - MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<!-- CSRF Token für AJAX-Anfragen -->
<meta name="csrf-token" content="{{ csrf_token() }}">
{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Einstellungen</h1>
<p class="mt-2 text-slate-600 dark:text-slate-400 transition-colors duration-300">Passen Sie die Anwendung an Ihre Bedürfnisse an</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Settings Sections (Left Side) -->
<div class="lg:col-span-2 space-y-8">
<!-- Appearance Settings -->
<div class="glass-card">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Erscheinungsbild</h2>
</div>
<div class="space-y-6">
<!-- Theme Settings -->
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Farbschema
</label>
<div class="flex items-center space-x-4">
<button id="light-theme-btn" class="theme-btn active px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
Hell
</button>
<button id="dark-theme-btn" class="theme-btn px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
Dunkel
</button>
<button id="system-theme-btn" class="theme-btn px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
System
</button>
</div>
</div>
<!-- Reduced Motion Settings -->
<div>
<div class="flex items-center justify-between">
<label for="reduced-motion" class="text-sm font-medium text-slate-700 dark:text-slate-300">
Reduzierte Bewegung
</label>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="reduced-motion" name="reduced_motion" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="reduced-motion" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">
Reduziert Animationen für bessere Barrierefreiheit
</p>
</div>
<!-- Contrast Settings -->
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Kontrast
</label>
<div class="flex items-center space-x-4">
<button id="normal-contrast-btn" class="contrast-btn active px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
Normal
</button>
<button id="high-contrast-btn" class="contrast-btn px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
Hoher Kontrast
</button>
</div>
</div>
</div>
</div>
<!-- Notification Settings -->
<div class="glass-card">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Benachrichtigungen</h2>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Neue Aufträge</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Benachrichtigung, wenn neue Aufträge erstellt werden</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="notify-new-jobs" name="notify_new_jobs" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
<label for="notify-new-jobs" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Auftragsaktualisierungen</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Benachrichtigung bei Statusänderungen Ihrer Aufträge</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="notify-job-updates" name="notify_job_updates" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
<label for="notify-job-updates" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Systembenachrichtigungen</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Wichtige Systemhinweise und Wartungsmeldungen</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="notify-system" name="notify_system" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
<label for="notify-system" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">E-Mail-Benachrichtigungen</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Zusammenfassung per E-Mail erhalten</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="notify-email" name="notify_email" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="notify-email" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
</div>
</div>
<!-- Privacy & Security -->
<div class="glass-card">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Datenschutz & Sicherheit</h2>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Aktivitätsprotokolle</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Protokollieren Sie Ihre Aktivitäten für erhöhte Sicherheit</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="activity-logs" name="activity_logs" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
<label for="activity-logs" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Zwei-Faktor-Authentifizierung</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Zusätzliche Sicherheitsebene für Ihr Konto</p>
</div>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="two-factor" name="two_factor" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="two-factor" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
</div>
</div>
<div class="flex items-center justify-between py-2">
<div>
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Automatische Abmeldung</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">Nach einer bestimmten Zeit der Inaktivität abmelden</p>
</div>
<select id="auto-logout" name="auto_logout" class="form-select rounded-lg bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-700 text-slate-900 dark:text-white py-1 px-3 text-sm">
<option value="never">Nie</option>
<option value="30">Nach 30 Minuten</option>
<option value="60" selected>Nach 1 Stunde</option>
<option value="120">Nach 2 Stunden</option>
<option value="480">Nach 8 Stunden</option>
</select>
</div>
</div>
<div class="mt-6 pt-4 border-t border-gray-200 dark:border-slate-700">
<button id="save-security-settings" class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium shadow-sm hover:shadow-md transition-all duration-300">
Sicherheitseinstellungen speichern
</button>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Settings Navigation -->
<div class="glass-card">
<h3 class="text-lg font-bold text-slate-900 dark:text-white transition-colors duration-300 mb-4">Einstellungen</h3>
<nav class="space-y-1">
<a href="#appearance" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
Erscheinungsbild
</a>
<a href="#notifications" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg text-slate-900 dark:text-white hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors duration-200">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
Benachrichtigungen
</a>
<a href="#privacy" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg text-slate-900 dark:text-white hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors duration-200">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
Datenschutz & Sicherheit
</a>
<a href="/user/profile" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg text-slate-900 dark:text-white hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors duration-200">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Profil
</a>
</nav>
</div>
<!-- About System -->
<div class="glass-card">
<h3 class="text-lg font-bold text-slate-900 dark:text-white transition-colors duration-300 mb-4">Über das System</h3>
<div class="space-y-3 text-sm">
<div>
<span class="text-slate-600 dark:text-slate-400">Version:</span>
<div class="font-medium text-slate-900 dark:text-white">3.0.0</div>
</div>
<div>
<span class="text-slate-600 dark:text-slate-400">Letzte Aktualisierung:</span>
<div class="font-medium text-slate-900 dark:text-white">15.06.2024</div>
</div>
<div>
<span class="text-slate-600 dark:text-slate-400">Support:</span>
<div class="font-medium text-blue-600 dark:text-blue-400">
<a href="mailto:till.tomczak@mercedes-benz.com" class="hover:underline">till.tomczak@mercedes-benz.com</a>
<br>
<a href="mailto:torben.haack@mercedes-benz.com" class="hover:underline">torben.haack@mercedes-benz.com</a>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-slate-700">
<a href="/terms" class="text-blue-600 dark:text-blue-400 hover:underline text-sm">
Nutzungsbedingungen
</a>
<span class="text-slate-400 mx-2">|</span>
<a href="/privacy" class="text-blue-600 dark:text-blue-400 hover:underline text-sm">
Datenschutzerklärung
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Theme Switcher
const lightThemeBtn = document.getElementById('light-theme-btn');
const darkThemeBtn = document.getElementById('dark-theme-btn');
const systemThemeBtn = document.getElementById('system-theme-btn');
const themeBtns = [lightThemeBtn, darkThemeBtn, systemThemeBtn];
// Initialize button states based on current theme
const STORAGE_KEY = 'myp-dark-mode';
const savedMode = localStorage.getItem(STORAGE_KEY);
if (savedMode === 'true') {
setActiveThemeButton(darkThemeBtn);
} else if (savedMode === 'false') {
setActiveThemeButton(lightThemeBtn);
} else {
setActiveThemeButton(systemThemeBtn);
}
// Theme button click handlers
lightThemeBtn.addEventListener('click', function() {
document.documentElement.classList.remove('dark');
localStorage.setItem(STORAGE_KEY, 'false');
setActiveThemeButton(lightThemeBtn);
});
darkThemeBtn.addEventListener('click', function() {
document.documentElement.classList.add('dark');
localStorage.setItem(STORAGE_KEY, 'true');
setActiveThemeButton(darkThemeBtn);
});
systemThemeBtn.addEventListener('click', function() {
localStorage.removeItem(STORAGE_KEY);
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
setActiveThemeButton(systemThemeBtn);
});
function setActiveThemeButton(activeBtn) {
themeBtns.forEach(btn => {
if (btn === activeBtn) {
btn.classList.add('active');
btn.classList.add('bg-blue-50');
btn.classList.add('dark:bg-blue-900/20');
btn.classList.add('text-blue-700');
btn.classList.add('dark:text-blue-300');
btn.classList.remove('bg-white');
btn.classList.remove('dark:bg-slate-800');
btn.classList.remove('text-slate-900');
btn.classList.remove('dark:text-white');
} else {
btn.classList.remove('active');
btn.classList.remove('bg-blue-50');
btn.classList.remove('dark:bg-blue-900/20');
btn.classList.remove('text-blue-700');
btn.classList.remove('dark:text-blue-300');
btn.classList.add('bg-white');
btn.classList.add('dark:bg-slate-800');
btn.classList.add('text-slate-900');
btn.classList.add('dark:text-white');
}
});
}
// Contrast Settings
const normalContrastBtn = document.getElementById('normal-contrast-btn');
const highContrastBtn = document.getElementById('high-contrast-btn');
const contrastBtns = [normalContrastBtn, highContrastBtn];
normalContrastBtn.addEventListener('click', function() {
document.documentElement.classList.remove('high-contrast');
localStorage.setItem('myp-contrast', 'normal');
setActiveContrastButton(normalContrastBtn);
});
highContrastBtn.addEventListener('click', function() {
document.documentElement.classList.add('high-contrast');
localStorage.setItem('myp-contrast', 'high');
setActiveContrastButton(highContrastBtn);
});
function setActiveContrastButton(activeBtn) {
contrastBtns.forEach(btn => {
if (btn === activeBtn) {
btn.classList.add('active');
btn.classList.add('bg-blue-50');
btn.classList.add('dark:bg-blue-900/20');
btn.classList.add('text-blue-700');
btn.classList.add('dark:text-blue-300');
btn.classList.remove('bg-white');
btn.classList.remove('dark:bg-slate-800');
btn.classList.remove('text-slate-900');
btn.classList.remove('dark:text-white');
} else {
btn.classList.remove('active');
btn.classList.remove('bg-blue-50');
btn.classList.remove('dark:bg-blue-900/20');
btn.classList.remove('text-blue-700');
btn.classList.remove('dark:text-blue-300');
btn.classList.add('bg-white');
btn.classList.add('dark:bg-slate-800');
btn.classList.add('text-slate-900');
btn.classList.add('dark:text-white');
}
});
}
// Save Settings Button
const saveSecurityBtn = document.getElementById('save-security-settings');
saveSecurityBtn.addEventListener('click', function() {
saveAllSettings();
});
// Toggle Switch Styling
document.querySelectorAll('.toggle-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', function() {
// Auto-save bei Änderung
const settingName = this.name.replace('_', ' ');
const status = this.checked ? 'aktiviert' : 'deaktiviert';
showFlashMessage(`${settingName} wurde ${status}`, 'info');
});
});
// Sammle alle Einstellungen und speichere sie
async function saveAllSettings() {
const saveButton = document.getElementById('save-security-settings');
const originalButtonText = saveButton.innerHTML;
try {
// Show loading state
saveButton.disabled = true;
saveButton.innerHTML = `
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Speichern...
`;
// Add loading state to settings cards
const settingsCards = document.querySelectorAll('.glass-card');
settingsCards.forEach(card => card.classList.add('settings-loading'));
// Erscheinungsbild-Einstellungen
const theme = localStorage.getItem(STORAGE_KEY) === 'true' ? 'dark' :
localStorage.getItem(STORAGE_KEY) === 'false' ? 'light' : 'system';
const reducedMotion = document.getElementById('reduced-motion').checked;
const contrast = localStorage.getItem('myp-contrast') || 'normal';
// Benachrichtigungseinstellungen
const notifyNewJobs = document.getElementById('notify-new-jobs').checked;
const notifyJobUpdates = document.getElementById('notify-job-updates').checked;
const notifySystem = document.getElementById('notify-system').checked;
const notifyEmail = document.getElementById('notify-email').checked;
// Datenschutz & Sicherheitseinstellungen
const activityLogs = document.getElementById('activity-logs').checked;
const twoFactor = document.getElementById('two-factor').checked;
const autoLogout = document.getElementById('auto-logout').value;
// Validate settings
if (!validateSettings({ theme, contrast, autoLogout })) {
throw new Error('Ungültige Einstellungen erkannt');
}
// Einstellungsobjekt erstellen
const settings = {
theme: theme,
reduced_motion: reducedMotion,
contrast: contrast,
notifications: {
new_jobs: notifyNewJobs,
job_updates: notifyJobUpdates,
system: notifySystem,
email: notifyEmail
},
privacy: {
activity_logs: activityLogs,
two_factor: twoFactor,
auto_logout: autoLogout
},
timestamp: new Date().toISOString()
};
// Apply reduced motion immediately if changed
if (reducedMotion) {
document.documentElement.style.setProperty('--tw-transition-duration', '0s');
} else {
document.documentElement.style.removeProperty('--tw-transition-duration');
}
// Einstellungen an den Server senden
const response = await fetch('/api/user/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: JSON.stringify(settings)
});
const result = await response.json();
if (result.success) {
// Success animation
settingsCards.forEach(card => {
card.classList.add('settings-saved');
setTimeout(() => card.classList.remove('settings-saved'), 600);
});
showFlashMessage('Alle Einstellungen wurden erfolgreich gespeichert', 'success');
// Cache settings locally for faster access
localStorage.setItem('myp-settings-cache', JSON.stringify(settings));
} else {
throw new Error(result.error || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Fehler beim Speichern der Einstellungen:', error);
showFlashMessage('Fehler beim Speichern der Einstellungen: ' + error.message, 'error');
} finally {
// Restore button and remove loading states
saveButton.disabled = false;
saveButton.innerHTML = originalButtonText;
const settingsCards = document.querySelectorAll('.glass-card');
settingsCards.forEach(card => card.classList.remove('settings-loading'));
}
}
// Validate settings before saving
function validateSettings(settings) {
const validThemes = ['light', 'dark', 'system'];
const validContrast = ['normal', 'high'];
const validLogoutValues = ['never', '30', '60', '120', '480'];
return validThemes.includes(settings.theme) &&
validContrast.includes(settings.contrast) &&
validLogoutValues.includes(settings.autoLogout);
}
// Enhanced settings loading with caching
async function loadUserSettings() {
try {
// Try to load from cache first for better performance
const cachedSettings = localStorage.getItem('myp-settings-cache');
if (cachedSettings) {
try {
const cached = JSON.parse(cachedSettings);
// Use cached settings if they're less than 5 minutes old
if (new Date() - new Date(cached.timestamp) < 5 * 60 * 1000) {
applySettings(cached);
return;
}
} catch (e) {
localStorage.removeItem('myp-settings-cache');
}
}
const response = await fetch('/api/user/settings');
const result = await response.json();
if (result.success) {
const settings = result.settings;
applySettings(settings);
// Cache the loaded settings
settings.timestamp = new Date().toISOString();
localStorage.setItem('myp-settings-cache', JSON.stringify(settings));
}
} catch (error) {
console.error('Fehler beim Laden der Einstellungen:', error);
// Use fallback defaults if loading fails
applyDefaultSettings();
}
}
function applySettings(settings) {
// Theme-Einstellungen anwenden
if (settings.theme === 'dark') {
localStorage.setItem(STORAGE_KEY, 'true');
document.documentElement.classList.add('dark');
setActiveThemeButton(darkThemeBtn);
} else if (settings.theme === 'light') {
localStorage.setItem(STORAGE_KEY, 'false');
document.documentElement.classList.remove('dark');
setActiveThemeButton(lightThemeBtn);
} else {
localStorage.removeItem(STORAGE_KEY);
setActiveThemeButton(systemThemeBtn);
}
// Apply reduced motion setting
if (settings.reduced_motion) {
document.documentElement.style.setProperty('--tw-transition-duration', '0s');
document.getElementById('reduced-motion').checked = true;
} else {
document.documentElement.style.removeProperty('--tw-transition-duration');
document.getElementById('reduced-motion').checked = false;
}
// Weitere Einstellungen anwenden
document.getElementById('notify-new-jobs').checked = settings.notifications?.new_jobs ?? true;
document.getElementById('notify-job-updates').checked = settings.notifications?.job_updates ?? true;
document.getElementById('notify-system').checked = settings.notifications?.system ?? true;
document.getElementById('notify-email').checked = settings.notifications?.email ?? false;
document.getElementById('activity-logs').checked = settings.privacy?.activity_logs ?? true;
document.getElementById('two-factor').checked = settings.privacy?.two_factor ?? false;
document.getElementById('auto-logout').value = settings.privacy?.auto_logout ?? '60';
// Kontrast-Einstellungen
if (settings.contrast === 'high') {
localStorage.setItem('myp-contrast', 'high');
document.documentElement.classList.add('high-contrast');
setActiveContrastButton(highContrastBtn);
} else {
localStorage.setItem('myp-contrast', 'normal');
document.documentElement.classList.remove('high-contrast');
setActiveContrastButton(normalContrastBtn);
}
}
function applyDefaultSettings() {
// Apply safe defaults if loading fails
setActiveThemeButton(systemThemeBtn);
setActiveContrastButton(normalContrastBtn);
document.getElementById('reduced-motion').checked = false;
document.getElementById('notify-new-jobs').checked = true;
document.getElementById('notify-job-updates').checked = true;
document.getElementById('notify-system').checked = true;
document.getElementById('notify-email').checked = false;
document.getElementById('activity-logs').checked = true;
document.getElementById('two-factor').checked = false;
document.getElementById('auto-logout').value = '60';
}
// Auto-logout implementation
let logoutTimer = null;
function setupAutoLogout() {
const autoLogoutSelect = document.getElementById('auto-logout');
// Event-Listener für Änderungen der Auto-Logout-Einstellung
autoLogoutSelect.addEventListener('change', async function() {
const newTimeout = this.value;
try {
const response = await fetch('/api/user/setting', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: JSON.stringify({ auto_logout: newTimeout })
});
if (response.ok) {
// Globales Auto-Logout-System benachrichtigen
if (window.autoLogoutManager) {
window.autoLogoutManager.updateSettings(newTimeout);
}
showFlashMessage('Auto-Logout-Einstellung aktualisiert', 'success');
}
} catch (error) {
console.error('Fehler beim Aktualisieren der Auto-Logout-Einstellung:', error);
showFlashMessage('Fehler beim Speichern der Einstellung', 'error');
}
});
}
// Enhanced toggle switches with keyboard support
function enhanceToggleSwitches() {
document.querySelectorAll('.toggle-checkbox').forEach(checkbox => {
// Add keyboard support
checkbox.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.click();
}
});
// Enhanced change handler with debouncing
let changeTimeout;
checkbox.addEventListener('change', function() {
clearTimeout(changeTimeout);
changeTimeout = setTimeout(() => {
const settingName = this.name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const status = this.checked ? 'aktiviert' : 'deaktiviert';
// Visual feedback
const label = this.nextElementSibling;
if (label) {
label.style.transform = 'scale(1.05)';
setTimeout(() => {
label.style.transform = '';
}, 150);
}
// Auto-save individual settings
saveIndividualSetting(this.name, this.checked);
showFlashMessage(`${settingName} wurde ${status}`, 'info');
}, 300); // Debounce to prevent spam
});
});
}
// Save individual settings for immediate feedback
async function saveIndividualSetting(settingName, value) {
try {
const response = await fetch('/api/user/setting', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: JSON.stringify({ [settingName]: value })
});
if (!response.ok) {
throw new Error('Fehler beim Speichern der Einstellung');
}
// Update cache
const cached = localStorage.getItem('myp-settings-cache');
if (cached) {
try {
const settings = JSON.parse(cached);
// Update the specific setting in cache
const keys = settingName.split('.');
let current = settings;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) current[keys[i]] = {};
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
settings.timestamp = new Date().toISOString();
localStorage.setItem('myp-settings-cache', JSON.stringify(settings));
} catch (e) {
localStorage.removeItem('myp-settings-cache');
}
}
} catch (error) {
console.error('Fehler beim Speichern der Einzeleinstellung:', error);
}
}
// Performance optimization: Intersection Observer for animations
function setupIntersectionObserver() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('in-view');
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.glass-card').forEach(card => {
observer.observe(card);
});
}
// Initialize all enhanced features
function initializeEnhancedFeatures() {
setupAutoLogout();
enhanceToggleSwitches();
setupIntersectionObserver();
setupNavigationLinks();
}
// Setup navigation links
function setupNavigationLinks() {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', function(e) {
// If it's a real link to another page, don't preventDefault
if (this.getAttribute('href').startsWith('#')) {
e.preventDefault();
// Update active state
navItems.forEach(navItem => {
navItem.classList.remove('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-700', 'dark:text-blue-300');
navItem.classList.add('text-slate-900', 'dark:text-white', 'hover:bg-gray-100', 'dark:hover:bg-slate-800');
});
this.classList.add('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-700', 'dark:text-blue-300');
this.classList.remove('text-slate-900', 'dark:text-white', 'hover:bg-gray-100', 'dark:hover:bg-slate-800');
// Scroll to section with offset for header
const targetId = this.getAttribute('href').substring(1);
const targetElement = document.querySelector(`[id="${targetId}"]`) ||
document.querySelector(`h2:contains("${targetId}")`);
if (targetElement) {
const offsetTop = targetElement.offsetTop - 100; // Account for header
window.scrollTo({
top: offsetTop,
behavior: 'smooth'
});
}
}
});
});
}
// Helper function to show flash messages
function showFlashMessage(message, type = 'info') {
// Use the global toast manager if available
if (window.showToast) {
window.showToast(message, type);
} else if (window.MYP && window.MYP.UI && window.MYP.UI.ToastManager) {
const toast = new window.MYP.UI.ToastManager();
toast.show(message, type);
} else {
// Fallback to simple notification
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 p-4 rounded-lg text-white z-50 ${
type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' :
type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'
}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => document.body.removeChild(notification), 300);
}, 3000);
}
}
// Initialize everything
loadUserSettings();
initializeEnhancedFeatures();
});
</script>
<style>
/* Glass Card Effect */
.glass-card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(229, 231, 235, 0.8);
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
padding: 1.5rem;
transition: all 0.3s ease;
}
.dark .glass-card {
background: rgba(30, 41, 59, 0.8);
border-color: rgba(100, 116, 139, 0.3);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.dark .glass-card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
}
/* Loading State Animations */
.settings-loading {
opacity: 0.6;
pointer-events: none;
position: relative;
}
.settings-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Enhanced Toggle Switches */
.toggle-checkbox {
transition: all 0.3s ease;
}
/* Toggle Switch Styling */
.toggle-checkbox:checked {
right: 0;
border-color: #3b82f6;
background-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.toggle-checkbox:checked + .toggle-label {
background-color: #3b82f6;
}
.dark .toggle-checkbox:checked + .toggle-label {
background-color: #2563eb;
}
.toggle-label {
transition: all 0.3s ease;
}
.toggle-checkbox:focus + .toggle-label {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Button states */
.theme-btn.active,
.contrast-btn.active {
box-shadow: 0 0 0 2px #3b82f6;
}
/* High contrast mode styles */
.high-contrast {
--tw-text-opacity: 1;
}
.high-contrast * {
outline: 2px solid transparent;
outline-offset: 2px;
}
.high-contrast button:focus,
.high-contrast input:focus,
.high-contrast select:focus {
outline: 3px solid #000 !important;
outline-offset: 2px;
}
.dark.high-contrast button:focus,
.dark.high-contrast input:focus,
.dark.high-contrast select:focus {
outline: 3px solid #fff !important;
}
/* Accessibility improvements */
@media (prefers-reduced-motion: reduce) {
.glass-card,
.toggle-checkbox,
.toggle-label {
transition: none !important;
}
.settings-loading::after {
animation: none !important;
}
}
/* Success feedback animation */
.settings-saved {
animation: settingsSaved 0.6s ease-in-out;
}
@keyframes settingsSaved {
0% { transform: scale(1); }
50% { transform: scale(1.02); background-color: rgba(34, 197, 94, 0.1); }
100% { transform: scale(1); }
}
/* Smooth section transitions */
.settings-section {
scroll-margin-top: 2rem;
}
</style>
{% endblock %}

View File

@ -0,0 +1,285 @@
{% extends "base.html" %}
{% block title %}Statistiken - MYP Platform{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Header -->
<div class="relative overflow-hidden rounded-2xl p-6 stats-card">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Statistiken</h1>
<p class="mt-2 text-slate-500 dark:text-slate-400">Übersicht über Systemleistung und Nutzungsstatistiken</p>
</div>
<div class="flex gap-4 mt-4 lg:mt-0">
<button onclick="refreshStats()" class="bg-black hover:bg-gray-800 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-6 py-2.5 rounded-xl transition-colors duration-300">
<svg class="h-5 w-5 mr-2 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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 onclick="exportStats()" class="bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-700 dark:text-white px-6 py-2.5 rounded-xl transition-colors duration-300 border border-slate-200 dark:border-slate-600">
<svg class="h-5 w-5 mr-2 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Exportieren
</button>
</div>
</div>
</div>
<!-- Overview Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
<!-- Total Jobs -->
<div class="stats-card p-6" id="stats-total-jobs">
<div class="absolute top-5 right-5 text-slate-900 dark:text-blue-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Gesamte Jobs</p>
<p id="total-jobs-count" class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">-</p>
</div>
<!-- Completed Jobs -->
<div class="stats-card p-6" id="stats-completed-jobs">
<div class="absolute top-5 right-5 text-green-600 dark:text-green-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Abgeschlossene Jobs</p>
<p id="completed-jobs-count" class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">-</p>
</div>
<!-- Active Printers -->
<div class="stats-card p-6" id="stats-active-printers">
<div class="absolute top-5 right-5 text-purple-600 dark:text-purple-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Online Drucker</p>
<p id="online-printers-count" class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">-</p>
</div>
<!-- Success Rate -->
<div class="stats-card p-6" id="stats-success-rate">
<div class="absolute top-5 right-5 text-amber-600 dark:text-amber-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Erfolgsrate</p>
<p id="success-rate-percent" class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">-%</p>
</div>
</div>
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Job Status Distribution -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Job-Status Verteilung</h2>
<div class="h-64">
<canvas id="job-status-chart"></canvas>
</div>
</div>
<!-- Printer Usage -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Drucker-Nutzung</h2>
<div class="h-64">
<canvas id="printer-usage-chart"></canvas>
</div>
</div>
</div>
<!-- Timeline and User Activity Charts -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Jobs Timeline -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Jobs der letzten 30 Tage</h2>
<div class="h-64">
<canvas id="jobs-timeline-chart"></canvas>
</div>
</div>
<!-- User Activity -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Top Benutzer-Aktivität</h2>
<div class="h-64">
<canvas id="user-activity-chart"></canvas>
</div>
</div>
</div>
<!-- System Performance Metrics -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Systemleistung</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Active Jobs -->
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
<svg class="h-8 w-8 mx-auto mb-3 text-slate-900 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">Aktive Jobs</p>
<p id="active-jobs-count" class="text-xl font-bold text-slate-900 dark:text-white">-</p>
</div>
<!-- Failed Jobs -->
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
<svg class="h-8 w-8 mx-auto mb-3 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">Fehlgeschlagene Jobs</p>
<p id="failed-jobs-count" class="text-xl font-bold text-slate-900 dark:text-white">-</p>
</div>
<!-- Total Users -->
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
<svg class="h-8 w-8 mx-auto mb-3 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">Registrierte Benutzer</p>
<p id="total-users-count" class="text-xl font-bold text-slate-900 dark:text-white">-</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- Chart.js CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.min.js"></script>
<!-- Global Refresh Functions -->
<script src="{{ url_for('static', filename='js/global-refresh-functions.js') }}"></script>
<!-- Charts JavaScript -->
<script src="{{ url_for('static', filename='js/charts.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Basis-Statistiken laden
loadBasicStats();
// Theme wechsel Event-Listener
window.addEventListener('darkModeChanged', function(e) {
if (window.updateChartsTheme) {
window.updateChartsTheme();
}
});
// Auto-refresh für Basis-Statistiken (alle 30 Sekunden)
setInterval(loadBasicStats, 30000);
});
async function loadBasicStats() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Fehler beim Laden der Statistiken');
}
// Statistiken aktualisieren
updateStatsCounter('total-jobs-count', data.total_jobs);
updateStatsCounter('completed-jobs-count', data.completed_jobs);
updateStatsCounter('online-printers-count', data.online_printers);
updateStatsCounter('success-rate-percent', data.success_rate + '%');
updateStatsCounter('active-jobs-count', data.active_jobs);
updateStatsCounter('failed-jobs-count', data.failed_jobs);
updateStatsCounter('total-users-count', data.total_users);
} catch (error) {
console.error('Fehler beim Laden der Basis-Statistiken:', error);
showToast('Fehler beim Laden der Statistiken', 'error');
}
}
function updateStatsCounter(elementId, value, animate = true) {
const element = document.getElementById(elementId);
if (!element) return;
if (animate) {
// Animierte Zählung
const currentValue = parseInt(element.textContent.replace(/[^\d]/g, '')) || 0;
const targetValue = parseInt(value.toString().replace(/[^\d]/g, '')) || 0;
if (currentValue !== targetValue) {
animateCounter(element, currentValue, targetValue, value.toString());
}
} else {
element.textContent = value;
}
}
function animateCounter(element, start, end, finalText) {
const duration = 1000; // 1 Sekunde
const startTime = performance.now();
function updateCounter(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing-Funktion (ease-out)
const easeOut = 1 - Math.pow(1 - progress, 3);
const currentValue = Math.round(start + (end - start) * easeOut);
if (finalText.includes('%')) {
element.textContent = currentValue + '%';
} else {
element.textContent = currentValue;
}
if (progress < 1) {
requestAnimationFrame(updateCounter);
} else {
element.textContent = finalText;
}
}
requestAnimationFrame(updateCounter);
}
// Statistiken neu laden
function refreshStats() {
// Feedback für den Benutzer
showToast('Statistiken werden aktualisiert...', 'info');
// Basis-Statistiken laden
loadBasicStats();
// Charts aktualisieren
if (window.refreshAllCharts) {
window.refreshAllCharts();
}
// Erfolgsmeldung
setTimeout(() => {
showToast('Statistiken erfolgreich aktualisiert', 'success');
}, 1000);
}
// Statistiken exportieren
function exportStats() {
// Direkter Download vom API-Endpunkt
window.location.href = '/api/stats/export';
}
// Helper-Funktion für Toast-Benachrichtigungen
function showToast(message, type) {
if (window.showToast) {
window.showToast(message, type);
} else {
// Fallback für einfache Alert
if (type === 'error') {
alert('Fehler: ' + message);
} else {
console.log(type + ': ' + message);
}
}
}
</script>
{% endblock %}

View File

@ -0,0 +1,356 @@
{% extends "base.html" %}
{% block title %}Nutzungsbedingungen - MYP Platform{% endblock %}
{% block content %}
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Hero Header mit Gradient und Animation -->
<div class="relative overflow-hidden rounded-3xl mb-12 p-8 md:p-12 bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-slate-900 dark:via-blue-900/20 dark:to-indigo-900/20 border border-blue-200/50 dark:border-blue-700/30">
<!-- Animated Background Pattern -->
<div class="absolute inset-0 opacity-10 dark:opacity-5">
<div class="absolute top-0 left-0 w-full h-full">
<svg class="animate-pulse" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="currentColor" stroke-width="0.5"/>
</pattern>
</defs>
<rect width="100" height="100" fill="url(#grid)" />
</svg>
</div>
</div>
<!-- Content -->
<div class="relative z-10 text-center">
<!-- Icon -->
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-blue-100 dark:bg-blue-900/50 mb-6">
<svg class="w-10 h-10 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
</div>
<h1 class="text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-4 tracking-tight">
Nutzungsbedingungen
</h1>
<p class="text-xl text-slate-600 dark:text-slate-300 mb-6 max-w-2xl mx-auto">
Rechtliche Grundlagen für die Nutzung der MYP Platform
</p>
<!-- Meta Information -->
<div class="inline-flex items-center gap-4 text-sm text-slate-500 dark:text-slate-400">
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>Gültig ab 15. Juni 2024</span>
</div>
<div class="flex items-center gap-2">
<svg class="w-4 h-4" 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>
<span>Lesezeit: ~5 Minuten</span>
</div>
</div>
</div>
</div>
<!-- Table of Contents -->
<div class="mb-12">
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white mb-4 flex items-center gap-3">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16"/>
</svg>
Inhaltsverzeichnis
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<a href="#section-1" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">1</span>
<span class="text-sm">Allgemeines</span>
</a>
<a href="#section-2" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">2</span>
<span class="text-sm">Zugang</span>
</a>
<a href="#section-3" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">3</span>
<span class="text-sm">Nutzungszweck</span>
</a>
<a href="#section-4" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">4</span>
<span class="text-sm">Verantwortlichkeiten</span>
</a>
<a href="#section-5" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">5</span>
<span class="text-sm">Einschränkungen</span>
</a>
<a href="#section-6" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">6</span>
<span class="text-sm">Verfügbarkeit</span>
</a>
</div>
</div>
</div>
<!-- Terms Content -->
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Sidebar Navigation (hidden on mobile) -->
<div class="hidden lg:block">
<div class="sticky top-8">
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-4 uppercase tracking-wider">Navigation</h3>
<nav class="space-y-2">
<a href="#section-1" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">1. Allgemeines</a>
<a href="#section-2" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">2. Zugang und Berechtigung</a>
<a href="#section-3" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">3. Nutzungszweck</a>
<a href="#section-4" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">4. Verantwortlichkeiten</a>
<a href="#section-5" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">5. Einschränkungen</a>
<a href="#section-6" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">6. Verfügbarkeit</a>
</nav>
</div>
</div>
</div>
<!-- Main Content -->
<div class="lg:col-span-3">
<div class="space-y-8">
<!-- Section 1 -->
<section id="section-1" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
<span class="text-blue-600 dark:text-blue-400 font-bold">1</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Allgemeines</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
Diese Nutzungsbedingungen regeln die Nutzung der Mercedes-Benz "Manage Your Printers" (MYP) Plattform. Durch die Nutzung der Plattform erklären Sie sich mit diesen Bedingungen einverstanden.
</p>
</div>
</section>
<!-- Section 2 -->
<section id="section-2" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/50 flex items-center justify-center">
<span class="text-green-600 dark:text-green-400 font-bold">2</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Zugang und Nutzungsberechtigung</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
Die MYP-Plattform steht ausschließlich Mitarbeitern der Mercedes-Benz Group AG und ihren verbundenen Unternehmen zur Verfügung. Der Zugang erfolgt über eine persönliche Benutzerkennung und ein Passwort, die nicht an Dritte weitergegeben werden dürfen.
</p>
</div>
</section>
<!-- Section 3 -->
<section id="section-3" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center">
<span class="text-purple-600 dark:text-purple-400 font-bold">3</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Nutzungszweck</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
Die MYP-Plattform dient ausschließlich der Verwaltung und Überwachung von 3D-Druckaufträgen im Rahmen der beruflichen Tätigkeit. Eine private Nutzung ist nicht gestattet.
</p>
</div>
</section>
<!-- Section 4 -->
<section id="section-4" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-orange-100 dark:bg-orange-900/50 flex items-center justify-center">
<span class="text-orange-600 dark:text-orange-400 font-bold">4</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Verantwortlichkeiten der Nutzer</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">Als Nutzer sind Sie verantwortlich für:</p>
<div class="grid gap-3">
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Die Geheimhaltung Ihrer Zugangsdaten</span>
</div>
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Die ordnungsgemäße Nutzung der Geräte und Ressourcen</span>
</div>
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Die Einhaltung der Unternehmensrichtlinien zum Umgang mit 3D-Druckern</span>
</div>
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Die Beachtung von Urheberrechten und Schutzrechten Dritter bei der Erstellung von 3D-Modellen</span>
</div>
</div>
</div>
</section>
<!-- Section 5 -->
<section id="section-5" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/50 flex items-center justify-center">
<span class="text-red-600 dark:text-red-400 font-bold">5</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Einschränkungen</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">Es ist untersagt:</p>
<div class="grid gap-3">
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" 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"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Die Plattform für nicht-geschäftliche Zwecke zu nutzen</span>
</div>
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" 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"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Unbefugten Zugang zu verschaffen</span>
</div>
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" 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"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Die Sicherheitsmaßnahmen der Plattform zu umgehen</span>
</div>
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" 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"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Schädlichen Code oder Malware hochzuladen</span>
</div>
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" 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"/>
</svg>
<span class="text-slate-700 dark:text-slate-300">Die Plattform zu überlasten oder ihre normale Funktion zu stören</span>
</div>
</div>
</div>
</section>
<!-- Weitere Abschnitte analog... -->
<section id="section-6" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
<div class="flex items-center gap-4 mb-6">
<div class="w-10 h-10 rounded-full bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center">
<span class="text-indigo-600 dark:text-indigo-400 font-bold">6</span>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Verfügbarkeit und Wartung</h2>
</div>
<div class="prose dark:prose-invert max-w-none">
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
Mercedes-Benz bemüht sich um eine hohe Verfügbarkeit der MYP-Plattform, kann jedoch keine ununterbrochene Verfügbarkeit garantieren. Wartungsarbeiten werden nach Möglichkeit im Voraus angekündigt.
</p>
</div>
</section>
<!-- Kompakte weitere Abschnitte -->
<div class="grid gap-6">
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
<span class="w-6 h-6 rounded-full bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400 text-sm font-bold flex items-center justify-center">7</span>
Haftung
</h3>
<p class="text-slate-700 dark:text-slate-300 text-sm mb-3">Mercedes-Benz übernimmt keine Haftung für:</p>
<ul class="text-sm text-slate-600 dark:text-slate-400 space-y-1">
<li>• Schäden, die durch fehlerhafte Druckaufträge entstehen</li>
<li>• Verlust von Daten oder Modellen</li>
<li>• Ausfallzeiten der Plattform</li>
<li>• Schäden durch unsachgemäße Verwendung der Drucker</li>
</ul>
</div>
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
<span class="w-6 h-6 rounded-full bg-cyan-100 dark:bg-cyan-900/50 text-cyan-600 dark:text-cyan-400 text-sm font-bold flex items-center justify-center">8</span>
Datenschutz
</h3>
<p class="text-slate-700 dark:text-slate-300 text-sm">
Die Erhebung und Verarbeitung personenbezogener Daten erfolgt gemäß der
<a href="/privacy" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">Datenschutzerklärung</a>.
Die Daten werden ausschließlich zur Verwaltung und Optimierung der Druckaufträge verwendet.
</p>
</div>
</div>
<!-- Kontakt Section -->
<div class="glass-card bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border border-blue-200/50 dark:border-blue-700/30 rounded-2xl p-8">
<div class="text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900/50 mb-6">
<svg class="w-8 h-8 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</div>
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-4">Haben Sie Fragen?</h3>
<p class="text-slate-600 dark:text-slate-400 mb-6">Bei Fragen zu diesen Nutzungsbedingungen wenden Sie sich gerne an unser Team.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="mailto:till.tomczak@mercedes-benz.com" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Till Tomczak
</a>
<a href="mailto:torben.haack@mercedes-benz.com" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Torben Haack
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-4 justify-end mt-12 pt-8 border-t border-slate-200 dark:border-slate-700">
<button onclick="window.print()" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-lg transition-all duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
Drucken
</button>
<a href="javascript:history.back()" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
<svg class="w-4 h-4" 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
</a>
</div>
</div>
<!-- Smooth Scrolling Script -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
});
</script>
{% endblock %}