This commit is contained in:
2025-06-04 10:03:22 +02:00
commit 785a2b6134
14182 changed files with 1764617 additions and 0 deletions

47
templates/404.html Normal file
View File

@@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}404 - Seite nicht gefunden - Mercedes-Benz MYP Platform{% endblock %}
{% block content %}
<div class="min-h-[80vh] flex flex-col items-center justify-center p-4">
<!-- 404 Error Container -->
<div class="w-full max-w-md">
<div class="bg-white dark:bg-gray-800 backdrop-blur-xl bg-opacity-95 dark:bg-opacity-95 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 p-8 text-center transition-all duration-300">
<!-- Mercedes-Benz Logo -->
<div class="flex justify-center mb-6">
<div class="w-16 h-16 text-gray-300 dark:text-gray-600 transition-transform duration-500 hover:scale-110">
<svg class="w-full h-full" fill="currentColor" viewBox="0 0 80 80">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>
</div>
</div>
<!-- Error Message -->
<h1 class="text-6xl font-bold text-gray-300 dark:text-gray-600 mb-4">404</h1>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Seite nicht gefunden</h2>
<p class="text-gray-600 dark:text-gray-400 mb-6">Die von Ihnen gesuchte Seite existiert nicht oder wurde verschoben.</p>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row justify-center gap-4 mt-8">
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center justify-center px-5 py-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium rounded-lg transition-all duration-300 transform hover:-translate-y-0.5">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
<span>Zum Dashboard</span>
</a>
<button onclick="window.history.back()" class="inline-flex items-center justify-center px-5 py-3 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-all duration-300 transform hover:-translate-y-0.5">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
<span>Zurück</span>
</button>
</div>
</div>
</div>
</div>
{% endblock %}

66
templates/500.html Normal file
View File

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

