📝 Commit Details:
This commit is contained in:
47
backend/templates/404.html
Normal file
47
backend/templates/404.html
Normal 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 %}
|
66
backend/templates/500.html
Normal file
66
backend/templates/500.html
Normal 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
1080
backend/templates/admin.html
Normal file
File diff suppressed because it is too large
Load Diff
114
backend/templates/admin_add_printer.html
Normal file
114
backend/templates/admin_add_printer.html
Normal 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 %}
|
92
backend/templates/admin_add_user.html
Normal file
92
backend/templates/admin_add_user.html
Normal 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 %}
|
168
backend/templates/admin_edit_user.html
Normal file
168
backend/templates/admin_edit_user.html
Normal 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 %}
|
372
backend/templates/admin_guest_requests.html
Normal file
372
backend/templates/admin_guest_requests.html
Normal 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 %}
|
1448
backend/templates/admin_guest_requests_overview.html
Normal file
1448
backend/templates/admin_guest_requests_overview.html
Normal file
File diff suppressed because it is too large
Load Diff
238
backend/templates/admin_manage_printer.html
Normal file
238
backend/templates/admin_manage_printer.html
Normal 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 %}
|
119
backend/templates/admin_printer_settings.html
Normal file
119
backend/templates/admin_printer_settings.html
Normal 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 %}
|
376
backend/templates/admin_settings.html
Normal file
376
backend/templates/admin_settings.html
Normal 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 %}
|
746
backend/templates/analytics.html
Normal file
746
backend/templates/analytics.html
Normal 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
712
backend/templates/base.html
Normal 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>
|
1294
backend/templates/calendar.html
Normal file
1294
backend/templates/calendar.html
Normal file
File diff suppressed because it is too large
Load Diff
952
backend/templates/dashboard.html
Normal file
952
backend/templates/dashboard.html
Normal 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 %}
|
35
backend/templates/errors/403.html
Normal file
35
backend/templates/errors/403.html
Normal 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 %}
|
47
backend/templates/errors/404.html
Normal file
47
backend/templates/errors/404.html
Normal 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 %}
|
66
backend/templates/errors/500.html
Normal file
66
backend/templates/errors/500.html
Normal 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 %}
|
410
backend/templates/guest_job_status.html
Normal file
410
backend/templates/guest_job_status.html
Normal 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 %}
|
1587
backend/templates/guest_request.html
Normal file
1587
backend/templates/guest_request.html
Normal file
File diff suppressed because it is too large
Load Diff
270
backend/templates/guest_requests_overview.html
Normal file
270
backend/templates/guest_requests_overview.html
Normal 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 %}
|
347
backend/templates/guest_start_job.html
Normal file
347
backend/templates/guest_start_job.html
Normal 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 %}
|
334
backend/templates/guest_status.html
Normal file
334
backend/templates/guest_status.html
Normal 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
1302
backend/templates/index.html
Normal file
File diff suppressed because it is too large
Load Diff
2146
backend/templates/jobs.html
Normal file
2146
backend/templates/jobs.html
Normal file
File diff suppressed because it is too large
Load Diff
116
backend/templates/jobs/new.html
Normal file
116
backend/templates/jobs/new.html
Normal 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
1058
backend/templates/login.html
Normal file
File diff suppressed because it is too large
Load Diff
850
backend/templates/new_job.html
Normal file
850
backend/templates/new_job.html
Normal 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 %}
|
2178
backend/templates/printers.html
Normal file
2178
backend/templates/printers.html
Normal file
File diff suppressed because it is too large
Load Diff
546
backend/templates/privacy.html
Normal file
546
backend/templates/privacy.html
Normal 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 %}
|
803
backend/templates/profile.html
Normal file
803
backend/templates/profile.html
Normal 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 %}
|
961
backend/templates/settings.html
Normal file
961
backend/templates/settings.html
Normal 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 %}
|
285
backend/templates/stats.html
Normal file
285
backend/templates/stats.html
Normal 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 %}
|
356
backend/templates/terms.html
Normal file
356
backend/templates/terms.html
Normal 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 %}
|
Reference in New Issue
Block a user