1540
templates/admin.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,208 @@
{% extends "base.html" %}
{% block title %}Drucker hinzufügen - MYP Admin{% endblock %}
{% block extra_css %}
<!-- Zusätzliche Styles für diese Seite -->
<style>
.form-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.dark .form-container {
background: rgba(30, 41, 59, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
}
</style>
{% endblock %}
{% block content %}
<div class="min-h-screen py-8">
<div class="max-w-2xl mx-auto px-4">
<!-- Header -->
<div class="form-container rounded-lg p-6 mb-6">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<i class="fas fa-print text-blue-600 dark:text-blue-400 text-2xl"></i>
<h1 class="text-2xl font-bold text-slate-800 dark:text-white">Neuen Drucker hinzufügen</h1>
</div>
<a href="{{ url_for('admin_page', tab='printers') }}"
class="bg-slate-500 hover:bg-slate-600 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-arrow-left mr-2"></i>Zurück
</a>
</div>
</div>
<!-- Formular -->
<div class="form-container rounded-lg p-6">
<form action="{{ url_for('admin_create_printer_form') }}" method="POST" class="space-y-6">
<!-- CSRF Token -->
<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">
<i class="fas fa-tag mr-2"></i>Drucker-Name *
</label>
<input type="text"
id="name"
name="name"
required
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
placeholder="3D-Drucker Raum A001">
</div>
<!-- IP-Adresse -->
<div>
<label for="ip_address" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<i class="fas fa-network-wired mr-2"></i>IP-Adresse *
</label>
<input type="text"
id="ip_address"
name="ip_address"
required
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]?)$"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
placeholder="192.168.1.100">
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">IP-Adresse der Tapo-Steckdose</p>
</div>
<!-- Modell -->
<div>
<label for="model" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<i class="fas fa-cogs mr-2"></i>Drucker-Modell
</label>
<input type="text"
id="model"
name="model"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
placeholder="Ender 3 V2">
</div>
<!-- Standort -->
<div>
<label for="location" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<i class="fas fa-map-marker-alt mr-2"></i>Standort
</label>
<input type="text"
id="location"
name="location"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
placeholder="Raum A001, Erdgeschoss">
</div>
<!-- Beschreibung -->
<div>
<label for="description" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<i class="fas fa-comment mr-2"></i>Beschreibung
</label>
<textarea id="description"
name="description"
rows="3"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 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">
<i class="fas fa-circle mr-2"></i>Anfangsstatus
</label>
<select id="status"
name="status"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
<option value="available">Verfügbar</option>
<option value="offline">Offline</option>
<option value="maintenance">Wartung</option>
</select>
</div>
<!-- Hinweise -->
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div class="flex">
<i class="fas fa-info-circle text-blue-500 dark:text-blue-400 mt-0.5 mr-3"></i>
<div class="text-sm text-blue-700 dark:text-blue-300">
<p class="font-semibold mb-1">Hinweise:</p>
<ul class="list-disc list-inside space-y-1">
<li>Felder mit * sind Pflichtfelder</li>
<li>Die IP-Adresse sollte die Adresse der Tapo-Steckdose sein</li>
<li>Der Drucker wird automatisch mit Standard-Tapo-Einstellungen konfiguriert</li>
<li>Status "Verfügbar" bedeutet bereit für Druckaufträge</li>
</ul>
</div>
</div>
</div>
<!-- Aktionen -->
<div class="flex space-x-3 pt-4">
<button type="submit"
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-save mr-2"></i>Drucker erstellen
</button>
<a href="{{ url_for('admin_page', tab='printers') }}"
class="flex-1 bg-slate-500 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-center transition-colors">
<i class="fas fa-times mr-2"></i>Abbrechen
</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- JavaScript für Form-Validierung -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const nameInput = document.getElementById('name');
const ipInput = document.getElementById('ip_address');
// IP-Adresse-Validierung
ipInput.addEventListener('blur', function() {
const ip = this.value;
const ipRegex = /^(?:(?: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]?)$/;
if (ip && !ipRegex.test(ip)) {
this.classList.add('border-red-500');
this.classList.remove('border-slate-300', 'dark:border-slate-600');
} else {
this.classList.remove('border-red-500');
this.classList.add('border-slate-300', 'dark:border-slate-600');
}
});
// Form-Submit-Validierung
form.addEventListener('submit', function(e) {
const name = nameInput.value.trim();
const ip = ipInput.value.trim();
const ipRegex = /^(?:(?: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]?)$/;
if (!name) {
e.preventDefault();
if (typeof showFlashMessage === 'function') {
showFlashMessage('Bitte geben Sie einen Drucker-Namen ein.', 'error');
} else {
alert('Bitte geben Sie einen Drucker-Namen ein.');
}
nameInput.focus();
return;
}
if (!ip || !ipRegex.test(ip)) {
e.preventDefault();
if (typeof showFlashMessage === 'function') {
showFlashMessage('Bitte geben Sie eine gültige IP-Adresse ein.', 'error');
} else {
alert('Bitte geben Sie eine gültige IP-Adresse ein.');
}
ipInput.focus();
return;
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,544 @@
{% extends "base.html" %}
{% block title %}Benutzer hinzufügen - Ausbilder-Bereich - Mercedes-Benz{% endblock %}
{% block extra_css %}
<style>
/* Spezielle Styles für Admin-Benutzer-Formular */
.admin-form-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04),
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
}
.dark .admin-form-container {
background: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.25),
0 10px 10px -5px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
}
.form-field-premium {
position: relative;
overflow: hidden;
}
.form-field-premium::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.5), transparent);
opacity: 0;
transition: opacity 0.3s ease;
}
.form-field-premium:focus-within::before {
opacity: 1;
}
.input-premium {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
border: 1px solid rgba(0, 0, 0, 0.1);
}
.dark .input-premium {
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.input-premium:focus {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(59, 130, 246, 0.5);
box-shadow:
0 0 0 3px rgba(59, 130, 246, 0.1),
0 10px 25px -5px rgba(59, 130, 246, 0.1);
transform: translateY(-1px);
}
.dark .input-premium:focus {
background: rgba(15, 23, 42, 0.95);
border-color: rgba(59, 130, 246, 0.5);
box-shadow:
0 0 0 3px rgba(59, 130, 246, 0.1),
0 10px 25px -5px rgba(59, 130, 246, 0.1);
}
.btn-mercedes-primary {
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
box-shadow:
0 4px 15px rgba(59, 130, 246, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-mercedes-primary:hover {
background: linear-gradient(135deg, #1d4ed8 0%, #2563eb 100%);
box-shadow:
0 8px 25px rgba(59, 130, 246, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.2) inset;
transform: translateY(-2px);
}
.btn-mercedes-secondary {
background: linear-gradient(135deg, #6b7280 0%, #9ca3af 100%);
box-shadow:
0 4px 15px rgba(107, 114, 128, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-mercedes-secondary:hover {
background: linear-gradient(135deg, #4b5563 0%, #6b7280 100%);
box-shadow:
0 8px 25px rgba(107, 114, 128, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.2) inset;
transform: translateY(-2px);
}
.info-box-premium {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
backdrop-filter: blur(10px);
}
.dark .info-box-premium {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
border: 1px solid rgba(59, 130, 246, 0.3);
}
.validation-error {
border-color: #ef4444 !important;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;
}
.validation-success {
border-color: #10b981 !important;
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1) !important;
}
</style>
{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto space-y-8">
<!-- Header Section mit Mercedes-Benz Design -->
<div class="admin-form-container rounded-2xl p-8 transition-all duration-300">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center space-x-4">
<!-- Mercedes Icon -->
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shadow-lg">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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.197m3 4.197a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
</div>
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white transition-colors duration-300">
Neuen Benutzer hinzufügen
</h1>
<p class="text-slate-600 dark:text-slate-400 mt-1 transition-colors duration-300">
Erstellen Sie einen neuen Benutzer für das Mercedes-Benz MYP System
</p>
</div>
</div>
<!-- Zurück Button -->
<a href="{{ url_for('admin_page', tab='users') }}"
class="btn-mercedes-secondary text-white px-6 py-3 rounded-xl font-medium transition-all duration-300 flex items-center space-x-2 hover:scale-105">
<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 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
<span>Zurück zur Übersicht</span>
</a>
</div>
</div>
<!-- Hauptformular -->
<div class="admin-form-container rounded-2xl p-8 transition-all duration-300">
<form id="userForm" action="{{ url_for('admin_create_user_form') }}" method="POST" class="space-y-8">
<!-- CSRF Token -->
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<!-- Formular-Header -->
<div class="border-b border-slate-200 dark:border-slate-700 pb-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-2 transition-colors duration-300">
Benutzerdaten eingeben
</h2>
<p class="text-slate-600 dark:text-slate-400 transition-colors duration-300">
Bitte füllen Sie alle erforderlichen Felder aus, um einen neuen Benutzer zu erstellen.
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Linke Spalte: Grunddaten -->
<div class="space-y-6">
<!-- E-Mail -->
<div class="form-field-premium">
<label for="email" class="block text-sm font-semibold text-slate-900 dark:text-white mb-3 transition-colors duration-300">
<svg class="w-4 h-4 inline mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"/>
</svg>
E-Mail-Adresse *
</label>
<input type="email"
id="email"
name="email"
required
class="input-premium w-full px-4 py-3 rounded-xl focus:outline-none transition-all duration-300 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400"
placeholder="beispiel@mercedes-benz.com"
data-validation="email">
<div class="validation-message hidden mt-2 text-sm text-red-500"></div>
</div>
<!-- Vollständiger Name -->
<div class="form-field-premium">
<label for="name" class="block text-sm font-semibold text-slate-900 dark:text-white mb-3 transition-colors duration-300">
<svg class="w-4 h-4 inline mr-2 text-blue-500" 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>
Vollständiger Name
</label>
<input type="text"
id="name"
name="name"
class="input-premium w-full px-4 py-3 rounded-xl focus:outline-none transition-all duration-300 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400"
placeholder="Max Mustermann"
data-validation="name">
<div class="validation-message hidden mt-2 text-sm text-gray-500">
Optional: Wird für die Anzeige im System verwendet
</div>
</div>
</div>
<!-- Rechte Spalte: Sicherheit & Rolle -->
<div class="space-y-6">
<!-- Passwort -->
<div class="form-field-premium">
<label for="password" class="block text-sm font-semibold text-slate-900 dark:text-white mb-3 transition-colors duration-300">
<svg class="w-4 h-4 inline mr-2 text-blue-500" 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>
Passwort *
</label>
<div class="relative">
<input type="password"
id="password"
name="password"
required
minlength="6"
class="input-premium w-full px-4 py-3 pr-12 rounded-xl focus:outline-none transition-all duration-300 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400"
placeholder="Mindestens 6 Zeichen"
data-validation="password">
<button type="button"
id="togglePassword"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors duration-300">
<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 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
</button>
</div>
<div class="validation-message hidden mt-2 text-sm text-red-500"></div>
<!-- Passwort-Stärke-Anzeige -->
<div id="passwordStrength" class="hidden mt-3">
<div class="flex space-x-1 mb-2">
<div class="strength-bar h-1 flex-1 bg-gray-200 dark:bg-gray-700 rounded"></div>
<div class="strength-bar h-1 flex-1 bg-gray-200 dark:bg-gray-700 rounded"></div>
<div class="strength-bar h-1 flex-1 bg-gray-200 dark:bg-gray-700 rounded"></div>
<div class="strength-bar h-1 flex-1 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
<div class="text-xs text-slate-600 dark:text-slate-400">
<span id="strengthText">Passwort-Stärke</span>
</div>
</div>
</div>
<!-- Benutzerrolle -->
<div class="form-field-premium">
<label for="role" class="block text-sm font-semibold text-slate-900 dark:text-white mb-3 transition-colors duration-300">
<svg class="w-4 h-4 inline mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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>
Benutzerrolle
</label>
<select id="role"
name="role"
class="input-premium w-full px-4 py-3 rounded-xl focus:outline-none transition-all duration-300 text-slate-900 dark:text-white">
<option value="user">👤 Standard-Benutzer</option>
<option value="admin">⚙️ Administrator (Ausbilder)</option>
</select>
<div class="mt-2 text-sm text-slate-600 dark:text-slate-400">
<span id="roleDescription">Standard-Zugriff auf das MYP System</span>
</div>
</div>
</div>
</div>
<!-- Informations-Box -->
<div class="info-box-premium rounded-xl p-6 transition-all duration-300">
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
<svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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>
<div class="flex-1">
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-2 transition-colors duration-300">
Wichtige Hinweise zum Erstellen von Benutzern
</h3>
<ul class="text-sm text-slate-700 dark:text-slate-300 space-y-1 transition-colors duration-300">
<li class="flex items-center space-x-2">
<svg class="w-4 h-4 text-green-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
<span>Felder mit * sind Pflichtfelder und müssen ausgefüllt werden</span>
</li>
<li class="flex items-center space-x-2">
<svg class="w-4 h-4 text-green-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
<span>Das Passwort muss mindestens 6 Zeichen lang sein</span>
</li>
<li class="flex items-center space-x-2">
<svg class="w-4 h-4 text-green-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
<span>Der Benutzername wird automatisch aus der E-Mail-Adresse generiert</span>
</li>
<li class="flex items-center space-x-2">
<svg class="w-4 h-4 text-amber-500 flex-shrink-0" 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>
<span>Administratoren haben Vollzugriff auf das Mercedes-Benz MYP System</span>
</li>
</ul>
</div>
</div>
</div>
<!-- Aktions-Buttons -->
<div class="flex flex-col sm:flex-row space-y-3 sm:space-y-0 sm:space-x-4 pt-6 border-t border-slate-200 dark:border-slate-700">
<button type="submit"
id="submitBtn"
class="btn-mercedes-primary text-white px-8 py-4 rounded-xl font-semibold transition-all duration-300 flex items-center justify-center space-x-3 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed">
<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="M5 13l4 4L19 7"/>
</svg>
<span>Benutzer erstellen</span>
<div class="loading-spinner hidden ml-2">
<svg class="animate-spin w-4 h-4 text-white" 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>
</div>
</button>
<a href="{{ url_for('admin_page', tab='users') }}"
class="btn-mercedes-secondary text-white px-8 py-4 rounded-xl font-semibold text-center transition-all duration-300 flex items-center justify-center space-x-3 hover:scale-105">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
<span>Abbrechen</span>
</a>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('userForm');
const emailInput = document.getElementById('email');
const passwordInput = document.getElementById('password');
const nameInput = document.getElementById('name');
const roleSelect = document.getElementById('role');
const submitBtn = document.getElementById('submitBtn');
const togglePasswordBtn = document.getElementById('togglePassword');
// Passwort-Sichtbarkeit umschalten
togglePasswordBtn.addEventListener('click', function() {
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
passwordInput.setAttribute('type', type);
// Icon ändern
const icon = this.querySelector('svg path:last-child');
if (type === 'text') {
// Auge durchgestrichen
icon.setAttribute('d', 'M3 3l18 18M10.584 10.587a2 2 0 002.828 2.829M9.363 5.365A9.466 9.466 0 0112 5c4.478 0 8.268 2.943 9.542 7a9.564 9.564 0 01-1.226 1.686m-2.854 2.852A9.465 9.465 0 0112 19c-4.478 0-8.268-2.943-9.542-7a9.564 9.564 0 011.226-1.686');
} else {
// Normales Auge
icon.setAttribute('d', 'M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z');
}
});
// Rolle-Beschreibung aktualisieren
const roleDescriptions = {
'user': 'Standard-Zugriff auf das MYP System - kann Reservierungen erstellen und verwalten',
'admin': 'Vollzugriff auf das System - kann Benutzer verwalten, Systemeinstellungen ändern und alle Bereiche einsehen'
};
roleSelect.addEventListener('change', function() {
const roleDescription = document.getElementById('roleDescription');
roleDescription.textContent = roleDescriptions[this.value] || roleDescriptions['user'];
});
// E-Mail-Validierung
function validateEmail(email) {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
}
// Passwort-Stärke bewerten
function getPasswordStrength(password) {
let score = 0;
if (password.length >= 6) score++;
if (password.length >= 8) score++;
if (/[A-Z]/.test(password)) score++;
if (/[a-z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++;
return Math.min(score, 4);
}
// Validierung anzeigen
function showValidation(input, isValid, message = '') {
const validationMessage = input.parentNode.querySelector('.validation-message');
input.classList.remove('validation-error', 'validation-success');
if (validationMessage) {
validationMessage.classList.add('hidden');
}
if (input.value) {
if (isValid) {
input.classList.add('validation-success');
} else {
input.classList.add('validation-error');
if (validationMessage && message) {
validationMessage.textContent = message;
validationMessage.classList.remove('hidden');
}
}
}
}
// Passwort-Stärke anzeigen
function updatePasswordStrength(password) {
const strengthContainer = document.getElementById('passwordStrength');
const strengthBars = strengthContainer.querySelectorAll('.strength-bar');
const strengthText = document.getElementById('strengthText');
if (password.length === 0) {
strengthContainer.classList.add('hidden');
return;
}
strengthContainer.classList.remove('hidden');
const strength = getPasswordStrength(password);
const colors = ['bg-red-500', 'bg-yellow-500', 'bg-blue-500', 'bg-green-500'];
const texts = ['Sehr schwach', 'Schwach', 'Mittel', 'Stark'];
strengthBars.forEach((bar, index) => {
bar.className = 'strength-bar h-1 flex-1 rounded transition-colors duration-300';
if (index < strength) {
bar.classList.add(colors[strength - 1]);
} else {
bar.classList.add('bg-gray-200', 'dark:bg-gray-700');
}
});
strengthText.textContent = strength > 0 ? `Passwort-Stärke: ${texts[strength - 1]}` : 'Passwort-Stärke';
}
// Event Listeners für Validierung
emailInput.addEventListener('blur', function() {
const isValid = validateEmail(this.value);
showValidation(this, isValid, 'Bitte geben Sie eine gültige E-Mail-Adresse ein');
});
emailInput.addEventListener('input', function() {
if (this.value) {
const isValid = validateEmail(this.value);
showValidation(this, isValid);
}
});
passwordInput.addEventListener('input', function() {
const isValid = this.value.length >= 6;
showValidation(this, isValid, 'Das Passwort muss mindestens 6 Zeichen lang sein');
updatePasswordStrength(this.value);
});
// Form-Submit-Validierung
form.addEventListener('submit', function(e) {
e.preventDefault();
const email = emailInput.value.trim();
const password = passwordInput.value.trim();
let isValid = true;
let firstErrorField = null;
// E-Mail validieren
if (!email) {
showValidation(emailInput, false, 'E-Mail-Adresse ist erforderlich');
isValid = false;
if (!firstErrorField) firstErrorField = emailInput;
} else if (!validateEmail(email)) {
showValidation(emailInput, false, 'Bitte geben Sie eine gültige E-Mail-Adresse ein');
isValid = false;
if (!firstErrorField) firstErrorField = emailInput;
}
// Passwort validieren
if (!password) {
showValidation(passwordInput, false, 'Passwort ist erforderlich');
isValid = false;
if (!firstErrorField) firstErrorField = passwordInput;
} else if (password.length < 6) {
showValidation(passwordInput, false, 'Das Passwort muss mindestens 6 Zeichen lang sein');
isValid = false;
if (!firstErrorField) firstErrorField = passwordInput;
}
if (!isValid) {
if (firstErrorField) {
firstErrorField.focus();
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return;
}
// Loading-State anzeigen
submitBtn.disabled = true;
submitBtn.querySelector('.loading-spinner').classList.remove('hidden');
submitBtn.querySelector('span').textContent = 'Wird erstellt...';
// Formular absenden
setTimeout(() => {
form.submit();
}, 500);
});
// Initiale Rolle-Beschreibung setzen
const roleDescription = document.getElementById('roleDescription');
roleDescription.textContent = roleDescriptions[roleSelect.value];
console.log('✅ Admin Benutzer-Formular erfolgreich initialisiert');
});
</script>
{% endblock %}

View File

@@ -0,0 +1,981 @@
{% extends "base.html" %}
{% block title %}Erweiterte Einstellungen - Mercedes-Benz TBA Marienfelde{% endblock %}
{% block head %}
{{ super() }}
<!-- CSRF Token für AJAX-Anfragen -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<style>
:root {
/* Mercedes-Benz Corporate Colors */
--mb-silver: #c4c4c4;
--mb-dark-silver: #a0a0a0;
--mb-black: #000000;
--mb-white: #ffffff;
--mb-blue: #0f4c75;
--mb-light-blue: #3282b8;
--mb-accent: #00adef;
--mb-success: #00d4aa;
--mb-warning: #ffb800;
--mb-error: #e74c3c;
/* Gradients */
--mb-gradient-primary: linear-gradient(135deg, var(--mb-blue) 0%, var(--mb-light-blue) 100%);
--mb-gradient-silver: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
--mb-gradient-dark: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
/* Shadows */
--mb-shadow-light: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--mb-shadow-medium: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--mb-shadow-large: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.mb-card {
background: var(--mb-gradient-silver);
border: 1px solid rgba(196, 196, 196, 0.3);
border-radius: 20px;
box-shadow: var(--mb-shadow-medium);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.mb-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--mb-gradient-primary);
opacity: 0;
transition: opacity 0.3s ease;
}
.mb-card:hover::before {
opacity: 1;
}
.dark .mb-card {
background: var(--mb-gradient-dark);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
}
.mb-card:hover {
transform: translateY(-4px);
box-shadow: var(--mb-shadow-large);
}
.dark .mb-card:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
}
.mb-stat-card {
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(196, 196, 196, 0.2);
border-radius: 16px;
padding: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
backdrop-filter: blur(10px);
}
.dark .mb-stat-card {
background: rgba(26, 26, 26, 0.9);
border-color: rgba(255, 255, 255, 0.1);
}
.mb-stat-card:hover {
transform: scale(1.02);
box-shadow: var(--mb-shadow-medium);
}
.mb-stat-card .stat-icon {
background: var(--mb-gradient-primary);
border-radius: 12px;
padding: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--mb-shadow-light);
}
.mb-toggle {
position: relative;
display: inline-block;
width: 64px;
height: 36px;
}
.mb-toggle input {
opacity: 0;
width: 0;
height: 0;
}
.mb-toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--mb-silver);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 36px;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.mb-toggle-slider:before {
position: absolute;
content: "";
height: 28px;
width: 28px;
left: 4px;
bottom: 4px;
background: var(--mb-white);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 50%;
box-shadow: var(--mb-shadow-light);
}
.mb-toggle input:checked + .mb-toggle-slider {
background: var(--mb-gradient-primary);
}
.mb-toggle input:checked + .mb-toggle-slider:before {
transform: translateX(28px);
box-shadow: var(--mb-shadow-medium);
}
.mb-btn {
background: var(--mb-gradient-primary);
color: var(--mb-white);
border: none;
border-radius: 12px;
padding: 0.875rem 1.5rem;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--mb-shadow-light);
position: relative;
overflow: hidden;
}
.mb-btn::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;
}
.mb-btn:hover::before {
left: 100%;
}
.mb-btn:hover {
transform: translateY(-2px);
box-shadow: var(--mb-shadow-medium);
}
.mb-btn:active {
transform: translateY(0);
}
.mb-btn-success {
background: linear-gradient(135deg, var(--mb-success) 0%, #00b894 100%);
}
.mb-btn-warning {
background: linear-gradient(135deg, var(--mb-warning) 0%, #f39c12 100%);
}
.mb-btn-error {
background: linear-gradient(135deg, var(--mb-error) 0%, #c0392b 100%);
}
.mb-btn-secondary {
background: linear-gradient(135deg, var(--mb-silver) 0%, var(--mb-dark-silver) 100%);
color: var(--mb-black);
}
.dark .mb-btn-secondary {
color: var(--mb-white);
}
.mb-input {
background: rgba(255, 255, 255, 0.9);
border: 2px solid rgba(196, 196, 196, 0.3);
border-radius: 12px;
padding: 0.875rem 1rem;
font-size: 0.875rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
}
.dark .mb-input {
background: rgba(26, 26, 26, 0.9);
border-color: rgba(255, 255, 255, 0.1);
color: var(--mb-white);
}
.mb-input:focus {
outline: none;
border-color: var(--mb-accent);
box-shadow: 0 0 0 3px rgba(0, 173, 239, 0.1);
transform: translateY(-1px);
}
.mb-select {
background: rgba(255, 255, 255, 0.9);
border: 2px solid rgba(196, 196, 196, 0.3);
border-radius: 12px;
padding: 0.875rem 1rem;
font-size: 0.875rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
.dark .mb-select {
background: rgba(26, 26, 26, 0.9);
border-color: rgba(255, 255, 255, 0.1);
color: var(--mb-white);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%9ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
}
.mb-select:focus {
outline: none;
border-color: var(--mb-accent);
box-shadow: 0 0 0 3px rgba(0, 173, 239, 0.1);
}
.mb-status-indicator {
background: rgba(0, 212, 170, 0.1);
border: 1px solid rgba(0, 212, 170, 0.3);
border-radius: 12px;
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.mb-status-dot {
width: 8px;
height: 8px;
background: var(--mb-success);
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.mb-header-gradient {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.95) 100%);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(196, 196, 196, 0.2);
}
.dark .mb-header-gradient {
background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(15, 23, 42, 0.95) 100%);
border-bottom-color: rgba(255, 255, 255, 0.1);
}
.mb-page-background {
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%);
min-height: 100vh;
}
.dark .mb-page-background {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
}
.mb-loading-overlay {
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
}
.mb-loading-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 2rem;
box-shadow: var(--mb-shadow-large);
backdrop-filter: blur(20px);
}
.dark .mb-loading-card {
background: rgba(26, 26, 26, 0.95);
}
.mb-spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(0, 173, 239, 0.3);
border-top: 3px solid var(--mb-accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
{% endblock %}
{% block content %}
<div>
<!-- Header -->
<div class="mb-header-gradient sticky top-0 z-40 rounded-t-3xl rounded-b-3xl">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<a href="{{ url_for('admin_page') }}" class="p-3 hover:bg-black/5 dark:hover:bg-white/5 rounded-2xl transition-all duration-300 group">
<svg class="w-6 h-6 text-slate-600 dark:text-slate-400 group-hover:text-slate-900 dark:group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</a>
<div>
<h1 class="text-3xl font-bold bg-gradient-to-r from-slate-900 to-slate-700 dark:from-white dark:to-slate-300 bg-clip-text text-transparent">
Erweiterte Einstellungen
</h1>
<p class="text-slate-600 dark:text-slate-400 mt-1 font-medium">System-Optimierung und Wartungsoptionen</p>
</div>
</div>
<!-- Status Indicator -->
<div class="mb-status-indicator rounded-2xl">
<div class="mb-status-dot"></div>
<span class="text-green-700 dark:text-green-400 font-semibold">System Online</span>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- System-Übersicht -->
<div class="mb-card p-8 mb-8">
<div class="flex items-center space-x-3 mb-6">
<div class="p-3 bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl">
<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>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">System-Übersicht</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="mb-stat-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-slate-600 dark:text-slate-400 font-medium">Registrierte Benutzer</p>
<p class="text-3xl font-bold text-slate-900 dark:text-white mt-1">{{ stats.total_users }}</p>
</div>
<div class="stat-icon">
<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 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
</div>
</div>
</div>
<div class="mb-stat-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-slate-600 dark:text-slate-400 font-medium">Aktive Drucker</p>
<p class="text-3xl font-bold text-slate-900 dark:text-white mt-1">{{ stats.active_printers }}/{{ stats.total_printers }}</p>
</div>
<div class="stat-icon">
<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="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2m2 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>
</div>
<div class="mb-stat-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-slate-600 dark:text-slate-400 font-medium">Warteschlange</p>
<p class="text-3xl font-bold text-slate-900 dark:text-white mt-1">{{ stats.pending_jobs }}</p>
</div>
<div class="stat-icon">
<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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
</div>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Optimierungs-Einstellungen -->
<div class="mb-card p-8">
<div class="flex items-center space-x-3 mb-6">
<div class="p-3 bg-gradient-to-r from-purple-500 to-purple-600 rounded-xl">
<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.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>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Optimierungs-Einstellungen</h2>
</div>
<form id="optimization-form" class="space-y-6">
<!-- Algorithmus-Auswahl -->
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">Optimierungs-Algorithmus</label>
<select name="algorithm" id="algorithm" class="mb-select w-full">
<option value="round_robin" {{ 'selected' if optimization_settings.algorithm == 'round_robin' else '' }}>Round Robin (Gleichmäßige Verteilung)</option>
<option value="load_balance" {{ 'selected' if optimization_settings.algorithm == 'load_balance' else '' }}>Load Balancing (Lastverteilung)</option>
<option value="priority_based" {{ 'selected' if optimization_settings.algorithm == 'priority_based' else '' }}>Prioritätsbasiert</option>
</select>
</div>
<!-- Toggle-Optionen -->
<div class="space-y-6">
<div class="flex items-center justify-between p-4 bg-slate-50/50 dark:bg-slate-800/50 rounded-xl">
<div>
<label class="text-sm font-semibold text-slate-700 dark:text-slate-300">Entfernung berücksichtigen</label>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">Drucker-Standort bei Optimierung einbeziehen</p>
</div>
<label class="mb-toggle">
<input type="checkbox" name="consider_distance" {{ 'checked' if optimization_settings.consider_distance else '' }}>
<span class="mb-toggle-slider"></span>
</label>
</div>
<div class="flex items-center justify-between p-4 bg-slate-50/50 dark:bg-slate-800/50 rounded-xl">
<div>
<label class="text-sm font-semibold text-slate-700 dark:text-slate-300">Rüstzeiten minimieren</label>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">Materialwechsel-Zeiten reduzieren</p>
</div>
<label class="mb-toggle">
<input type="checkbox" name="minimize_changeover" {{ 'checked' if optimization_settings.minimize_changeover else '' }}>
<span class="mb-toggle-slider"></span>
</label>
</div>
<div class="flex items-center justify-between p-4 bg-slate-50/50 dark:bg-slate-800/50 rounded-xl">
<div>
<label class="text-sm font-semibold text-slate-700 dark:text-slate-300">Auto-Optimierung</label>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">Automatische Optimierung alle 30 Minuten</p>
</div>
<label class="mb-toggle">
<input type="checkbox" name="auto_optimization_enabled" {{ 'checked' if optimization_settings.auto_optimization_enabled else '' }}>
<span class="mb-toggle-slider"></span>
</label>
</div>
</div>
<!-- Numerische Einstellungen -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">Max. Batch-Größe</label>
<input type="number" name="max_batch_size" value="{{ optimization_settings.max_batch_size }}" min="1" max="50" class="mb-input w-full">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">Zeitfenster (Stunden)</label>
<input type="number" name="time_window" value="{{ optimization_settings.time_window }}" min="1" max="168" class="mb-input w-full">
</div>
</div>
<!-- Speichern Button -->
<button type="submit" class="mb-btn w-full">
<span class="flex items-center justify-center space-x-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="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>
<span>Einstellungen speichern</span>
</span>
</button>
</form>
</div>
<!-- Wartungs-Aktionen -->
<div class="mb-card p-8">
<div class="flex items-center space-x-3 mb-6">
<div class="p-3 bg-gradient-to-r from-orange-500 to-orange-600 rounded-xl">
<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.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>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Wartungs-Aktionen</h2>
</div>
<!-- Wartungs-Informationen -->
<div class="bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-700 rounded-xl p-6 mb-6 border border-slate-200 dark:border-slate-600">
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Wartungs-Status</h3>
<div class="space-y-3 text-sm">
<div class="flex justify-between items-center">
<span class="text-slate-600 dark:text-slate-400 font-medium">Letztes Backup:</span>
<span class="text-slate-900 dark:text-white font-semibold">{{ maintenance_info.last_backup }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-slate-600 dark:text-slate-400 font-medium">Log-Dateien:</span>
<span class="text-slate-900 dark:text-white font-semibold">{{ maintenance_info.log_files_count }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-slate-600 dark:text-slate-400 font-medium">Cache-Größe:</span>
<span class="text-slate-900 dark:text-white font-semibold">{{ maintenance_info.cache_size }}</span>
</div>
</div>
</div>
<!-- Wartungs-Buttons -->
<div class="space-y-3">
<button id="advanced-clear-cache" class="mb-btn mb-btn-secondary w-full">
<span class="flex items-center justify-center space-x-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="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>
<span>System-Cache leeren</span>
</span>
</button>
<button id="advanced-optimize-db" class="mb-btn mb-btn-success w-full">
<span class="flex items-center justify-center space-x-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 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>
</svg>
<span>Datenbank optimieren</span>
</span>
</button>
<button id="advanced-create-backup" class="mb-btn w-full">
<span class="flex items-center justify-center space-x-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="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>
<span>Vollständiges Backup erstellen</span>
</span>
</button>
<button id="advanced-cleanup-logs" class="mb-btn mb-btn-warning w-full">
<span class="flex items-center justify-center space-x-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="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>Log-Dateien bereinigen</span>
</span>
</button>
<button id="advanced-system-check" class="mb-btn w-full" style="background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);">
<span class="flex items-center justify-center space-x-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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>System-Integritätsprüfung</span>
</span>
</button>
</div>
</div>
</div>
<!-- Erweiterte Optionen -->
<div class="mb-card p-8 mt-8">
<div class="flex items-center space-x-3 mb-6">
<div class="p-3 bg-gradient-to-r from-indigo-500 to-indigo-600 rounded-xl">
<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="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Erweiterte Optionen</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Debug-Modus -->
<div class="bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-700 rounded-xl p-6 border border-slate-200 dark:border-slate-600 transition-all duration-300 hover:shadow-lg">
<div class="flex items-center space-x-3 mb-4">
<div class="p-2 bg-red-500 rounded-lg">
<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 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
</div>
<h3 class="text-lg font-bold text-slate-900 dark:text-white">Debug-Modus</h3>
</div>
<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">Erweiterte Protokollierung für Fehlerdiagnose aktivieren</p>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">Status</span>
<label class="mb-toggle">
<input type="checkbox" id="debug-mode">
<span class="mb-toggle-slider"></span>
</label>
</div>
</div>
<!-- Wartungsmodus -->
<div class="bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-700 rounded-xl p-6 border border-slate-200 dark:border-slate-600 transition-all duration-300 hover:shadow-lg">
<div class="flex items-center space-x-3 mb-4">
<div class="p-2 bg-yellow-500 rounded-lg">
<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 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
</div>
<h3 class="text-lg font-bold text-slate-900 dark:text-white">Wartungsmodus</h3>
</div>
<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">System für Wartungsarbeiten temporär sperren</p>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">Status</span>
<label class="mb-toggle">
<input type="checkbox" id="maintenance-mode">
<span class="mb-toggle-slider"></span>
</label>
</div>
</div>
<!-- Performance-Monitoring -->
<div class="bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-700 rounded-xl p-6 border border-slate-200 dark:border-slate-600 transition-all duration-300 hover:shadow-lg">
<div class="flex items-center space-x-3 mb-4">
<div class="p-2 bg-green-500 rounded-lg">
<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="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>
<h3 class="text-lg font-bold text-slate-900 dark:text-white">Performance-Monitoring</h3>
</div>
<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">Detaillierte Leistungsüberwachung und Metriken</p>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">Status</span>
<label class="mb-toggle">
<input type="checkbox" id="performance-monitoring">
<span class="mb-toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 mb-loading-overlay z-50 hidden">
<div class="flex items-center justify-center h-full">
<div class="mb-loading-card">
<div class="flex items-center space-x-4">
<div class="mb-spinner"></div>
<span class="text-slate-900 dark:text-white font-semibold text-lg">Wird verarbeitet...</span>
</div>
</div>
</div>
</div>
<script>
// ===== ERWEITERTE EINSTELLUNGEN JAVASCRIPT =====
class AdvancedSettings {
constructor() {
this.csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
this.initializeEventListeners();
}
initializeEventListeners() {
// Optimierungs-Formular
const optimizationForm = document.getElementById('optimization-form');
if (optimizationForm) {
optimizationForm.addEventListener('submit', (e) => {
e.preventDefault();
this.saveOptimizationSettings(new FormData(optimizationForm));
});
}
// Wartungs-Buttons
document.getElementById('advanced-clear-cache')?.addEventListener('click', () => this.clearCache());
document.getElementById('advanced-optimize-db')?.addEventListener('click', () => this.optimizeDatabase());
document.getElementById('advanced-create-backup')?.addEventListener('click', () => this.createBackup());
document.getElementById('advanced-cleanup-logs')?.addEventListener('click', () => this.cleanupLogs());
document.getElementById('advanced-system-check')?.addEventListener('click', () => this.systemCheck());
// Erweiterte Optionen
document.getElementById('debug-mode')?.addEventListener('change', (e) => this.toggleDebugMode(e.target.checked));
document.getElementById('maintenance-mode')?.addEventListener('change', (e) => this.toggleMaintenanceMode(e.target.checked));
document.getElementById('performance-monitoring')?.addEventListener('change', (e) => this.togglePerformanceMonitoring(e.target.checked));
}
async saveOptimizationSettings(formData) {
try {
this.showLoading(true);
const settings = {
algorithm: formData.get('algorithm'),
consider_distance: formData.get('consider_distance') === 'on',
minimize_changeover: formData.get('minimize_changeover') === 'on',
auto_optimization_enabled: formData.get('auto_optimization_enabled') === 'on',
max_batch_size: parseInt(formData.get('max_batch_size')),
time_window: parseInt(formData.get('time_window'))
};
const response = await fetch('/api/optimization/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.csrfToken
},
body: JSON.stringify(settings)
});
const result = await response.json();
if (result.success) {
this.showNotification('✅ Optimierungs-Einstellungen erfolgreich gespeichert!', 'success');
} else {
this.showNotification('❌ Fehler beim Speichern der Einstellungen', 'error');
}
} catch (error) {
console.error('Fehler beim Speichern der Optimierungs-Einstellungen:', error);
this.showNotification('❌ Fehler beim Speichern der Einstellungen', 'error');
} finally {
this.showLoading(false);
}
}
async clearCache() {
if (!confirm('🗑️ Möchten Sie wirklich den System-Cache leeren?')) return;
try {
this.showLoading(true);
const response = await fetch('/api/admin/maintenance/clear-cache', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.csrfToken
}
});
const result = await response.json();
if (result.success) {
this.showNotification('✅ System-Cache erfolgreich geleert!', 'success');
} else {
this.showNotification('❌ Fehler beim Leeren des Cache', 'error');
}
} catch (error) {
this.showNotification('❌ Fehler beim Leeren des Cache', 'error');
} finally {
this.showLoading(false);
}
}
async optimizeDatabase() {
if (!confirm('🔧 Möchten Sie die Datenbank optimieren? Dies kann einige Minuten dauern.')) return;
try {
this.showLoading(true);
const response = await fetch('/api/admin/maintenance/optimize-database', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.csrfToken
}
});
const result = await response.json();
if (result.success) {
this.showNotification('✅ Datenbank erfolgreich optimiert!', 'success');
} else {
this.showNotification('❌ Fehler bei der Datenbank-Optimierung', 'error');
}
} catch (error) {
this.showNotification('❌ Fehler bei der Datenbank-Optimierung', 'error');
} finally {
this.showLoading(false);
}
}
async createBackup() {
if (!confirm('💾 Möchten Sie ein vollständiges System-Backup erstellen?')) return;
try {
this.showLoading(true);
const response = await fetch('/api/admin/maintenance/create-backup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.csrfToken
}
});
const result = await response.json();
if (result.success) {
this.showNotification('✅ Backup erfolgreich erstellt!', 'success');
} else {
this.showNotification('❌ Fehler beim Erstellen des Backups', 'error');
}
} catch (error) {
this.showNotification('❌ Fehler beim Erstellen des Backups', 'error');
} finally {
this.showLoading(false);
}
}
async cleanupLogs() {
if (!confirm('📋 Möchten Sie alte Log-Dateien bereinigen? (älter als 30 Tage)')) return;
try {
this.showLoading(true);
const response = await fetch('/api/admin/maintenance/cleanup-logs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.csrfToken
}
});
const result = await response.json();
if (result.success) {
this.showNotification('✅ Log-Dateien erfolgreich bereinigt!', 'success');
} else {
this.showNotification('❌ Fehler beim Bereinigen der Log-Dateien', 'error');
}
} catch (error) {
this.showNotification('❌ Fehler beim Bereinigen der Log-Dateien', 'error');
} finally {
this.showLoading(false);
}
}
async systemCheck() {
try {
this.showLoading(true);
const response = await fetch('/api/admin/maintenance/system-check', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.csrfToken
}
});
const result = await response.json();
if (result.success) {
this.showNotification('✅ System-Integritätsprüfung abgeschlossen!', 'success');
} else {
this.showNotification('❌ Fehler bei der System-Integritätsprüfung', 'error');
}
} catch (error) {
this.showNotification('❌ Fehler bei der System-Integritätsprüfung', 'error');
} finally {
this.showLoading(false);
}
}
toggleDebugMode(enabled) {
console.log('Debug-Modus:', enabled ? 'aktiviert' : 'deaktiviert');
this.showNotification(`🐛 Debug-Modus ${enabled ? 'aktiviert' : 'deaktiviert'}`, 'info');
}
toggleMaintenanceMode(enabled) {
console.log('Wartungsmodus:', enabled ? 'aktiviert' : 'deaktiviert');
this.showNotification(`🚧 Wartungsmodus ${enabled ? 'aktiviert' : 'deaktiviert'}`, enabled ? 'warning' : 'info');
}
togglePerformanceMonitoring(enabled) {
console.log('Performance-Monitoring:', enabled ? 'aktiviert' : 'deaktiviert');
this.showNotification(`📈 Performance-Monitoring ${enabled ? 'aktiviert' : 'deaktiviert'}`, 'info');
}
showLoading(show) {
const overlay = document.getElementById('loading-overlay');
if (overlay) {
if (show) {
overlay.classList.remove('hidden');
} else {
overlay.classList.add('hidden');
}
}
}
showNotification(message, type = 'info') {
// Erstelle temporäre Notification mit Mercedes-Benz Design
const notification = document.createElement('div');
notification.className = `fixed top-6 right-6 z-50 p-4 rounded-xl shadow-2xl max-w-sm transition-all duration-500 transform translate-x-full opacity-0 backdrop-blur-xl border ${
type === 'success' ? 'bg-gradient-to-r from-green-500 to-green-600 text-white border-green-400' :
type === 'error' ? 'bg-gradient-to-r from-red-500 to-red-600 text-white border-red-400' :
type === 'warning' ? 'bg-gradient-to-r from-yellow-500 to-yellow-600 text-white border-yellow-400' :
'bg-gradient-to-r from-blue-500 to-blue-600 text-white border-blue-400'
}`;
notification.innerHTML = `
<div class="flex items-center space-x-3">
<div class="flex-shrink-0 p-1 rounded-lg bg-white/20">
${type === 'success' ?
'<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="M5 13l4 4L19 7"/></svg>' :
type === 'error' ?
'<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="M6 18L18 6M6 6l12 12"/></svg>' :
type === 'warning' ?
'<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 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>' :
'<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
}
</div>
<div class="text-sm font-semibold flex-1">${message}</div>
<button onclick="this.parentElement.parentElement.remove()" class="ml-auto p-1 rounded-lg hover:bg-white/20 transition-colors">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
`;
document.body.appendChild(notification);
// Animation einblenden
setTimeout(() => {
notification.style.transform = 'translateX(0)';
notification.style.opacity = '1';
}, 100);
// Automatisch entfernen nach 5 Sekunden
setTimeout(() => {
if (notification.parentNode) {
notification.style.transform = 'translateX(100%)';
notification.style.opacity = '0';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 500);
}
}, 5000);
}
}
// Initialisierung nach DOM-Laden
document.addEventListener('DOMContentLoaded', function() {
new AdvancedSettings();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,382 @@
{% extends "base.html" %}
{% block title %}Drucker bearbeiten - MYP Admin{% endblock %}
{% block extra_css %}
<!-- Zusätzliche Styles für diese Seite -->
<style>
.form-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.dark .form-container {
background: rgba(30, 41, 59, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
}
</style>
{% endblock %}
{% block content %}
<div class="min-h-screen py-8">
<div class="max-w-2xl mx-auto px-4">
<!-- Header -->
<div class="form-container rounded-lg p-6 mb-6">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<i class="fas fa-edit text-blue-600 dark:text-blue-400 text-2xl"></i>
<h1 class="text-2xl font-bold text-slate-800 dark:text-white">Drucker bearbeiten</h1>
</div>
<a href="{{ url_for('admin_page', tab='printers') }}"
class="bg-slate-500 hover:bg-slate-600 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-arrow-left mr-2"></i>Zurück
</a>
</div>
<div class="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<p class="text-sm text-blue-700 dark:text-blue-300">
<i class="fas fa-info-circle mr-2"></i>
<strong>Drucker-ID:</strong> {{ printer.id }} |
<strong>Erstellt am:</strong> {{ printer.created_at[:10] if printer.created_at else 'Unbekannt' }}
</p>
</div>
</div>
<!-- Formular -->
<div class="form-container rounded-lg p-6">
<form action="{{ url_for('admin_update_printer_form', printer_id=printer.id) }}" method="POST" class="space-y-6">
<!-- CSRF Token -->
<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">
<i class="fas fa-tag mr-2"></i>Drucker-Name *
</label>
<input type="text"
id="name"
name="name"
required
value="{{ printer.name }}"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
placeholder="3D-Drucker Raum A001">
</div>
<!-- IP-Adresse -->
<div>
<label for="ip_address" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<i class="fas fa-network-wired mr-2"></i>IP-Adresse *
</label>
<input type="text"
id="ip_address"
name="ip_address"
required
value="{{ printer.plug_ip }}"
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]?)$"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
placeholder="192.168.1.100">
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">IP-Adresse der Tapo-Steckdose</p>
</div>
<!-- Modell -->
<div>
<label for="model" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<i class="fas fa-cogs mr-2"></i>Drucker-Modell
</label>
<input type="text"
id="model"
name="model"
value="{{ printer.model }}"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
placeholder="Ender 3 V2">
</div>
<!-- Standort -->
<div>
<label for="location" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<i class="fas fa-map-marker-alt mr-2"></i>Standort
</label>
<input type="text"
id="location"
name="location"
value="{{ printer.location }}"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
placeholder="Raum A001, Erdgeschoss">
</div>
<!-- Beschreibung -->
<div>
<label for="description" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<i class="fas fa-comment mr-2"></i>Beschreibung
</label>
<textarea id="description"
name="description"
rows="3"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 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">
<i class="fas fa-circle mr-2"></i>Aktueller Status
</label>
<select id="status"
name="status"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
<option value="available" {{ 'selected' if printer.status == 'available' else '' }}>Verfügbar</option>
<option value="offline" {{ 'selected' if printer.status == 'offline' else '' }}>Offline</option>
<option value="maintenance" {{ 'selected' if printer.status == 'maintenance' else '' }}>Wartung</option>
<option value="online" {{ 'selected' if printer.status == 'online' else '' }}>Online</option>
<option value="printing" {{ 'selected' if printer.status == 'printing' else '' }}>Druckt</option>
</select>
</div>
<!-- Aktiv-Status -->
<div>
<label class="flex items-center space-x-3">
<input type="checkbox"
name="is_active"
{{ 'checked' if printer.active else '' }}
class="w-4 h-4 text-blue-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-blue-500 focus:ring-2">
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
<i class="fas fa-power-off mr-2"></i>Drucker aktiv
</span>
</label>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">Inaktive Drucker werden nicht für neue Aufträge verwendet</p>
</div>
<!-- Erweiterte Informationen -->
<div class="bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-lg p-4">
<h3 class="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
<i class="fas fa-info-circle mr-2"></i>Drucker-Informationen
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div>
<span class="font-medium text-slate-600 dark:text-slate-400">MAC-Adresse:</span>
<span class="text-slate-800 dark:text-slate-200">{{ printer.mac_address or 'Nicht verfügbar' }}</span>
</div>
<div>
<span class="font-medium text-slate-600 dark:text-slate-400">Letzter Check:</span>
<span class="text-slate-800 dark:text-slate-200">{{ printer.last_checked or 'Nie' }}</span>
</div>
</div>
</div>
<!-- Warnung -->
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<div class="flex">
<i class="fas fa-exclamation-triangle text-yellow-500 dark:text-yellow-400 mt-0.5 mr-3"></i>
<div class="text-sm text-yellow-700 dark:text-yellow-300">
<p class="font-semibold mb-1">Wichtige Hinweise:</p>
<ul class="list-disc list-inside space-y-1">
<li>Änderungen an der IP-Adresse können die Verbindung unterbrechen</li>
<li>Stellen Sie sicher, dass die Tapo-Steckdose unter der neuen IP erreichbar ist</li>
<li>Bei Status-Änderungen werden laufende Jobs möglicherweise beeinflusst</li>
</ul>
</div>
</div>
</div>
<!-- Aktionen -->
<div class="flex space-x-3 pt-4">
<button type="submit"
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-save mr-2"></i>Änderungen speichern
</button>
<a href="{{ url_for('admin_page', tab='printers') }}"
class="flex-1 bg-slate-500 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-center transition-colors">
<i class="fas fa-times mr-2"></i>Abbrechen
</a>
</div>
</form>
</div>
<!-- Zusätzliche Aktionen -->
<div class="form-container rounded-lg p-6 mt-6">
<h3 class="text-lg font-semibold text-slate-800 dark:text-white mb-4">
<i class="fas fa-tools mr-2"></i>Drucker-Aktionen
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<button data-printer-id="{{ printer.id }}"
data-action="test"
class="printer-action-btn bg-green-600 hover:bg-green-700 text-white px-4 py-3 rounded-lg transition-colors">
<i class="fas fa-plug mr-2"></i>Verbindung testen
</button>
<button data-printer-id="{{ printer.id }}"
data-action="toggle"
class="printer-action-btn bg-orange-600 hover:bg-orange-700 text-white px-4 py-3 rounded-lg transition-colors">
<i class="fas fa-power-off mr-2"></i>Ein/Ausschalten
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const nameInput = document.getElementById('name');
const ipInput = document.getElementById('ip_address');
// IP-Adresse-Validierung
ipInput.addEventListener('blur', function() {
const ip = this.value;
const ipRegex = /^(?:(?: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]?)$/;
if (ip && !ipRegex.test(ip)) {
this.classList.add('border-red-500');
this.classList.remove('border-slate-300', 'dark:border-slate-600');
} else {
this.classList.remove('border-red-500');
this.classList.add('border-slate-300', 'dark:border-slate-600');
}
});
// Form-Submit-Validierung
form.addEventListener('submit', function(e) {
const name = nameInput.value.trim();
const ip = ipInput.value.trim();
const ipRegex = /^(?:(?: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]?)$/;
if (!name) {
e.preventDefault();
if (typeof showFlashMessage === 'function') {
showFlashMessage('Bitte geben Sie einen Drucker-Namen ein.', 'error');
} else {
alert('Bitte geben Sie einen Drucker-Namen ein.');
}
nameInput.focus();
return;
}
if (!ip || !ipRegex.test(ip)) {
e.preventDefault();
if (typeof showFlashMessage === 'function') {
showFlashMessage('Bitte geben Sie eine gültige IP-Adresse ein.', 'error');
} else {
alert('Bitte geben Sie eine gültige IP-Adresse ein.');
}
ipInput.focus();
return;
}
});
// Event-Listener für Drucker-Aktions-Buttons
document.querySelectorAll('.printer-action-btn').forEach(button => {
button.addEventListener('click', function() {
const printerId = this.getAttribute('data-printer-id');
const action = this.getAttribute('data-action');
if (action === 'test') {
testPrinterConnection(printerId, this);
} else if (action === 'toggle') {
togglePrinterPower(printerId, this);
}
});
});
});
// Verbindungstest
function testPrinterConnection(printerId, button) {
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Teste...';
button.disabled = true;
fetch(`/api/admin/printers/${printerId}/test-tapo`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
})
.then(response => response.json())
.then(data => {
if (data.tapo_test && data.tapo_test.success) {
const message = '✅ Verbindung erfolgreich!\n\nStatus: ' + (data.tapo_test.device_info ? data.tapo_test.device_info.device_on ? 'EIN' : 'AUS' : 'Unbekannt');
if (typeof showFlashMessage === 'function') {
showFlashMessage(message, 'success');
} else {
alert(message);
}
} else {
const message = '❌ Verbindung fehlgeschlagen!\n\nFehler: ' + (data.tapo_test ? data.tapo_test.error : 'Unbekannter Fehler');
if (typeof showFlashMessage === 'function') {
showFlashMessage(message, 'error');
} else {
alert(message);
}
}
})
.catch(error => {
const message = '❌ Verbindungstest fehlgeschlagen!\n\nFehler: ' + error.message;
if (typeof showFlashMessage === 'function') {
showFlashMessage(message, 'error');
} else {
alert(message);
}
})
.finally(() => {
button.innerHTML = originalText;
button.disabled = false;
});
}
// Drucker ein/ausschalten
function togglePrinterPower(printerId, button) {
const originalText = button.innerHTML;
if (!confirm('Möchten Sie den Drucker ein-/ausschalten?')) {
return;
}
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Schaltet...';
button.disabled = true;
fetch(`/api/admin/printers/${printerId}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
const message = '✅ Drucker erfolgreich ' + data.action + '!';
if (typeof showFlashMessage === 'function') {
showFlashMessage(message, 'success');
} else {
alert(message);
}
// Seite neu laden um aktuellen Status zu zeigen
setTimeout(() => location.reload(), 1000);
} else {
const message = '❌ Fehler beim Schalten!\n\nFehler: ' + (data.error || 'Unbekannter Fehler');
if (typeof showFlashMessage === 'function') {
showFlashMessage(message, 'error');
} else {
alert(message);
}
}
})
.catch(error => {
const message = '❌ Schaltvorgang fehlgeschlagen!\n\nFehler: ' + error.message;
if (typeof showFlashMessage === 'function') {
showFlashMessage(message, 'error');
} else {
alert(message);
}
})
.finally(() => {
button.innerHTML = originalText;
button.disabled = false;
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,596 @@
{% extends "base.html" %}
{% block title %}Benutzer bearbeiten - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
<style>
/* Modern Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 52px;
height: 28px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #e2e8f0, #cbd5e1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 28px;
border: 2px solid rgba(148, 163, 184, 0.2);
box-shadow:
inset 0 2px 4px rgba(0, 0, 0, 0.1),
0 1px 3px rgba(0, 0, 0, 0.1);
}
.toggle-slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 2px;
bottom: 2px;
background: linear-gradient(135deg, #ffffff, #f8fafc);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 50%;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.15),
0 1px 4px rgba(0, 0, 0, 0.1);
}
input:checked + .toggle-slider {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
border-color: rgba(59, 130, 246, 0.3);
box-shadow:
inset 0 2px 4px rgba(29, 78, 216, 0.2),
0 0 0 3px rgba(59, 130, 246, 0.1),
0 4px 12px rgba(59, 130, 246, 0.2);
}
input:checked + .toggle-slider:before {
transform: translateX(24px);
background: linear-gradient(135deg, #ffffff, #f1f5f9);
box-shadow:
0 3px 12px rgba(0, 0, 0, 0.2),
0 1px 6px rgba(0, 0, 0, 0.15);
}
/* Premium Input Fields */
.premium-input {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(248, 250, 252, 0.95));
backdrop-filter: blur(8px);
border: 2px solid transparent;
background-clip: padding-box;
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.premium-input:before {
content: '';
position: absolute;
inset: 0;
padding: 2px;
background: linear-gradient(135deg, #e2e8f0, #cbd5e1, #e2e8f0);
border-radius: inherit;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask-composite: xor;
}
.premium-input:focus {
transform: translateY(-1px);
box-shadow:
0 10px 25px rgba(59, 130, 246, 0.15),
0 4px 10px rgba(59, 130, 246, 0.1),
0 0 0 3px rgba(59, 130, 246, 0.1);
}
.premium-input:focus:before {
background: linear-gradient(135deg, #3b82f6, #1d4ed8, #3b82f6);
}
/* Dark mode styles */
.dark .premium-input {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.9), rgba(15, 23, 42, 0.95));
color: #f1f5f9;
}
.dark .premium-input:before {
background: linear-gradient(135deg, #475569, #334155, #475569);
}
.dark .toggle-slider {
background: linear-gradient(135deg, #475569, #334155);
border-color: rgba(71, 85, 105, 0.3);
}
/* Permission Card Animations */
.permission-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.permission-card:hover {
transform: translateY(-2px);
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* Floating Labels */
.floating-label {
position: relative;
}
.floating-label input:focus + label,
.floating-label input:not(:placeholder-shown) + label {
transform: translateY(-24px) scale(0.875);
color: #3b82f6;
}
.floating-label label {
position: absolute;
left: 12px;
top: 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
color: #64748b;
}
/* Button Hover Effects */
.btn-gradient {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
position: relative;
overflow: hidden;
}
.btn-gradient: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;
}
.btn-gradient:hover:before {
left: 100%;
}
/* Glass Effect Cards */
.glass-card {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
}
.dark .glass-card {
background: rgba(15, 23, 42, 0.85);
border: 1px solid rgba(255, 255, 255, 0.1);
}
</style>
{% 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-blue-900 dark:to-indigo-900 relative overflow-hidden">
<!-- Animated Background Elements -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute -top-40 -right-32 w-96 h-96 bg-gradient-to-br from-blue-400/20 to-indigo-600/20 rounded-full blur-3xl animate-pulse"></div>
<div class="absolute -bottom-40 -left-32 w-96 h-96 bg-gradient-to-tr from-purple-400/20 to-pink-600/20 rounded-full blur-3xl animate-pulse delay-1000"></div>
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-gradient-to-r from-blue-300/10 to-indigo-300/10 rounded-full blur-2xl animate-pulse delay-500"></div>
</div>
<div class="relative z-10 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Modern Header with Glass Effect -->
<div class="mb-12">
<div class="glass-card rounded-3xl p-8 mb-8">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-6">
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center shadow-xl">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
</svg>
</div>
<div>
<h1 class="text-4xl font-bold bg-gradient-to-r from-slate-900 to-slate-700 dark:from-white dark:to-gray-200 bg-clip-text text-transparent">
Benutzer bearbeiten
</h1>
<p class="text-lg text-slate-600 dark:text-slate-400 mt-2 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-500" 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>
{{ user.name or user.email }}
</p>
</div>
</div>
<a href="{{ url_for('admin_page', tab='users') }}"
class="group inline-flex items-center px-6 py-3 bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm text-slate-700 dark:text-slate-300 rounded-2xl hover:bg-white dark:hover:bg-slate-700 transition-all duration-300 shadow-lg hover:shadow-xl border border-white/20 dark:border-slate-700/50">
<svg class="w-5 h-5 mr-2 transition-transform group-hover:-translate-x-1" 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 class="font-medium">Zurück zur Verwaltung</span>
</a>
</div>
</div>
</div>
<!-- Modern Form with Glass Effect -->
<div class="glass-card rounded-3xl p-10 shadow-2xl">
<form method="POST" action="{{ url_for('admin_update_user_form', user_id=user.id) }}" class="space-y-8" id="userEditForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="_method" value="PUT"/>
<!-- Form Header -->
<div class="border-b border-slate-200 dark:border-slate-700 pb-8">
<h2 class="text-2xl font-bold text-slate-900 dark:text-white mb-3 flex items-center">
<svg class="w-6 h-6 mr-3 text-blue-500" 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-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Benutzerdaten bearbeiten
</h2>
<p class="text-slate-600 dark:text-slate-400 text-lg">
Bearbeiten Sie die Informationen und Berechtigungen für diesen Benutzer
</p>
</div>
<!-- Personal Information Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left Column: Basic Info -->
<div class="space-y-6">
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-2xl p-6 border border-blue-200/50 dark:border-blue-800/50">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-500" 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>
Persönliche Informationen
</h3>
<!-- Username Field -->
<div class="mb-6">
<label for="username" class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
Benutzername
</label>
<div class="relative group">
<input type="text"
name="username"
id="username"
required
value="{{ user.username }}"
class="premium-input w-full px-5 py-4 rounded-xl text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none"
placeholder="max.mustermann">
<div class="absolute inset-y-0 right-4 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-slate-400 group-focus-within:text-blue-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"/>
</svg>
</div>
</div>
</div>
<!-- Full Name Field -->
<div class="mb-6">
<label for="name" class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
Vollständiger Name
</label>
<div class="relative group">
<input type="text"
name="name"
id="name"
value="{{ user.name }}"
class="premium-input w-full px-5 py-4 rounded-xl text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none"
placeholder="Max Mustermann">
<div class="absolute inset-y-0 right-4 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-slate-400 group-focus-within:text-blue-500 transition-colors" 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>
</div>
<!-- Password Field -->
<div>
<label for="password" class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
Neues Passwort
<span class="text-xs font-normal text-slate-500 dark:text-slate-400 ml-2">(optional)</span>
</label>
<div class="relative group">
<input type="password"
name="password"
id="password"
class="premium-input w-full px-5 py-4 rounded-xl text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none"
placeholder="Leer lassen, um beizubehalten">
<div class="absolute inset-y-0 right-4 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-slate-400 group-focus-within:text-blue-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 0h12a2 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>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-2 ml-1">
Lassen Sie das Feld leer, um das aktuelle Passwort beizubehalten
</p>
</div>
</div>
</div>
<!-- Right Column: Role & Status -->
<div class="space-y-6">
<div class="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-2xl p-6 border border-green-200/50 dark:border-green-800/50">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6 flex items-center">
<svg class="w-5 h-5 mr-2 text-green-500" 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>
Rolle & Status
</h3>
<!-- Role Selection -->
<div class="mb-6">
<label for="role" class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
Benutzerrolle
</label>
<div class="relative">
<select name="role"
id="role"
class="premium-input w-full px-5 py-4 rounded-xl text-slate-900 dark:text-white focus:outline-none appearance-none cursor-pointer">
<option value="user" {% if not user.is_admin %}selected{% endif %}>
👤 Standard-Benutzer
</option>
<option value="admin" {% if user.is_admin %}selected{% endif %}>
👑 Administrator
</option>
</select>
<div class="absolute inset-y-0 right-4 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
</div>
</div>
<!-- Status Selection -->
<div>
<label for="is_active" class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
Kontostatus
</label>
<div class="relative">
<select name="is_active"
id="is_active"
class="premium-input w-full px-5 py-4 rounded-xl text-slate-900 dark:text-white focus:outline-none appearance-none cursor-pointer">
<option value="true" {% if user.active %}selected{% endif %}>
✅ Aktiv
</option>
<option value="false" {% if not user.active %}selected{% endif %}>
❌ Deaktiviert
</option>
</select>
<div class="absolute inset-y-0 right-4 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Advanced Permissions Section -->
<div class="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-2xl p-8 border border-purple-200/50 dark:border-purple-800/50">
<div class="mb-8">
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-3 flex items-center">
<svg class="w-6 h-6 mr-3 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 0h12a2 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>
Erweiterte Berechtigungen
</h3>
<p class="text-slate-600 dark:text-slate-400">
Konfigurieren Sie die spezifischen Zugriffsrechte für diesen Benutzer
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Permission: Start Jobs -->
<div class="permission-card bg-white/60 dark:bg-slate-800/60 backdrop-blur-sm rounded-2xl p-6 border border-white/50 dark:border-slate-700/50">
<div class="flex items-start justify-between mb-4">
<div class="flex-1">
<div class="flex items-center mb-2">
<svg class="w-5 h-5 text-green-500 mr-2" 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.01M19 10a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<h4 class="font-semibold text-slate-900 dark:text-white">Jobs starten</h4>
</div>
<p class="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
Benutzer kann eigene Druckjobs ohne Admin-Genehmigung starten
</p>
</div>
<div class="ml-4">
<label class="toggle-switch">
<input type="checkbox"
name="can_start_jobs"
{% if user.permissions and user.permissions.can_start_jobs %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- Permission: Needs Approval -->
<div class="permission-card bg-white/60 dark:bg-slate-800/60 backdrop-blur-sm rounded-2xl p-6 border border-white/50 dark:border-slate-700/50">
<div class="flex items-start justify-between mb-4">
<div class="flex-1">
<div class="flex items-center mb-2">
<svg class="w-5 h-5 text-orange-500 mr-2" 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>
<h4 class="font-semibold text-slate-900 dark:text-white">Genehmigungspflicht</h4>
</div>
<p class="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
Jobs des Benutzers müssen von einem Admin genehmigt werden
</p>
</div>
<div class="ml-4">
<label class="toggle-switch">
<input type="checkbox"
name="needs_approval"
{% if not user.permissions or user.permissions.needs_approval %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- Permission: Can Approve -->
<div class="permission-card bg-white/60 dark:bg-slate-800/60 backdrop-blur-sm rounded-2xl p-6 border border-white/50 dark:border-slate-700/50">
<div class="flex items-start justify-between mb-4">
<div class="flex-1">
<div class="flex items-center mb-2">
<svg class="w-5 h-5 text-blue-500 mr-2" 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>
<h4 class="font-semibold text-slate-900 dark:text-white">Jobs genehmigen</h4>
</div>
<p class="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
Benutzer kann Gastanfragen und fremde Jobs genehmigen
</p>
</div>
<div class="ml-4">
<label class="toggle-switch">
<input type="checkbox"
name="can_approve_jobs"
{% if user.permissions and user.permissions.can_approve_jobs %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex items-center justify-between pt-8 border-t border-slate-200 dark:border-slate-700">
<div class="flex items-center space-x-4">
<div class="flex items-center text-sm text-slate-500 dark:text-slate-400">
<svg class="w-4 h-4 mr-2" 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>
<span>Änderungen werden sofort gespeichert</span>
</div>
</div>
<div class="flex items-center space-x-4">
<a href="{{ url_for('admin_page', tab='users') }}"
class="group inline-flex items-center px-8 py-4 bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm text-slate-700 dark:text-slate-300 rounded-2xl hover:bg-white dark:hover:bg-slate-700 transition-all duration-300 shadow-lg hover:shadow-xl border border-white/20 dark:border-slate-700/50">
<svg class="w-5 h-5 mr-2 transition-transform group-hover:-translate-x-1" 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="font-medium">Abbrechen</span>
</a>
<button type="submit"
class="btn-gradient group inline-flex items-center px-8 py-4 text-white rounded-2xl hover:shadow-2xl transition-all duration-300 shadow-xl font-semibold text-lg relative overflow-hidden">
<svg class="w-5 h-5 mr-3 transition-transform group-hover:scale-110" 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>Änderungen speichern</span>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Enhanced JavaScript for better UX -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('userEditForm');
const submitButton = form.querySelector('button[type="submit"]');
// Add smooth loading state
form.addEventListener('submit', function(e) {
submitButton.disabled = true;
submitButton.innerHTML = `
<svg class="w-5 h-5 mr-3 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>Speichere...</span>
`;
});
// Enhanced input focus effects
const inputs = form.querySelectorAll('.premium-input');
inputs.forEach(input => {
input.addEventListener('focus', function() {
this.parentElement.classList.add('focused');
});
input.addEventListener('blur', function() {
this.parentElement.classList.remove('focused');
});
});
// Smooth scroll on form errors
const errors = document.querySelectorAll('.error-message');
if (errors.length > 0) {
errors[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// Toggle switch animations
const toggles = document.querySelectorAll('.toggle-switch input');
toggles.forEach(toggle => {
toggle.addEventListener('change', function() {
const slider = this.nextElementSibling;
if (this.checked) {
slider.style.transform = 'scale(1.1)';
setTimeout(() => {
slider.style.transform = 'scale(1)';
}, 150);
}
});
});
// Form validation feedback
const requiredInputs = form.querySelectorAll('input[required]');
requiredInputs.forEach(input => {
input.addEventListener('blur', function() {
if (this.value.trim() === '') {
this.classList.add('border-red-300');
this.classList.remove('border-green-300');
} else {
this.classList.add('border-green-300');
this.classList.remove('border-red-300');
}
});
});
// Auto-save notification (mock)
let saveTimeout;
inputs.forEach(input => {
input.addEventListener('input', function() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
// Could implement auto-save here
console.log('Changes detected - auto-save ready');
}, 2000);
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,372 @@
{% extends "base.html" %}
{% block title %}Gastaufträge verwalten - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<!-- CSRF Token für AJAX-Anfragen -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<script src="{{ url_for('static', filename='js/admin-guest-requests.js') }}" defer></script>
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center hidden">
<div class="bg-white dark:bg-slate-900 rounded-2xl p-8 shadow-2xl border border-gray-200 dark:border-slate-700">
<div class="flex items-center space-x-4">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 dark:border-blue-400"></div>
<span class="text-lg font-medium text-gray-900 dark:text-slate-100">Wird geladen...</span>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<!-- Moderne Gastaufträge-Verwaltung -->
<div class="min-h-screen">
<!-- Hero Header -->
<div class="relative overflow-hidden bg-gradient-to-r from-slate-900 via-blue-900 to-indigo-900 dark:from-slate-950 dark:via-blue-950 dark:to-indigo-950 text-white rounded-3xl mx-4 mt-4">
<div class="absolute inset-0 bg-black/30 dark:bg-black/50"></div>
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 dark:via-white/3 to-transparent"></div>
<!-- Live Status Indicator -->
<div class="absolute top-4 right-4 flex items-center space-x-2">
<div class="flex items-center space-x-2 bg-white/15 dark:bg-white/10 backdrop-blur-sm border border-white/30 dark:border-white/20 rounded-full px-3 py-1">
<div id="live-indicator" class="w-2 h-2 bg-green-400 dark:bg-green-300 rounded-full animate-pulse"></div>
<span class="text-sm font-medium text-white">Live</span>
</div>
<div class="bg-white/15 dark:bg-white/10 backdrop-blur-sm border border-white/30 dark:border-white/20 rounded-full px-3 py-1">
<span id="live-time" class="text-sm font-medium text-white"></span>
</div>
</div>
<!-- Animated Background Pattern -->
<div class="absolute inset-0 opacity-10 dark:opacity-5 rounded-3xl overflow-hidden">
<div class="absolute inset-0" style="background-image: radial-gradient(circle at 25% 25%, white 2px, transparent 2px), radial-gradient(circle at 75% 75%, white 2px, transparent 2px); background-size: 50px 50px;"></div>
</div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div class="flex justify-between items-center">
<div>
<!-- Mercedes-Benz Logo -->
<div class="inline-flex items-center justify-center w-20 h-20 bg-white/15 dark:bg-white/10 backdrop-blur-sm rounded-full mb-6 border border-white/30 dark:border-white/20">
<svg class="w-10 h-10 text-white" viewBox="0 0 80 80" fill="currentColor">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>
</div>
<h1 class="text-4xl md:text-5xl font-bold mb-4 tracking-tight">
<span class="bg-gradient-to-r from-white via-blue-100 to-slate-100 dark:from-white dark:via-blue-200 dark:to-slate-200 bg-clip-text text-transparent">
Gastaufträge Verwaltung
</span>
</h1>
<p class="text-xl text-blue-100 dark:text-blue-200 max-w-2xl leading-relaxed">
Verwalten Sie Gastdruckaufträge mit modernster Technologie und Mercedes-Benz Qualität
</p>
</div>
<!-- Back to Admin Button -->
<div>
<a href="{{ url_for('admin_page') }}"
class="inline-flex items-center px-6 py-3 bg-white/15 dark:bg-white/10 backdrop-blur-sm border border-white/30 dark:border-white/20 rounded-xl text-white hover:bg-white/25 dark:hover:bg-white/15 transition-all duration-300 hover:scale-105">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
Zurück zum Admin
</a>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10">
<!-- Quick Stats Dashboard -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-12">
<!-- Pending Requests -->
<div class="group relative bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-orange-500/10 dark:from-orange-400/20 to-red-500/10 dark:to-red-400/20 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative">
<div class="flex items-center justify-between mb-4">
<div class="p-3 bg-gradient-to-br from-orange-500 to-orange-600 dark:from-orange-400 dark:to-orange-500 rounded-xl shadow-lg">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="text-right">
<div id="pending-count" class="text-2xl font-bold text-slate-900 dark:text-slate-100">-</div>
<div class="text-sm text-slate-500 dark:text-slate-400">Wartend</div>
</div>
</div>
<div class="flex items-center space-x-2 mb-2">
<div class="w-2 h-2 bg-orange-400 dark:bg-orange-300 rounded-full animate-pulse"></div>
<span class="text-xs text-orange-600 dark:text-orange-400 font-medium">Benötigt Aufmerksamkeit</span>
</div>
</div>
</div>
<!-- Approved Requests -->
<div class="group relative bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-green-500/10 dark:from-green-400/20 to-emerald-500/10 dark:to-emerald-400/20 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative">
<div class="flex items-center justify-between mb-4">
<div class="p-3 bg-gradient-to-br from-green-500 to-green-600 dark:from-green-400 dark:to-green-500 rounded-xl shadow-lg">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="text-right">
<div id="approved-count" class="text-2xl font-bold text-slate-900 dark:text-slate-100">-</div>
<div class="text-sm text-slate-500 dark:text-slate-400">Genehmigt</div>
</div>
</div>
<div class="flex items-center space-x-2 mb-2">
<div class="w-2 h-2 bg-green-400 dark:bg-green-300 rounded-full animate-pulse"></div>
<span class="text-xs text-green-600 dark:text-green-400 font-medium">Aktiv</span>
</div>
</div>
</div>
<!-- Rejected Requests -->
<div class="group relative bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-red-500/10 dark:from-red-400/20 to-pink-500/10 dark:to-pink-400/20 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative">
<div class="flex items-center justify-between mb-4">
<div class="p-3 bg-gradient-to-br from-red-500 to-red-600 dark:from-red-400 dark:to-red-500 rounded-xl shadow-lg">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="text-right">
<div id="rejected-count" class="text-2xl font-bold text-slate-900 dark:text-slate-100">-</div>
<div class="text-sm text-slate-500 dark:text-slate-400">Abgelehnt</div>
</div>
</div>
<div class="flex items-center space-x-2 mb-2">
<div class="w-2 h-2 bg-red-400 dark:bg-red-300 rounded-full"></div>
<span class="text-xs text-red-600 dark:text-red-400 font-medium">Archiviert</span>
</div>
</div>
</div>
<!-- Total Requests -->
<div class="group relative bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/10 dark:from-blue-400/20 to-indigo-500/10 dark:to-indigo-400/20 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative">
<div class="flex items-center justify-between mb-4">
<div class="p-3 bg-gradient-to-br from-blue-500 to-blue-600 dark:from-blue-400 dark:to-blue-500 rounded-xl shadow-lg">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div>
<div class="text-right">
<div id="total-count" class="text-2xl font-bold text-slate-900 dark:text-slate-100">-</div>
<div class="text-sm text-slate-500 dark:text-slate-400">Gesamt</div>
</div>
</div>
<div class="flex items-center space-x-2 mb-2">
<div class="w-2 h-2 bg-blue-400 dark:bg-blue-300 rounded-full animate-pulse"></div>
<span class="text-xs text-blue-600 dark:text-blue-400 font-medium">Alle Zeit</span>
</div>
</div>
</div>
</div>
<!-- Control Panel -->
<div class="bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl dark:shadow-2xl mb-8">
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center space-y-4 lg:space-y-0">
<!-- Search and Filters -->
<div class="flex flex-col sm:flex-row gap-4 flex-1">
<div class="relative flex-1 max-w-md">
<input type="text" id="search-requests" placeholder="Gastaufträge durchsuchen..."
class="w-full pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
bg-white/80 dark:bg-slate-800/80 text-slate-900 dark:text-slate-100
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent
placeholder-slate-500 dark:placeholder-slate-400">
<svg class="w-5 h-5 text-slate-400 dark:text-slate-500 absolute left-3 top-1/2 transform -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<select id="status-filter"
class="px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
bg-white/80 dark:bg-slate-800/80 text-slate-900 dark:text-slate-100
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent min-w-[150px]">
<option value="all">Alle Status</option>
<option value="pending">Wartend</option>
<option value="approved">Genehmigt</option>
<option value="rejected">Abgelehnt</option>
<option value="expired">Abgelaufen</option>
</select>
<select id="sort-order"
class="px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
bg-white/80 dark:bg-slate-800/80 text-slate-900 dark:text-slate-100
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent min-w-[150px]">
<option value="newest">Neueste zuerst</option>
<option value="oldest">Älteste zuerst</option>
<option value="priority">Nach Priorität</option>
</select>
</div>
<!-- Action Buttons -->
<div class="flex space-x-3 ml-6">
<button id="refresh-btn"
class="inline-flex items-center px-4 py-3 bg-blue-500 dark:bg-blue-600 text-white rounded-xl hover:bg-blue-600 dark:hover:bg-blue-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Aktualisieren
</button>
<button id="export-btn"
class="inline-flex items-center px-4 py-3 bg-green-500 dark:bg-green-600 text-white rounded-xl hover:bg-green-600 dark:hover:bg-green-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Exportieren
</button>
<button id="bulk-actions-btn"
class="inline-flex items-center px-4 py-3 bg-purple-500 dark:bg-purple-600 text-white rounded-xl hover:bg-purple-600 dark:hover:bg-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
Massenaktionen
</button>
</div>
</div>
</div>
<!-- Guest Requests Table -->
<div class="bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 shadow-xl dark:shadow-2xl overflow-hidden">
<div class="p-8 border-b border-slate-200 dark:border-slate-700">
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Gastaufträge</h2>
<div class="flex items-center space-x-2 text-sm text-slate-500 dark:text-slate-400">
<span>Automatische Aktualisierung:</span>
<div class="w-2 h-2 bg-green-400 dark:bg-green-300 rounded-full animate-pulse"></div>
<span>Aktiv</span>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead class="bg-slate-50 dark:bg-slate-900/80">
<tr>
<th class="px-6 py-4 text-left">
<input type="checkbox" id="select-all"
class="rounded border-slate-300 dark:border-slate-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 bg-white dark:bg-slate-800">
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Antragsteller
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Datei & Details
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Status
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Zeitstempel
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Priorität
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Aktionen
</th>
</tr>
</thead>
<tbody id="requests-table-body" class="bg-white dark:bg-slate-900/50 divide-y divide-slate-200 dark:divide-slate-700">
<!-- Dynamic content will be loaded here -->
</tbody>
</table>
</div>
<!-- Loading State -->
<div id="table-loading" class="flex items-center justify-center py-12 hidden">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 dark:border-blue-400 mr-4"></div>
<span class="text-slate-600 dark:text-slate-400">Lade Gastaufträge...</span>
</div>
<!-- Empty State -->
<div id="empty-state" class="text-center py-12 hidden">
<svg class="mx-auto h-12 w-12 text-slate-400 dark:text-slate-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
</svg>
<h3 class="text-lg font-medium text-slate-900 dark:text-slate-100 mb-2">Keine Gastaufträge gefunden</h3>
<p class="text-slate-500 dark:text-slate-400">Es sind derzeit keine Gastaufträge vorhanden oder sie entsprechen nicht den Filterkriterien.</p>
</div>
</div>
<!-- Pagination -->
<div id="pagination" class="mt-8 flex justify-center">
<!-- Pagination controls will be dynamically generated -->
</div>
</div>
</div>
<!-- Guest Request Detail Modal -->
<div id="detail-modal" class="fixed inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm z-50 hidden">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-slate-700 max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div id="modal-content">
<!-- Modal content will be dynamically loaded -->
</div>
</div>
</div>
</div>
<!-- Bulk Actions Modal -->
<div id="bulk-modal" class="fixed inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm z-50 hidden">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-slate-700 max-w-md w-full">
<div class="p-6 border-b border-gray-200 dark:border-slate-700">
<h3 class="text-xl font-bold text-gray-900 dark:text-slate-100">Massenaktionen</h3>
</div>
<div class="p-6">
<div class="space-y-4">
<button onclick="performBulkAction('approve')"
class="w-full px-4 py-3 bg-green-500 dark:bg-green-600 text-white rounded-lg hover:bg-green-600 dark:hover:bg-green-700 transition-colors">
Ausgewählte genehmigen
</button>
<button onclick="performBulkAction('reject')"
class="w-full px-4 py-3 bg-red-500 dark:bg-red-600 text-white rounded-lg hover:bg-red-600 dark:hover:bg-red-700 transition-colors">
Ausgewählte ablehnen
</button>
<button onclick="performBulkAction('delete')"
class="w-full px-4 py-3 bg-gray-500 dark:bg-gray-600 text-white rounded-lg hover:bg-gray-600 dark:hover:bg-gray-700 transition-colors">
Ausgewählte löschen
</button>
</div>
<div class="mt-6 text-center">
<button onclick="closeBulkModal()"
class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
Abbrechen
</button>
</div>
</div>
</div>
</div>
</div>
<script>
// Initialize live time display
function updateLiveTime() {
const timeElement = document.getElementById('live-time');
if (timeElement) {
timeElement.textContent = new Date().toLocaleTimeString('de-DE');
}
}
// Update time every second
setInterval(updateLiveTime, 1000);
updateLiveTime();
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,802 @@
{% extends "base.html" %}
{% block title %}Steckdosenschaltzeiten - Admin{% endblock %}
{% block head %}
<!-- FullCalendar CSS -->
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/main.min.css' rel='stylesheet' />
<link href='https://cdn.jsdelivr.net/npm/@fullcalendar/daygrid@6.1.10/main.min.css' rel='stylesheet' />
<link href='https://cdn.jsdelivr.net/npm/@fullcalendar/timegrid@6.1.10/main.min.css' rel='stylesheet' />
<!-- Chart.js für Statistiken -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
/* ===== DARK MODE CALENDAR OPTIMIERUNG ===== */
.fc {
background: transparent;
}
.fc-theme-standard .fc-view-harness {
background: transparent;
}
.fc-theme-standard .fc-scrollgrid {
border-color: rgb(148 163 184 / 0.3);
}
.dark .fc-theme-standard .fc-scrollgrid {
border-color: rgb(71 85 105 / 0.4);
}
.fc-theme-standard td,
.fc-theme-standard th {
border-color: rgb(148 163 184 / 0.2);
}
.dark .fc-theme-standard td,
.dark .fc-theme-standard th {
border-color: rgb(71 85 105 / 0.3);
}
.fc-col-header-cell {
background: rgb(248 250 252);
color: rgb(51 65 85);
}
.dark .fc-col-header-cell {
background: rgb(15 23 42);
color: rgb(203 213 225);
}
.fc-daygrid-day {
background: transparent;
}
.fc-day-today {
background: rgb(59 130 246 / 0.1) !important;
}
.dark .fc-day-today {
background: rgb(59 130 246 / 0.2) !important;
}
.fc-button-primary {
background: rgb(59 130 246);
border-color: rgb(59 130 246);
color: white;
}
.fc-button-primary:hover {
background: rgb(37 99 235);
border-color: rgb(37 99 235);
}
.fc-button-primary:disabled {
background: rgb(148 163 184);
border-color: rgb(148 163 184);
}
.dark .fc-button-primary:disabled {
background: rgb(71 85 105);
border-color: rgb(71 85 105);
}
.fc-event {
font-size: 11px;
border-radius: 4px;
padding: 2px 4px;
font-weight: 500;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.dark .fc-event {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.fc-h-event {
border: none !important;
}
/* ===== KALENDER CONTAINER ===== */
.calendar-container {
background: rgb(255 255 255);
border: 1px solid rgb(226 232 240);
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: 20px;
margin-bottom: 20px;
backdrop-filter: blur(12px);
}
.dark .calendar-container {
background: rgb(15 23 42);
border-color: rgb(51 65 85);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -1px rgba(0, 0, 0, 0.15);
}
/* ===== STATISTIK KARTEN ===== */
.stats-card {
background: linear-gradient(135deg, rgb(59 130 246) 0%, rgb(99 102 241) 100%);
border-radius: 12px;
padding: 20px;
color: white;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
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;
}
.stats-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px -8px rgba(59, 130, 246, 0.3);
}
.dark .stats-card {
background: linear-gradient(135deg, rgb(37 99 235) 0%, rgb(79 70 229) 100%);
border-color: rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.25);
}
/* ===== KONTROLLPANEL ===== */
.control-panel {
background: rgb(255 255 255 / 0.8);
border: 1px solid rgb(226 232 240);
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
backdrop-filter: blur(12px);
}
.dark .control-panel {
background: rgb(15 23 42 / 0.8);
border-color: rgb(51 65 85);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.filter-group {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
/* ===== BUTTONS ===== */
.btn-calendar {
background: rgb(59 130 246);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
border: 1px solid transparent;
}
.btn-calendar:hover {
background: rgb(37 99 235);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
.btn-calendar.active {
background: rgb(29 78 216);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
.dark .btn-calendar {
background: rgb(37 99 235);
border-color: rgb(59 130 246);
}
.dark .btn-calendar:hover {
background: rgb(29 78 216);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.6);
}
.dark .btn-calendar.active {
background: rgb(30 64 175);
}
/* ===== LEGENDE ===== */
.legend {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin-top: 15px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: rgb(71 85 105);
transition: color 0.3s ease;
}
.dark .legend-item {
color: rgb(203 213 225);
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.dark .legend-color {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
/* ===== FORM ELEMENTS ===== */
select, input {
background: rgb(255 255 255);
border: 1px solid rgb(203 213 225);
color: rgb(51 65 85);
transition: all 0.2s ease;
}
select:focus, input:focus {
border-color: rgb(59 130 246);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
outline: none;
}
.dark select, .dark input {
background: rgb(30 41 59);
border-color: rgb(71 85 105);
color: rgb(203 213 225);
}
.dark select:focus, .dark input:focus {
border-color: rgb(99 102 241);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
/* ===== MODAL STYLING ===== */
.modal-overlay {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.dark .modal-overlay {
background: rgba(0, 0, 0, 0.7);
}
.modal-content {
background: rgb(255 255 255);
border: 1px solid rgb(226 232 240);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.dark .modal-content {
background: rgb(15 23 42);
border-color: rgb(51 65 85);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
}
/* ===== RESPONSIVE IMPROVEMENTS ===== */
@media (max-width: 768px) {
.calendar-container {
padding: 15px;
}
.control-panel {
padding: 15px;
}
.filter-group {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.legend {
gap: 15px;
}
}
</style>
{% endblock %}
{% block content %}
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 dark:from-slate-900 dark:to-slate-800 transition-colors duration-300">
<!-- Header mit Breadcrumb -->
<div class="border-b border-slate-200 dark:border-slate-700 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-6">
<nav class="text-sm font-medium text-slate-600 dark:text-slate-400 mb-4">
<ol class="list-none p-0 inline-flex">
<li class="flex items-center">
<a href="{{ url_for('admin_page') }}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">Admin-Dashboard</a>
<svg class="fill-current w-3 h-3 mx-3 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
<path d="m285.476 272.971c4.686 4.686 4.686 12.284 0 16.97l-133.952 133.954c-4.686 4.686-12.284 4.686-16.97 0l-133.952-133.954c-4.686-4.686-4.686-12.284 0-16.97 4.686-4.686 12.284-4.686 16.97 0l125.462 125.463 125.462-125.463c4.686-4.686 12.284-4.686 16.97 0z"/>
</svg>
</li>
<li class="text-slate-900 dark:text-white font-semibold">
Steckdosenschaltzeiten
</li>
</ol>
</nav>
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">
<i class="fas fa-plug text-blue-600 dark:text-blue-400 mr-3"></i>
Steckdosenschaltzeiten
</h1>
<p class="mt-2 text-slate-600 dark:text-slate-400">
Kalenderübersicht aller Drucker-Steckdosenschaltungen mit detaillierter Analyse
</p>
</div>
<div class="flex gap-3">
<button id="refreshData" class="btn-calendar flex items-center">
<i class="fas fa-sync-alt mr-2"></i>
Aktualisieren
</button>
<button id="cleanupLogs" class="bg-red-600 dark:bg-red-700 text-white px-4 py-2 rounded-lg hover:bg-red-700 dark:hover:bg-red-600 transition-all duration-200 font-medium flex items-center">
<i class="fas fa-trash mr-2"></i>
Alte Logs löschen
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Haupt-Container -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Statistik-Karten -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<div class="stats-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm opacity-80 font-medium">Schaltungen (24h)</p>
<p class="text-2xl font-bold" id="totalLogs">{{ stats.total_logs or 0 }}</p>
<p class="text-xs opacity-70 mt-1">Gesamte Aktivität</p>
</div>
<div class="text-2xl opacity-60">
<i class="fas fa-chart-line"></i>
</div>
</div>
</div>
<div class="stats-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm opacity-80 font-medium">Erfolgsrate</p>
<p class="text-2xl font-bold" id="successRate">{{ "%.1f"|format(100 - stats.error_rate) }}%</p>
<p class="text-xs opacity-70 mt-1">Ohne Fehler</p>
</div>
<div class="text-2xl opacity-60">
<i class="fas fa-check-circle"></i>
</div>
</div>
</div>
<div class="stats-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm opacity-80 font-medium">Ø Antwortzeit</p>
<p class="text-2xl font-bold" id="avgResponseTime">
{% if stats.average_response_time_ms %}
{{ "%.0f"|format(stats.average_response_time_ms) }}ms
{% else %}
N/A
{% endif %}
</p>
<p class="text-xs opacity-70 mt-1">Durchschnitt</p>
</div>
<div class="text-2xl opacity-60">
<i class="fas fa-stopwatch"></i>
</div>
</div>
</div>
<div class="stats-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm opacity-80 font-medium">Fehlerzahl</p>
<p class="text-2xl font-bold" id="errorCount">{{ stats.error_count or 0 }}</p>
<p class="text-xs opacity-70 mt-1">Letzte 24h</p>
</div>
<div class="text-2xl opacity-60">
<i class="fas fa-exclamation-triangle"></i>
</div>
</div>
</div>
</div>
<!-- Filter und Steuerung -->
<div class="control-panel">
<div class="filter-group">
<div class="flex items-center gap-3">
<label for="printerFilter" class="font-medium text-slate-700 dark:text-slate-300 whitespace-nowrap">Drucker filtern:</label>
<select id="printerFilter" class="border border-slate-300 dark:border-slate-600 rounded-lg px-3 py-2 bg-white dark:bg-slate-800 text-slate-900 dark:text-white min-w-40">
<option value="">Alle Drucker</option>
{% for printer in printers %}
<option value="{{ printer.id }}">{{ printer.name }}</option>
{% endfor %}
</select>
</div>
<div class="border-l border-slate-300 dark:border-slate-600 pl-4 ml-4">
<label class="font-medium text-slate-700 dark:text-slate-300 mr-3">Ansicht:</label>
<div class="inline-flex gap-1">
<button id="monthView" class="btn-calendar active">Monat</button>
<button id="weekView" class="btn-calendar">Woche</button>
<button id="dayView" class="btn-calendar">Tag</button>
</div>
</div>
</div>
<!-- Legende -->
<div class="legend">
<div class="legend-item">
<div class="legend-color" style="background-color: #10b981;"></div>
<span>Steckdose EIN</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #f59e0b;"></div>
<span>Steckdose AUS</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #3b82f6;"></div>
<span>Verbunden</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #ef4444;"></div>
<span>Getrennt</span>
</div>
</div>
</div>
<!-- Kalender-Container -->
<div class="calendar-container">
<div id="calendar"></div>
</div>
<!-- Detailansicht Modal -->
<div id="eventDetailModal" class="fixed inset-0 modal-overlay hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="modal-content rounded-lg max-w-lg w-full p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Schaltung Details</h3>
<button id="closeModal" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors">
<i class="fas fa-times text-lg"></i>
</button>
</div>
<div id="modalContent" class="text-slate-700 dark:text-slate-300">
<!-- Wird dynamisch gefüllt -->
</div>
<div class="mt-6 flex justify-end">
<button id="closeModalBtn" class="px-4 py-2 bg-slate-300 dark:bg-slate-600 text-slate-700 dark:text-slate-200 rounded-lg hover:bg-slate-400 dark:hover:bg-slate-500 transition-colors font-medium">
Schließen
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- FullCalendar JS -->
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
let calendar;
let currentPrinterFilter = '';
// Kalender initialisieren
function initCalendar() {
const calendarEl = document.getElementById('calendar');
calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
locale: 'de',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
height: 'auto',
events: function(info, successCallback, failureCallback) {
loadCalendarEvents(info.start, info.end, currentPrinterFilter, successCallback, failureCallback);
},
eventClick: function(info) {
showEventDetails(info.event);
},
eventDidMount: function(info) {
// Tooltip hinzufügen
info.el.title = info.event.title + '\nKlicken für Details';
}
});
calendar.render();
}
// Events vom Server laden
function loadCalendarEvents(start, end, printerFilter, successCallback, failureCallback) {
const params = new URLSearchParams({
start: start.toISOString(),
end: end.toISOString()
});
if (printerFilter) {
params.append('printer_id', printerFilter);
}
fetch(`/api/admin/plug-schedules/calendar?${params}`)
.then(response => response.json())
.then(data => {
successCallback(data);
})
.catch(error => {
console.error('Fehler beim Laden der Kalender-Daten:', error);
failureCallback(error);
});
}
// Event-Details anzeigen
function showEventDetails(event) {
const props = event.extendedProps;
const modal = document.getElementById('eventDetailModal');
const content = document.getElementById('modalContent');
const startTime = new Date(event.start).toLocaleString('de-DE');
let detailsHtml = `
<div class="space-y-4">
<div class="flex items-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
<span class="w-4 h-4 rounded mr-3 flex-shrink-0" style="background-color: ${event.backgroundColor}"></span>
<span class="font-medium text-slate-900 dark:text-white">${event.title}</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Zeitpunkt:</span>
<p class="text-slate-900 dark:text-white">${startTime}</p>
</div>
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Drucker:</span>
<p class="text-slate-900 dark:text-white">${props.printer_name}</p>
</div>
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Status:</span>
<p class="text-slate-900 dark:text-white capitalize font-medium">${props.status}</p>
</div>
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Quelle:</span>
<p class="text-slate-900 dark:text-white capitalize">${props.source}</p>
</div>
`;
if (props.user_name) {
detailsHtml += `
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Benutzer:</span>
<p class="text-slate-900 dark:text-white">${props.user_name}</p>
</div>
`;
}
if (props.response_time_ms) {
detailsHtml += `
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Antwortzeit:</span>
<p class="text-slate-900 dark:text-white">${props.response_time_ms}ms</p>
</div>
`;
}
if (props.power_consumption) {
detailsHtml += `
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Verbrauch:</span>
<p class="text-slate-900 dark:text-white">${props.power_consumption}W</p>
</div>
`;
}
if (props.voltage) {
detailsHtml += `
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Spannung:</span>
<p class="text-slate-900 dark:text-white">${props.voltage}V</p>
</div>
`;
}
if (props.current) {
detailsHtml += `
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Strom:</span>
<p class="text-slate-900 dark:text-white">${props.current}A</p>
</div>
`;
}
detailsHtml += `</div></div>`;
if (props.notes) {
detailsHtml += `
<div class="mt-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<span class="font-medium text-blue-800 dark:text-blue-200 block mb-2">Notizen:</span>
<p class="text-sm text-blue-700 dark:text-blue-300">${props.notes}</p>
</div>
`;
}
if (props.error_message) {
detailsHtml += `
<div class="mt-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
<span class="font-medium text-red-800 dark:text-red-200 block mb-2">Fehlermeldung:</span>
<p class="text-sm text-red-700 dark:text-red-300">${props.error_message}</p>
</div>
`;
}
content.innerHTML = detailsHtml;
modal.classList.remove('hidden');
}
// Modal schließen
function closeModal() {
document.getElementById('eventDetailModal').classList.add('hidden');
}
// Event-Listener
document.getElementById('closeModal').addEventListener('click', closeModal);
document.getElementById('closeModalBtn').addEventListener('click', closeModal);
// Escape-Taste zum Schließen des Modals
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeModal();
}
});
// Modal schließen bei Klick außerhalb
document.getElementById('eventDetailModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
// Drucker-Filter
document.getElementById('printerFilter').addEventListener('change', function() {
currentPrinterFilter = this.value;
calendar.refetchEvents();
});
// Ansicht-Buttons
document.getElementById('monthView').addEventListener('click', function() {
calendar.changeView('dayGridMonth');
updateActiveViewButton(this);
});
document.getElementById('weekView').addEventListener('click', function() {
calendar.changeView('timeGridWeek');
updateActiveViewButton(this);
});
document.getElementById('dayView').addEventListener('click', function() {
calendar.changeView('timeGridDay');
updateActiveViewButton(this);
});
function updateActiveViewButton(activeBtn) {
document.querySelectorAll('.btn-calendar').forEach(btn => {
btn.classList.remove('active');
});
activeBtn.classList.add('active');
}
// Aktualisieren-Button
document.getElementById('refreshData').addEventListener('click', function() {
const btn = this;
const icon = btn.querySelector('i');
// Button-State während des Ladens
btn.disabled = true;
icon.classList.add('fa-spin');
Promise.all([
calendar.refetchEvents(),
loadStatistics()
]).finally(() => {
btn.disabled = false;
icon.classList.remove('fa-spin');
});
});
// Cleanup-Button
document.getElementById('cleanupLogs').addEventListener('click', function() {
if (confirm('Möchten Sie wirklich alte Logs löschen? (älter als 30 Tage)\n\nDieser Vorgang kann nicht rückgängig gemacht werden.')) {
const btn = this;
btn.disabled = true;
fetch('/api/admin/plug-schedules/cleanup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ days: 30 })
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(`Erfolgreich ${data.deleted_count} alte Einträge gelöscht`, 'success', 5000, {
title: 'Bereinigung erfolgreich'
});
} else {
alert(`Erfolgreich ${data.deleted_count} alte Einträge gelöscht`);
}
calendar.refetchEvents();
loadStatistics();
} else {
if (typeof showToast === 'function') {
showToast('Fehler beim Löschen: ' + data.error, 'error', 5000, {
title: 'Bereinigung fehlgeschlagen'
});
} else {
alert('Fehler beim Löschen: ' + data.error);
}
}
})
.catch(error => {
console.error('Fehler:', error);
if (typeof showToast === 'function') {
showToast('Fehler beim Löschen der Logs', 'error', 5000, {
title: 'Netzwerkfehler'
});
} else {
alert('Fehler beim Löschen der Logs');
}
})
.finally(() => {
btn.disabled = false;
});
}
});
// Statistiken laden
function loadStatistics() {
return fetch('/api/admin/plug-schedules/statistics')
.then(response => response.json())
.then(data => {
if (data.success) {
const stats = data.statistics;
document.getElementById('totalLogs').textContent = stats.total_logs || 0;
document.getElementById('successRate').textContent = (100 - (stats.error_rate || 0)).toFixed(1) + '%';
document.getElementById('avgResponseTime').textContent =
stats.average_response_time_ms ? Math.round(stats.average_response_time_ms) + 'ms' : 'N/A';
document.getElementById('errorCount').textContent = stats.error_count || 0;
}
})
.catch(error => {
console.error('Fehler beim Laden der Statistiken:', error);
});
}
// Kalender initialisieren
initCalendar();
// Initial Statistics Load
loadStatistics();
});
</script>
{% endblock %}

View File

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

View File

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

748
templates/analytics.html Normal file
View File

@@ -0,0 +1,748 @@
{% 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 %}
<!-- Chart.js - Lokale Version -->
<script src="{{ url_for('static', filename='js/charts/chart.min.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 (typeof showFlashMessage === 'function') {
showFlashMessage(message, 'error');
} else {
alert(message);
}
}
showSuccess(message) {
if (typeof showFlashMessage === 'function') {
showFlashMessage(message, 'success');
} else {
alert(message);
}
}
}
// Dashboard initialisieren
document.addEventListener('DOMContentLoaded', () => {
new AnalyticsDashboard();
});
</script>
{% endblock %}

218
templates/base-fast.html Normal file
View File

@@ -0,0 +1,218 @@
<!DOCTYPE html>
<html lang="de" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="MYP Platform - Mercedes-Benz 3D Druck Management System">
<meta name="robots" content="noindex, nofollow">
<meta name="theme-color" content="#000000">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{% block title %}MYP Platform - Mercedes-Benz{% endblock %}</title>
<!-- Critical CSS inline for instant rendering -->
<style>
/* Critical CSS for above-the-fold content */
*,::after,::before{box-sizing:border-box}
html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}
body{margin:0;font-family:inherit;line-height:inherit}
.dark{color-scheme:dark}
.dark body{background-color:#0f172a;color:#e2e8f0}
body{background-color:#fff;color:#1e293b}
/* Glassmorphism navbar - preserved */
.glass-navbar{background:rgba(255,255,255,0.85);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.3);box-shadow:0 2px 4px rgba(0,0,0,0.05)}
.dark .glass-navbar{background:rgba(15,23,42,0.85);border:1px solid rgba(255,255,255,0.1)}
/* Hide content until styles load */
.no-fouc{visibility:hidden;opacity:0}
.fonts-loaded .no-fouc{visibility:visible;opacity:1;transition:opacity 0.2s}
</style>
<!-- Preconnect to speed up font loading -->
<link rel="preconnect" href="{{ url_for('static', filename='fontawesome/webfonts', _external=True) }}" crossorigin>
<!-- Optimized CSS loading -->
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/performance-optimized.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/core-utilities.min.css') }}" rel="stylesheet">
<!-- Non-critical CSS -->
<link href="{{ url_for('static', filename='fontawesome/css/all.min.css') }}" rel="stylesheet" media="print" onload="this.media='all'">
<!-- PWA -->
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<!-- Dark Mode Script (inline to prevent flash) -->
<script>
(function(){
const savedMode = localStorage.getItem('myp-dark-mode');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = savedMode === 'true' || (savedMode === null && prefersDark);
if (isDark) {
document.documentElement.classList.add('dark');
document.querySelector('meta[name="theme-color"]').content = '#000000';
} else {
document.documentElement.classList.remove('dark');
document.querySelector('meta[name="theme-color"]').content = '#ffffff';
}
})();
</script>
{% block extra_css %}{% endblock %}
</head>
<body class="min-h-screen bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 no-fouc">
<!-- Skip to content -->
<a href="#main" class="sr-only focus:not-sr-only">Skip to main content</a>
<!-- Header with glassmorphism navbar -->
<header class="glass-navbar sticky top-0 z-40 w-full">
<nav class="container mx-auto px-4 py-3">
<div class="flex items-center justify-between">
<!-- Logo -->
<a href="{{ url_for('index') }}" class="flex items-center space-x-3">
<svg class="w-8 h-8" 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.5C27,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,40c0-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.8C53.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.9C67.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,40c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>
<span class="font-semibold text-xl">MYP Platform</span>
</a>
<!-- Navigation -->
<div class="flex items-center space-x-4">
{% if current_user.is_authenticated %}
<!-- Simplified navigation links -->
<a href="{{ url_for('dashboard') }}" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800">Dashboard</a>
<a href="{{ url_for('printers') }}" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800">Drucker</a>
<a href="{{ url_for('jobs') }}" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800">Aufträge</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin') }}" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800">Admin</a>
{% endif %}
<!-- User menu -->
<div class="relative">
<button id="user-menu-button" class="flex items-center p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800">
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-sm font-medium">
{{ current_user.email[0].upper() if current_user.email else 'U' }}
</div>
</button>
<div id="user-dropdown" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-slate-800 rounded-lg shadow-lg">
<a href="{{ url_for('user_profile') }}" class="block px-4 py-2 text-sm hover:bg-slate-100 dark:hover:bg-slate-700">Profil</a>
<a href="{{ url_for('auth.logout') }}" class="block px-4 py-2 text-sm hover:bg-slate-100 dark:hover:bg-slate-700">Abmelden</a>
</div>
</div>
<!-- Dark mode toggle -->
<button id="darkModeToggle" class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800">
<svg class="w-5 h-5 sun-icon" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"></path>
</svg>
<svg class="w-5 h-5 moon-icon hidden" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
</button>
{% else %}
<a href="{{ url_for('auth.login') }}" class="px-4 py-2 rounded-md text-sm font-medium bg-blue-600 text-white hover:bg-blue-700">Anmelden</a>
{% endif %}
</div>
</div>
</nav>
</header>
<!-- Flash messages container -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div id="flask-flash-messages"
data-flash-count="{{ messages|length }}"
{% for i, (category, message) in enumerate(messages, 1) %}
data-flash-{{ i }}="{{ category }}|{{ message }}"
{% endfor %}
class="hidden"></div>
{% endif %}
{% endwith %}
<!-- Main content -->
<main id="main" class="container mx-auto px-4 py-8">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="mt-auto py-8 text-center text-sm text-slate-600 dark:text-slate-400">
<p>&copy; 2024 Mercedes-Benz AG. Alle Rechte vorbehalten.</p>
</footer>
<!-- Core JavaScript bundle -->
<script src="{{ url_for('static', filename='js/core-bundle.min.js') }}"></script>
<!-- Inline initialization script -->
<script>
// Font loading detection
if ('fonts' in document) {
document.fonts.ready.then(() => {
document.documentElement.classList.add('fonts-loaded');
});
} else {
// Fallback for browsers without font loading API
setTimeout(() => {
document.documentElement.classList.add('fonts-loaded');
}, 100);
}
// Initialize core functionality
document.addEventListener('DOMContentLoaded', function() {
// Dark mode toggle
const darkModeToggle = document.getElementById('darkModeToggle');
if (darkModeToggle) {
darkModeToggle.addEventListener('click', function() {
document.documentElement.classList.toggle('dark');
const isDark = document.documentElement.classList.contains('dark');
localStorage.setItem('myp-dark-mode', isDark);
document.querySelector('meta[name="theme-color"]').content = isDark ? '#000000' : '#ffffff';
// Update icons
document.querySelector('.sun-icon').classList.toggle('hidden', isDark);
document.querySelector('.moon-icon').classList.toggle('hidden', !isDark);
});
}
// Simple dropdown
const userMenuButton = document.getElementById('user-menu-button');
const userDropdown = document.getElementById('user-dropdown');
if (userMenuButton && userDropdown) {
userMenuButton.addEventListener('click', function(e) {
e.stopPropagation();
userDropdown.classList.toggle('hidden');
});
document.addEventListener('click', function() {
userDropdown.classList.add('hidden');
});
}
// Flash messages
const flashContainer = document.getElementById('flask-flash-messages');
if (flashContainer && window.MYP && window.MYP.utils) {
const flashCount = parseInt(flashContainer.getAttribute('data-flash-count')) || 0;
for (let i = 1; i <= flashCount; i++) {
const flashData = flashContainer.getAttribute('data-flash-' + i);
if (flashData) {
const [category, message] = flashData.split('|', 2);
let messageType = category === 'danger' ? 'error' : category;
setTimeout(() => {
window.MYP.utils.notifications[messageType](message, 6000);
}, i * 100);
}
}
flashContainer.remove();
}
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1230
templates/base.html Normal file

File diff suppressed because it is too large Load Diff

1702
templates/calendar.html Normal file

File diff suppressed because it is too large Load Diff

952
templates/dashboard.html Normal file
View File

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

35
templates/errors/403.html Normal file
View File

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

47
templates/errors/404.html Normal file
View File

@@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}404 - Seite nicht gefunden - Mercedes-Benz MYP Platform{% endblock %}
{% block content %}
<div class="min-h-[80vh] flex flex-col items-center justify-center p-4">
<!-- 404 Error Container -->
<div class="w-full max-w-md">
<div class="bg-white dark:bg-gray-800 backdrop-blur-xl bg-opacity-95 dark:bg-opacity-95 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 p-8 text-center transition-all duration-300">
<!-- Mercedes-Benz Logo -->
<div class="flex justify-center mb-6">
<div class="w-16 h-16 text-gray-300 dark:text-gray-600 transition-transform duration-500 hover:scale-110">
<svg class="w-full h-full" fill="currentColor" viewBox="0 0 80 80">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>
</div>
</div>
<!-- Error Message -->
<h1 class="text-6xl font-bold text-gray-300 dark:text-gray-600 mb-4">404</h1>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Seite nicht gefunden</h2>
<p class="text-gray-600 dark:text-gray-400 mb-6">Die von Ihnen gesuchte Seite existiert nicht oder wurde verschoben.</p>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row justify-center gap-4 mt-8">
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center justify-center px-5 py-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium rounded-lg transition-all duration-300 transform hover:-translate-y-0.5">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
<span>Zum Dashboard</span>
</a>
<button onclick="window.history.back()" class="inline-flex items-center justify-center px-5 py-3 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-all duration-300 transform hover:-translate-y-0.5">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
<span>Zurück</span>
</button>
</div>
</div>
</div>
</div>
{% endblock %}

66
templates/errors/500.html Normal file
View File

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

View File

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

1545
templates/guest_request.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,289 @@
{% extends "base.html" %}
{% block title %}Meine Druckanträge - 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="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>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Meine Druckanträge</h1>
<p class="text-slate-500 dark:text-slate-400 mt-1">Übersicht Ihrer eingereichten Anträge für {{ email }}</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>
</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" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
<span>Alle Anträge</span>
</a>
</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("request.status", "equalto", "pending")|list|length %}
{% set approved_requests = requests|selectattr("request.status", "equalto", "approved")|list|length %}
{% set denied_requests = requests|selectattr("request.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 gefunden</h3>
<p class="text-slate-500 dark:text-slate-400 mb-4">
Für die E-Mail-Adresse {{ email }} wurden keine Druckanträge gefunden.
</p>
<a href="{{ url_for('guest.guest_request_form') }}"
class="btn-primary">
Ersten Antrag stellen
</a>
</div>
{% else %}
<div class="space-y-4">
{% for req_data in requests %}
{% set request = req_data.request %}
{% set job = req_data.job %}
<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 job %}
<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">
Job: {{ 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="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="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">
{% if request.printer %}
{{ request.printer.name }}
{% else %}
Automatisch zuweisen
{% endif %}
</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="text-sm text-slate-600 dark:text-slate-300">{{ request.reason }}</div>
</div>
{% endif %}
{% if job %}
<div class="mt-4 bg-blue-50 dark:bg-blue-900/30 p-3 rounded-lg">
<div class="text-blue-700 dark:text-blue-400 font-medium mb-2">Zugewiesener Job</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm">
<div>
<span class="text-slate-500 dark:text-slate-400">Job-Name:</span>
<span class="text-slate-600 dark:text-slate-300 ml-1">{{ job.name }}</span>
</div>
<div>
<span class="text-slate-500 dark:text-slate-400">Drucker:</span>
<span class="text-slate-600 dark:text-slate-300 ml-1">{{ job.printer.name if job.printer else 'N/A' }}</span>
</div>
<div>
<span class="text-slate-500 dark:text-slate-400">Status:</span>
<span class="text-slate-600 dark:text-slate-300 ml-1">{{ job.status|title }}</span>
</div>
</div>
{% if job.status == 'running' %}
<div class="mt-2">
<a href="{{ url_for('guest.guest_job_status', job_id=job.id) }}"
class="btn-sm btn-primary">
Job-Status anzeigen
</a>
</div>
{% endif %}
</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">
<a href="{{ url_for('guest.guest_request_status', request_id=request.id) }}"
class="btn-sm btn-success">
Details anzeigen
</a>
</div>
{% elif request.status == 'pending' %}
<div class="mt-3">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
In Bearbeitung
</span>
</div>
{% elif request.status == 'denied' %}
<div class="mt-3">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400">
Abgelehnt
</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 = `Meine Anträge (${timeString}) - Mercedes-Benz MYP Platform`;
}
updateTabTitle();
setInterval(updateTabTitle, 1000);
// Auto-Refresh alle 30 Sekunden
setInterval(function() {
if (!document.hidden) {
window.location.reload();
}
}, 30000);
});
</script>
{% endblock %}

View File

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

View File

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

334
templates/guest_status.html Normal file
View File

@@ -0,0 +1,334 @@
{% extends "base.html" %}
{% block title %}Anfrage-Status - Mercedes-Benz MYP Platform{% endblock %}
{% block head %}
{{ super() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta http-equiv="refresh" content="30">
{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Page Header -->
<div class="dashboard-card p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 flex-shrink-0">
<svg class="w-full h-full text-slate-900 dark:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Anfrage-Status</h1>
<p class="text-slate-500 dark:text-slate-400 mt-1">Verfolgen Sie den Status Ihrer Gastanfrage für 3D-Druck</p>
</div>
</div>
<div class="flex flex-wrap gap-3">
<button onclick="location.reload()"
class="btn-secondary flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span>Aktualisieren</span>
</button>
<a href="{{ url_for('guest.guest_request_form') }}"
class="btn-primary flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
<span>Neue Anfrage</span>
</a>
</div>
</div>
</div>
<!-- Status Badge -->
<div class="dashboard-card p-6 text-center">
{% if request.status == 'pending' %}
<div class="inline-flex items-center px-6 py-3 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-lg font-semibold">
<svg class="w-6 h-6 mr-3 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Wird geprüft
</div>
{% elif request.status == 'approved' %}
<div class="inline-flex items-center px-6 py-3 rounded-full bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 text-lg font-semibold">
<svg class="w-6 h-6 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
Genehmigt
</div>
{% elif request.status == 'denied' %}
<div class="inline-flex items-center px-6 py-3 rounded-full bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 text-lg font-semibold">
<svg class="w-6 h-6 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
Abgelehnt
</div>
{% endif %}
</div>
<!-- Anfrage-Details -->
<div class="dashboard-card p-6">
<div class="text-center mb-8">
<h2 class="text-2xl font-bold text-slate-900 dark:text-white mb-2">
Anfrage Details
</h2>
<p class="text-slate-500 dark:text-slate-400">
Übersicht Ihrer eingereichten Gastanfrage
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Anfrage-ID</h3>
<p class="text-xl font-bold text-slate-900 dark:text-white">#{{ request.id }}</p>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Erstellt am</h3>
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ request.created_at|format_datetime }}</p>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Name</h3>
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ request.name }}</p>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Gewünschte Dauer</h3>
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ request.duration_min }} Minuten</p>
</div>
</div>
{% if request.reason %}
<div class="mt-6 bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Begründung</h3>
<p class="text-slate-700 dark:text-slate-300">{{ request.reason }}</p>
</div>
{% endif %}
{% if request.printer %}
<div class="mt-6 bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Drucker</h3>
<p class="text-slate-700 dark:text-slate-300">{{ request.printer.name }} {% if request.printer.location %}({{ request.printer.location }}){% endif %}</p>
</div>
{% endif %}
</div>
<!-- Status-spezifische Inhalte -->
{% if request.status == 'pending' %}
<div class="dashboard-card p-6 border-l-4 border-yellow-400">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-yellow-100 dark:bg-yellow-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Ihre Anfrage wird geprüft</h3>
<p class="text-slate-600 dark:text-slate-400">
Unser Team prüft Ihre Anfrage mit höchster Priorität. Sie erhalten eine sofortige Benachrichtigung, sobald eine Entscheidung getroffen wurde.
Diese Seite aktualisiert sich automatisch alle 30 Sekunden.
</p>
</div>
</div>
</div>
{% elif request.status == 'approved' %}
<div class="dashboard-card p-6 border-l-4 border-green-400">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Anfrage genehmigt!</h3>
<p class="text-slate-600 dark:text-slate-400">Ihr Druckauftrag wurde genehmigt und ist bereit zum Start.</p>
</div>
</div>
</div>
{% if otp_code %}
<!-- Code-Anzeige -->
<div class="dashboard-card p-6">
<h4 class="text-xl font-bold text-slate-900 dark:text-white mb-6 text-center">Ihr Zugangscode</h4>
<!-- 6-stelliger Code in schönen Boxen -->
<div class="flex justify-center gap-3 mb-6">
{% for char in otp_code %}
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 text-blue-900 dark:text-blue-100 rounded-lg flex items-center justify-center text-2xl font-bold">
{{ char }}
</div>
{% endfor %}
</div>
<!-- Code zum Kopieren -->
<div class="text-center mb-6">
<div class="inline-flex items-center bg-gray-50 dark:bg-slate-700/30 rounded-lg px-4 py-3">
<span class="text-xl font-mono font-bold text-slate-900 dark:text-white mr-3" id="otpCode">{{ otp_code }}</span>
<button onclick="copyCode()" class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors" title="Code kopieren">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
</button>
</div>
</div>
<!-- Wichtige Hinweise -->
<div class="bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-orange-100 dark:bg-orange-900/50 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.734 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
</div>
<div class="flex-1">
<h5 class="text-sm font-semibold text-orange-800 dark:text-orange-300 mb-2">Wichtige Hinweise:</h5>
<ul class="text-sm text-orange-700 dark:text-orange-400 space-y-1">
<li>• Dieser Code ist nur <strong>einmalig verwendbar</strong></li>
<li>• Notieren Sie sich den Code oder speichern Sie diese Seite</li>
<li>• Bei Verlust des Codes kontaktieren Sie den Administrator</li>
</ul>
</div>
</div>
</div>
<!-- Start-Button -->
<div class="text-center">
<a href="{{ url_for('guest.guest_start_job_form') }}"
class="btn-primary flex items-center gap-2 mx-auto">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M12 5v.01M12 19v.01M12 12h.01M12 9a3 3 0 100-6 3 3 0 000 6zm0 0a3 3 0 100 6 3 3 0 000-6z"/>
</svg>
Job jetzt starten
</a>
</div>
</div>
{% elif show_start_link %}
<!-- Code bereits vorhanden, aber noch nicht verwendet -->
<div class="dashboard-card p-6 text-center">
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-xl flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/>
</svg>
</div>
<h4 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Zugangscode bereits generiert</h4>
<p class="text-slate-500 dark:text-slate-400 mb-6">Ihr persönlicher Code wurde bereits erstellt und ist bereit zur Verwendung.</p>
<a href="{{ url_for('guest.guest_start_job_form') }}"
class="btn-primary flex items-center gap-2 mx-auto">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/>
</svg>
Code eingeben und starten
</a>
</div>
{% endif %}
{% elif request.status == 'denied' %}
<div class="dashboard-card p-6 border-l-4 border-red-400">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Anfrage abgelehnt</h3>
<p class="text-slate-600 dark:text-slate-400">
Ihre Anfrage wurde leider abgelehnt. Bei Fragen wenden Sie sich bitte an unser Team.
</p>
</div>
</div>
</div>
{% endif %}
</div>
<script>
// Code-Kopier-Funktion
function copyCode() {
const codeElement = document.getElementById('otpCode');
if (codeElement) {
const code = codeElement.textContent;
// Versuche den Code in die Zwischenablage zu kopieren
if (navigator.clipboard) {
navigator.clipboard.writeText(code).then(function() {
showCopySuccess();
}).catch(function(err) {
console.error('Fehler beim Kopieren:', err);
fallbackCopyText(code);
});
} else {
fallbackCopyText(code);
}
}
}
// Fallback für ältere Browser
function fallbackCopyText(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showCopySuccess();
} catch (err) {
console.error('Fallback-Kopieren fehlgeschlagen:', err);
alert('Code konnte nicht kopiert werden. Bitte manuell markieren und kopieren.');
}
document.body.removeChild(textArea);
}
// Erfolgs-Animation für das Kopieren
function showCopySuccess() {
const button = event.target.closest('button');
const originalHTML = button.innerHTML;
button.innerHTML = `
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
`;
// Nach 2 Sekunden zurück zum Original
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
// Optional: Toast-Benachrichtigung
showToast('Code wurde in die Zwischenablage kopiert!');
}
// Toast-Benachrichtigung
function showToast(message) {
// Toast-Element erstellen, falls nicht vorhanden
let toast = document.getElementById('copyToast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'copyToast';
toast.className = 'fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 translate-x-full opacity-0 z-50';
document.body.appendChild(toast);
}
toast.textContent = message;
// Animation einblenden
setTimeout(() => {
toast.classList.remove('translate-x-full', 'opacity-0');
}, 100);
// Nach 3 Sekunden ausblenden
setTimeout(() => {
toast.classList.add('translate-x-full', 'opacity-0');
}, 3000);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,442 @@
{% extends "base.html" %}
{% block title %}Gastauftrag Status-Abfrage - Mercedes-Benz TBA Marienfelde{% endblock %}
{% block extra_css %}
<!-- Zusätzliche Styles für diese Seite -->
<style>
.status-card {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.95) 100%);
border: 1px solid rgba(226, 232, 240, 0.8);
border-radius: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.dark .status-card {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.95) 100%);
border-color: rgba(51, 65, 85, 0.8);
}
.otp-input {
font-family: 'Courier New', monospace;
font-size: 1.5rem;
text-align: center;
letter-spacing: 0.5rem;
text-transform: uppercase;
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 9999px;
font-weight: 500;
font-size: 0.875rem;
}
.status-pending {
background-color: rgba(254, 243, 199, 0.8);
color: #92400e;
}
.status-approved {
background-color: rgba(209, 250, 229, 0.8);
color: #065f46;
}
.status-rejected {
background-color: rgba(254, 226, 226, 0.8);
color: #991b1b;
}
.dark .status-pending {
background-color: rgba(146, 64, 14, 0.2);
color: #fbbf24;
}
.dark .status-approved {
background-color: rgba(6, 95, 70, 0.2);
color: #34d399;
}
.dark .status-rejected {
background-color: rgba(153, 27, 27, 0.2);
color: #f87171;
}
.loading-spinner {
border: 2px solid rgba(243, 244, 246, 0.3);
border-top: 2px solid #0073ce;
border-radius: 50%;
width: 1rem;
height: 1rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.mercedes-blue {
background-color: #0073ce;
}
.font-mercedes {
font-family: 'Mercedes-Benz Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
</style>
{% endblock %}
{% block content %}
<div class="min-h-screen flex items-center justify-center p-4">
<div class="max-w-md w-full space-y-8">
<!-- Header -->
<div class="text-center">
<div class="mx-auto h-12 w-12 mercedes-blue rounded-full flex items-center justify-center">
<svg class="h-6 w-6 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="mt-6 text-3xl font-bold text-slate-900 dark:text-white">
Auftragsstatus prüfen
</h2>
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
Geben Sie Ihren Statuscode ein, um Informationen über Ihren Gastauftrag zu erhalten
</p>
</div>
<!-- Status-Abfrage-Formular -->
<div class="status-card p-6" id="query-form">
<form id="status-form" class="space-y-6">
<div>
<label for="otp_code" class="block text-sm font-medium text-slate-700 dark:text-slate-300">
Statuscode (16 Zeichen)
</label>
<input type="text"
id="otp_code"
name="otp_code"
maxlength="16"
class="otp-input mt-1 appearance-none relative block w-full px-3 py-2 border border-slate-300 dark:border-slate-600 placeholder-slate-500 dark:placeholder-slate-400 text-slate-900 dark:text-white bg-white dark:bg-slate-800 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="XXXXXXXXXXXXXXXX"
required>
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
Der Code wurde Ihnen bei der Antragsstellung mitgeteilt
</p>
</div>
<div>
<label for="email" class="block text-sm font-medium text-slate-700 dark:text-slate-300">
E-Mail-Adresse (optional)
</label>
<input type="email"
id="email"
name="email"
class="mt-1 appearance-none relative block w-full px-3 py-2 border border-slate-300 dark:border-slate-600 placeholder-slate-500 dark:placeholder-slate-400 text-slate-900 dark:text-white bg-white dark:bg-slate-800 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="ihre.email@example.com">
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
Zusätzliche Sicherheit (empfohlen)
</p>
</div>
<div>
<button type="submit"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white mercedes-blue hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<span id="submit-text">Status prüfen</span>
<span id="loading-spinner" class="loading-spinner ml-2 hidden"></span>
</button>
</div>
</form>
</div>
<!-- Status-Ergebnis -->
<div id="status-result" class="hidden">
<div class="status-card p-6">
<div id="status-content">
<!-- Wird per JavaScript gefüllt -->
</div>
<div class="mt-6 flex gap-3">
<button onclick="resetForm()"
class="flex-1 py-2 px-4 border border-slate-300 dark:border-slate-600 rounded-md text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Neue Abfrage
</button>
<button onclick="refreshStatus()"
class="flex-1 py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white mercedes-blue hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Aktualisieren
</button>
</div>
</div>
</div>
<!-- Fehleranzeige -->
<div id="error-message" class="hidden">
<div class="status-card p-6 border-l-4 border-red-500">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 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="ml-3">
<h3 class="text-sm font-medium text-red-800 dark:text-red-400">Fehler</h3>
<div class="mt-2 text-sm text-red-700 dark:text-red-300" id="error-text">
<!-- Wird per JavaScript gefüllt -->
</div>
</div>
</div>
<div class="mt-4">
<button onclick="resetForm()"
class="py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Erneut versuchen
</button>
</div>
</div>
</div>
<!-- Footer -->
<div class="text-center">
<p class="text-xs text-slate-500 dark:text-slate-400">
Mercedes-Benz Technische Berufsausbildung Marienfelde<br>
<a href="/guest/request" class="text-blue-600 dark:text-blue-400 hover:underline">Neuen Antrag stellen</a>
</p>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentRequestData = null;
// Formular-Submit-Handler
document.getElementById('status-form').addEventListener('submit', async function(e) {
e.preventDefault();
const otpCode = document.getElementById('otp_code').value.trim();
const email = document.getElementById('email').value.trim();
if (!otpCode) {
showError('Bitte geben Sie Ihren Statuscode ein.');
return;
}
if (otpCode.length !== 16) {
showError('Der Statuscode muss genau 16 Zeichen lang sein.');
return;
}
setLoading(true);
hideAll();
try {
const response = await fetch('/guest/api/guest/status', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
otp_code: otpCode,
email: email || undefined
})
});
const data = await response.json();
if (data.success) {
currentRequestData = data.request;
showStatus(data.request);
} else {
showError(data.message || 'Ungültiger Code oder E-Mail-Adresse');
}
} catch (error) {
console.error('Fehler bei Status-Abfrage:', error);
showError('Verbindungsfehler. Bitte versuchen Sie es später erneut.');
} finally {
setLoading(false);
}
});
// Status anzeigen
function showStatus(request) {
const statusContent = document.getElementById('status-content');
// Status-Badge
let statusBadge = '';
let statusIcon = '';
switch (request.status) {
case 'pending':
statusBadge = '<span class="status-badge status-pending">🕒 In Bearbeitung</span>';
statusIcon = '🕒';
break;
case 'approved':
statusBadge = '<span class="status-badge status-approved">✅ Genehmigt</span>';
statusIcon = '✅';
break;
case 'rejected':
statusBadge = '<span class="status-badge status-rejected">❌ Abgelehnt</span>';
statusIcon = '❌';
break;
default:
statusBadge = '<span class="status-badge">❓ Unbekannt</span>';
statusIcon = '❓';
}
let html = `
<div class="text-center mb-6">
<div class="text-4xl mb-2">${statusIcon}</div>
<h3 class="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Antrag von ${request.name}
</h3>
${statusBadge}
</div>
<div class="space-y-4">
<div class="bg-slate-50 dark:bg-slate-800/50 p-4 rounded-lg">
<h4 class="font-medium text-slate-900 dark:text-white mb-2">Antragsdetails</h4>
<dl class="space-y-1 text-sm">
<div class="flex justify-between">
<dt class="text-slate-600 dark:text-slate-400">Erstellt am:</dt>
<dd class="text-slate-900 dark:text-white">${formatDate(request.created_at)}</dd>
</div>
<div class="flex justify-between">
<dt class="text-slate-600 dark:text-slate-400">Dauer:</dt>
<dd class="text-slate-900 dark:text-white">${request.duration_min} Minuten</dd>
</div>
${request.file_name ? `
<div class="flex justify-between">
<dt class="text-slate-600 dark:text-slate-400">Datei:</dt>
<dd class="text-slate-900 dark:text-white">${request.file_name}</dd>
</div>
` : ''}
</dl>
</div>
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<p class="text-sm text-blue-800 dark:text-blue-300">
${request.message}
</p>
</div>
${request.status === 'approved' && request.can_start_job ? `
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
<h4 class="font-medium text-green-800 dark:text-green-300 mb-2">🎯 Bereit zum Drucken!</h4>
<p class="text-sm text-green-700 dark:text-green-400 mb-3">
Ihr Auftrag wurde genehmigt. Sie können mit dem 3D-Druck beginnen.
</p>
<a href="/guest/start-job"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700">
🚀 Jetzt drucken
</a>
</div>
` : ''}
${request.status === 'rejected' && request.rejection_reason ? `
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<h4 class="font-medium text-red-800 dark:text-red-300 mb-2">Ablehnungsgrund:</h4>
<p class="text-sm text-red-700 dark:text-red-400">${request.rejection_reason}</p>
</div>
` : ''}
${request.job ? `
<div class="bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-200 dark:border-indigo-800 rounded-lg p-4">
<h4 class="font-medium text-indigo-800 dark:text-indigo-300 mb-2">📋 Job-Informationen</h4>
<dl class="space-y-1 text-sm">
<div class="flex justify-between">
<dt class="text-indigo-600 dark:text-indigo-400">Job-Name:</dt>
<dd class="text-indigo-900 dark:text-indigo-200">${request.job.name}</dd>
</div>
<div class="flex justify-between">
<dt class="text-indigo-600 dark:text-indigo-400">Status:</dt>
<dd class="text-indigo-900 dark:text-indigo-200">${request.job.status}</dd>
</div>
${request.job.printer_name ? `
<div class="flex justify-between">
<dt class="text-indigo-600 dark:text-indigo-400">Drucker:</dt>
<dd class="text-indigo-900 dark:text-indigo-200">${request.job.printer_name}</dd>
</div>
` : ''}
</dl>
</div>
` : ''}
</div>
`;
statusContent.innerHTML = html;
document.getElementById('status-result').classList.remove('hidden');
}
// Fehler anzeigen
function showError(message) {
document.getElementById('error-text').textContent = message;
document.getElementById('error-message').classList.remove('hidden');
}
// Loading-Zustand setzen
function setLoading(loading) {
const submitText = document.getElementById('submit-text');
const loadingSpinner = document.getElementById('loading-spinner');
const submitButton = document.querySelector('button[type="submit"]');
if (loading) {
submitText.textContent = 'Prüfe...';
loadingSpinner.classList.remove('hidden');
submitButton.disabled = true;
} else {
submitText.textContent = 'Status prüfen';
loadingSpinner.classList.add('hidden');
submitButton.disabled = false;
}
}
// Alle Anzeigen ausblenden
function hideAll() {
document.getElementById('status-result').classList.add('hidden');
document.getElementById('error-message').classList.add('hidden');
}
// Formular zurücksetzen
function resetForm() {
document.getElementById('status-form').reset();
hideAll();
document.getElementById('query-form').classList.remove('hidden');
currentRequestData = null;
}
// Status aktualisieren
function refreshStatus() {
if (currentRequestData) {
const otpCode = document.getElementById('otp_code').value;
const email = document.getElementById('email').value;
// Formular erneut abschicken
document.getElementById('status-form').dispatchEvent(new Event('submit'));
}
}
// Datum formatieren
function formatDate(dateString) {
if (!dateString) return 'Unbekannt';
try {
const date = new Date(dateString);
return date.toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (error) {
return dateString;
}
}
// OTP-Input Formatierung
document.getElementById('otp_code').addEventListener('input', function(e) {
// Nur alphanumerische Zeichen erlauben
e.target.value = e.target.value.replace(/[^A-Fa-f0-9]/g, '').toUpperCase();
});
</script>
{% endblock %}

203
templates/imprint.html Normal file
View File

@@ -0,0 +1,203 @@
{% extends "base.html" %}
{% block title %}{{ title }} - MYP Platform{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 max-w-4xl">
<!-- Header -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8 mb-8">
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-gradient-to-r from-blue-600 to-blue-700 rounded-lg flex items-center justify-center mr-4">
<i class="fas fa-info-circle text-white text-xl"></i>
</div>
<div>
<h1 class="text-3xl font-bold text-gray-900">Impressum</h1>
<p class="text-gray-600">Rechtliche Angaben gemäß § 5 TMG</p>
</div>
</div>
<!-- Unternehmensinformationen -->
<div class="space-y-8">
<section>
<h2 class="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<i class="fas fa-building text-blue-600 mr-3"></i>
Anbieter
</h2>
<div class="bg-gray-50 rounded-lg p-6">
<div class="grid md:grid-cols-2 gap-6">
<div>
<h3 class="font-semibold text-gray-900 mb-2">Unternehmen</h3>
<p class="text-gray-700">Mercedes-Benz AG</p>
<p class="text-gray-700">Ausbildungsabteilung</p>
<p class="text-gray-700">3D-Druck & Digitale Fertigung</p>
</div>
<div>
<h3 class="font-semibold text-gray-900 mb-2">Adresse</h3>
<p class="text-gray-700">Mercedes-Benz Platz 1</p>
<p class="text-gray-700">70546 Stuttgart</p>
<p class="text-gray-700">Deutschland</p>
</div>
</div>
</div>
</section>
<!-- Kontaktinformationen -->
<section>
<h2 class="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<i class="fas fa-envelope text-blue-600 mr-3"></i>
Kontakt
</h2>
<div class="bg-gray-50 rounded-lg p-6">
<div class="grid md:grid-cols-2 gap-6">
<div>
<h3 class="font-semibold text-gray-900 mb-2">E-Mail</h3>
<p class="text-gray-700">
<a href="mailto:till.tomczak@mercedes-benz.com" class="text-blue-600 hover:text-blue-800">
till.tomczak@mercedes-benz.com
</a>
</p>
</div>
<div>
<h3 class="font-semibold text-gray-900 mb-2">Telefon</h3>
<p class="text-gray-700">+49 (0) 711 17-0</p>
</div>
</div>
</div>
</section>
<!-- Rechtliche Angaben -->
<section>
<h2 class="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<i class="fas fa-gavel text-blue-600 mr-3"></i>
Rechtliche Angaben
</h2>
<div class="bg-gray-50 rounded-lg p-6">
<div class="grid md:grid-cols-2 gap-6">
<div>
<h3 class="font-semibold text-gray-900 mb-2">Registergericht</h3>
<p class="text-gray-700">Amtsgericht Stuttgart</p>
<p class="text-gray-700">HRB 19360</p>
</div>
<div>
<h3 class="font-semibold text-gray-900 mb-2">Umsatzsteuer-ID</h3>
<p class="text-gray-700">DE811944017</p>
</div>
</div>
</div>
</section>
<!-- Verantwortlich für den Inhalt -->
<section>
<h2 class="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<i class="fas fa-user-tie text-blue-600 mr-3"></i>
Verantwortlich für den Inhalt
</h2>
<div class="bg-gray-50 rounded-lg p-6">
<p class="text-gray-700">Till Tomczak</p>
<p class="text-gray-700">Projektleiter MYP Platform</p>
<p class="text-gray-700">Mercedes-Benz AG</p>
<p class="text-gray-700">Ausbildungsabteilung</p>
</div>
</section>
<!-- Haftungsausschluss -->
<section>
<h2 class="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<i class="fas fa-shield-alt text-blue-600 mr-3"></i>
Haftungsausschluss
</h2>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-6">
<h3 class="font-semibold text-gray-900 mb-3">Haftung für Inhalte</h3>
<p class="text-gray-700 mb-4">
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den
allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht
unter der Verpflichtung, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach
Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
</p>
<h3 class="font-semibold text-gray-900 mb-3">Haftung für Links</h3>
<p class="text-gray-700 mb-4">
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben.
Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten
Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.
</p>
<h3 class="font-semibold text-gray-900 mb-3">Urheberrecht</h3>
<p class="text-gray-700">
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen
Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der
Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.
</p>
</div>
</section>
<!-- Streitschlichtung -->
<section>
<h2 class="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<i class="fas fa-balance-scale text-blue-600 mr-3"></i>
Streitschlichtung
</h2>
<div class="bg-gray-50 rounded-lg p-6">
<p class="text-gray-700">
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
<a href="https://ec.europa.eu/consumers/odr/" target="_blank" class="text-blue-600 hover:text-blue-800 underline">
https://ec.europa.eu/consumers/odr/
</a>
</p>
<p class="text-gray-700 mt-2">
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer
Verbraucherschlichtungsstelle teilzunehmen.
</p>
</div>
</section>
<!-- System-Information -->
<section>
<h2 class="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<i class="fas fa-cogs text-blue-600 mr-3"></i>
System-Information
</h2>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
<div class="grid md:grid-cols-2 gap-6">
<div>
<h3 class="font-semibold text-gray-900 mb-2">MYP Platform</h3>
<p class="text-gray-700">Manage Your Printers</p>
<p class="text-gray-700">Version 2.0.0</p>
</div>
<div>
<h3 class="font-semibold text-gray-900 mb-2">Entwicklung</h3>
<p class="text-gray-700">Mercedes-Benz AG</p>
<p class="text-gray-700">Interne Projektarbeit</p>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- Navigation -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex flex-wrap gap-4 justify-center">
<a href="{{ url_for('index') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">
<i class="fas fa-home mr-2"></i>
Zur Startseite
</a>
<a href="{{ url_for('legal') }}" class="inline-flex items-center px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors">
<i class="fas fa-file-contract mr-2"></i>
Rechtliche Hinweise
</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors">
<i class="fas fa-chart-line mr-2"></i>
Dashboard
</a>
{% endif %}
</div>
</div>
</div>
<!-- Letzte Aktualisierung -->
<div class="text-center text-sm text-gray-500 mt-8 pb-8">
<p>Letzte Aktualisierung: {{ moment().format('DD.MM.YYYY') }}</p>
</div>
{% endblock %}

1271
templates/index.html Normal file

File diff suppressed because it is too large Load Diff

2198
templates/jobs.html Normal file

File diff suppressed because it is too large Load Diff

116
templates/jobs/new.html Normal file
View File

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

510
templates/legal.html Normal file
View File

@@ -0,0 +1,510 @@
{% extends "base.html" %}
{% block title %}{{ title }} - MYP Platform{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 max-w-6xl">
<!-- Header -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8 mb-8">
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-gradient-to-r from-purple-600 to-purple-700 rounded-lg flex items-center justify-center mr-4">
<i class="fas fa-file-contract text-white text-xl"></i>
</div>
<div>
<h1 class="text-3xl font-bold text-gray-900">Rechtliche Hinweise</h1>
<p class="text-gray-600">Datenschutz, Nutzungsbedingungen und weitere rechtliche Informationen</p>
</div>
</div>
<!-- Navigation Links -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-8">
<a href="#datenschutz" class="flex items-center p-4 bg-blue-50 rounded-lg hover:bg-blue-100 transition-colors">
<i class="fas fa-shield-alt text-blue-600 mr-3"></i>
<span class="font-medium text-blue-900">Datenschutz</span>
</a>
<a href="#nutzungsbedingungen" class="flex items-center p-4 bg-green-50 rounded-lg hover:bg-green-100 transition-colors">
<i class="fas fa-file-signature text-green-600 mr-3"></i>
<span class="font-medium text-green-900">AGB</span>
</a>
<a href="#cookies" class="flex items-center p-4 bg-amber-50 rounded-lg hover:bg-amber-100 transition-colors">
<i class="fas fa-cookie-bite text-amber-600 mr-3"></i>
<span class="font-medium text-amber-900">Cookies</span>
</a>
<a href="#sicherheit" class="flex items-center p-4 bg-red-50 rounded-lg hover:bg-red-100 transition-colors">
<i class="fas fa-lock text-red-600 mr-3"></i>
<span class="font-medium text-red-900">Sicherheit</span>
</a>
</div>
</div>
<!-- Datenschutzerklärung -->
<section id="datenschutz" class="bg-white rounded-lg shadow-sm border border-gray-200 p-8 mb-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<i class="fas fa-shield-alt text-blue-600 mr-3"></i>
Datenschutzerklärung
</h2>
<div class="space-y-6">
<!-- Grundsätzliches -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">1. Grundsätzliches zum Datenschutz</h3>
<div class="bg-blue-50 rounded-lg p-6">
<p class="text-gray-700 mb-4">
Der Schutz Ihrer persönlichen Daten ist uns wichtig. Diese Datenschutzerklärung informiert Sie über
die Art, den Umfang und Zweck der Verarbeitung personenbezogener Daten innerhalb des MYP-Systems
(Manage Your Printers) der Mercedes-Benz AG.
</p>
<p class="text-gray-700">
Verantwortlicher im Sinne der Datenschutz-Grundverordnung (DSGVO) ist die Mercedes-Benz AG,
vertreten durch die Ausbildungsabteilung.
</p>
</div>
</div>
<!-- Datenerhebung -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">2. Erhebung und Verarbeitung personenbezogener Daten</h3>
<div class="grid md:grid-cols-2 gap-6">
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="font-semibold text-gray-900 mb-3">Registrierungsdaten</h4>
<ul class="text-gray-700 space-y-2">
<li>• Benutzername</li>
<li>• E-Mail-Adresse (Mercedes-Benz)</li>
<li>• Name und Abteilung</li>
<li>• Rolle im System</li>
</ul>
</div>
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="font-semibold text-gray-900 mb-3">Nutzungsdaten</h4>
<ul class="text-gray-700 space-y-2">
<li>• Druckaufträge und -verlauf</li>
<li>• Login-Zeiten und -Häufigkeit</li>
<li>• IP-Adresse und Browser-Info</li>
<li>• Systemaktivitäten</li>
</ul>
</div>
</div>
</div>
<!-- Zweck der Datenverarbeitung -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">3. Zweck der Datenverarbeitung</h3>
<div class="bg-green-50 rounded-lg p-6">
<div class="grid md:grid-cols-2 gap-6">
<div>
<h4 class="font-semibold text-gray-900 mb-3">Primäre Zwecke</h4>
<ul class="text-gray-700 space-y-2">
<li>• Bereitstellung der 3D-Druck-Services</li>
<li>• Verwaltung von Druckaufträgen</li>
<li>• Benutzerauthentifizierung</li>
<li>• Ressourcenplanung</li>
</ul>
</div>
<div>
<h4 class="font-semibold text-gray-900 mb-3">Sekundäre Zwecke</h4>
<ul class="text-gray-700 space-y-2">
<li>• Systemoptimierung</li>
<li>• Qualitätssicherung</li>
<li>• Ausbildungszwecke</li>
<li>• Sicherheitsüberwachung</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Rechtsgrundlage -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">4. Rechtsgrundlage</h3>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-6">
<p class="text-gray-700 mb-4">
Die Verarbeitung erfolgt auf Grundlage von:
</p>
<ul class="text-gray-700 space-y-2">
<li><strong>Art. 6 Abs. 1 lit. b DSGVO:</strong> Vertragserfüllung (Nutzung der Druckdienste)</li>
<li><strong>Art. 6 Abs. 1 lit. f DSGVO:</strong> Berechtigte Interessen (Systemsicherheit, Optimierung)</li>
<li><strong>Art. 6 Abs. 1 lit. c DSGVO:</strong> Rechtliche Verpflichtung (Dokumentation, Compliance)</li>
</ul>
</div>
</div>
<!-- Ihre Rechte -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">5. Ihre Rechte</h3>
<div class="grid md:grid-cols-3 gap-4">
<div class="bg-blue-50 rounded-lg p-4">
<h4 class="font-semibold text-blue-900 mb-2">Auskunftsrecht</h4>
<p class="text-blue-800 text-sm">Art. 15 DSGVO</p>
</div>
<div class="bg-green-50 rounded-lg p-4">
<h4 class="font-semibold text-green-900 mb-2">Berichtigungsrecht</h4>
<p class="text-green-800 text-sm">Art. 16 DSGVO</p>
</div>
<div class="bg-red-50 rounded-lg p-4">
<h4 class="font-semibold text-red-900 mb-2">Löschungsrecht</h4>
<p class="text-red-800 text-sm">Art. 17 DSGVO</p>
</div>
<div class="bg-purple-50 rounded-lg p-4">
<h4 class="font-semibold text-purple-900 mb-2">Einschränkungsrecht</h4>
<p class="text-purple-800 text-sm">Art. 18 DSGVO</p>
</div>
<div class="bg-amber-50 rounded-lg p-4">
<h4 class="font-semibold text-amber-900 mb-2">Datenübertragbarkeit</h4>
<p class="text-amber-800 text-sm">Art. 20 DSGVO</p>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<h4 class="font-semibold text-gray-900 mb-2">Widerspruchsrecht</h4>
<p class="text-gray-700 text-sm">Art. 21 DSGVO</p>
</div>
</div>
</div>
</div>
</section>
<!-- Nutzungsbedingungen -->
<section id="nutzungsbedingungen" class="bg-white rounded-lg shadow-sm border border-gray-200 p-8 mb-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<i class="fas fa-file-signature text-green-600 mr-3"></i>
Allgemeine Nutzungsbedingungen
</h2>
<div class="space-y-6">
<!-- Geltungsbereich -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">1. Geltungsbereich</h3>
<div class="bg-gray-50 rounded-lg p-6">
<p class="text-gray-700">
Diese Nutzungsbedingungen gelten für die Nutzung des MYP-Systems (Manage Your Printers)
durch Mitarbeiter und Auszubildende der Mercedes-Benz AG. Mit der Registrierung und
Nutzung des Systems erkennen Sie diese Bedingungen an.
</p>
</div>
</div>
<!-- Nutzungsrechte -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">2. Nutzungsrechte und -pflichten</h3>
<div class="grid md:grid-cols-2 gap-6">
<div class="bg-green-50 rounded-lg p-6">
<h4 class="font-semibold text-green-900 mb-3">Erlaubte Nutzung</h4>
<ul class="text-green-800 space-y-2">
<li>• Druckaufträge für Ausbildungszwecke</li>
<li>• Prototyping und Projektarbeit</li>
<li>• Lernmaterialien und Demonstrationen</li>
<li>• Interne Mercedes-Benz Projekte</li>
</ul>
</div>
<div class="bg-red-50 rounded-lg p-6">
<h4 class="font-semibold text-red-900 mb-3">Verbotene Nutzung</h4>
<ul class="text-red-800 space-y-2">
<li>• Kommerzielle Zwecke ohne Genehmigung</li>
<li>• Urheberrechtsverletzungen</li>
<li>• Gefährliche oder illegale Objekte</li>
<li>• Systemmanipulation oder -missbrauch</li>
</ul>
</div>
</div>
</div>
<!-- Verantwortlichkeiten -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">3. Verantwortlichkeiten</h3>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-6">
<div class="grid md:grid-cols-2 gap-6">
<div>
<h4 class="font-semibold text-gray-900 mb-3">Nutzer-Verantwortung</h4>
<ul class="text-gray-700 space-y-2">
<li>• Sichere Aufbewahrung der Zugangsdaten</li>
<li>• Einhaltung der Sicherheitsrichtlinien</li>
<li>• Ordnungsgemäße Nutzung der Geräte</li>
<li>• Meldung von Problemen</li>
</ul>
</div>
<div>
<h4 class="font-semibold text-gray-900 mb-3">System-Verantwortung</h4>
<ul class="text-gray-700 space-y-2">
<li>• Bereitstellung der Infrastruktur</li>
<li>• Wartung und Support</li>
<li>• Datenschutz und Sicherheit</li>
<li>• Kontinuierliche Verbesserung</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Cookie-Policy -->
<section id="cookies" class="bg-white rounded-lg shadow-sm border border-gray-200 p-8 mb-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<i class="fas fa-cookie-bite text-amber-600 mr-3"></i>
Cookie-Richtlinie
</h2>
<div class="space-y-6">
<!-- Was sind Cookies -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">Was sind Cookies?</h3>
<div class="bg-amber-50 rounded-lg p-6">
<p class="text-gray-700">
Cookies sind kleine Textdateien, die beim Besuch einer Website auf Ihrem Computer gespeichert werden.
Sie helfen dabei, Ihre Präferenzen zu speichern und die Funktionalität der Website zu verbessern.
</p>
</div>
</div>
<!-- Cookie-Kategorien -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">Verwendete Cookie-Kategorien</h3>
<div class="grid md:grid-cols-2 gap-6">
<div class="bg-blue-50 rounded-lg p-6">
<h4 class="font-semibold text-blue-900 mb-3">Technisch notwendige Cookies</h4>
<ul class="text-blue-800 space-y-2">
<li>• Session-Management</li>
<li>• Anmeldestatus</li>
<li>• CSRF-Schutz</li>
<li>• Spracheinstellungen</li>
</ul>
<p class="text-blue-700 text-sm mt-3">Diese Cookies sind für die Funktionalität der Website erforderlich.</p>
</div>
<div class="bg-green-50 rounded-lg p-6">
<h4 class="font-semibold text-green-900 mb-3">Funktionale Cookies</h4>
<ul class="text-green-800 space-y-2">
<li>• Benutzereinstellungen</li>
<li>• Dashboard-Konfiguration</li>
<li>• Theme-Präferenzen</li>
<li>• Accessibility-Optionen</li>
</ul>
<p class="text-green-700 text-sm mt-3">Diese Cookies verbessern die Benutzererfahrung.</p>
</div>
</div>
</div>
<!-- Cookie-Kontrolle -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">Cookie-Kontrolle</h3>
<div class="bg-gray-50 rounded-lg p-6">
<p class="text-gray-700 mb-4">
Sie können Cookies in Ihren Browser-Einstellungen verwalten. Beachten Sie jedoch, dass das
Deaktivieren bestimmter Cookies die Funktionalität der Website beeinträchtigen kann.
</p>
<div class="grid md:grid-cols-3 gap-4">
<div class="bg-white rounded-lg p-4 border">
<h5 class="font-semibold text-gray-900 mb-2">Chrome</h5>
<p class="text-gray-600 text-sm">Einstellungen → Datenschutz und Sicherheit → Cookies</p>
</div>
<div class="bg-white rounded-lg p-4 border">
<h5 class="font-semibold text-gray-900 mb-2">Firefox</h5>
<p class="text-gray-600 text-sm">Einstellungen → Datenschutz & Sicherheit</p>
</div>
<div class="bg-white rounded-lg p-4 border">
<h5 class="font-semibold text-gray-900 mb-2">Edge</h5>
<p class="text-gray-600 text-sm">Einstellungen → Cookies und Websiteberechtigungen</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Sicherheitsrichtlinien -->
<section id="sicherheit" class="bg-white rounded-lg shadow-sm border border-gray-200 p-8 mb-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<i class="fas fa-lock text-red-600 mr-3"></i>
Sicherheitsrichtlinien
</h2>
<div class="space-y-6">
<!-- Technische Sicherheit -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">Technische Sicherheitsmaßnahmen</h3>
<div class="grid md:grid-cols-2 gap-6">
<div class="bg-red-50 rounded-lg p-6">
<h4 class="font-semibold text-red-900 mb-3">Infrastruktursicherheit</h4>
<ul class="text-red-800 space-y-2">
<li>• HTTPS-Verschlüsselung</li>
<li>• Sichere Datenübertragung</li>
<li>• Regelmäßige Security-Updates</li>
<li>• Firewalls und Intrusion Detection</li>
</ul>
</div>
<div class="bg-blue-50 rounded-lg p-6">
<h4 class="font-semibold text-blue-900 mb-3">Anwendungssicherheit</h4>
<ul class="text-blue-800 space-y-2">
<li>• Sichere Authentifizierung</li>
<li>• Rollenbasierte Zugriffskontrolle</li>
<li>• Input-Validierung</li>
<li>• Session-Management</li>
</ul>
</div>
</div>
</div>
<!-- Benutzer-Sicherheit -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">Empfehlungen für Benutzer</h3>
<div class="bg-green-50 rounded-lg p-6">
<div class="grid md:grid-cols-2 gap-6">
<div>
<h4 class="font-semibold text-green-900 mb-3">Passwort-Sicherheit</h4>
<ul class="text-green-800 space-y-2">
<li>• Verwenden Sie starke Passwörter</li>
<li>• Teilen Sie keine Zugangsdaten</li>
<li>• Melden Sie sich nach der Nutzung ab</li>
<li>• Verwenden Sie nicht öffentliche Computer</li>
</ul>
</div>
<div>
<h4 class="font-semibold text-green-900 mb-3">Allgemeine Sicherheit</h4>
<ul class="text-green-800 space-y-2">
<li>• Halten Sie Ihren Browser aktuell</li>
<li>• Verwenden Sie Antivirus-Software</li>
<li>• Seien Sie vorsichtig bei Downloads</li>
<li>• Melden Sie verdächtige Aktivitäten</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Incident Response -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-3">Sicherheitsvorfälle melden</h3>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-6">
<p class="text-gray-700 mb-4">
Falls Sie einen Sicherheitsvorfall bemerken oder vermuten, wenden Sie sich umgehend an:
</p>
<div class="grid md:grid-cols-2 gap-6">
<div>
<h4 class="font-semibold text-gray-900 mb-2">Technischer Support</h4>
<p class="text-gray-700">
E-Mail: <a href="mailto:till.tomczak@mercedes-benz.com" class="text-blue-600 hover:text-blue-800">
till.tomczak@mercedes-benz.com
</a>
</p>
</div>
<div>
<h4 class="font-semibold text-gray-900 mb-2">IT-Sicherheit</h4>
<p class="text-gray-700">
Interne IT-Security-Hotline
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Kontakt und Weitere Informationen -->
<section class="bg-white rounded-lg shadow-sm border border-gray-200 p-8 mb-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<i class="fas fa-info-circle text-blue-600 mr-3"></i>
Weitere Informationen
</h2>
<div class="grid md:grid-cols-2 gap-8">
<!-- Kontakt -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-4">Bei Fragen wenden Sie sich an:</h3>
<div class="space-y-4">
<div class="flex items-start">
<i class="fas fa-envelope text-blue-600 mt-1 mr-3"></i>
<div>
<p class="font-medium text-gray-900">E-Mail</p>
<a href="mailto:till.tomczak@mercedes-benz.com" class="text-blue-600 hover:text-blue-800">
till.tomczak@mercedes-benz.com
</a>
</div>
</div>
<div class="flex items-start">
<i class="fas fa-building text-blue-600 mt-1 mr-3"></i>
<div>
<p class="font-medium text-gray-900">Abteilung</p>
<p class="text-gray-700">Ausbildungsabteilung - 3D-Druck</p>
</div>
</div>
</div>
</div>
<!-- Updates -->
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-4">Aktualisierungen</h3>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-gray-700 mb-2">
Diese rechtlichen Hinweise können bei Bedarf aktualisiert werden.
Über wesentliche Änderungen werden Sie informiert.
</p>
<p class="text-sm text-gray-600">
Stand: {{ moment().format('DD.MM.YYYY') }}
</p>
</div>
</div>
</div>
</section>
<!-- Navigation -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex flex-wrap gap-4 justify-center">
<a href="{{ url_for('index') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">
<i class="fas fa-home mr-2"></i>
Zur Startseite
</a>
<a href="{{ url_for('imprint') }}" class="inline-flex items-center px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors">
<i class="fas fa-info-circle mr-2"></i>
Impressum
</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors">
<i class="fas fa-chart-line mr-2"></i>
Dashboard
</a>
<a href="{{ url_for('user_settings') }}" class="inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors">
<i class="fas fa-cog mr-2"></i>
Einstellungen
</a>
{% endif %}
</div>
</div>
</div>
<!-- Scroll-to-Top Button -->
<button id="scrollToTop" class="fixed bottom-6 right-6 bg-blue-600 text-white p-3 rounded-full shadow-lg hover:bg-blue-700 transition-all duration-300 opacity-0 pointer-events-none">
<i class="fas fa-chevron-up"></i>
</button>
<script>
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Scroll-to-top functionality
const scrollToTopBtn = document.getElementById('scrollToTop');
window.addEventListener('scroll', () => {
if (window.pageYOffset > 300) {
scrollToTopBtn.classList.remove('opacity-0', 'pointer-events-none');
scrollToTopBtn.classList.add('opacity-100');
} else {
scrollToTopBtn.classList.add('opacity-0', 'pointer-events-none');
scrollToTopBtn.classList.remove('opacity-100');
}
});
scrollToTopBtn.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
</script>
{% endblock %}

1058
templates/login.html Normal file

File diff suppressed because it is too large Load Diff

850
templates/new_job.html Normal file
View File

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

2399
templates/printers.html Normal file

File diff suppressed because it is too large Load Diff

546
templates/privacy.html Normal file
View File

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

803
templates/profile.html Normal file
View File

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

961
templates/settings.html Normal file
View File

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

511
templates/socket_test.html Normal file
View File

@@ -0,0 +1,511 @@
{% extends "base.html" %}
{% block title %}Steckdosen-Test - Mercedes-Benz TBA Marienfelde{% endblock %}
{% block extra_css %}
<style>
.test-card {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border: 1px solid #e2e8f0;
border-radius: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.dark .test-card {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-color: #334155;
}
.risk-low { border-left: 4px solid #10b981; }
.risk-medium { border-left: 4px solid #f59e0b; }
.risk-high { border-left: 4px solid #ef4444; }
.socket-online { color: #10b981; }
.socket-offline { color: #ef4444; }
.socket-error { color: #f59e0b; }
.test-button {
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.test-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn-test-on { background: #16a34a; color: white; }
.btn-test-off { background: #ef4444; color: white; }
.btn-test-status { background: #0073ce; color: white; }
.warning-banner {
background: linear-gradient(90deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.05) 100%);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
.info-banner {
background: linear-gradient(90deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.05) 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
.loading-spinner {
border: 2px solid #f3f4f6;
border-top: 2px solid #0073ce;
border-radius: 50%;
width: 1rem;
height: 1rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Header -->
<div class="dashboard-card p-6">
<div class="flex items-center gap-6">
<div class="w-16 h-16 bg-red-600 text-white rounded-xl flex items-center justify-center">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<div>
<h1 class="text-4xl font-bold text-mercedes-black dark:text-white">⚡ Steckdosen-Test</h1>
<p class="text-mercedes-gray dark:text-slate-400 mt-1">Sichere Testfunktion für Ausbilder und Administratoren</p>
</div>
</div>
</div>
<!-- Sicherheitshinweis -->
<div class="warning-banner">
<div class="flex items-start gap-3">
<svg class="w-6 h-6 text-red-600 flex-shrink-0 mt-0.5" 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.964-.833-2.732 0L3.268 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<div>
<h3 class="font-semibold text-red-800 dark:text-red-200">⚠️ SICHERHEITSHINWEIS</h3>
<p class="text-red-700 dark:text-red-300 mt-1">
Diese Funktion ist nur für geschulte Ausbilder und Administratoren bestimmt.
Prüfen Sie immer den Status vor dem Ein-/Ausschalten von Steckdosen.
</p>
</div>
</div>
</div>
<!-- Übersicht aller Steckdosen -->
<div class="dashboard-card p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-mercedes-black dark:text-white">Übersicht aller Steckdosen</h2>
<button onclick="loadAllSocketsStatus()" class="test-button btn-test-status">
<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>
Alle Status aktualisieren
</button>
</div>
<!-- Statistiken -->
<div id="socket-summary" class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
<!-- Wird per JavaScript gefüllt -->
</div>
<!-- Steckdosen-Liste -->
<div id="all-sockets-list" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="flex items-center justify-center p-8">
<div class="loading-spinner"></div>
<span class="ml-2">Lade Steckdosen-Status...</span>
</div>
</div>
</div>
<!-- Einzeltest -->
<div class="dashboard-card p-6">
<h2 class="text-2xl font-bold text-mercedes-black dark:text-white mb-6">Einzelne Steckdose testen</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Drucker-Auswahl -->
<div class="space-y-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Drucker auswählen:
</label>
<select id="printer-select" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
<option value="">Bitte Drucker auswählen...</option>
</select>
<button onclick="loadSingleSocketStatus()" id="load-status-btn"
class="test-button btn-test-status w-full" disabled>
<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="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"/>
</svg>
Status prüfen
</button>
</div>
<!-- Status-Anzeige -->
<div id="single-socket-status" class="space-y-4">
<div class="info-banner">
<p class="text-blue-700 dark:text-blue-300">
Wählen Sie einen Drucker aus um den Steckdosen-Status zu prüfen.
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Test-Bestätigungsmodal -->
<div id="test-confirmation-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg max-w-md w-full p-6">
<div class="flex items-center gap-3 mb-4">
<svg class="w-8 h-8 text-red-600" 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.964-.833-2.732 0L3.268 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<h3 class="text-lg font-semibold">Test bestätigen</h3>
</div>
<div id="test-modal-content">
<!-- Wird per JavaScript gefüllt -->
</div>
<div class="flex gap-3 mt-6">
<button onclick="closeTestModal()" class="test-button bg-gray-500 text-white flex-1">
Abbrechen
</button>
<button onclick="executeTest()" id="confirm-test-btn" class="test-button bg-red-600 text-white flex-1">
Test durchführen
</button>
</div>
</div>
</div>
<script>
let currentTestData = null;
let printers = [];
// Seite initialisieren
document.addEventListener('DOMContentLoaded', function() {
loadPrinters();
loadAllSocketsStatus();
});
// Drucker laden
async function loadPrinters() {
try {
const response = await fetch('/api/printers');
const data = await response.json();
if (data.success) {
printers = data.printers;
const select = document.getElementById('printer-select');
select.innerHTML = '<option value="">Bitte Drucker auswählen...</option>';
printers.forEach(printer => {
if (printer.plug_ip) { // Nur Drucker mit Steckdose
const option = document.createElement('option');
option.value = printer.id;
option.textContent = `${printer.name} (${printer.location || 'Unbekannter Standort'})`;
select.appendChild(option);
}
});
select.addEventListener('change', function() {
const loadBtn = document.getElementById('load-status-btn');
loadBtn.disabled = !this.value;
});
}
} catch (error) {
console.error('Fehler beim Laden der Drucker:', error);
showNotification('Fehler beim Laden der Drucker', 'error');
}
}
// Alle Steckdosen-Status laden
async function loadAllSocketsStatus() {
try {
const response = await fetch('/api/printers/test/all-sockets');
const data = await response.json();
if (data.success) {
displaySocketsSummary(data.summary);
displayAllSockets(data.sockets);
} else {
throw new Error(data.error || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Fehler beim Laden der Steckdosen:', error);
showNotification('Fehler beim Laden der Steckdosen: ' + error.message, 'error');
}
}
// Einzelnen Steckdosen-Status laden
async function loadSingleSocketStatus() {
const printerId = document.getElementById('printer-select').value;
if (!printerId) return;
const statusDiv = document.getElementById('single-socket-status');
statusDiv.innerHTML = '<div class="flex items-center"><div class="loading-spinner"></div><span class="ml-2">Status wird geladen...</span></div>';
try {
const response = await fetch(`/api/printers/test/socket/${printerId}`);
const data = await response.json();
if (data.success) {
displaySingleSocketStatus(data);
} else {
statusDiv.innerHTML = `<div class="warning-banner"><p class="text-red-700">${data.error}</p></div>`;
}
} catch (error) {
console.error('Fehler beim Laden des Socket-Status:', error);
statusDiv.innerHTML = `<div class="warning-banner"><p class="text-red-700">Fehler: ${error.message}</p></div>`;
}
}
// Zusammenfassung anzeigen
function displaySocketsSummary(summary) {
const summaryDiv = document.getElementById('socket-summary');
summaryDiv.innerHTML = `
<div class="bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400">${summary.total_sockets}</div>
<div class="text-sm text-blue-800 dark:text-blue-300">Gesamt</div>
</div>
<div class="bg-green-50 dark:bg-green-900 p-4 rounded-lg">
<div class="text-2xl font-bold text-green-600 dark:text-green-400">${summary.online}</div>
<div class="text-sm text-green-800 dark:text-green-300">Online</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<div class="text-2xl font-bold text-gray-600 dark:text-gray-400">${summary.offline}</div>
<div class="text-sm text-gray-800 dark:text-gray-300">Offline</div>
</div>
<div class="bg-red-50 dark:bg-red-900 p-4 rounded-lg">
<div class="text-2xl font-bold text-red-600 dark:text-red-400">${summary.error}</div>
<div class="text-sm text-red-800 dark:text-red-300">Fehler</div>
</div>
<div class="bg-orange-50 dark:bg-orange-900 p-4 rounded-lg">
<div class="text-2xl font-bold text-orange-600 dark:text-orange-400">${summary.with_warnings}</div>
<div class="text-sm text-orange-800 dark:text-orange-300">Mit Warnungen</div>
</div>
`;
}
// Alle Steckdosen anzeigen
function displayAllSockets(sockets) {
const listDiv = document.getElementById('all-sockets-list');
if (sockets.length === 0) {
listDiv.innerHTML = '<div class="col-span-full text-center p-8"><p class="text-gray-500">Keine konfigurierten Steckdosen gefunden.</p></div>';
return;
}
listDiv.innerHTML = sockets.map(socket => `
<div class="test-card p-4 ${getRiskClass(socket.warnings.length)}">
<div class="flex items-start justify-between mb-4">
<div>
<h3 class="font-semibold text-lg">${socket.printer.name}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">${socket.printer.location || 'Unbekannter Standort'}</p>
</div>
<div class="text-right">
<div class="socket-${socket.socket.status} font-semibold">
${getStatusText(socket.socket.status, socket.socket.device_on)}
</div>
${socket.socket.current_power ? `<div class="text-sm text-gray-600">${socket.socket.current_power}W</div>` : ''}
</div>
</div>
${socket.warnings.length > 0 ? `
<div class="bg-yellow-50 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded p-3 mb-4">
<div class="font-medium text-yellow-800 dark:text-yellow-200 mb-1">⚠️ Warnungen:</div>
${socket.warnings.map(warning => `<div class="text-sm text-yellow-700 dark:text-yellow-300">• ${warning}</div>`).join('')}
</div>
` : ''}
<div class="flex gap-2">
<button onclick="testSocketControl(${socket.printer.id}, 'on')"
class="test-button btn-test-on flex-1 ${socket.socket.device_on ? 'opacity-50' : ''}">
⚡ Einschalten
</button>
<button onclick="testSocketControl(${socket.printer.id}, 'off')"
class="test-button btn-test-off flex-1 ${!socket.socket.device_on ? 'opacity-50' : ''}">
🔌 Ausschalten
</button>
</div>
</div>
`).join('');
}
// Einzelstatus anzeigen
function displaySingleSocketStatus(data) {
const statusDiv = document.getElementById('single-socket-status');
const riskClass = getRiskClass(data.safety.warnings.length);
statusDiv.innerHTML = `
<div class="test-card p-4 ${riskClass}">
<div class="mb-4">
<h3 class="font-semibold text-lg">${data.printer.name}</h3>
<p class="text-sm text-gray-600">${data.printer.location}</p>
<div class="mt-2">
<span class="socket-${data.socket.status} font-semibold">
${getStatusText(data.socket.status, data.socket.info?.device_on)}
</span>
${data.socket.info?.current_power ? `${data.socket.info.current_power}W` : ''}
</div>
</div>
${data.safety.warnings.length > 0 ? `
<div class="warning-banner mb-4">
<div class="font-medium mb-2">⚠️ Sicherheitswarnungen:</div>
${data.safety.warnings.map(warning => `<div>• ${warning}</div>`).join('')}
</div>
` : ''}
${data.safety.recommendations.length > 0 ? `
<div class="info-banner mb-4">
<div class="font-medium mb-2">💡 Empfehlungen:</div>
${data.safety.recommendations.map(rec => `<div>• ${rec}</div>`).join('')}
</div>
` : ''}
<div class="flex gap-2">
<button onclick="testSocketControl(${data.printer.id}, 'on')"
class="test-button btn-test-on flex-1">
⚡ Test: Einschalten
</button>
<button onclick="testSocketControl(${data.printer.id}, 'off')"
class="test-button btn-test-off flex-1">
🔌 Test: Ausschalten
</button>
</div>
</div>
`;
}
// Test-Modal öffnen
function testSocketControl(printerId, action) {
const printer = printers.find(p => p.id == printerId);
if (!printer) return;
currentTestData = { printerId, action, printer };
const modal = document.getElementById('test-confirmation-modal');
const content = document.getElementById('test-modal-content');
content.innerHTML = `
<p class="mb-4">
<strong>Drucker:</strong> ${printer.name}<br>
<strong>Aktion:</strong> Steckdose ${action === 'on' ? 'einschalten' : 'ausschalten'}
</p>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Grund für den Test:</label>
<input type="text" id="test-reason" class="w-full p-2 border rounded"
placeholder="z.B. Routinetest, Wartung, etc." value="Routinetest">
</div>
<div class="mb-4">
<label class="flex items-center">
<input type="checkbox" id="force-test" class="mr-2">
<span class="text-sm">Sicherheitswarnungen überschreiben (force)</span>
</label>
</div>
`;
modal.classList.remove('hidden');
}
// Test ausführen
async function executeTest() {
if (!currentTestData) return;
const reason = document.getElementById('test-reason').value || 'Routinetest';
const force = document.getElementById('force-test').checked;
const confirmBtn = document.getElementById('confirm-test-btn');
confirmBtn.innerHTML = '<div class="loading-spinner"></div> Teste...';
confirmBtn.disabled = true;
try {
const response = await fetch(`/api/printers/test/socket/${currentTestData.printerId}/control`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name=csrf-token]').getAttribute('content')
},
body: JSON.stringify({
action: currentTestData.action,
test_reason: reason,
force: force
})
});
const data = await response.json();
if (data.success) {
showNotification(`Test erfolgreich: ${data.message}`, 'success');
loadAllSocketsStatus();
loadSingleSocketStatus();
} else {
if (data.requires_force) {
showNotification('Test blockiert: ' + data.error + ' Aktivieren Sie "force" um fortzufahren.', 'warning');
} else {
showNotification('Test fehlgeschlagen: ' + data.error, 'error');
}
}
} catch (error) {
console.error('Fehler beim Test:', error);
showNotification('Fehler beim Test: ' + error.message, 'error');
} finally {
closeTestModal();
}
}
// Modal schließen
function closeTestModal() {
const modal = document.getElementById('test-confirmation-modal');
modal.classList.add('hidden');
currentTestData = null;
const confirmBtn = document.getElementById('confirm-test-btn');
confirmBtn.innerHTML = 'Test durchführen';
confirmBtn.disabled = false;
}
// Hilfsfunktionen
function getRiskClass(warningCount) {
if (warningCount === 0) return 'risk-low';
if (warningCount <= 2) return 'risk-medium';
return 'risk-high';
}
function getStatusText(status, deviceOn) {
switch (status) {
case 'online': return deviceOn ? '🟢 Eingeschaltet' : '🔴 Ausgeschaltet';
case 'offline': return '🔴 Ausgeschaltet';
case 'error': return '⚠️ Fehler';
default: return '❓ Unbekannt';
}
}
function showNotification(message, type) {
// Einfache Benachrichtigung - kann durch Toast-System ersetzt werden
alert(message);
}
</script>
{% endblock %}

282
templates/stats.html Normal file
View File

@@ -0,0 +1,282 @@
{% 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 - Lokale Version -->
<script src="{{ url_for('static', filename='js/charts/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 mit defensiven Checks
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);
console.log('✅ Basis-Statistiken erfolgreich geladen:', data);
} catch (error) {
console.error('Fehler beim Laden der Basis-Statistiken:', error);
if (typeof showFlashMessage === 'function') {
showFlashMessage('Fehler beim Laden der Statistiken', 'error');
}
}
}
function updateStatsCounter(elementId, value, animate = true) {
const element = document.getElementById(elementId);
if (!element) {
console.warn(`Element mit ID '${elementId}' nicht gefunden - wird übersprungen`);
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
if (typeof showFlashMessage === 'function') {
showFlashMessage('Statistiken werden aktualisiert...', 'info');
}
// Basis-Statistiken laden
loadBasicStats();
// Charts aktualisieren
if (window.refreshAllCharts) {
window.refreshAllCharts();
}
// Erfolgsmeldung
setTimeout(() => {
if (typeof showFlashMessage === 'function') {
showFlashMessage('Statistiken erfolgreich aktualisiert', 'success');
}
}, 1000);
}
// Statistiken exportieren
function exportStats() {
// Direkter Download vom API-Endpunkt
window.location.href = '/api/stats/export';
}
</script>
{% endblock %}

356
templates/terms.html Normal file
View File

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