FIN INIT
This commit is contained in:
47
templates/404.html
Normal file
47
templates/404.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}404 - Seite nicht gefunden - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-[80vh] flex flex-col items-center justify-center p-4">
|
||||
<!-- 404 Error Container -->
|
||||
<div class="w-full max-w-md">
|
||||
<div class="bg-white dark:bg-gray-800 backdrop-blur-xl bg-opacity-95 dark:bg-opacity-95 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 p-8 text-center transition-all duration-300">
|
||||
<!-- Mercedes-Benz Logo -->
|
||||
<div class="flex justify-center mb-6">
|
||||
<div class="w-16 h-16 text-gray-300 dark:text-gray-600 transition-transform duration-500 hover:scale-110">
|
||||
<svg class="w-full h-full" fill="currentColor" viewBox="0 0 80 80">
|
||||
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
|
||||
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
|
||||
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
|
||||
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
|
||||
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
|
||||
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<h1 class="text-6xl font-bold text-gray-300 dark:text-gray-600 mb-4">404</h1>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Seite nicht gefunden</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">Die von Ihnen gesuchte Seite existiert nicht oder wurde verschoben.</p>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row justify-center gap-4 mt-8">
|
||||
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center justify-center px-5 py-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium rounded-lg transition-all duration-300 transform hover:-translate-y-0.5">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||
</svg>
|
||||
<span>Zum Dashboard</span>
|
||||
</a>
|
||||
<button onclick="window.history.back()" class="inline-flex items-center justify-center px-5 py-3 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-all duration-300 transform hover:-translate-y-0.5">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
<span>Zurück</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
66
templates/500.html
Normal file
66
templates/500.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Interner Serverfehler - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-red-50 to-orange-50 dark:from-slate-900 dark:via-red-900/20 dark:to-orange-900/20 flex items-center justify-center px-4">
|
||||
<div class="max-w-2xl w-full text-center">
|
||||
<!-- Error Icon -->
|
||||
<div class="mb-8">
|
||||
<div class="inline-flex items-center justify-center w-24 h-24 bg-red-100 dark:bg-red-900/30 rounded-full mb-6">
|
||||
<svg class="w-12 h-12 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<h1 class="text-6xl font-bold text-slate-900 dark:text-white mb-4">500</h1>
|
||||
<h2 class="text-2xl font-semibold text-slate-700 dark:text-slate-300 mb-6">Interner Serverfehler</h2>
|
||||
<p class="text-lg text-slate-600 dark:text-slate-400 mb-8 max-w-lg mx-auto">
|
||||
Es ist ein unerwarteter Fehler aufgetreten. Unser Team wurde automatisch benachrichtigt und arbeitet an einer Lösung.
|
||||
</p>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-xl transition-colors duration-200 shadow-lg hover:shadow-xl">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||
</svg>
|
||||
Zurück zum Dashboard
|
||||
</a>
|
||||
<button onclick="window.location.reload()" class="inline-flex items-center px-6 py-3 bg-slate-200 hover:bg-slate-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 font-medium rounded-xl transition-colors duration-200">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Seite neu laden
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Additional Info -->
|
||||
<div class="mt-12 p-6 bg-white/60 dark:bg-slate-800/60 backdrop-blur-sm rounded-2xl border border-slate-200 dark:border-slate-700">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-3">Was können Sie tun?</h3>
|
||||
<ul class="text-left text-slate-600 dark:text-slate-400 space-y-2">
|
||||
<li class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Versuchen Sie, die Seite neu zu laden
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Kehren Sie zum Dashboard zurück
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Kontaktieren Sie den Administrator, falls das Problem weiterhin besteht
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
1540
templates/admin.html
Normal file
1540
templates/admin.html
Normal file
File diff suppressed because it is too large
Load Diff
208
templates/admin_add_printer.html
Normal file
208
templates/admin_add_printer.html
Normal 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 %}
|
||||
544
templates/admin_add_user.html
Normal file
544
templates/admin_add_user.html
Normal 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 %}
|
||||
981
templates/admin_advanced_settings.html
Normal file
981
templates/admin_advanced_settings.html
Normal 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 %}
|
||||
382
templates/admin_edit_printer.html
Normal file
382
templates/admin_edit_printer.html
Normal 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 %}
|
||||
596
templates/admin_edit_user.html
Normal file
596
templates/admin_edit_user.html
Normal 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 %}
|
||||
372
templates/admin_guest_requests.html
Normal file
372
templates/admin_guest_requests.html
Normal file
@@ -0,0 +1,372 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Gastaufträge verwalten - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<!-- CSRF Token für AJAX-Anfragen -->
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<script src="{{ url_for('static', filename='js/admin-guest-requests.js') }}" defer></script>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div id="loading-overlay" class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center hidden">
|
||||
<div class="bg-white dark:bg-slate-900 rounded-2xl p-8 shadow-2xl border border-gray-200 dark:border-slate-700">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 dark:border-blue-400"></div>
|
||||
<span class="text-lg font-medium text-gray-900 dark:text-slate-100">Wird geladen...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Moderne Gastaufträge-Verwaltung -->
|
||||
<div class="min-h-screen">
|
||||
|
||||
<!-- Hero Header -->
|
||||
<div class="relative overflow-hidden bg-gradient-to-r from-slate-900 via-blue-900 to-indigo-900 dark:from-slate-950 dark:via-blue-950 dark:to-indigo-950 text-white rounded-3xl mx-4 mt-4">
|
||||
<div class="absolute inset-0 bg-black/30 dark:bg-black/50"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 dark:via-white/3 to-transparent"></div>
|
||||
|
||||
<!-- Live Status Indicator -->
|
||||
<div class="absolute top-4 right-4 flex items-center space-x-2">
|
||||
<div class="flex items-center space-x-2 bg-white/15 dark:bg-white/10 backdrop-blur-sm border border-white/30 dark:border-white/20 rounded-full px-3 py-1">
|
||||
<div id="live-indicator" class="w-2 h-2 bg-green-400 dark:bg-green-300 rounded-full animate-pulse"></div>
|
||||
<span class="text-sm font-medium text-white">Live</span>
|
||||
</div>
|
||||
<div class="bg-white/15 dark:bg-white/10 backdrop-blur-sm border border-white/30 dark:border-white/20 rounded-full px-3 py-1">
|
||||
<span id="live-time" class="text-sm font-medium text-white"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Animated Background Pattern -->
|
||||
<div class="absolute inset-0 opacity-10 dark:opacity-5 rounded-3xl overflow-hidden">
|
||||
<div class="absolute inset-0" style="background-image: radial-gradient(circle at 25% 25%, white 2px, transparent 2px), radial-gradient(circle at 75% 75%, white 2px, transparent 2px); background-size: 50px 50px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<!-- Mercedes-Benz Logo -->
|
||||
<div class="inline-flex items-center justify-center w-20 h-20 bg-white/15 dark:bg-white/10 backdrop-blur-sm rounded-full mb-6 border border-white/30 dark:border-white/20">
|
||||
<svg class="w-10 h-10 text-white" viewBox="0 0 80 80" fill="currentColor">
|
||||
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
|
||||
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
|
||||
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
|
||||
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
|
||||
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
|
||||
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-4 tracking-tight">
|
||||
<span class="bg-gradient-to-r from-white via-blue-100 to-slate-100 dark:from-white dark:via-blue-200 dark:to-slate-200 bg-clip-text text-transparent">
|
||||
Gastaufträge Verwaltung
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-xl text-blue-100 dark:text-blue-200 max-w-2xl leading-relaxed">
|
||||
Verwalten Sie Gastdruckaufträge mit modernster Technologie und Mercedes-Benz Qualität
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Back to Admin Button -->
|
||||
<div>
|
||||
<a href="{{ url_for('admin_page') }}"
|
||||
class="inline-flex items-center px-6 py-3 bg-white/15 dark:bg-white/10 backdrop-blur-sm border border-white/30 dark:border-white/20 rounded-xl text-white hover:bg-white/25 dark:hover:bg-white/15 transition-all duration-300 hover:scale-105">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Zurück zum Admin
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10">
|
||||
|
||||
<!-- Quick Stats Dashboard -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-12">
|
||||
<!-- Pending Requests -->
|
||||
<div class="group relative bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-orange-500/10 dark:from-orange-400/20 to-red-500/10 dark:to-red-400/20 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-3 bg-gradient-to-br from-orange-500 to-orange-600 dark:from-orange-400 dark:to-orange-500 rounded-xl shadow-lg">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div id="pending-count" class="text-2xl font-bold text-slate-900 dark:text-slate-100">-</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Wartend</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 mb-2">
|
||||
<div class="w-2 h-2 bg-orange-400 dark:bg-orange-300 rounded-full animate-pulse"></div>
|
||||
<span class="text-xs text-orange-600 dark:text-orange-400 font-medium">Benötigt Aufmerksamkeit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Approved Requests -->
|
||||
<div class="group relative bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-green-500/10 dark:from-green-400/20 to-emerald-500/10 dark:to-emerald-400/20 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-3 bg-gradient-to-br from-green-500 to-green-600 dark:from-green-400 dark:to-green-500 rounded-xl shadow-lg">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div id="approved-count" class="text-2xl font-bold text-slate-900 dark:text-slate-100">-</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Genehmigt</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 mb-2">
|
||||
<div class="w-2 h-2 bg-green-400 dark:bg-green-300 rounded-full animate-pulse"></div>
|
||||
<span class="text-xs text-green-600 dark:text-green-400 font-medium">Aktiv</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rejected Requests -->
|
||||
<div class="group relative bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-red-500/10 dark:from-red-400/20 to-pink-500/10 dark:to-pink-400/20 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-3 bg-gradient-to-br from-red-500 to-red-600 dark:from-red-400 dark:to-red-500 rounded-xl shadow-lg">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div id="rejected-count" class="text-2xl font-bold text-slate-900 dark:text-slate-100">-</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Abgelehnt</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 mb-2">
|
||||
<div class="w-2 h-2 bg-red-400 dark:bg-red-300 rounded-full"></div>
|
||||
<span class="text-xs text-red-600 dark:text-red-400 font-medium">Archiviert</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Requests -->
|
||||
<div class="group relative bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl hover:shadow-2xl dark:shadow-2xl dark:hover:shadow-slate-900/50 transition-all duration-500 hover:-translate-y-2">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/10 dark:from-blue-400/20 to-indigo-500/10 dark:to-indigo-400/20 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-3 bg-gradient-to-br from-blue-500 to-blue-600 dark:from-blue-400 dark:to-blue-500 rounded-xl shadow-lg">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div id="total-count" class="text-2xl font-bold text-slate-900 dark:text-slate-100">-</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Gesamt</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 mb-2">
|
||||
<div class="w-2 h-2 bg-blue-400 dark:bg-blue-300 rounded-full animate-pulse"></div>
|
||||
<span class="text-xs text-blue-600 dark:text-blue-400 font-medium">Alle Zeit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-gray-200/50 dark:border-slate-700/50 p-8 shadow-xl dark:shadow-2xl mb-8">
|
||||
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center space-y-4 lg:space-y-0">
|
||||
<!-- Search and Filters -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 flex-1">
|
||||
<div class="relative flex-1 max-w-md">
|
||||
<input type="text" id="search-requests" placeholder="Gastaufträge durchsuchen..."
|
||||
class="w-full pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
|
||||
bg-white/80 dark:bg-slate-800/80 text-slate-900 dark:text-slate-100
|
||||
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent
|
||||
placeholder-slate-500 dark:placeholder-slate-400">
|
||||
<svg class="w-5 h-5 text-slate-400 dark:text-slate-500 absolute left-3 top-1/2 transform -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<select id="status-filter"
|
||||
class="px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
|
||||
bg-white/80 dark:bg-slate-800/80 text-slate-900 dark:text-slate-100
|
||||
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent min-w-[150px]">
|
||||
<option value="all">Alle Status</option>
|
||||
<option value="pending">Wartend</option>
|
||||
<option value="approved">Genehmigt</option>
|
||||
<option value="rejected">Abgelehnt</option>
|
||||
<option value="expired">Abgelaufen</option>
|
||||
</select>
|
||||
|
||||
<select id="sort-order"
|
||||
class="px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl
|
||||
bg-white/80 dark:bg-slate-800/80 text-slate-900 dark:text-slate-100
|
||||
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent min-w-[150px]">
|
||||
<option value="newest">Neueste zuerst</option>
|
||||
<option value="oldest">Älteste zuerst</option>
|
||||
<option value="priority">Nach Priorität</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex space-x-3 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 %}
|
||||
1473
templates/admin_guest_requests_overview.html
Normal file
1473
templates/admin_guest_requests_overview.html
Normal file
File diff suppressed because it is too large
Load Diff
238
templates/admin_manage_printer.html
Normal file
238
templates/admin_manage_printer.html
Normal file
@@ -0,0 +1,238 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Drucker verwalten - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">{{ printer.name }} verwalten</h1>
|
||||
<p class="text-slate-600 dark:text-slate-400 mt-2">Verwaltung und Überwachung des Druckers</p>
|
||||
</div>
|
||||
<a href="{{ url_for('admin_page', tab='printers') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Zurück zur Druckerverwaltung
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drucker-Info -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
<!-- Status Card -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Status</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-600 dark:text-slate-400">Aktueller Status:</span>
|
||||
<span class="px-3 py-1 rounded-full text-sm font-medium
|
||||
{% if printer.status == 'available' %}bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200{% elif printer.status == 'busy' %}bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200{% elif printer.status == 'maintenance' %}bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200{% else %}bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200{% endif %}">
|
||||
{{ printer.status|title }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-600 dark:text-slate-400">IP-Adresse:</span>
|
||||
<span class="text-slate-900 dark:text-white font-mono">{{ printer.ip_address }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-600 dark:text-slate-400">Standort:</span>
|
||||
<span class="text-slate-900 dark:text-white">{{ printer.location or 'Nicht angegeben' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktionen Card -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Aktionen</h3>
|
||||
<div class="space-y-3">
|
||||
<button onclick="togglePrinter({{ printer.id }})"
|
||||
class="w-full px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600 transition-all duration-300">
|
||||
{% if printer.status == 'available' %}Deaktivieren{% else %}Aktivieren{% endif %}
|
||||
</button>
|
||||
<button onclick="testConnection({{ printer.id }})"
|
||||
class="w-full px-4 py-2 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-all duration-300">
|
||||
Verbindung testen
|
||||
</button>
|
||||
<a href="{{ url_for('admin_printer_settings_page', printer_id=printer.id) }}"
|
||||
class="block w-full px-4 py-2 bg-slate-500 text-white rounded-xl hover:bg-slate-600 transition-all duration-300 text-center">
|
||||
Einstellungen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistiken Card -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Statistiken</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-600 dark:text-slate-400">Gesamte Jobs:</span>
|
||||
<span class="text-slate-900 dark:text-white font-semibold" id="total-jobs">-</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-600 dark:text-slate-400">Aktive Jobs:</span>
|
||||
<span class="text-slate-900 dark:text-white font-semibold" id="active-jobs">-</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-600 dark:text-slate-400">Erfolgsrate:</span>
|
||||
<span class="text-slate-900 dark:text-white font-semibold" id="success-rate">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktuelle Jobs -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Aktuelle Jobs</h3>
|
||||
<div id="current-jobs" class="space-y-4">
|
||||
<div class="text-center text-slate-500 dark:text-slate-400 py-8">
|
||||
Lade Jobs...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// CSRF Token
|
||||
function getCsrfToken() {
|
||||
const token = document.querySelector('meta[name="csrf-token"]');
|
||||
return token ? token.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
// Notification anzeigen
|
||||
function showNotification(message, type = 'info') {
|
||||
if (type === 'success') {
|
||||
alert('✓ ' + message);
|
||||
} else if (type === 'error') {
|
||||
alert('✗ ' + message);
|
||||
} else {
|
||||
alert('ℹ ' + message);
|
||||
}
|
||||
}
|
||||
|
||||
// Drucker aktivieren/deaktivieren
|
||||
async function togglePrinter(printerId) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/printers/${printerId}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showNotification('Drucker-Status erfolgreich geändert', 'success');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler beim Ändern des Drucker-Status', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Verbindung testen
|
||||
async function testConnection(printerId) {
|
||||
try {
|
||||
const response = await fetch(`/api/printers/${printerId}/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (result.connected) {
|
||||
showNotification('Verbindung erfolgreich', 'success');
|
||||
} else {
|
||||
showNotification('Verbindung fehlgeschlagen', 'error');
|
||||
}
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler beim Testen der Verbindung', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Statistiken laden
|
||||
async function loadStats() {
|
||||
try {
|
||||
const response = await fetch(`/api/printers/{{ printer.id }}/stats`);
|
||||
const stats = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('total-jobs').textContent = stats.total_jobs || 0;
|
||||
document.getElementById('active-jobs').textContent = stats.active_jobs || 0;
|
||||
document.getElementById('success-rate').textContent = (stats.success_rate || 0) + '%';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Statistiken:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Jobs laden
|
||||
async function loadJobs() {
|
||||
try {
|
||||
const response = await fetch(`/api/printers/{{ printer.id }}/jobs`);
|
||||
const jobs = await response.json();
|
||||
|
||||
const container = document.getElementById('current-jobs');
|
||||
|
||||
if (response.ok && jobs.length > 0) {
|
||||
container.innerHTML = jobs.map(job => `
|
||||
<div class="border border-slate-200 dark:border-slate-700 rounded-xl p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 class="font-medium text-slate-900 dark:text-white">${job.name}</h4>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">von ${job.user_name}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="px-3 py-1 rounded-full text-sm font-medium
|
||||
${job.status === 'running' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
|
||||
job.status === 'pending' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' :
|
||||
'bg-slate-100 text-slate-800 dark:bg-slate-900 dark:text-slate-200'}">
|
||||
${job.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
container.innerHTML = '<div class="text-center text-slate-500 dark:text-slate-400 py-8">Keine aktiven Jobs</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Jobs:', error);
|
||||
document.getElementById('current-jobs').innerHTML = '<div class="text-center text-red-500 py-8">Fehler beim Laden der Jobs</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadStats();
|
||||
loadJobs();
|
||||
|
||||
// Alle 30 Sekunden aktualisieren
|
||||
setInterval(() => {
|
||||
loadStats();
|
||||
loadJobs();
|
||||
}, 30000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
802
templates/admin_plug_schedules.html
Normal file
802
templates/admin_plug_schedules.html
Normal 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 %}
|
||||
119
templates/admin_printer_settings.html
Normal file
119
templates/admin_printer_settings.html
Normal file
@@ -0,0 +1,119 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Drucker-Einstellungen - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
|
||||
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">{{ printer.name }} - Einstellungen</h1>
|
||||
<p class="text-slate-600 dark:text-slate-400 mt-2">Konfiguration und Einstellungen des Druckers</p>
|
||||
</div>
|
||||
<a href="{{ url_for('admin_manage_printer_page', printer_id=printer.id) }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Zurück zur Verwaltung
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-8">
|
||||
<form method="POST" action="{{ url_for('admin_update_printer_form', printer_id=printer.id) }}" class="space-y-6">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="_method" value="PUT"/>
|
||||
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Drucker-Name
|
||||
</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
value="{{ printer.name }}"
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
|
||||
placeholder="Prusa i3 MK3S+">
|
||||
</div>
|
||||
|
||||
<!-- IP-Adresse -->
|
||||
<div>
|
||||
<label for="ip_address" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
IP-Adresse
|
||||
</label>
|
||||
<input type="text" name="ip_address" id="ip_address" required
|
||||
value="{{ printer.ip_address }}"
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
|
||||
placeholder="192.168.1.100"
|
||||
pattern="^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
|
||||
</div>
|
||||
|
||||
<!-- Modell -->
|
||||
<div>
|
||||
<label for="model" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Drucker-Modell
|
||||
</label>
|
||||
<input type="text" name="model" id="model"
|
||||
value="{{ printer.model or '' }}"
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
|
||||
placeholder="Prusa i3 MK3S+">
|
||||
</div>
|
||||
|
||||
<!-- Standort -->
|
||||
<div>
|
||||
<label for="location" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Standort
|
||||
</label>
|
||||
<input type="text" name="location" id="location"
|
||||
value="{{ printer.location or '' }}"
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
|
||||
placeholder="Werkstatt A, Regal 3">
|
||||
</div>
|
||||
|
||||
<!-- Beschreibung -->
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Beschreibung
|
||||
</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
|
||||
placeholder="Zusätzliche Informationen zum Drucker...">{{ printer.description or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div>
|
||||
<label for="status" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Status
|
||||
</label>
|
||||
<select name="status" id="status"
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
|
||||
<option value="available" {% if printer.status == 'available' %}selected{% endif %}>Verfügbar</option>
|
||||
<option value="maintenance" {% if printer.status == 'maintenance' %}selected{% endif %}>Wartung</option>
|
||||
<option value="offline" {% if printer.status == 'offline' %}selected{% endif %}>Offline</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex items-center justify-end space-x-4 pt-4">
|
||||
<a href="{{ url_for('admin_manage_printer_page', printer_id=printer.id) }}"
|
||||
class="px-6 py-3 border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-all duration-300">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300 shadow-lg">
|
||||
Einstellungen speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
376
templates/admin_settings.html
Normal file
376
templates/admin_settings.html
Normal file
@@ -0,0 +1,376 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Admin-Einstellungen - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Admin-Einstellungen</h1>
|
||||
<p class="text-slate-600 dark:text-slate-400 mt-2">Systemkonfiguration und Verwaltungsoptionen</p>
|
||||
</div>
|
||||
<a href="{{ url_for('admin_page') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Zurück zum Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- System-Wartung -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">System-Wartung</h3>
|
||||
<div class="space-y-4">
|
||||
<button onclick="clearCache()"
|
||||
class="w-full px-4 py-3 bg-blue-500 text-white rounded-xl hover:bg-blue-600 transition-all duration-300">
|
||||
Cache leeren
|
||||
</button>
|
||||
<button onclick="optimizeDatabase()"
|
||||
class="w-full px-4 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-all duration-300">
|
||||
Datenbank optimieren
|
||||
</button>
|
||||
<button onclick="createBackup()"
|
||||
class="w-full px-4 py-3 bg-purple-500 text-white rounded-xl hover:bg-purple-600 transition-all duration-300">
|
||||
Backup erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drucker-Verwaltung -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Drucker-Verwaltung</h3>
|
||||
<div class="space-y-4">
|
||||
<button onclick="updatePrinters()"
|
||||
class="w-full px-4 py-3 bg-orange-500 text-white rounded-xl hover:bg-orange-600 transition-all duration-300">
|
||||
Drucker-Status aktualisieren
|
||||
</button>
|
||||
<button onclick="testAllPrinters()"
|
||||
class="w-full px-4 py-3 bg-teal-500 text-white rounded-xl hover:bg-teal-600 transition-all duration-300">
|
||||
Alle Drucker testen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System-Informationen -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">System-Informationen</h3>
|
||||
<div class="space-y-3" id="system-info">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-600 dark:text-slate-400">Server-Status:</span>
|
||||
<span class="text-slate-900 dark:text-white font-semibold" id="server-status">Lade...</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-600 dark:text-slate-400">Datenbank:</span>
|
||||
<span class="text-slate-900 dark:text-white font-semibold" id="db-status">Lade...</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-600 dark:text-slate-400">Uptime:</span>
|
||||
<span class="text-slate-900 dark:text-white font-semibold" id="uptime">Lade...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs und Überwachung -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Logs und Überwachung</h3>
|
||||
<div class="space-y-4">
|
||||
<button onclick="downloadLogs()"
|
||||
class="w-full px-4 py-3 bg-slate-500 text-white rounded-xl hover:bg-slate-600 transition-all duration-300">
|
||||
Logs herunterladen
|
||||
</button>
|
||||
<button onclick="runMaintenance()"
|
||||
class="w-full px-4 py-3 bg-indigo-500 text-white rounded-xl hover:bg-indigo-600 transition-all duration-300">
|
||||
Wartung ausführen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Erweiterte Einstellungen -->
|
||||
<div class="mt-8 bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Erweiterte Einstellungen</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Automatische Backup-Intervall (Stunden)
|
||||
</label>
|
||||
<input type="number" id="backup-interval" min="1" max="168" value="24"
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Maximale Job-Laufzeit (Stunden)
|
||||
</label>
|
||||
<input type="number" id="max-job-time" min="1" max="72" value="12"
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button onclick="saveSettings()"
|
||||
class="px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300 shadow-lg">
|
||||
Einstellungen speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// CSRF Token
|
||||
function getCsrfToken() {
|
||||
const token = document.querySelector('meta[name="csrf-token"]');
|
||||
return token ? token.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
// Notification anzeigen
|
||||
function showNotification(message, type = 'info') {
|
||||
if (type === 'success') {
|
||||
alert('✓ ' + message);
|
||||
} else if (type === 'error') {
|
||||
alert('✗ ' + message);
|
||||
} else {
|
||||
alert('ℹ ' + message);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache leeren
|
||||
async function clearCache() {
|
||||
if (!confirm('Möchten Sie den Cache wirklich leeren?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/cache/clear', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showNotification('Cache erfolgreich geleert', 'success');
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler beim Leeren des Cache', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Datenbank optimieren
|
||||
async function optimizeDatabase() {
|
||||
if (!confirm('Möchten Sie die Datenbank optimieren? Dies kann einige Minuten dauern.')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/database/optimize', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showNotification('Datenbank erfolgreich optimiert', 'success');
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler bei der Datenbankoptimierung', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Backup erstellen
|
||||
async function createBackup() {
|
||||
if (!confirm('Möchten Sie ein Backup erstellen?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/backup/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showNotification('Backup erfolgreich erstellt', 'success');
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler beim Erstellen des Backups', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Drucker aktualisieren
|
||||
async function updatePrinters() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/printers/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showNotification('Drucker-Status erfolgreich aktualisiert', 'success');
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler beim Aktualisieren der Drucker', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Alle Drucker testen
|
||||
async function testAllPrinters() {
|
||||
try {
|
||||
const response = await fetch('/api/printers/status');
|
||||
const printers = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
const onlineCount = printers.filter(p => p.status === 'available').length;
|
||||
showNotification(`${onlineCount} von ${printers.length} Druckern sind online`, 'info');
|
||||
} else {
|
||||
showNotification('Fehler beim Testen der Drucker', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Logs herunterladen
|
||||
async function downloadLogs() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/logs/download');
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'myp-logs.zip';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
showNotification('Logs werden heruntergeladen', 'success');
|
||||
} else {
|
||||
showNotification('Fehler beim Herunterladen der Logs', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Wartung ausführen
|
||||
async function runMaintenance() {
|
||||
if (!confirm('Möchten Sie die Systemwartung ausführen?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/maintenance/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showNotification('Wartung erfolgreich ausgeführt', 'success');
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler bei der Wartung', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Einstellungen speichern
|
||||
async function saveSettings() {
|
||||
const backupInterval = document.getElementById('backup-interval').value;
|
||||
const maxJobTime = document.getElementById('max-job-time').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
backup_interval: parseInt(backupInterval),
|
||||
max_job_time: parseInt(maxJobTime)
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showNotification('Einstellungen erfolgreich gespeichert', 'success');
|
||||
} else {
|
||||
showNotification(result.error || 'Fehler beim Speichern der Einstellungen', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// System-Informationen laden
|
||||
async function loadSystemInfo() {
|
||||
try {
|
||||
const [systemResponse, dbResponse] = await Promise.all([
|
||||
fetch('/api/admin/system/status'),
|
||||
fetch('/api/admin/database/status')
|
||||
]);
|
||||
|
||||
const systemData = await systemResponse.json();
|
||||
const dbData = await dbResponse.json();
|
||||
|
||||
if (systemResponse.ok) {
|
||||
document.getElementById('server-status').textContent = systemData.status || 'Online';
|
||||
document.getElementById('uptime').textContent = systemData.uptime || 'Unbekannt';
|
||||
}
|
||||
|
||||
if (dbResponse.ok) {
|
||||
document.getElementById('db-status').textContent = dbData.connected ? 'Verbunden' : 'Getrennt';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der System-Informationen:', error);
|
||||
document.getElementById('server-status').textContent = 'Fehler';
|
||||
document.getElementById('db-status').textContent = 'Fehler';
|
||||
document.getElementById('uptime').textContent = 'Fehler';
|
||||
}
|
||||
}
|
||||
|
||||
// Beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadSystemInfo();
|
||||
|
||||
// Alle 30 Sekunden aktualisieren
|
||||
setInterval(loadSystemInfo, 30000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
748
templates/analytics.html
Normal file
748
templates/analytics.html
Normal 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
218
templates/base-fast.html
Normal 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>© 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>
|
||||
1238
templates/base-optimized.html
Normal file
1238
templates/base-optimized.html
Normal file
File diff suppressed because it is too large
Load Diff
1248
templates/base-original-backup.html
Normal file
1248
templates/base-original-backup.html
Normal file
File diff suppressed because it is too large
Load Diff
1230
templates/base.html
Normal file
1230
templates/base.html
Normal file
File diff suppressed because it is too large
Load Diff
1702
templates/calendar.html
Normal file
1702
templates/calendar.html
Normal file
File diff suppressed because it is too large
Load Diff
952
templates/dashboard.html
Normal file
952
templates/dashboard.html
Normal file
@@ -0,0 +1,952 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Professional Mercedes-Benz Dashboard Styles */
|
||||
.mb-dashboard-card {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .mb-dashboard-card {
|
||||
background: var(--mb-black, #000000); /* Schwarzer Hintergrund im Dark Mode */
|
||||
border-color: var(--border-primary, #334155);
|
||||
}
|
||||
|
||||
.mb-dashboard-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.dark .mb-dashboard-card:hover {
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.mb-stat-card {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e6f2ff 100%);
|
||||
color: #0f172a;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dark .mb-stat-card {
|
||||
background: linear-gradient(135deg, #000000 0%, #111827 100%); /* Mercedes Schwarz */
|
||||
color: var(--text-primary, #f8fafc);
|
||||
}
|
||||
|
||||
.mb-stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(0, 114, 206, 0.1) 0%, transparent 70%);
|
||||
animation: pulse-glow 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dark .mb-stat-card::before {
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.05) 0%, transparent 70%); /* Subtle glow for dark */
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { opacity: 0.5; transform: scale(1); }
|
||||
50% { opacity: 0.8; transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.mb-stat-icon {
|
||||
background: rgba(0, 0, 0, 0.1); /* Schwarz statt Blau */
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .mb-stat-icon {
|
||||
background: rgba(255, 255, 255, 0.1); /* Lighter icon background for contrast */
|
||||
}
|
||||
|
||||
.mb-stat-card:hover .mb-stat-icon {
|
||||
transform: scale(1.1);
|
||||
background: rgba(0, 0, 0, 0.2); /* Schwarz statt Blau */
|
||||
}
|
||||
|
||||
.dark .mb-stat-card:hover .mb-stat-icon {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.mb-status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.mb-status-indicator::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
animation: status-pulse 2s infinite;
|
||||
}
|
||||
|
||||
.mb-status-online {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.mb-status-online::after {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.mb-status-busy {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.mb-status-busy::after {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.mb-status-offline {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.mb-status-offline::after {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.mb-status-idle {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.mb-status-idle::after {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
@keyframes status-pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.5); opacity: 0.5; }
|
||||
100% { transform: scale(2); opacity: 0; }
|
||||
}
|
||||
|
||||
.mb-progress-container {
|
||||
background: #f3f4f6;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dark .mb-progress-container {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.mb-progress-bar {
|
||||
background: linear-gradient(90deg, #000000 0%, #333333 100%); /* Schwarz statt Blau */
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .mb-progress-bar {
|
||||
background: linear-gradient(90deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||
}
|
||||
|
||||
.mb-progress-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
animation: progress-shine 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes progress-shine {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.mb-activity-item {
|
||||
border-left: 4px solid #000000; /* Schwarz statt Blau */
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.05) 0%, transparent 100%); /* Schwarz statt Blau */
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.dark .mb-activity-item {
|
||||
border-left-color: #f3f4f6;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.02) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
/* Light/Dark Mode compatible cards */
|
||||
.glass-card-light {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(229, 231, 235, 0.8);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .glass-card-light {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-color: rgba(100, 116, 139, 0.3);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #1e293b;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dark .section-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.dark .stat-value {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.dark .stat-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<!-- Dashboard Header Card -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="w-16 h-16 flex-shrink-0">
|
||||
<svg class="w-full h-full text-slate-900 dark:text-white" fill="currentColor" viewBox="0 0 80 80" aria-hidden="true">
|
||||
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
|
||||
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
|
||||
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
|
||||
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
|
||||
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
|
||||
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Dashboard</h1>
|
||||
<p class="text-slate-500 dark:text-slate-400 mt-1">Übersicht über Ihre 3D-Druck Aktivitäten</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button id="refreshDashboard"
|
||||
class="btn-secondary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Aktualisieren</span>
|
||||
</button>
|
||||
<a href="{{ url_for('jobs_page') }}"
|
||||
class="btn-primary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span>Neuer Auftrag</span>
|
||||
</a>
|
||||
<a href="{{ url_for('guest.guest_requests_overview') }}"
|
||||
class="btn-secondary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<span>Anträge Übersicht</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Overview Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Active Jobs Card -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Aktive Aufträge</h3>
|
||||
<div class="stat-value">{{ active_jobs_count }}</div>
|
||||
</div>
|
||||
<div class="mb-stat-icon text-slate-900 dark:text-white">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Printers Card -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Verfügbare Drucker</h3>
|
||||
<div class="stat-value">{{ available_printers_count }}</div>
|
||||
</div>
|
||||
<div class="mb-stat-icon text-green-600 dark:text-green-400">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Jobs Card -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Aufträge (gesamt)</h3>
|
||||
<div class="stat-value">{{ total_jobs_count }}</div>
|
||||
</div>
|
||||
<div class="mb-stat-icon text-purple-600 dark:text-purple-400">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Rate Card -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Erfolgsrate</h3>
|
||||
<div class="stat-value">{{ success_rate }}%</div>
|
||||
</div>
|
||||
<div class="mb-stat-icon text-amber-600 dark:text-amber-400">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Jobs Section -->
|
||||
<div class="dashboard-card p-6">
|
||||
<h2 class="section-title">Aktuelle Druckaufträge</h2>
|
||||
<div class="overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
|
||||
<thead>
|
||||
<tr class="text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
|
||||
<th class="px-6 py-3">Status</th>
|
||||
<th class="px-6 py-3">Auftrag</th>
|
||||
<th class="px-6 py-3">Drucker</th>
|
||||
<th class="px-6 py-3">Startzeit</th>
|
||||
<th class="px-6 py-3">Fortschritt</th>
|
||||
<th class="px-6 py-3">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
|
||||
{% if active_jobs and active_jobs|length > 0 %}
|
||||
{% for job in active_jobs %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="mb-status-indicator {{ job.status_class }}"></div>
|
||||
<span class="ml-2 text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{{ job.status_text }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-slate-900 dark:text-white">{{ job.name }}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">{{ job.file_name }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-slate-700 dark:text-slate-300">{{ job.printer }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-slate-700 dark:text-slate-300">{{ job.start_time }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="mb-progress-container">
|
||||
<div class="mb-progress-bar" style="width: {{ job.progress }}%"></div>
|
||||
</div>
|
||||
<div class="text-xs text-right mt-1 text-slate-500 dark:text-slate-400">{{ job.progress }}%</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<a href="{{ url_for('job_detail', job_id=job.id) }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 font-medium">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-8 text-center">
|
||||
<div class="text-slate-500 dark:text-slate-400">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine aktiven Druckaufträge</p>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm">Starten Sie einen neuen Druckauftrag, um ihn hier zu sehen.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4 text-right">
|
||||
<a href="{{ url_for('jobs_page') }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 text-sm font-medium">
|
||||
Alle Druckaufträge anzeigen →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last Row: Printer Status and Recent Activity -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Available Printers -->
|
||||
<div class="dashboard-card p-6">
|
||||
<h2 class="section-title">Druckerstatus</h2>
|
||||
<div class="space-y-4">
|
||||
{% if printers and printers|length > 0 %}
|
||||
{% for printer in printers %}
|
||||
<div class="flex items-center justify-between p-4 rounded-xl bg-gray-50 dark:bg-slate-700/30">
|
||||
<div class="flex items-center">
|
||||
<div class="mb-status-indicator {{ printer.status_class }} mr-3"></div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-slate-900 dark:text-white">{{ printer.name }}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">{{ printer.model }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ printer.status_text }}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">{{ printer.location }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
</svg>
|
||||
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine Drucker gefunden</p>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm">Prüfen Sie die Verbindung oder fügen Sie Drucker hinzu.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-4 text-right">
|
||||
<a href="{{ url_for('printers_page') }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 text-sm font-medium">
|
||||
Alle Drucker anzeigen →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="dashboard-card p-6">
|
||||
<h2 class="section-title">Letzte Aktivitäten</h2>
|
||||
<div class="space-y-3">
|
||||
{% if activities and activities|length > 0 %}
|
||||
{% for activity in activities %}
|
||||
<div class="mb-activity-item pl-4 py-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-slate-700 dark:text-slate-300">{{ activity.description }}</p>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">{{ activity.time }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine Aktivitäten</p>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm">Ihre Aktivitäten werden hier angezeigt.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
class DashboardManager {
|
||||
constructor() {
|
||||
this.updateInterval = 30000; // 30 Sekunden
|
||||
this.autoUpdateTimer = null;
|
||||
this.isUpdating = false;
|
||||
this.wsConnection = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.setupAutoUpdate();
|
||||
this.setupWebSocket();
|
||||
this.animateCounters();
|
||||
console.log('🚀 Dashboard Manager initialisiert');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Refresh Button
|
||||
const refreshBtn = document.getElementById('refreshDashboard');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
this.refreshDashboard();
|
||||
});
|
||||
}
|
||||
|
||||
// Job-Zeilen klickbar machen für Details
|
||||
this.setupJobRowClicks();
|
||||
|
||||
// Drucker-Karten klickbar machen
|
||||
this.setupPrinterClicks();
|
||||
|
||||
// Dark Mode Updates
|
||||
window.addEventListener('darkModeChanged', (e) => {
|
||||
this.updateThemeElements(e.detail.isDark);
|
||||
});
|
||||
|
||||
// Visibility Change Detection für intelligente Updates
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
this.pauseAutoUpdate();
|
||||
} else {
|
||||
this.resumeAutoUpdate();
|
||||
this.refreshDashboard(); // Einmalige Aktualisierung bei Rückkehr
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupJobRowClicks() {
|
||||
const jobRows = document.querySelectorAll('tbody tr[data-job-id]');
|
||||
jobRows.forEach(row => {
|
||||
row.style.cursor = 'pointer';
|
||||
row.addEventListener('click', () => {
|
||||
const jobId = row.dataset.jobId;
|
||||
if (jobId) {
|
||||
window.location.href = `/jobs/${jobId}`;
|
||||
}
|
||||
});
|
||||
|
||||
// Hover-Effekt verstärken
|
||||
row.addEventListener('mouseenter', () => {
|
||||
row.style.transform = 'scale(1.01)';
|
||||
row.style.transition = 'transform 0.2s ease';
|
||||
});
|
||||
|
||||
row.addEventListener('mouseleave', () => {
|
||||
row.style.transform = 'scale(1)';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupPrinterClicks() {
|
||||
const printerCards = document.querySelectorAll('[data-printer-id]');
|
||||
printerCards.forEach(card => {
|
||||
card.style.cursor = 'pointer';
|
||||
card.addEventListener('click', () => {
|
||||
const printerId = card.dataset.printerId;
|
||||
if (printerId) {
|
||||
window.location.href = `/printers/${printerId}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupAutoUpdate() {
|
||||
this.autoUpdateTimer = setInterval(() => {
|
||||
if (!document.hidden && !this.isUpdating) {
|
||||
this.updateDashboardData();
|
||||
}
|
||||
}, this.updateInterval);
|
||||
}
|
||||
|
||||
pauseAutoUpdate() {
|
||||
if (this.autoUpdateTimer) {
|
||||
clearInterval(this.autoUpdateTimer);
|
||||
this.autoUpdateTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
resumeAutoUpdate() {
|
||||
if (!this.autoUpdateTimer) {
|
||||
this.setupAutoUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
setupWebSocket() {
|
||||
// WebSocket für Real-time Updates
|
||||
try {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/dashboard`;
|
||||
|
||||
this.wsConnection = new WebSocket(wsUrl);
|
||||
|
||||
this.wsConnection.onopen = () => {
|
||||
console.log('📡 WebSocket Verbindung zu Dashboard hergestellt');
|
||||
};
|
||||
|
||||
this.wsConnection.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleWebSocketUpdate(data);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Verarbeiten der WebSocket-Nachricht:', error);
|
||||
}
|
||||
};
|
||||
|
||||
this.wsConnection.onclose = () => {
|
||||
console.log('📡 WebSocket Verbindung geschlossen - versuche Wiederverbindung in 5s');
|
||||
setTimeout(() => {
|
||||
this.setupWebSocket();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
this.wsConnection.onerror = (error) => {
|
||||
console.error('WebSocket Fehler:', error);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.log('WebSocket nicht verfügbar, verwende Polling-Updates');
|
||||
}
|
||||
}
|
||||
|
||||
handleWebSocketUpdate(data) {
|
||||
console.log('📡 Erhaltenes WebSocket-Update:', data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'job_status_update':
|
||||
this.updateJobStatus(data.job_id, data.status, data.progress);
|
||||
break;
|
||||
case 'printer_status_update':
|
||||
this.updatePrinterStatus(data.printer_id, data.status);
|
||||
break;
|
||||
case 'new_activity':
|
||||
this.addNewActivity(data.activity);
|
||||
break;
|
||||
case 'stats_update':
|
||||
this.updateStats(data.stats);
|
||||
break;
|
||||
default:
|
||||
console.log('Unbekannter WebSocket-Update-Typ:', data.type);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshDashboard() {
|
||||
if (this.isUpdating) return;
|
||||
|
||||
this.isUpdating = true;
|
||||
const refreshBtn = document.getElementById('refreshDashboard');
|
||||
|
||||
try {
|
||||
// Button-Status aktualisieren
|
||||
if (refreshBtn) {
|
||||
refreshBtn.disabled = true;
|
||||
refreshBtn.innerHTML = `
|
||||
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>Aktualisiert...</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// Vollständige Seitenaktualisierung
|
||||
window.location.reload();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren:', error);
|
||||
this.showToast('Fehler beim Aktualisieren des Dashboards', 'error');
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
|
||||
// Button zurücksetzen
|
||||
if (refreshBtn) {
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.innerHTML = `
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Aktualisieren</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateDashboardData() {
|
||||
if (this.isUpdating) return;
|
||||
|
||||
this.isUpdating = true;
|
||||
|
||||
try {
|
||||
// Stille Hintergrund-Updates ohne vollständige Neuladen
|
||||
const responses = await Promise.all([
|
||||
fetch('/api/dashboard/stats'),
|
||||
fetch('/api/dashboard/active-jobs'),
|
||||
fetch('/api/dashboard/printers'),
|
||||
fetch('/api/dashboard/activities')
|
||||
]);
|
||||
|
||||
const [statsData, jobsData, printersData, activitiesData] = await Promise.all(
|
||||
responses.map(r => r.json())
|
||||
);
|
||||
|
||||
// UI-Updates
|
||||
this.updateStats(statsData);
|
||||
this.updateActiveJobs(jobsData);
|
||||
this.updatePrinters(printersData);
|
||||
this.updateActivities(activitiesData);
|
||||
|
||||
console.log('🔄 Dashboard-Daten erfolgreich aktualisiert');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Dashboard-Daten:', error);
|
||||
// Fehlschlag stumm - verwende WebSocket oder warte auf nächste Aktualisierung
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateJobStatus(jobId, status, progress) {
|
||||
const jobRow = document.querySelector(`tr[data-job-id="${jobId}"]`);
|
||||
if (jobRow) {
|
||||
// Status-Indikator aktualisieren
|
||||
const statusIndicator = jobRow.querySelector('.mb-status-indicator');
|
||||
const statusText = jobRow.querySelector('.text-sm.font-medium');
|
||||
const progressBar = jobRow.querySelector('.mb-progress-bar');
|
||||
const progressText = jobRow.querySelector('.text-xs.text-right');
|
||||
|
||||
if (statusIndicator) {
|
||||
statusIndicator.className = `mb-status-indicator ${this.getStatusClass(status)}`;
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
statusText.textContent = this.getStatusText(status);
|
||||
}
|
||||
|
||||
if (progressBar && progress !== undefined) {
|
||||
progressBar.style.width = `${progress}%`;
|
||||
}
|
||||
|
||||
if (progressText && progress !== undefined) {
|
||||
progressText.textContent = `${progress}%`;
|
||||
}
|
||||
|
||||
// Animation für Updates
|
||||
jobRow.style.backgroundColor = 'rgba(59, 130, 246, 0.1)';
|
||||
setTimeout(() => {
|
||||
jobRow.style.backgroundColor = '';
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
updatePrinterStatus(printerId, status) {
|
||||
const printerCard = document.querySelector(`[data-printer-id="${printerId}"]`);
|
||||
if (printerCard) {
|
||||
const statusIndicator = printerCard.querySelector('.mb-status-indicator');
|
||||
const statusText = printerCard.querySelector('.text-sm.font-medium:last-child');
|
||||
|
||||
if (statusIndicator) {
|
||||
statusIndicator.className = `mb-status-indicator ${this.getStatusClass(status)}`;
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
statusText.textContent = this.getStatusText(status);
|
||||
}
|
||||
|
||||
// Animation für Updates
|
||||
printerCard.style.transform = 'scale(1.02)';
|
||||
setTimeout(() => {
|
||||
printerCard.style.transform = 'scale(1)';
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
addNewActivity(activity) {
|
||||
const activitiesContainer = document.querySelector('.space-y-3');
|
||||
if (activitiesContainer) {
|
||||
const newActivity = document.createElement('div');
|
||||
newActivity.className = 'mb-activity-item pl-4 py-3 opacity-0';
|
||||
newActivity.innerHTML = `
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-slate-700 dark:text-slate-300">${activity.description}</p>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">${activity.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Am Anfang einfügen
|
||||
activitiesContainer.insertBefore(newActivity, activitiesContainer.firstChild);
|
||||
|
||||
// Animation
|
||||
setTimeout(() => {
|
||||
newActivity.style.opacity = '1';
|
||||
newActivity.style.transition = 'opacity 0.5s ease';
|
||||
}, 100);
|
||||
|
||||
// Alte Aktivitäten entfernen (max 10)
|
||||
const activities = activitiesContainer.querySelectorAll('.mb-activity-item');
|
||||
if (activities.length > 10) {
|
||||
activities[activities.length - 1].remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStats(stats) {
|
||||
// Statistik-Karten aktualisieren mit Animation
|
||||
const statValues = document.querySelectorAll('.stat-value');
|
||||
|
||||
const statsMapping = [
|
||||
{ element: statValues[0], value: stats.active_jobs_count },
|
||||
{ element: statValues[1], value: stats.available_printers_count },
|
||||
{ element: statValues[2], value: stats.total_jobs_count },
|
||||
{ element: statValues[3], value: `${stats.success_rate}%` }
|
||||
];
|
||||
|
||||
statsMapping.forEach(({ element, value }) => {
|
||||
if (element && element.textContent !== value.toString()) {
|
||||
this.animateValueChange(element, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
animateValueChange(element, newValue) {
|
||||
element.style.transform = 'scale(1.1)';
|
||||
element.style.transition = 'transform 0.3s ease';
|
||||
|
||||
setTimeout(() => {
|
||||
element.textContent = newValue;
|
||||
element.style.transform = 'scale(1)';
|
||||
}, 150);
|
||||
}
|
||||
|
||||
animateCounters() {
|
||||
// Initialanimation für Counter
|
||||
const counters = document.querySelectorAll('.stat-value');
|
||||
counters.forEach((counter, index) => {
|
||||
const finalValue = parseInt(counter.textContent) || 0;
|
||||
if (finalValue > 0) {
|
||||
let currentValue = 0;
|
||||
const increment = finalValue / 30; // 30 Schritte
|
||||
|
||||
const timer = setInterval(() => {
|
||||
currentValue += increment;
|
||||
if (currentValue >= finalValue) {
|
||||
counter.textContent = finalValue + (counter.textContent.includes('%') ? '%' : '');
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
counter.textContent = Math.floor(currentValue) + (counter.textContent.includes('%') ? '%' : '');
|
||||
}
|
||||
}, 50 + (index * 100)); // Verzögerung zwischen Countern
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getStatusClass(status) {
|
||||
const statusClasses = {
|
||||
'running': 'mb-status-busy',
|
||||
'completed': 'mb-status-online',
|
||||
'failed': 'mb-status-offline',
|
||||
'paused': 'mb-status-idle',
|
||||
'queued': 'mb-status-idle',
|
||||
'online': 'mb-status-online',
|
||||
'offline': 'mb-status-offline',
|
||||
'busy': 'mb-status-busy',
|
||||
'idle': 'mb-status-idle'
|
||||
};
|
||||
return statusClasses[status] || 'mb-status-idle';
|
||||
}
|
||||
|
||||
getStatusText(status) {
|
||||
const statusTexts = {
|
||||
'running': 'Druckt',
|
||||
'completed': 'Abgeschlossen',
|
||||
'failed': 'Fehlgeschlagen',
|
||||
'paused': 'Pausiert',
|
||||
'queued': 'Warteschlange',
|
||||
'online': 'Online',
|
||||
'offline': 'Offline',
|
||||
'busy': 'Beschäftigt',
|
||||
'idle': 'Bereit'
|
||||
};
|
||||
return statusTexts[status] || 'Unbekannt';
|
||||
}
|
||||
|
||||
updateThemeElements(isDark) {
|
||||
// Theme-spezifische Updates
|
||||
console.log(`🎨 Dashboard Theme aktualisiert: ${isDark ? 'Dark' : 'Light'} Mode`);
|
||||
|
||||
// Spezielle Animationen für Theme-Wechsel
|
||||
const cards = document.querySelectorAll('.dashboard-card');
|
||||
cards.forEach(card => {
|
||||
card.style.transition = 'all 0.3s ease';
|
||||
});
|
||||
}
|
||||
|
||||
showToast(message, type = 'info') {
|
||||
// Toast-Benachrichtigung anzeigen
|
||||
if (window.MYP && window.MYP.UI && window.MYP.UI.ToastManager) {
|
||||
const toast = new window.MYP.UI.ToastManager();
|
||||
toast.show(message, type, 5000);
|
||||
} else {
|
||||
console.log(`Toast: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup beim Verlassen der Seite
|
||||
cleanup() {
|
||||
if (this.autoUpdateTimer) {
|
||||
clearInterval(this.autoUpdateTimer);
|
||||
}
|
||||
|
||||
if (this.wsConnection) {
|
||||
this.wsConnection.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard Manager initialisieren
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.dashboardManager = new DashboardManager();
|
||||
|
||||
// Cleanup beim Verlassen
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (window.dashboardManager) {
|
||||
window.dashboardManager.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
35
templates/errors/403.html
Normal file
35
templates/errors/403.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}403 - Zugriff verweigert{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-md mx-auto bg-white rounded-lg shadow-lg p-8 text-center">
|
||||
<div class="mb-6">
|
||||
<div class="text-6xl text-red-500 mb-4">🚫</div>
|
||||
<h1 class="text-3xl font-bold text-gray-800 mb-2">403</h1>
|
||||
<h2 class="text-xl text-gray-600 mb-4">Zugriff verweigert</h2>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<p class="text-gray-600 mb-4">
|
||||
Sie haben keine Berechtigung, auf diese Seite zuzugreifen.
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Falls Sie glauben, dass dies ein Fehler ist, wenden Sie sich an einen Administrator.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<a href="{{ url_for('index') }}"
|
||||
class="block w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
||||
Zur Startseite
|
||||
</a>
|
||||
<button onclick="history.back()"
|
||||
class="block w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
||||
Zurück
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
47
templates/errors/404.html
Normal file
47
templates/errors/404.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}404 - Seite nicht gefunden - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-[80vh] flex flex-col items-center justify-center p-4">
|
||||
<!-- 404 Error Container -->
|
||||
<div class="w-full max-w-md">
|
||||
<div class="bg-white dark:bg-gray-800 backdrop-blur-xl bg-opacity-95 dark:bg-opacity-95 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 p-8 text-center transition-all duration-300">
|
||||
<!-- Mercedes-Benz Logo -->
|
||||
<div class="flex justify-center mb-6">
|
||||
<div class="w-16 h-16 text-gray-300 dark:text-gray-600 transition-transform duration-500 hover:scale-110">
|
||||
<svg class="w-full h-full" fill="currentColor" viewBox="0 0 80 80">
|
||||
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
|
||||
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
|
||||
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
|
||||
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
|
||||
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
|
||||
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<h1 class="text-6xl font-bold text-gray-300 dark:text-gray-600 mb-4">404</h1>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Seite nicht gefunden</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">Die von Ihnen gesuchte Seite existiert nicht oder wurde verschoben.</p>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row justify-center gap-4 mt-8">
|
||||
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center justify-center px-5 py-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium rounded-lg transition-all duration-300 transform hover:-translate-y-0.5">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||
</svg>
|
||||
<span>Zum Dashboard</span>
|
||||
</a>
|
||||
<button onclick="window.history.back()" class="inline-flex items-center justify-center px-5 py-3 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-all duration-300 transform hover:-translate-y-0.5">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
<span>Zurück</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
66
templates/errors/500.html
Normal file
66
templates/errors/500.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Interner Serverfehler - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-red-50 to-orange-50 dark:from-slate-900 dark:via-red-900/20 dark:to-orange-900/20 flex items-center justify-center px-4">
|
||||
<div class="max-w-2xl w-full text-center">
|
||||
<!-- Error Icon -->
|
||||
<div class="mb-8">
|
||||
<div class="inline-flex items-center justify-center w-24 h-24 bg-red-100 dark:bg-red-900/30 rounded-full mb-6">
|
||||
<svg class="w-12 h-12 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<h1 class="text-6xl font-bold text-slate-900 dark:text-white mb-4">500</h1>
|
||||
<h2 class="text-2xl font-semibold text-slate-700 dark:text-slate-300 mb-6">Interner Serverfehler</h2>
|
||||
<p class="text-lg text-slate-600 dark:text-slate-400 mb-8 max-w-lg mx-auto">
|
||||
Es ist ein unerwarteter Fehler aufgetreten. Unser Team wurde automatisch benachrichtigt und arbeitet an einer Lösung.
|
||||
</p>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="{{ url_for('dashboard') }}" class="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-xl transition-colors duration-200 shadow-lg hover:shadow-xl">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||
</svg>
|
||||
Zurück zum Dashboard
|
||||
</a>
|
||||
<button onclick="window.location.reload()" class="inline-flex items-center px-6 py-3 bg-slate-200 hover:bg-slate-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 font-medium rounded-xl transition-colors duration-200">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Seite neu laden
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Additional Info -->
|
||||
<div class="mt-12 p-6 bg-white/60 dark:bg-slate-800/60 backdrop-blur-sm rounded-2xl border border-slate-200 dark:border-slate-700">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-3">Was können Sie tun?</h3>
|
||||
<ul class="text-left text-slate-600 dark:text-slate-400 space-y-2">
|
||||
<li class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Versuchen Sie, die Seite neu zu laden
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Kehren Sie zum Dashboard zurück
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Kontaktieren Sie den Administrator, falls das Problem weiterhin besteht
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
410
templates/guest_job_status.html
Normal file
410
templates/guest_job_status.html
Normal file
@@ -0,0 +1,410 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Job-Status - {{ job.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
|
||||
|
||||
<!-- Header Section -->
|
||||
<div class="relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-blue-600/10 to-purple-600/10"></div>
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
|
||||
<h1 class="text-4xl lg:text-6xl font-bold text-slate-900 dark:text-white mb-6">
|
||||
<span class="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
Job-Status
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-xl text-slate-600 dark:text-slate-400 max-w-3xl mx-auto leading-relaxed">
|
||||
Überwachen Sie den aktuellen Status Ihres Druckauftrags in Echtzeit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10">
|
||||
|
||||
<!-- Status Container -->
|
||||
<div class="form-container professional-shadow p-8 lg:p-12">
|
||||
|
||||
<!-- Job-Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="w-20 h-20 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-slate-900 dark:text-white mb-4">
|
||||
{{ job.name }}
|
||||
</h2>
|
||||
<div id="statusBadge" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium">
|
||||
<!-- Wird von JavaScript aktualisiert -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live-Status-Updates -->
|
||||
<div id="liveStatusContainer" class="mb-8">
|
||||
<!-- Wird von JavaScript gefüllt -->
|
||||
</div>
|
||||
|
||||
<!-- Fortschrittsbalken -->
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">Fortschritt</span>
|
||||
<span id="progressText" class="text-sm text-slate-500 dark:text-slate-400">0%</span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-3">
|
||||
<div id="progressBar" class="bg-gradient-to-r from-blue-500 to-indigo-500 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job-Details -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Job-Details</h3>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-slate-500 dark:text-slate-400">Job-ID:</span>
|
||||
<span class="text-sm font-medium text-slate-900 dark:text-white">#{{ job.id }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-slate-500 dark:text-slate-400">Geplante Dauer:</span>
|
||||
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ job.duration_minutes }} Minuten</span>
|
||||
</div>
|
||||
|
||||
{% if job.start_at %}
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-slate-500 dark:text-slate-400">Gestartet:</span>
|
||||
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ job.start_at|format_datetime('%H:%M') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if job.end_at %}
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-slate-500 dark:text-slate-400">Geplantes Ende:</span>
|
||||
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ job.end_at|format_datetime('%H:%M') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Drucker-Details</h3>
|
||||
|
||||
{% if job.printer %}
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-slate-500 dark:text-slate-400">Drucker:</span>
|
||||
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ job.printer.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-slate-500 dark:text-slate-400">Standort:</span>
|
||||
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ job.printer.location or 'Unbekannt' }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Kein Drucker zugewiesen</p>
|
||||
{% endif %}
|
||||
|
||||
{% if guest_request %}
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-slate-500 dark:text-slate-400">Antragsteller:</span>
|
||||
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ guest_request.name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstatus -->
|
||||
<div class="bg-slate-50 dark:bg-slate-800 rounded-xl p-6 mb-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400" id="elapsedTime">--</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Verstrichene Zeit</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-green-600 dark:text-green-400" id="remainingTime">--</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Verbleibende Zeit</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-purple-600 dark:text-purple-400" id="currentTime">--</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Aktuelle Zeit</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktionen -->
|
||||
<div class="text-center">
|
||||
<div class="space-y-4 sm:space-y-0 sm:space-x-4 sm:flex sm:justify-center">
|
||||
<button onclick="refreshStatus()"
|
||||
class="w-full sm:w-auto px-6 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-colors duration-200">
|
||||
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Status aktualisieren
|
||||
</button>
|
||||
|
||||
<a href="{{ url_for('guest.guest_request_form') }}"
|
||||
class="w-full sm:w-auto inline-flex items-center justify-center px-6 py-3 bg-gray-600 text-white rounded-xl hover:bg-gray-700 transition-colors duration-200">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
Neue Anfrage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.dark .form-container {
|
||||
background: rgba(30, 41, 59, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.professional-shadow {
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.status-scheduled {
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-completed, .status-finished {
|
||||
background-color: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.status-failed, .status-cancelled {
|
||||
background-color: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.dark .status-scheduled {
|
||||
background-color: rgba(251, 191, 36, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.dark .status-running {
|
||||
background-color: rgba(34, 197, 94, 0.2);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.dark .status-completed, .dark .status-finished {
|
||||
background-color: rgba(59, 130, 246, 0.2);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.dark .status-failed, .dark .status-cancelled {
|
||||
background-color: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var jobId = parseInt('{{ job.id }}');
|
||||
var refreshInterval;
|
||||
var jobData = {};
|
||||
|
||||
// Initial load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
refreshStatus();
|
||||
startAutoRefresh();
|
||||
updateCurrentTime();
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
});
|
||||
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
const response = await fetch(`/api/guest/job/${jobId}/status`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
jobData = result.job;
|
||||
updateDisplay(jobData);
|
||||
} else {
|
||||
console.error('Fehler beim Laden des Job-Status:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Status-Update:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateDisplay(job) {
|
||||
// Status-Badge aktualisieren
|
||||
const statusBadge = document.getElementById('statusBadge');
|
||||
statusBadge.className = `inline-flex items-center px-4 py-2 rounded-full text-sm font-medium status-${job.status}`;
|
||||
statusBadge.innerHTML = getStatusText(job.status);
|
||||
|
||||
// Fortschrittsbalken
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressText = document.getElementById('progressText');
|
||||
progressBar.style.width = job.progress_percent + '%';
|
||||
progressText.textContent = job.progress_percent + '%';
|
||||
|
||||
// Fortschrittsbalken-Farbe je nach Status
|
||||
if (job.status === 'completed' || job.status === 'finished') {
|
||||
progressBar.className = 'bg-gradient-to-r from-green-500 to-emerald-500 h-3 rounded-full transition-all duration-300';
|
||||
} else if (job.status === 'running') {
|
||||
progressBar.className = 'bg-gradient-to-r from-blue-500 to-indigo-500 h-3 rounded-full transition-all duration-300';
|
||||
} else if (job.status === 'failed' || job.status === 'cancelled') {
|
||||
progressBar.className = 'bg-gradient-to-r from-red-500 to-rose-500 h-3 rounded-full transition-all duration-300';
|
||||
}
|
||||
|
||||
// Zeit-Updates
|
||||
updateTimeDisplays(job);
|
||||
|
||||
// Live-Status-Container
|
||||
updateLiveStatus(job);
|
||||
}
|
||||
|
||||
function updateTimeDisplays(job) {
|
||||
const now = new Date();
|
||||
|
||||
if (job.start_at && job.status === 'running') {
|
||||
const startTime = new Date(job.start_at);
|
||||
const elapsed = Math.floor((now - startTime) / 1000 / 60);
|
||||
document.getElementById('elapsedTime').textContent = elapsed + ' Min';
|
||||
|
||||
if (job.remaining_minutes !== undefined) {
|
||||
document.getElementById('remainingTime').textContent = job.remaining_minutes + ' Min';
|
||||
}
|
||||
} else {
|
||||
document.getElementById('elapsedTime').textContent = '--';
|
||||
document.getElementById('remainingTime').textContent = '--';
|
||||
}
|
||||
}
|
||||
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
document.getElementById('currentTime').textContent = now.toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function updateLiveStatus(job) {
|
||||
const container = document.getElementById('liveStatusContainer');
|
||||
let statusHtml = '';
|
||||
|
||||
if (job.status === 'scheduled') {
|
||||
statusHtml = `
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-xl p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center mr-4">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-yellow-800 dark:text-yellow-200">Job ist geplant</h3>
|
||||
<p class="text-yellow-700 dark:text-yellow-300">Der Job wartet auf den Start.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (job.status === 'running') {
|
||||
statusHtml = `
|
||||
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-700 rounded-xl p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center mr-4">
|
||||
<svg class="w-5 h-5 text-white animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-green-800 dark:text-green-200">Job läuft</h3>
|
||||
<p class="text-green-700 dark:text-green-300">Ihr Druckauftrag wird gerade ausgeführt.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (job.status === 'completed' || job.status === 'finished') {
|
||||
statusHtml = `
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-xl p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center mr-4">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-blue-800 dark:text-blue-200">Job abgeschlossen</h3>
|
||||
<p class="text-blue-700 dark:text-blue-300">Ihr Druckauftrag wurde erfolgreich abgeschlossen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (job.status === 'failed' || job.status === 'cancelled') {
|
||||
const statusText = job.status === 'failed' ? 'fehlgeschlagen' : 'abgebrochen';
|
||||
statusHtml = `
|
||||
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 rounded-xl p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-red-500 rounded-full flex items-center justify-center mr-4">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-red-800 dark:text-red-200">Job ${statusText}</h3>
|
||||
<p class="text-red-700 dark:text-red-300">Der Druckauftrag konnte nicht abgeschlossen werden.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = statusHtml;
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
const statusTexts = {
|
||||
'scheduled': 'Geplant',
|
||||
'running': 'Läuft',
|
||||
'completed': 'Abgeschlossen',
|
||||
'finished': 'Beendet',
|
||||
'failed': 'Fehlgeschlagen',
|
||||
'cancelled': 'Abgebrochen'
|
||||
};
|
||||
return statusTexts[status] || status;
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
// Nur bei aktiven Jobs automatisch aktualisieren
|
||||
if (jobData.is_active) {
|
||||
refreshInterval = setInterval(refreshStatus, 5000); // Alle 5 Sekunden
|
||||
} else if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-Refresh stoppen wenn Job abgeschlossen ist
|
||||
function checkAutoRefresh(job) {
|
||||
if (!job.is_active && refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
} else if (job.is_active && !refreshInterval) {
|
||||
refreshInterval = setInterval(refreshStatus, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup beim Verlassen der Seite
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
1545
templates/guest_request.html
Normal file
1545
templates/guest_request.html
Normal file
File diff suppressed because it is too large
Load Diff
289
templates/guest_requests_by_email.html
Normal file
289
templates/guest_requests_by_email.html
Normal 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 %}
|
||||
270
templates/guest_requests_overview.html
Normal file
270
templates/guest_requests_overview.html
Normal file
@@ -0,0 +1,270 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Druckanträge Übersicht - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<!-- Page Header -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 flex-shrink-0">
|
||||
<svg class="w-full h-full text-slate-900 dark:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Anträge Übersicht</h1>
|
||||
<p class="text-slate-500 dark:text-slate-400 mt-1">Transparente Übersicht aller eingereichten Druckanträge</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a href="{{ url_for('guest.guest_request_form') }}"
|
||||
class="btn-primary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
<span>Neuen Antrag stellen</span>
|
||||
</a>
|
||||
<button onclick="location.reload()"
|
||||
class="btn-secondary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
<span>Aktualisieren</span>
|
||||
</button>
|
||||
<a href="{{ url_for('index') }}"
|
||||
class="btn-secondary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||
</svg>
|
||||
<span>Startseite</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Banner -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">
|
||||
Datenschutz & Transparenz
|
||||
</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-4">
|
||||
Diese Übersicht zeigt alle eingereichten Druckanträge in anonymisierter Form. Persönliche Daten sind durch "***" zensiert, um die Privatsphäre zu schützen und gleichzeitig Transparenz über den Bearbeitungsstand zu gewährleisten.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
|
||||
Datenkonform
|
||||
</span>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
||||
Anonymisiert
|
||||
</span>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400">
|
||||
Transparent
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
{% set total_requests = requests|length %}
|
||||
{% set pending_requests = requests|selectattr("status", "equalto", "pending")|list|length %}
|
||||
{% set approved_requests = requests|selectattr("status", "equalto", "approved")|list|length %}
|
||||
{% set denied_requests = requests|selectattr("status", "equalto", "denied")|list|length %}
|
||||
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Gesamt</h3>
|
||||
<div class="stat-value">{{ total_requests }}</div>
|
||||
</div>
|
||||
<div class="mb-stat-icon text-slate-600 dark:text-slate-400">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Prüfung</h3>
|
||||
<div class="stat-value text-yellow-600 dark:text-yellow-400">{{ pending_requests }}</div>
|
||||
</div>
|
||||
<div class="mb-stat-icon text-yellow-600 dark:text-yellow-400">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Genehmigt</h3>
|
||||
<div class="stat-value text-green-600 dark:text-green-400">{{ approved_requests }}</div>
|
||||
</div>
|
||||
<div class="mb-stat-icon text-green-600 dark:text-green-400">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Abgelehnt</h3>
|
||||
<div class="stat-value text-red-600 dark:text-red-400">{{ denied_requests }}</div>
|
||||
</div>
|
||||
<div class="mb-stat-icon text-red-600 dark:text-red-400">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requests List -->
|
||||
{% if error %}
|
||||
<div class="dashboard-card p-8 text-center">
|
||||
<div class="text-red-500 dark:text-red-400 mb-4">
|
||||
<svg class="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Fehler beim Laden</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400">{{ error }}</p>
|
||||
</div>
|
||||
{% elif requests|length == 0 %}
|
||||
<div class="dashboard-card p-8 text-center">
|
||||
<div class="text-slate-400 dark:text-slate-500 mb-4">
|
||||
<svg class="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Keine Druckanträge</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400 mb-4">
|
||||
Derzeit sind keine Druckanträge vorhanden.
|
||||
</p>
|
||||
<a href="{{ url_for('guest.guest_request_form') }}"
|
||||
class="btn-primary">
|
||||
Ersten Antrag stellen
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="space-y-4">
|
||||
{% for request in requests %}
|
||||
<div class="dashboard-card p-6 border-l-4 {% if request.status == 'pending' %}border-yellow-400{% elif request.status == 'approved' %}border-green-400{% elif request.status == 'denied' %}border-red-400{% endif %}">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||
|
||||
<!-- Left Section: Request Info -->
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<span class="text-lg font-bold text-slate-900 dark:text-white">
|
||||
#{{ request.id }}
|
||||
</span>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {% if request.status == 'pending' %}bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400{% elif request.status == 'approved' %}bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400{% elif request.status == 'denied' %}bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400{% endif %}">
|
||||
{% if request.status == 'pending' %}
|
||||
Wird geprüft
|
||||
{% elif request.status == 'approved' %}
|
||||
Genehmigt
|
||||
{% elif request.status == 'denied' %}
|
||||
Abgelehnt
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if request.job_status %}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400">
|
||||
Druckstatus: {{ request.job_status|title }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
||||
<div class="bg-gray-50 dark:bg-slate-700/30 p-3 rounded-lg">
|
||||
<div class="text-slate-500 dark:text-slate-400 font-medium mb-1">Antragsteller</div>
|
||||
<div class="font-mono text-slate-600 dark:text-slate-300">{{ request.name }}</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-slate-700/30 p-3 rounded-lg">
|
||||
<div class="text-slate-500 dark:text-slate-400 font-medium mb-1">E-Mail</div>
|
||||
<div class="font-mono text-slate-600 dark:text-slate-300">{{ request.email }}</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-slate-700/30 p-3 rounded-lg">
|
||||
<div class="text-slate-500 dark:text-slate-400 font-medium mb-1">Drucker</div>
|
||||
<div class="text-slate-600 dark:text-slate-300">{{ request.printer_name if request.printer_name else 'Automatisch' }}</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-slate-700/30 p-3 rounded-lg">
|
||||
<div class="text-slate-500 dark:text-slate-400 font-medium mb-1">Dauer</div>
|
||||
<div class="text-slate-600 dark:text-slate-300">{{ request.duration_min }} Min</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.reason %}
|
||||
<div class="mt-4 bg-gray-50 dark:bg-slate-700/30 p-3 rounded-lg">
|
||||
<div class="text-slate-500 dark:text-slate-400 font-medium mb-2">Projektbeschreibung</div>
|
||||
<div class="font-mono text-sm text-slate-600 dark:text-slate-300">{{ request.reason }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Right Section: Timestamp and Actions -->
|
||||
<div class="lg:text-right">
|
||||
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg text-center lg:text-right">
|
||||
<div class="text-sm font-medium text-slate-900 dark:text-white mb-1">
|
||||
{{ request.created_at.strftime('%d.%m.%Y') }}
|
||||
</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">
|
||||
{{ request.created_at.strftime('%H:%M') }} Uhr
|
||||
</div>
|
||||
|
||||
{% if request.status == 'approved' %}
|
||||
<div class="mt-3">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
||||
Startbereit
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Live-Zeitanzeige im Browser-Tab
|
||||
function updateTabTitle() {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString('de-DE');
|
||||
document.title = `Druckanträge (${timeString}) - Mercedes-Benz MYP Platform`;
|
||||
}
|
||||
|
||||
updateTabTitle();
|
||||
setInterval(updateTabTitle, 1000);
|
||||
|
||||
// Auto-Refresh alle 30 Sekunden
|
||||
setInterval(function() {
|
||||
if (!document.hidden) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
347
templates/guest_start_job.html
Normal file
347
templates/guest_start_job.html
Normal file
@@ -0,0 +1,347 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Job mit Code starten - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<!-- Page Header -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 flex-shrink-0">
|
||||
<svg class="w-full h-full text-slate-900 dark:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Job starten</h1>
|
||||
<p class="text-slate-500 dark:text-slate-400 mt-1">Geben Sie Ihren 6-stelligen Zugangscode ein</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a href="{{ url_for('guest.guest_request_form') }}"
|
||||
class="btn-secondary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
<span>Neue Anfrage</span>
|
||||
</a>
|
||||
<a href="{{ url_for('index') }}"
|
||||
class="btn-secondary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||
</svg>
|
||||
<span>Startseite</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Message (versteckt) -->
|
||||
<div id="successMessage" class="hidden">
|
||||
<div class="dashboard-card p-6 border-l-4 border-green-400">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-green-900 dark:text-green-100 mb-2">Job erfolgreich gestartet!</h3>
|
||||
<p class="text-green-700 dark:text-green-300" id="successDetails"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message (versteckt) -->
|
||||
<div id="errorMessage" class="hidden">
|
||||
<div class="dashboard-card p-6 border-l-4 border-red-400">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-red-900 dark:text-red-100 mb-2">Fehler beim Starten</h3>
|
||||
<p class="text-red-700 dark:text-red-300" id="errorDetails"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Code-Eingabe Container -->
|
||||
<div class="dashboard-card p-8">
|
||||
<div class="text-center mb-8">
|
||||
<div class="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-xl flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white mb-3">
|
||||
Zugangscode eingeben
|
||||
</h2>
|
||||
<p class="text-slate-500 dark:text-slate-400">
|
||||
Ihr persönlicher 6-stelliger Code wurde Ihnen nach der Genehmigung mitgeteilt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form id="codeForm" class="max-w-lg mx-auto">
|
||||
<!-- Code-Eingabe -->
|
||||
<div class="mb-8">
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-6 text-center">
|
||||
6-stelliger Zugangscode <span class="text-red-500">*</span>
|
||||
</label>
|
||||
|
||||
<!-- Code-Input mit einzelnen Feldern -->
|
||||
<div class="flex justify-center gap-3 mb-6">
|
||||
<input type="text" id="code1" maxlength="1"
|
||||
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
|
||||
oninput="moveToNext(this, 'code2')" onkeydown="handleBackspace(event, this, null)">
|
||||
<input type="text" id="code2" maxlength="1"
|
||||
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
|
||||
oninput="moveToNext(this, 'code3')" onkeydown="handleBackspace(event, this, 'code1')">
|
||||
<input type="text" id="code3" maxlength="1"
|
||||
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
|
||||
oninput="moveToNext(this, 'code4')" onkeydown="handleBackspace(event, this, 'code2')">
|
||||
<input type="text" id="code4" maxlength="1"
|
||||
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
|
||||
oninput="moveToNext(this, 'code5')" onkeydown="handleBackspace(event, this, 'code3')">
|
||||
<input type="text" id="code5" maxlength="1"
|
||||
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
|
||||
oninput="moveToNext(this, 'code6')" onkeydown="handleBackspace(event, this, 'code4')">
|
||||
<input type="text" id="code6" maxlength="1"
|
||||
class="code-input w-12 h-12 text-center text-xl font-bold border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300"
|
||||
oninput="moveToNext(this, null)" onkeydown="handleBackspace(event, this, 'code5')">
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm">
|
||||
Der Code besteht aus 6 Zeichen (Großbuchstaben und Zahlen)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="text-center">
|
||||
<button type="submit" id="submitBtn"
|
||||
class="btn-primary disabled:opacity-50 disabled:cursor-not-allowed px-8 py-3 flex items-center gap-2 mx-auto">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M12 5v.01M12 19v.01M12 12h.01M12 9a3 3 0 100-6 3 3 0 000 6zm0 0a3 3 0 100 6 3 3 0 000-6z"/>
|
||||
</svg>
|
||||
Job jetzt starten
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Hilfe-Sektion -->
|
||||
<div class="dashboard-card p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6 text-center">
|
||||
Brauchen Sie Hilfe?
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="text-center p-4 bg-gray-50 dark:bg-slate-700/30 rounded-lg">
|
||||
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/30 rounded-lg mx-auto mb-3 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">Ihr Zugangscode wurde Ihnen nach der Genehmigung mitgeteilt</p>
|
||||
</div>
|
||||
<div class="text-center p-4 bg-gray-50 dark:bg-slate-700/30 rounded-lg">
|
||||
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/30 rounded-lg mx-auto mb-3 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">Der Code ist nur einmalig verwendbar und hat eine begrenzte Gültigkeit</p>
|
||||
</div>
|
||||
<div class="text-center p-4 bg-gray-50 dark:bg-slate-700/30 rounded-lg">
|
||||
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/30 rounded-lg mx-auto mb-3 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M12 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">Bei Problemen wenden Sie sich an den Administrator</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Code-Eingabe-Logik
|
||||
function moveToNext(current, nextId) {
|
||||
const value = current.value.toUpperCase();
|
||||
|
||||
// Nur alphanumerische Zeichen erlauben
|
||||
if (!/^[A-Z0-9]$/.test(value)) {
|
||||
current.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
current.value = value;
|
||||
current.classList.add('border-green-500', 'bg-green-50', 'dark:bg-green-900/20');
|
||||
|
||||
// Zum nächsten Feld wechseln
|
||||
if (nextId && value) {
|
||||
document.getElementById(nextId).focus();
|
||||
}
|
||||
|
||||
// Prüfen ob alle Felder ausgefüllt sind
|
||||
checkFormComplete();
|
||||
}
|
||||
|
||||
function handleBackspace(event, current, prevId) {
|
||||
if (event.key === 'Backspace') {
|
||||
if (current.value === '' && prevId) {
|
||||
event.preventDefault();
|
||||
const prevField = document.getElementById(prevId);
|
||||
prevField.focus();
|
||||
prevField.value = '';
|
||||
prevField.classList.remove('border-green-500', 'bg-green-50', 'dark:bg-green-900/20');
|
||||
} else if (current.value !== '') {
|
||||
current.classList.remove('border-green-500', 'bg-green-50', 'dark:bg-green-900/20');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkFormComplete() {
|
||||
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
|
||||
const allFilled = inputs.every(id => document.getElementById(id).value !== '');
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = !allFilled;
|
||||
}
|
||||
|
||||
function getCodeValue() {
|
||||
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
|
||||
return inputs.map(id => document.getElementById(id).value).join('');
|
||||
}
|
||||
|
||||
function clearCode() {
|
||||
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
|
||||
inputs.forEach(id => {
|
||||
const input = document.getElementById(id);
|
||||
input.value = '';
|
||||
input.classList.remove('border-green-500', 'bg-green-50', 'dark:bg-green-900/20');
|
||||
});
|
||||
checkFormComplete();
|
||||
document.getElementById('code1').focus();
|
||||
}
|
||||
|
||||
function showSuccess(message, details) {
|
||||
document.getElementById('successMessage').classList.remove('hidden');
|
||||
document.getElementById('successDetails').textContent = details;
|
||||
document.getElementById('errorMessage').classList.add('hidden');
|
||||
|
||||
// Scroll zur Nachricht
|
||||
document.getElementById('successMessage').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
function showError(message, details) {
|
||||
document.getElementById('errorMessage').classList.remove('hidden');
|
||||
document.getElementById('errorDetails').textContent = details;
|
||||
document.getElementById('successMessage').classList.add('hidden');
|
||||
|
||||
// Scroll zur Nachricht
|
||||
document.getElementById('errorMessage').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
// Form-Submit-Handler
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('codeForm');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
// Erstes Feld fokussieren
|
||||
document.getElementById('code1').focus();
|
||||
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const code = getCodeValue();
|
||||
if (code.length !== 6) {
|
||||
showError('Ungültiger Code', 'Bitte geben Sie alle 6 Zeichen ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Button-Animation
|
||||
submitBtn.disabled = true;
|
||||
const originalContent = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = `
|
||||
<svg class="animate-spin w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Wird überprüft...
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/guest/start-job', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({ code: code })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showSuccess(
|
||||
'Job erfolgreich gestartet!',
|
||||
`Ihr Job "${result.job_name}" wurde gestartet und läuft bis ${result.end_time}.`
|
||||
);
|
||||
|
||||
// Form deaktivieren
|
||||
form.style.opacity = '0.5';
|
||||
form.style.pointerEvents = 'none';
|
||||
|
||||
// Nach 3 Sekunden zur Job-Status-Seite weiterleiten
|
||||
setTimeout(() => {
|
||||
if (result.job_id) {
|
||||
window.location.href = `/guest/job/${result.job_id}/status`;
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
} else {
|
||||
showError('Code ungültig', result.error || 'Der eingegebene Code ist ungültig oder bereits verwendet.');
|
||||
clearCode();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showError('Verbindungsfehler', 'Es gab ein Problem bei der Verbindung. Bitte versuchen Sie es erneut.');
|
||||
clearCode();
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalContent;
|
||||
checkFormComplete();
|
||||
}
|
||||
});
|
||||
|
||||
// Paste-Handler für kompletten Code
|
||||
document.addEventListener('paste', function(e) {
|
||||
const target = e.target;
|
||||
if (target.classList.contains('code-input')) {
|
||||
e.preventDefault();
|
||||
const paste = (e.clipboardData || window.clipboardData).getData('text').toUpperCase();
|
||||
|
||||
if (paste.length === 6 && /^[A-Z0-9]+$/.test(paste)) {
|
||||
const inputs = ['code1', 'code2', 'code3', 'code4', 'code5', 'code6'];
|
||||
inputs.forEach((id, index) => {
|
||||
const input = document.getElementById(id);
|
||||
input.value = paste[index] || '';
|
||||
if (input.value) {
|
||||
input.classList.add('border-green-500', 'bg-green-50', 'dark:bg-green-900/20');
|
||||
}
|
||||
});
|
||||
checkFormComplete();
|
||||
document.getElementById('code6').focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
334
templates/guest_status.html
Normal file
334
templates/guest_status.html
Normal file
@@ -0,0 +1,334 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Anfrage-Status - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<meta http-equiv="refresh" content="30">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<!-- Page Header -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 flex-shrink-0">
|
||||
<svg class="w-full h-full text-slate-900 dark:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Anfrage-Status</h1>
|
||||
<p class="text-slate-500 dark:text-slate-400 mt-1">Verfolgen Sie den Status Ihrer Gastanfrage für 3D-Druck</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button onclick="location.reload()"
|
||||
class="btn-secondary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
<span>Aktualisieren</span>
|
||||
</button>
|
||||
<a href="{{ url_for('guest.guest_request_form') }}"
|
||||
class="btn-primary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
<span>Neue Anfrage</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<div class="dashboard-card p-6 text-center">
|
||||
{% if request.status == 'pending' %}
|
||||
<div class="inline-flex items-center px-6 py-3 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-lg font-semibold">
|
||||
<svg class="w-6 h-6 mr-3 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Wird geprüft
|
||||
</div>
|
||||
{% elif request.status == 'approved' %}
|
||||
<div class="inline-flex items-center px-6 py-3 rounded-full bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 text-lg font-semibold">
|
||||
<svg class="w-6 h-6 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
Genehmigt
|
||||
</div>
|
||||
{% elif request.status == 'denied' %}
|
||||
<div class="inline-flex items-center px-6 py-3 rounded-full bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 text-lg font-semibold">
|
||||
<svg class="w-6 h-6 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
Abgelehnt
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Anfrage-Details -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white mb-2">
|
||||
Anfrage Details
|
||||
</h2>
|
||||
<p class="text-slate-500 dark:text-slate-400">
|
||||
Übersicht Ihrer eingereichten Gastanfrage
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Anfrage-ID</h3>
|
||||
<p class="text-xl font-bold text-slate-900 dark:text-white">#{{ request.id }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Erstellt am</h3>
|
||||
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ request.created_at|format_datetime }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Name</h3>
|
||||
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ request.name }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Gewünschte Dauer</h3>
|
||||
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ request.duration_min }} Minuten</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.reason %}
|
||||
<div class="mt-6 bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Begründung</h3>
|
||||
<p class="text-slate-700 dark:text-slate-300">{{ request.reason }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.printer %}
|
||||
<div class="mt-6 bg-gray-50 dark:bg-slate-700/30 p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">Drucker</h3>
|
||||
<p class="text-slate-700 dark:text-slate-300">{{ request.printer.name }} {% if request.printer.location %}({{ request.printer.location }}){% endif %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Status-spezifische Inhalte -->
|
||||
{% if request.status == 'pending' %}
|
||||
<div class="dashboard-card p-6 border-l-4 border-yellow-400">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 bg-yellow-100 dark:bg-yellow-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Ihre Anfrage wird geprüft</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400">
|
||||
Unser Team prüft Ihre Anfrage mit höchster Priorität. Sie erhalten eine sofortige Benachrichtigung, sobald eine Entscheidung getroffen wurde.
|
||||
Diese Seite aktualisiert sich automatisch alle 30 Sekunden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif request.status == 'approved' %}
|
||||
<div class="dashboard-card p-6 border-l-4 border-green-400">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Anfrage genehmigt!</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400">Ihr Druckauftrag wurde genehmigt und ist bereit zum Start.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if otp_code %}
|
||||
<!-- Code-Anzeige -->
|
||||
<div class="dashboard-card p-6">
|
||||
<h4 class="text-xl font-bold text-slate-900 dark:text-white mb-6 text-center">Ihr Zugangscode</h4>
|
||||
|
||||
<!-- 6-stelliger Code in schönen Boxen -->
|
||||
<div class="flex justify-center gap-3 mb-6">
|
||||
{% for char in otp_code %}
|
||||
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 text-blue-900 dark:text-blue-100 rounded-lg flex items-center justify-center text-2xl font-bold">
|
||||
{{ char }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Code zum Kopieren -->
|
||||
<div class="text-center mb-6">
|
||||
<div class="inline-flex items-center bg-gray-50 dark:bg-slate-700/30 rounded-lg px-4 py-3">
|
||||
<span class="text-xl font-mono font-bold text-slate-900 dark:text-white mr-3" id="otpCode">{{ otp_code }}</span>
|
||||
<button onclick="copyCode()" class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors" title="Code kopieren">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wichtige Hinweise -->
|
||||
<div class="bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-8 h-8 bg-orange-100 dark:bg-orange-900/50 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-5 h-5 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.734 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h5 class="text-sm font-semibold text-orange-800 dark:text-orange-300 mb-2">Wichtige Hinweise:</h5>
|
||||
<ul class="text-sm text-orange-700 dark:text-orange-400 space-y-1">
|
||||
<li>• Dieser Code ist nur <strong>einmalig verwendbar</strong></li>
|
||||
<li>• Notieren Sie sich den Code oder speichern Sie diese Seite</li>
|
||||
<li>• Bei Verlust des Codes kontaktieren Sie den Administrator</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Start-Button -->
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('guest.guest_start_job_form') }}"
|
||||
class="btn-primary flex items-center gap-2 mx-auto">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M12 5v.01M12 19v.01M12 12h.01M12 9a3 3 0 100-6 3 3 0 000 6zm0 0a3 3 0 100 6 3 3 0 000-6z"/>
|
||||
</svg>
|
||||
Job jetzt starten
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% elif show_start_link %}
|
||||
<!-- Code bereits vorhanden, aber noch nicht verwendet -->
|
||||
<div class="dashboard-card p-6 text-center">
|
||||
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Zugangscode bereits generiert</h4>
|
||||
<p class="text-slate-500 dark:text-slate-400 mb-6">Ihr persönlicher Code wurde bereits erstellt und ist bereit zur Verwendung.</p>
|
||||
|
||||
<a href="{{ url_for('guest.guest_start_job_form') }}"
|
||||
class="btn-primary flex items-center gap-2 mx-auto">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-3a1 1 0 011-1h2.586l6.243-6.243A6 6 0 0121 9z"/>
|
||||
</svg>
|
||||
Code eingeben und starten
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif request.status == 'denied' %}
|
||||
<div class="dashboard-card p-6 border-l-4 border-red-400">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Anfrage abgelehnt</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400">
|
||||
Ihre Anfrage wurde leider abgelehnt. Bei Fragen wenden Sie sich bitte an unser Team.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Code-Kopier-Funktion
|
||||
function copyCode() {
|
||||
const codeElement = document.getElementById('otpCode');
|
||||
if (codeElement) {
|
||||
const code = codeElement.textContent;
|
||||
|
||||
// Versuche den Code in die Zwischenablage zu kopieren
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(code).then(function() {
|
||||
showCopySuccess();
|
||||
}).catch(function(err) {
|
||||
console.error('Fehler beim Kopieren:', err);
|
||||
fallbackCopyText(code);
|
||||
});
|
||||
} else {
|
||||
fallbackCopyText(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback für ältere Browser
|
||||
function fallbackCopyText(text) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
showCopySuccess();
|
||||
} catch (err) {
|
||||
console.error('Fallback-Kopieren fehlgeschlagen:', err);
|
||||
alert('Code konnte nicht kopiert werden. Bitte manuell markieren und kopieren.');
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Erfolgs-Animation für das Kopieren
|
||||
function showCopySuccess() {
|
||||
const button = event.target.closest('button');
|
||||
const originalHTML = button.innerHTML;
|
||||
|
||||
button.innerHTML = `
|
||||
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Nach 2 Sekunden zurück zum Original
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
|
||||
// Optional: Toast-Benachrichtigung
|
||||
showToast('Code wurde in die Zwischenablage kopiert!');
|
||||
}
|
||||
|
||||
// Toast-Benachrichtigung
|
||||
function showToast(message) {
|
||||
// Toast-Element erstellen, falls nicht vorhanden
|
||||
let toast = document.getElementById('copyToast');
|
||||
if (!toast) {
|
||||
toast = document.createElement('div');
|
||||
toast.id = 'copyToast';
|
||||
toast.className = 'fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 translate-x-full opacity-0 z-50';
|
||||
document.body.appendChild(toast);
|
||||
}
|
||||
|
||||
toast.textContent = message;
|
||||
|
||||
// Animation einblenden
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-x-full', 'opacity-0');
|
||||
}, 100);
|
||||
|
||||
// Nach 3 Sekunden ausblenden
|
||||
setTimeout(() => {
|
||||
toast.classList.add('translate-x-full', 'opacity-0');
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
442
templates/guest_status_check.html
Normal file
442
templates/guest_status_check.html
Normal 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
203
templates/imprint.html
Normal 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
1271
templates/index.html
Normal file
File diff suppressed because it is too large
Load Diff
2198
templates/jobs.html
Normal file
2198
templates/jobs.html
Normal file
File diff suppressed because it is too large
Load Diff
116
templates/jobs/new.html
Normal file
116
templates/jobs/new.html
Normal file
@@ -0,0 +1,116 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Neuer Druckauftrag - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<!-- Page Header -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="w-12 h-12 bg-mercedes-blue text-white rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-mercedes-black dark:text-white">Neuer Druckauftrag</h1>
|
||||
<p class="text-mercedes-gray dark:text-slate-400">Erstellen Sie einen neuen 3D-Druckauftrag</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Creation Form -->
|
||||
<div class="dashboard-card p-8">
|
||||
<form id="newJobForm" action="{{ url_for('create_job') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Job Details -->
|
||||
<div>
|
||||
<label for="job_title" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
|
||||
Job-Titel <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" id="job_title" name="job_title" required
|
||||
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg"
|
||||
placeholder="Beschreibender Titel für den Druckauftrag">
|
||||
</div>
|
||||
|
||||
<!-- Printer Selection -->
|
||||
<div>
|
||||
<label for="printer_id" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
|
||||
Drucker <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select id="printer_id" name="printer_id" required
|
||||
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg">
|
||||
<option value="">Drucker auswählen...</option>
|
||||
{% for printer in printers %}
|
||||
<option value="{{ printer.id }}">{{ printer.name }} ({{ printer.location }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Start Time -->
|
||||
<div>
|
||||
<label for="start_time" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
|
||||
Startzeit <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="datetime-local" id="start_time" name="start_time" required
|
||||
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg">
|
||||
</div>
|
||||
|
||||
<!-- Duration -->
|
||||
<div>
|
||||
<label for="duration" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
|
||||
Geschätzte Dauer (Minuten) <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="number" id="duration" name="duration" required min="1" max="7200"
|
||||
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg"
|
||||
placeholder="60">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
|
||||
Beschreibung
|
||||
</label>
|
||||
<textarea id="description" name="description" rows="3"
|
||||
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg resize-none"
|
||||
placeholder="Beschreibung des Druckauftrags..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- File Upload -->
|
||||
<div>
|
||||
<label for="stl_file" class="block text-sm font-medium text-mercedes-black dark:text-slate-300 mb-2">
|
||||
3D-Datei hochladen (optional)
|
||||
</label>
|
||||
<input type="file" id="stl_file" name="stl_file" accept=".stl,.obj,.3mf,.gcode"
|
||||
class="block w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg">
|
||||
<p class="text-sm text-mercedes-gray dark:text-slate-400 mt-1">
|
||||
Unterstützte Formate: STL, OBJ, 3MF, GCODE (max. 100MB)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="flex items-center justify-end pt-6 border-t border-gray-200 dark:border-slate-600">
|
||||
<a href="{{ url_for('jobs_page') }}" class="btn-secondary mr-3">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit" class="btn-primary">
|
||||
Auftrag erstellen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Set default start time to now + 1 hour
|
||||
const now = new Date();
|
||||
now.setHours(now.getHours() + 1);
|
||||
const formatted = now.toISOString().slice(0, 16);
|
||||
document.getElementById('start_time').value = formatted;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
510
templates/legal.html
Normal file
510
templates/legal.html
Normal 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
1058
templates/login.html
Normal file
File diff suppressed because it is too large
Load Diff
850
templates/new_job.html
Normal file
850
templates/new_job.html
Normal file
@@ -0,0 +1,850 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Neuer Druckauftrag - MYP Platform{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Mercedes-Benz Corporate Design */
|
||||
.text-mercedes-black { color: #000000; }
|
||||
.text-mercedes-gray { color: #6b7280; }
|
||||
.text-mercedes-silver { color: #9ca3af; }
|
||||
.text-mercedes-blue { color: #0073ce; }
|
||||
.text-mercedes-green { color: #008c32; }
|
||||
.text-mercedes-red { color: #dc2626; }
|
||||
|
||||
.bg-mercedes-black { background-color: #000000; }
|
||||
.bg-mercedes-silver { background-color: #e5e7eb; }
|
||||
.bg-mercedes-blue { background-color: #0073ce; }
|
||||
.bg-mercedes-green { background-color: #008c32; }
|
||||
|
||||
.border-mercedes-silver { border-color: #d1d5db; }
|
||||
.border-mercedes-blue { border-color: #0073ce; }
|
||||
|
||||
.hover\:border-mercedes-blue:hover { border-color: #0073ce; }
|
||||
.focus\:ring-mercedes-blue:focus {
|
||||
--tw-ring-color: #0073ce;
|
||||
--tw-ring-opacity: 0.5;
|
||||
}
|
||||
.focus\:border-mercedes-blue:focus { border-color: #0073ce; }
|
||||
|
||||
/* Mercedes Card Effect */
|
||||
.mercedes-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .mercedes-card {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||
border-color: #334155;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.mercedes-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Mercedes Button */
|
||||
.mercedes-button {
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mercedes-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.mercedes-button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
/* Enhanced File Upload Area - KRITISCHER FIX */
|
||||
#file-upload-area {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border: 2px dashed #cbd5e1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer !important;
|
||||
user-select: none;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dark #file-upload-area {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
#file-upload-area:hover {
|
||||
border-color: #0073ce;
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
#file-upload-area.drag-over {
|
||||
border-color: #0073ce;
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.dark #file-upload-area.drag-over {
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
|
||||
}
|
||||
|
||||
/* WICHTIG: Sicherstellen dass alle Child-Elemente klickbar sind */
|
||||
#file-upload-area * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* File Preview Animation */
|
||||
#file-preview {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Progress Ring for Upload */
|
||||
.progress-ring {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.progress-ring__circle {
|
||||
stroke: #0073ce;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 4;
|
||||
fill: transparent;
|
||||
r: 26;
|
||||
cx: 30;
|
||||
cy: 30;
|
||||
stroke-dasharray: 163.36;
|
||||
stroke-dashoffset: 163.36;
|
||||
transition: stroke-dashoffset 0.3s ease;
|
||||
}
|
||||
|
||||
/* Form Enhancements */
|
||||
.form-input {
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #0073ce;
|
||||
box-shadow: 0 0 0 3px rgba(0, 115, 206, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dark .form-input {
|
||||
background-color: #1e293b;
|
||||
border-color: #475569;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.dark .form-input:focus {
|
||||
border-color: #0ea5e9;
|
||||
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
||||
}
|
||||
|
||||
/* Loading Overlay */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.dark .loading-content {
|
||||
background: #1e293b;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
/* Upload Area Debug */
|
||||
.upload-debug {
|
||||
border: 2px solid red !important;
|
||||
background: rgba(255, 0, 0, 0.1) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-mercedes-black">Neuer Druckauftrag</h1>
|
||||
<p class="mt-2 text-mercedes-gray">Erstellen Sie einen neuen 3D-Druckauftrag</p>
|
||||
</div>
|
||||
<a href="/jobs" class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Zurück zu Aufträgen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Debug Info -->
|
||||
<div id="debug-info" class="mb-4 p-3 bg-blue-100 border border-blue-200 rounded-lg hidden">
|
||||
<h3 class="font-bold text-blue-800">Debug Information:</h3>
|
||||
<p id="debug-text" class="text-blue-700"></p>
|
||||
</div>
|
||||
|
||||
<!-- Job Creation Form -->
|
||||
<div class="mercedes-card rounded-xl p-8">
|
||||
<form id="jobForm" class="space-y-8">
|
||||
<!-- File Upload Section -->
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-mercedes-black mb-4">
|
||||
3D-Datei hochladen
|
||||
<span class="text-sm font-normal text-mercedes-gray">(STL, GCODE, 3MF, OBJ)</span>
|
||||
</h2>
|
||||
|
||||
<!-- Hidden File Input -->
|
||||
<input type="file" id="file-input" name="file" accept=".stl,.gcode,.3mf,.obj" class="hidden">
|
||||
|
||||
<!-- Klickbarer Upload-Bereich -->
|
||||
<div id="file-upload-area" class="border-2 border-dashed border-mercedes-silver rounded-xl p-8 text-center hover:border-mercedes-blue transition-all duration-200">
|
||||
|
||||
<div id="upload-placeholder" class="space-y-4">
|
||||
<svg class="h-16 w-16 text-mercedes-silver mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-lg font-medium text-mercedes-black">Datei hier ablegen oder klicken zum Auswählen</p>
|
||||
<p class="text-sm text-mercedes-gray mt-2">Unterstützte Formate: STL, GCODE, 3MF, OBJ (max. 100MB)</p>
|
||||
<button type="button" id="upload-trigger" class="mt-4 bg-mercedes-blue hover:bg-blue-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
📁 Datei auswählen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="file-preview" class="hidden space-y-4">
|
||||
<svg class="h-16 w-16 text-mercedes-green mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p id="file-name" class="text-lg font-medium text-mercedes-black"></p>
|
||||
<p id="file-size" class="text-sm text-mercedes-gray"></p>
|
||||
<p id="file-type" class="text-xs text-mercedes-blue font-medium mt-1"></p>
|
||||
</div>
|
||||
<button type="button" id="clear-file-btn" class="text-mercedes-red hover:text-red-700 text-sm transition-colors duration-200">
|
||||
🗑️ Datei entfernen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status-Anzeige -->
|
||||
<div id="upload-status" class="mt-4 hidden">
|
||||
<div class="bg-green-100 border border-green-200 rounded-lg p-3">
|
||||
<p class="text-green-800 text-sm">✅ Datei erfolgreich ausgewählt</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Settings Section -->
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-mercedes-black mb-4">Druckeinstellungen</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Printer Selection -->
|
||||
<div>
|
||||
<label for="printer-select" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Drucker auswählen *
|
||||
</label>
|
||||
<select id="printer-select" name="printer_id" required
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
<option value="">Drucker auswählen...</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-mercedes-gray">Nur verfügbare Drucker werden angezeigt</p>
|
||||
</div>
|
||||
|
||||
<!-- Priority -->
|
||||
<div>
|
||||
<label for="priority-select" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Priorität
|
||||
</label>
|
||||
<select id="priority-select" name="priority"
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
<option value="normal">Normal</option>
|
||||
<option value="high">Hoch</option>
|
||||
<option value="urgent">Dringend</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Material -->
|
||||
<div>
|
||||
<label for="material-input" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Material
|
||||
</label>
|
||||
<input type="text" id="material-input" name="material" placeholder="z.B. PLA, ABS, PETG"
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
</div>
|
||||
|
||||
<!-- Color -->
|
||||
<div>
|
||||
<label for="color-input" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Farbe
|
||||
</label>
|
||||
<input type="text" id="color-input" name="color" placeholder="z.B. Weiß, Schwarz, Rot"
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
</div>
|
||||
|
||||
<!-- Layer Height -->
|
||||
<div>
|
||||
<label for="layer-height-select" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Schichthöhe
|
||||
</label>
|
||||
<select id="layer-height-select" name="layer_height"
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
<option value="">Standard</option>
|
||||
<option value="0.1">0.1mm (Hoch)</option>
|
||||
<option value="0.2">0.2mm (Normal)</option>
|
||||
<option value="0.3">0.3mm (Schnell)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Infill -->
|
||||
<div>
|
||||
<label for="infill-select" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Füllung
|
||||
</label>
|
||||
<select id="infill-select" name="infill"
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
<option value="">Standard</option>
|
||||
<option value="10">10% (Leicht)</option>
|
||||
<option value="20">20% (Normal)</option>
|
||||
<option value="50">50% (Stabil)</option>
|
||||
<option value="100">100% (Massiv)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Section -->
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-mercedes-black mb-4">Zusätzliche Informationen</h2>
|
||||
|
||||
<div>
|
||||
<label for="notes-textarea" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Notizen
|
||||
</label>
|
||||
<textarea id="notes-textarea" name="notes" rows="4"
|
||||
placeholder="Besondere Anweisungen oder Hinweise für den Druck..."
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Section -->
|
||||
<div class="flex items-center justify-between pt-6 border-t border-mercedes-silver">
|
||||
<div class="text-sm text-mercedes-gray">
|
||||
<p>* Pflichtfelder</p>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-4">
|
||||
<button type="button" id="reset-btn"
|
||||
class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
Zurücksetzen
|
||||
</button>
|
||||
<button type="submit" id="submit-button" disabled
|
||||
class="bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span id="submit-text">Auftrag erstellen</span>
|
||||
<svg id="submit-spinner" class="hidden animate-spin h-5 w-5 inline ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Globale Variablen
|
||||
let selectedFile = null;
|
||||
let printers = [];
|
||||
let isUploading = false;
|
||||
|
||||
// Debug-Funktion
|
||||
function debugLog(message) {
|
||||
console.log('🔧 DEBUG:', message);
|
||||
const debugInfo = document.getElementById('debug-info');
|
||||
const debugText = document.getElementById('debug-text');
|
||||
if (debugInfo && debugText) {
|
||||
debugText.textContent = message;
|
||||
debugInfo.classList.remove('hidden');
|
||||
setTimeout(() => debugInfo.classList.add('hidden'), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// API-Hilfsfunktion mit besserer Fehlerbehandlung
|
||||
async function apiCall(url, options = {}) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API-Fehler:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Toast-Benachrichtigung (Fallback falls nicht verfügbar)
|
||||
function showToast(message, type = 'info') {
|
||||
if (window.showToast) {
|
||||
window.showToast(message, type);
|
||||
} else {
|
||||
// Fallback: Einfache Alert
|
||||
const colors = {
|
||||
'success': '✅',
|
||||
'error': '❌',
|
||||
'warning': '⚠️',
|
||||
'info': 'ℹ️'
|
||||
};
|
||||
console.log(`${colors[type] || 'ℹ️'} ${message}`);
|
||||
|
||||
// Temporäre Anzeige in der Seite
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 p-4 rounded-lg text-white z-50 ${type === 'error' ? 'bg-red-500' : type === 'success' ? 'bg-green-500' : type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Dokumentbereitschaft
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
debugLog('Seite geladen - Initialisierung startet...');
|
||||
|
||||
try {
|
||||
initializeFileUpload();
|
||||
loadPrinters();
|
||||
setupFormValidation();
|
||||
setupEventListeners();
|
||||
|
||||
debugLog('Initialisierung erfolgreich abgeschlossen');
|
||||
showToast('Upload-Interface bereit', 'success');
|
||||
} catch (error) {
|
||||
debugLog('Fehler bei Initialisierung: ' + error.message);
|
||||
showToast('Fehler beim Laden der Seite', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Datei-Upload initialisieren - HAUPTFUNKTION
|
||||
function initializeFileUpload() {
|
||||
const uploadArea = document.getElementById('file-upload-area');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const uploadTrigger = document.getElementById('upload-trigger');
|
||||
const clearFileBtn = document.getElementById('clear-file-btn');
|
||||
|
||||
if (!uploadArea || !fileInput) {
|
||||
throw new Error('Upload-Elemente nicht gefunden');
|
||||
}
|
||||
|
||||
debugLog('Upload-Elemente gefunden, Event-Listener werden registriert...');
|
||||
|
||||
// HAUPTKLICK-EVENT - Direkt auf Upload-Area
|
||||
uploadArea.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
debugLog('Upload-Area geklickt');
|
||||
|
||||
if (!selectedFile && !isUploading) {
|
||||
debugLog('Datei-Dialog wird geöffnet...');
|
||||
fileInput.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Zusätzlicher Klick-Handler für Upload-Button
|
||||
if (uploadTrigger) {
|
||||
uploadTrigger.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
debugLog('Upload-Button geklickt');
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
// Datei-Input Change-Event
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
debugLog('Datei-Input geändert');
|
||||
handleFileSelection(e.target.files[0]);
|
||||
});
|
||||
|
||||
// Drag & Drop
|
||||
uploadArea.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.add('drag-over');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', function(e) {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('drag-over');
|
||||
debugLog('Datei dropped');
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
handleFileSelection(files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear-Button
|
||||
if (clearFileBtn) {
|
||||
clearFileBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clearSelectedFile();
|
||||
});
|
||||
}
|
||||
|
||||
debugLog('Alle Upload-Event-Listener registriert');
|
||||
}
|
||||
|
||||
// Datei-Auswahl behandeln
|
||||
function handleFileSelection(file) {
|
||||
if (!file) {
|
||||
debugLog('Keine Datei ausgewählt');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog(`Datei ausgewählt: ${file.name} (${formatFileSize(file.size)})`);
|
||||
|
||||
// Datei validieren
|
||||
const validation = validateFile(file);
|
||||
if (!validation.valid) {
|
||||
showToast(validation.message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFile = file;
|
||||
showFilePreview(file);
|
||||
updateFormValidation();
|
||||
|
||||
showToast(`Datei "${file.name}" erfolgreich ausgewählt`, 'success');
|
||||
}
|
||||
|
||||
// Datei validieren
|
||||
function validateFile(file) {
|
||||
const allowedExtensions = ['.stl', '.gcode', '.3mf', '.obj'];
|
||||
const maxSize = 100 * 1024 * 1024; // 100MB
|
||||
|
||||
const extension = '.' + file.name.split('.').pop().toLowerCase();
|
||||
|
||||
if (!allowedExtensions.includes(extension)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Ungültiger Dateityp "${extension}". Erlaubt: ${allowedExtensions.join(', ')}`
|
||||
};
|
||||
}
|
||||
|
||||
if (file.size > maxSize) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Datei zu groß (${formatFileSize(file.size)}). Maximum: 100MB`
|
||||
};
|
||||
}
|
||||
|
||||
if (file.size < 100) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Datei ist zu klein (weniger als 100 Bytes)'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// Datei-Vorschau anzeigen
|
||||
function showFilePreview(file) {
|
||||
const placeholder = document.getElementById('upload-placeholder');
|
||||
const preview = document.getElementById('file-preview');
|
||||
const fileName = document.getElementById('file-name');
|
||||
const fileSize = document.getElementById('file-size');
|
||||
const fileType = document.getElementById('file-type');
|
||||
const uploadStatus = document.getElementById('upload-status');
|
||||
|
||||
if (placeholder) placeholder.classList.add('hidden');
|
||||
if (preview) preview.classList.remove('hidden');
|
||||
if (fileName) fileName.textContent = file.name;
|
||||
if (fileSize) fileSize.textContent = formatFileSize(file.size);
|
||||
if (fileType) {
|
||||
const extension = '.' + file.name.split('.').pop().toLowerCase();
|
||||
fileType.textContent = getFileTypeDescription(extension);
|
||||
}
|
||||
if (uploadStatus) uploadStatus.classList.remove('hidden');
|
||||
|
||||
debugLog('Datei-Vorschau aktualisiert');
|
||||
}
|
||||
|
||||
// Ausgewählte Datei löschen
|
||||
function clearSelectedFile() {
|
||||
selectedFile = null;
|
||||
|
||||
const placeholder = document.getElementById('upload-placeholder');
|
||||
const preview = document.getElementById('file-preview');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const uploadStatus = document.getElementById('upload-status');
|
||||
|
||||
if (placeholder) placeholder.classList.remove('hidden');
|
||||
if (preview) preview.classList.add('hidden');
|
||||
if (fileInput) fileInput.value = '';
|
||||
if (uploadStatus) uploadStatus.classList.add('hidden');
|
||||
|
||||
updateFormValidation();
|
||||
showToast('Datei entfernt', 'info');
|
||||
debugLog('Datei entfernt');
|
||||
}
|
||||
|
||||
// Drucker laden
|
||||
async function loadPrinters() {
|
||||
const select = document.getElementById('printer-select');
|
||||
|
||||
try {
|
||||
debugLog('Lade Drucker...');
|
||||
select.innerHTML = '<option value="">Lade Drucker...</option>';
|
||||
select.disabled = true;
|
||||
|
||||
const response = await apiCall('/api/printers');
|
||||
printers = response.printers || [];
|
||||
|
||||
debugLog(`${printers.length} Drucker geladen`);
|
||||
|
||||
select.innerHTML = '<option value="">Drucker auswählen...</option>';
|
||||
select.disabled = false;
|
||||
|
||||
if (printers.length === 0) {
|
||||
select.innerHTML = '<option value="">Keine Drucker verfügbar</option>';
|
||||
select.disabled = true;
|
||||
showToast('Keine Drucker verfügbar', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
printers.forEach(printer => {
|
||||
const option = document.createElement('option');
|
||||
option.value = printer.id;
|
||||
option.textContent = `${printer.name} (${printer.model || 'Unbekannt'})`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
showToast(`${printers.length} Drucker geladen`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
debugLog('Fehler beim Laden der Drucker: ' + error.message);
|
||||
select.innerHTML = '<option value="">Fehler beim Laden</option>';
|
||||
select.disabled = true;
|
||||
showToast('Fehler beim Laden der Drucker', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Event-Listener einrichten
|
||||
function setupEventListeners() {
|
||||
const resetBtn = document.getElementById('reset-btn');
|
||||
const form = document.getElementById('jobForm');
|
||||
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', function() {
|
||||
if (confirm('Möchten Sie alle Eingaben zurücksetzen?')) {
|
||||
form.reset();
|
||||
clearSelectedFile();
|
||||
showToast('Formular zurückgesetzt', 'info');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', handleFormSubmission);
|
||||
}
|
||||
|
||||
// Drucker-Auswahl Change-Event
|
||||
const printerSelect = document.getElementById('printer-select');
|
||||
if (printerSelect) {
|
||||
printerSelect.addEventListener('change', updateFormValidation);
|
||||
}
|
||||
}
|
||||
|
||||
// Formular-Validierung
|
||||
function setupFormValidation() {
|
||||
updateFormValidation();
|
||||
}
|
||||
|
||||
function updateFormValidation() {
|
||||
const printerSelected = document.getElementById('printer-select').value;
|
||||
const submitButton = document.getElementById('submit-button');
|
||||
|
||||
const isValid = selectedFile && printerSelected && !isUploading;
|
||||
|
||||
if (submitButton) {
|
||||
submitButton.disabled = !isValid;
|
||||
|
||||
if (isValid) {
|
||||
submitButton.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
submitButton.classList.add('hover:bg-green-700');
|
||||
} else {
|
||||
submitButton.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
submitButton.classList.remove('hover:bg-green-700');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Formular-Übermittlung
|
||||
async function handleFormSubmission(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!selectedFile) {
|
||||
showToast('Bitte wählen Sie eine Datei aus', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUploading) {
|
||||
showToast('Upload bereits in Bearbeitung', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
isUploading = true;
|
||||
updateFormValidation();
|
||||
|
||||
try {
|
||||
debugLog('Formular wird übermittelt...');
|
||||
showLoadingOverlay('Druckauftrag wird erstellt...');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile);
|
||||
formData.append('printer_id', document.getElementById('printer-select').value);
|
||||
formData.append('priority', document.getElementById('priority-select').value);
|
||||
formData.append('material', document.getElementById('material-input').value);
|
||||
formData.append('color', document.getElementById('color-input').value);
|
||||
formData.append('layer_height', document.getElementById('layer-height-select').value);
|
||||
formData.append('infill', document.getElementById('infill-select').value);
|
||||
formData.append('notes', document.getElementById('notes-textarea').value);
|
||||
|
||||
const response = await fetch('/api/jobs', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
hideLoadingOverlay();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showToast('Druckauftrag erfolgreich erstellt!', 'success');
|
||||
debugLog(`Job erstellt: ID ${result.job_id}`);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = '/jobs';
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(result.message || 'Unbekannter Fehler');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
hideLoadingOverlay();
|
||||
debugLog('Fehler beim Erstellen des Jobs: ' + error.message);
|
||||
showToast('Fehler beim Erstellen des Auftrags: ' + error.message, 'error');
|
||||
} finally {
|
||||
isUploading = false;
|
||||
updateFormValidation();
|
||||
}
|
||||
}
|
||||
|
||||
// Hilfsfunktionen
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function getFileTypeDescription(extension) {
|
||||
const descriptions = {
|
||||
'.stl': 'STL - 3D Mesh Datei',
|
||||
'.gcode': 'G-Code - Druckfertiger Code',
|
||||
'.3mf': '3MF - 3D Manufacturing Format',
|
||||
'.obj': 'OBJ - Wavefront 3D Object'
|
||||
};
|
||||
return descriptions[extension] || 'Unbekannter Dateityp';
|
||||
}
|
||||
|
||||
function showLoadingOverlay(message) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'loading-overlay';
|
||||
overlay.className = 'loading-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="loading-content">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<h3 class="text-lg font-semibold mt-4">${message}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-2">Bitte warten Sie...</p>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function hideLoadingOverlay() {
|
||||
const overlay = document.getElementById('loading-overlay');
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
2399
templates/printers.html
Normal file
2399
templates/printers.html
Normal file
File diff suppressed because it is too large
Load Diff
546
templates/privacy.html
Normal file
546
templates/privacy.html
Normal file
@@ -0,0 +1,546 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Datenschutzerklärung - MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Hero Header mit Gradient und Animation -->
|
||||
<div class="relative overflow-hidden rounded-3xl mb-12 p-8 md:p-12 bg-gradient-to-br from-emerald-50 via-green-50 to-teal-50 dark:from-slate-900 dark:via-emerald-900/20 dark:to-green-900/20 border border-emerald-200/50 dark:border-emerald-700/30">
|
||||
<!-- Animated Background Pattern -->
|
||||
<div class="absolute inset-0 opacity-10 dark:opacity-5">
|
||||
<div class="absolute top-0 left-0 w-full h-full">
|
||||
<svg class="animate-pulse" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<pattern id="privacy-grid" width="8" height="8" patternUnits="userSpaceOnUse">
|
||||
<circle cx="4" cy="4" r="1" fill="currentColor" opacity="0.3"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100" height="100" fill="url(#privacy-grid)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 text-center">
|
||||
<!-- Icon -->
|
||||
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-emerald-100 dark:bg-emerald-900/50 mb-6">
|
||||
<svg class="w-10 h-10 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-4 tracking-tight">
|
||||
Datenschutzerklärung
|
||||
</h1>
|
||||
<p class="text-xl text-slate-600 dark:text-slate-300 mb-6 max-w-2xl mx-auto">
|
||||
Transparenz über die Verarbeitung Ihrer personenbezogenen Daten
|
||||
</p>
|
||||
|
||||
<!-- Meta Information -->
|
||||
<div class="inline-flex items-center gap-4 text-sm text-slate-500 dark:text-slate-400">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span>Gültig ab 15. Juni 2024</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Lesezeit: ~8 Minuten</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
<span>DSGVO-konform</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table of Contents -->
|
||||
<div class="mb-12">
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h2 class="text-lg font-semibold text-slate-900 dark:text-white mb-4 flex items-center gap-3">
|
||||
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16"/>
|
||||
</svg>
|
||||
Inhaltsverzeichnis
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<a href="#section-1" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">1</span>
|
||||
<span class="text-sm">Verantwortliche Stelle</span>
|
||||
</a>
|
||||
<a href="#section-2" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">2</span>
|
||||
<span class="text-sm">Erhobene Daten</span>
|
||||
</a>
|
||||
<a href="#section-3" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">3</span>
|
||||
<span class="text-sm">Verarbeitungszweck</span>
|
||||
</a>
|
||||
<a href="#section-4" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">4</span>
|
||||
<span class="text-sm">Rechtsgrundlage</span>
|
||||
</a>
|
||||
<a href="#section-5" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">5</span>
|
||||
<span class="text-sm">Speicherdauer</span>
|
||||
</a>
|
||||
<a href="#section-6" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">6</span>
|
||||
<span class="text-sm">Ihre Rechte</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Content -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
<!-- Sidebar Navigation (hidden on mobile) -->
|
||||
<div class="hidden lg:block">
|
||||
<div class="sticky top-8">
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-4 uppercase tracking-wider">Navigation</h3>
|
||||
<nav class="space-y-2">
|
||||
<a href="#section-1" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">1. Verantwortliche Stelle</a>
|
||||
<a href="#section-2" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">2. Erhobene Daten</a>
|
||||
<a href="#section-3" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">3. Verarbeitungszweck</a>
|
||||
<a href="#section-4" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">4. Rechtsgrundlage</a>
|
||||
<a href="#section-5" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">5. Speicherdauer</a>
|
||||
<a href="#section-6" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 py-2 border-l-2 border-transparent hover:border-emerald-600 dark:hover:border-emerald-400 pl-3 transition-all duration-200">6. Ihre Rechte</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="lg:col-span-3">
|
||||
<div class="space-y-8">
|
||||
<!-- Section 1 - Verantwortliche Stelle -->
|
||||
<section id="section-1" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-emerald-100 dark:bg-emerald-900/50 flex items-center justify-center">
|
||||
<span class="text-emerald-600 dark:text-emerald-400 font-bold">1</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Verantwortliche Stelle</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">
|
||||
Verantwortlich für die Datenverarbeitung im Rahmen der MYP-Plattform ist:
|
||||
</p>
|
||||
<div class="bg-slate-50/50 dark:bg-slate-800/50 rounded-lg p-4 border-l-4 border-emerald-500">
|
||||
<p class="text-slate-700 dark:text-slate-300 mb-0">
|
||||
<strong>Mercedes-Benz Group AG</strong><br>
|
||||
Mercedesstraße 120<br>
|
||||
70372 Stuttgart<br>
|
||||
Deutschland
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 2 - Art der erhobenen Daten -->
|
||||
<section id="section-2" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
|
||||
<span class="text-blue-600 dark:text-blue-400 font-bold">2</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Art der erhobenen Daten</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">
|
||||
Im Rahmen der Nutzung der MYP-Plattform werden folgende personenbezogene Daten erhoben und verarbeitet:
|
||||
</p>
|
||||
<div class="grid gap-3">
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-blue-50/50 dark:bg-blue-900/10 border border-blue-200/50 dark:border-blue-800/50">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<span class="font-medium text-slate-700 dark:text-slate-300">Personenstammdaten</span>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">Name, E-Mail-Adresse, Abteilung</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-purple-50/50 dark:bg-purple-900/10 border border-purple-200/50 dark:border-purple-800/50">
|
||||
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<span class="font-medium text-slate-700 dark:text-slate-300">Benutzerkonto-Informationen</span>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">Benutzername, verschlüsseltes Passwort</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-green-50/50 dark:bg-green-900/10 border border-green-200/50 dark:border-green-800/50">
|
||||
<svg class="w-5 h-5 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<span class="font-medium text-slate-700 dark:text-slate-300">Nutzungsdaten</span>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">Druckaufträge, Zeitstempel, verwendete Geräte</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-orange-50/50 dark:bg-orange-900/10 border border-orange-200/50 dark:border-orange-800/50">
|
||||
<svg class="w-5 h-5 text-orange-600 dark:text-orange-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<span class="font-medium text-slate-700 dark:text-slate-300">Protokolldaten</span>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">IP-Adresse, Zugriffszeiten, Aktivitäten</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-indigo-50/50 dark:bg-indigo-900/10 border border-indigo-200/50 dark:border-indigo-800/50">
|
||||
<svg class="w-5 h-5 text-indigo-600 dark:text-indigo-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<span class="font-medium text-slate-700 dark:text-slate-300">Einstellungen</span>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">Präferenzen zur Personalisierung der Plattform</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 3 - Zweck der Datenverarbeitung -->
|
||||
<section id="section-3" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center">
|
||||
<span class="text-purple-600 dark:text-purple-400 font-bold">3</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Zweck der Datenverarbeitung</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">Die Verarbeitung der Daten erfolgt zu folgenden Zwecken:</p>
|
||||
<div class="grid gap-3">
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Bereitstellung und Verwaltung der MYP-Plattform</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Authentifizierung und Autorisierung der Nutzer</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Verwaltung und Optimierung von Druckaufträgen</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Ressourcenplanung und -optimierung</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Gewährleistung der Systemsicherheit</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Erstellung anonymisierter Statistiken zur Systemnutzung</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 4 - Rechtsgrundlage -->
|
||||
<section id="section-4" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-orange-100 dark:bg-orange-900/50 flex items-center justify-center">
|
||||
<span class="text-orange-600 dark:text-orange-400 font-bold">4</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Rechtsgrundlage der Verarbeitung</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">Die Datenverarbeitung erfolgt auf Grundlage:</p>
|
||||
<div class="space-y-3">
|
||||
<div class="p-4 rounded-lg bg-blue-50/50 dark:bg-blue-900/10 border border-blue-200/50 dark:border-blue-800/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-blue-600 dark:text-blue-400 font-bold text-sm">Art. 6 Abs. 1 lit. b DSGVO</span>
|
||||
</div>
|
||||
<p class="text-slate-700 dark:text-slate-300 text-sm mt-2">Der Erfüllung des Arbeitsverhältnisses</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg bg-green-50/50 dark:bg-green-900/10 border border-green-200/50 dark:border-green-800/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-green-600 dark:text-green-400 font-bold text-sm">Art. 6 Abs. 1 lit. f DSGVO</span>
|
||||
</div>
|
||||
<p class="text-slate-700 dark:text-slate-300 text-sm mt-2">Der Wahrung berechtigter Interessen (effiziente Ressourcenverwaltung, Systemsicherheit)</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg bg-purple-50/50 dark:bg-purple-900/10 border border-purple-200/50 dark:border-purple-800/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-purple-600 dark:text-purple-400 font-bold text-sm">Art. 6 Abs. 1 lit. a DSGVO</span>
|
||||
</div>
|
||||
<p class="text-slate-700 dark:text-slate-300 text-sm mt-2">Gegebenenfalls einer Einwilligung für optionale Funktionen</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 5 - Speicherdauer -->
|
||||
<section id="section-5" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center">
|
||||
<span class="text-indigo-600 dark:text-indigo-400 font-bold">5</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Speicherdauer</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">
|
||||
Die personenbezogenen Daten werden nur so lange gespeichert, wie es für die genannten Zwecke erforderlich ist oder gesetzliche Aufbewahrungspflichten bestehen:
|
||||
</p>
|
||||
<div class="grid gap-4">
|
||||
<div class="flex items-center gap-4 p-4 rounded-lg bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/10 dark:to-indigo-900/10 border border-blue-200/50 dark:border-blue-800/50">
|
||||
<div class="w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white">Benutzerkontodaten</h4>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">Für die Dauer der Unternehmenszugehörigkeit</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-4 rounded-lg bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/10 dark:to-emerald-900/10 border border-green-200/50 dark:border-green-800/50">
|
||||
<div class="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/50 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white">Druckauftragsdaten</h4>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">12 Monate nach Abschluss des Auftrags</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-4 rounded-lg bg-gradient-to-r from-orange-50 to-red-50 dark:from-orange-900/10 dark:to-red-900/10 border border-orange-200/50 dark:border-orange-800/50">
|
||||
<div class="w-12 h-12 rounded-full bg-orange-100 dark:bg-orange-900/50 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white">Protokolldaten</h4>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">90 Tage</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 6 - Ihre Rechte -->
|
||||
<section id="section-6" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-emerald-100 dark:bg-emerald-900/50 flex items-center justify-center">
|
||||
<span class="text-emerald-600 dark:text-emerald-400 font-bold">6</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Rechte der betroffenen Personen</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-6">
|
||||
Als Nutzer der MYP-Plattform haben Sie folgende Rechte:
|
||||
</p>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Recht auf Auskunft</h4>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 15 DSGVO - über die gespeicherten Daten</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-green-600 dark:text-green-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2V7a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Recht auf Berichtigung</h4>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 16 DSGVO - unrichtiger Daten</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Recht auf Löschung</h4>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 17 DSGVO - der Daten</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18.364 5.636M5.636 18.364l12.728-12.728"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Recht auf Einschränkung</h4>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 18 DSGVO - der Verarbeitung</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-indigo-600 dark:text-indigo-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Recht auf Datenübertragbarkeit</h4>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 20 DSGVO</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-orange-600 dark:text-orange-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white text-sm">Widerspruchsrecht</h4>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">Art. 21 DSGVO - gegen die Verarbeitung</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Kompakte weitere Abschnitte -->
|
||||
<div class="grid gap-6">
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<span class="w-6 h-6 rounded-full bg-teal-100 dark:bg-teal-900/50 text-teal-600 dark:text-teal-400 text-sm font-bold flex items-center justify-center">7</span>
|
||||
Datensicherheit
|
||||
</h3>
|
||||
<p class="text-slate-700 dark:text-slate-300 text-sm mb-3">Mercedes-Benz trifft angemessene technische und organisatorische Maßnahmen, um die Sicherheit der personenbezogenen Daten zu gewährleisten:</p>
|
||||
<div class="grid sm:grid-cols-2 gap-2 text-sm text-slate-600 dark:text-slate-400">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span>Verschlüsselung</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span>Zugriffskontrollen</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span>Sicherheitsüberprüfungen</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span>Mitarbeiterschulungen</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-sm font-bold flex items-center justify-center">8</span>
|
||||
Datenschutzbeauftragter
|
||||
</h3>
|
||||
<p class="text-slate-700 dark:text-slate-300 text-sm mb-3">Bei Fragen zum Datenschutz oder zur Ausübung Ihrer Rechte wenden Sie sich an:</p>
|
||||
<div class="bg-slate-50/50 dark:bg-slate-800/50 rounded-lg p-3 border-l-4 border-blue-500">
|
||||
<p class="text-slate-700 dark:text-slate-300 text-sm mb-0">
|
||||
<strong>Datenschutzbeauftragter</strong><br>
|
||||
Mercedes-Benz Group AG<br>
|
||||
HPC G353<br>
|
||||
70546 Stuttgart<br>
|
||||
<a href="mailto:data.protection@mercedes-benz.com" class="text-blue-600 dark:text-blue-400 hover:underline">data.protection@mercedes-benz.com</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kontakt Section -->
|
||||
<div class="glass-card bg-gradient-to-br from-emerald-50 to-green-50 dark:from-emerald-900/20 dark:to-green-900/20 border border-emerald-200/50 dark:border-emerald-700/30 rounded-2xl p-8">
|
||||
<div class="text-center">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-emerald-100 dark:bg-emerald-900/50 mb-6">
|
||||
<svg class="w-8 h-8 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-4">Fragen zum Datenschutz?</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-6">Bei Fragen zur Datenverarbeitung in der MYP-Plattform wenden Sie sich gerne an unser Team.</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="mailto:till.tomczak@mercedes-benz.com" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Till Tomczak
|
||||
</a>
|
||||
<a href="mailto:torben.haack@mercedes-benz.com" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Torben Haack
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-end mt-12 pt-8 border-t border-slate-200 dark:border-slate-700">
|
||||
<button onclick="window.print()" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-lg transition-all duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
|
||||
</svg>
|
||||
Drucken
|
||||
</button>
|
||||
<a href="javascript:history.back()" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Zurück
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Smooth Scrolling Script -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Smooth scrolling for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetElement = document.querySelector(targetId);
|
||||
if (targetElement) {
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
803
templates/profile.html
Normal file
803
templates/profile.html
Normal file
@@ -0,0 +1,803 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Profil - MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Mein Profil</h1>
|
||||
<p class="mt-2 text-slate-500 dark:text-slate-400">Verwalten Sie Ihre Kontoinformationen und Einstellungen</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Profile Information -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50 mb-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white">Persönliche Informationen</h2>
|
||||
<button onclick="toggleEditMode()" id="edit-button"
|
||||
class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-4 py-2 rounded-lg transition-all duration-200">
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="profile-form" onsubmit="handleProfileUpdate(event)">
|
||||
<!-- Avatar Section -->
|
||||
<div class="flex items-center space-x-6 mb-8 pb-6 border-b border-gray-200 dark:border-slate-600">
|
||||
<div class="relative">
|
||||
<div class="w-24 h-24 rounded-full bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center text-white text-2xl font-bold shadow-lg">
|
||||
<span id="avatar-text">{{ (current_user.first_name[0] if current_user.first_name else current_user.email[0]) | upper }}</span>
|
||||
<img id="avatar-image" src="{{ current_user.avatar_url if current_user.avatar_url else '' }}"
|
||||
alt="Profilbild" class="w-24 h-24 rounded-full object-cover {{ 'hidden' if not current_user.avatar_url else '' }}">
|
||||
</div>
|
||||
<button type="button" onclick="triggerAvatarUpload()"
|
||||
class="absolute bottom-0 right-0 bg-blue-600 hover:bg-blue-700 text-white rounded-full p-2 shadow-lg transition-all duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-1">
|
||||
{{ current_user.first_name + ' ' + current_user.last_name if current_user.first_name and current_user.last_name else current_user.email.split('@')[0] }}
|
||||
</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400">{{ current_user.email }}</p>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
|
||||
{{ 'Administrator' if current_user.is_admin else 'Benutzer' }} •
|
||||
{{ current_user.department or 'Keine Abteilung' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input type="file" id="avatar-upload" name="avatar" accept="image/*" class="hidden" onchange="handleAvatarUpload(event)">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="first-name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Vorname
|
||||
</label>
|
||||
<input type="text" id="first-name" name="first_name" value="{{ current_user.first_name or '' }}" disabled
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="last-name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Nachname
|
||||
</label>
|
||||
<input type="text" id="last-name" name="last_name" value="{{ current_user.last_name or '' }}" disabled
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
E-Mail-Adresse
|
||||
</label>
|
||||
<input type="email" id="email" name="email" value="{{ current_user.email }}" disabled
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="phone" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Telefonnummer
|
||||
</label>
|
||||
<input type="tel" id="phone" name="phone" value="{{ current_user.phone or '' }}" disabled
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="department" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Abteilung
|
||||
</label>
|
||||
<input type="text" id="department" name="department" value="{{ current_user.department or '' }}" disabled
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-slate-800 disabled:cursor-not-allowed">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="role" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Rolle
|
||||
</label>
|
||||
<input type="text" id="role" value="{{ 'Administrator' if current_user.is_admin else 'Benutzer' }}" disabled
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-gray-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 cursor-not-allowed">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="form-actions" class="hidden mt-6 flex space-x-4">
|
||||
<button type="submit"
|
||||
class="bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-500 text-white px-6 py-2 rounded-lg transition-all duration-200">
|
||||
Änderungen speichern
|
||||
</button>
|
||||
<button type="button" onclick="cancelEdit()"
|
||||
class="bg-gray-200 hover:bg-gray-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-800 dark:text-white px-6 py-2 rounded-lg transition-all duration-200">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Password Change -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
|
||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-6">Passwort ändern</h2>
|
||||
|
||||
<form id="password-form" onsubmit="handlePasswordChange(event)" class="space-y-4">
|
||||
<div>
|
||||
<label for="current-password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Aktuelles Passwort
|
||||
</label>
|
||||
<input type="password" id="current-password" name="current_password" required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="new-password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Neues Passwort
|
||||
</label>
|
||||
<input type="password" id="new-password" name="new_password" required minlength="8"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white">
|
||||
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">Mindestens 8 Zeichen</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirm-password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Passwort bestätigen
|
||||
</label>
|
||||
<input type="password" id="confirm-password" name="confirm_password" required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white">
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-6 py-2 rounded-lg transition-all duration-200">
|
||||
Passwort ändern
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Profile Stats -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Statistiken</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-slate-600 dark:text-slate-400">Gesamte Aufträge</span>
|
||||
<span id="total-jobs" class="text-lg font-bold text-slate-900 dark:text-white">-</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-slate-600 dark:text-slate-400">Abgeschlossene Aufträge</span>
|
||||
<span id="completed-jobs" class="text-lg font-bold text-green-600 dark:text-green-400">-</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-slate-600 dark:text-slate-400">Aktive Aufträge</span>
|
||||
<span id="active-jobs" class="text-lg font-bold text-blue-600 dark:text-blue-400">-</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-slate-600 dark:text-slate-400">Fehlgeschlagene Aufträge</span>
|
||||
<span id="failed-jobs" class="text-lg font-bold text-red-600 dark:text-red-400">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Info -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Kontoinformationen</h3>
|
||||
|
||||
<div class="space-y-3 text-sm">
|
||||
<div>
|
||||
<span class="text-slate-600 dark:text-slate-400">Mitglied seit:</span>
|
||||
<div class="font-medium text-slate-900 dark:text-white">{{ current_user.created_at | format_datetime('%d.%m.%Y') if current_user.created_at else 'Unbekannt' }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-slate-600 dark:text-slate-400">Letzte Anmeldung:</span>
|
||||
<div class="font-medium text-slate-900 dark:text-white">{{ current_user.last_login | format_datetime if current_user.last_login else 'Nie' }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-slate-600 dark:text-slate-400">Konto-Status:</span>
|
||||
<div class="font-medium">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-600 dark:bg-green-700 text-white">
|
||||
Aktiv
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-md border border-gray-200 dark:border-slate-700/50">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Schnellaktionen</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<a href="/new-job"
|
||||
class="block w-full bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-500 text-white text-center py-2 px-4 rounded-lg transition-all duration-200">
|
||||
Neuer Auftrag
|
||||
</a>
|
||||
|
||||
<a href="/my/jobs"
|
||||
class="block w-full bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white text-center py-2 px-4 rounded-lg transition-all duration-200">
|
||||
Meine Aufträge
|
||||
</a>
|
||||
|
||||
<button onclick="downloadUserData()"
|
||||
class="block w-full bg-gray-200 hover:bg-gray-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-800 dark:text-white text-center py-2 px-4 rounded-lg transition-all duration-200">
|
||||
Daten exportieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let isEditMode = false;
|
||||
let originalFormData = {};
|
||||
let avatarFile = null;
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadUserStats();
|
||||
storeOriginalFormData();
|
||||
setupPasswordStrengthMeter();
|
||||
setupFormValidation();
|
||||
});
|
||||
|
||||
// Store original form data
|
||||
function storeOriginalFormData() {
|
||||
const form = document.getElementById('profile-form');
|
||||
const formData = new FormData(form);
|
||||
originalFormData = {};
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key !== 'avatar') { // Don't store file data
|
||||
originalFormData[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar Upload Functions
|
||||
function triggerAvatarUpload() {
|
||||
document.getElementById('avatar-upload').click();
|
||||
}
|
||||
|
||||
async function handleAvatarUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showFlashMessage('Bitte wählen Sie eine gültige Bilddatei aus', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) { // 5MB limit
|
||||
showFlashMessage('Die Datei ist zu groß. Maximale Größe: 5MB', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
const uploadButton = event.target.parentElement.querySelector('button');
|
||||
const originalButtonContent = uploadButton.innerHTML;
|
||||
uploadButton.innerHTML = `
|
||||
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
`;
|
||||
uploadButton.disabled = true;
|
||||
|
||||
// Preview image
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const avatarImage = document.getElementById('avatar-image');
|
||||
const avatarText = document.getElementById('avatar-text');
|
||||
|
||||
avatarImage.src = e.target.result;
|
||||
avatarImage.classList.remove('hidden');
|
||||
avatarText.style.display = 'none';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// Upload avatar
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', file);
|
||||
|
||||
const response = await fetch('/api/user/avatar', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showFlashMessage('Profilbild erfolgreich aktualisiert', 'success');
|
||||
avatarFile = file;
|
||||
} else {
|
||||
throw new Error(result.message || 'Upload fehlgeschlagen');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Avatar upload error:', error);
|
||||
showFlashMessage('Fehler beim Hochladen des Profilbilds: ' + error.message, 'error');
|
||||
|
||||
// Reset preview on error
|
||||
const avatarImage = document.getElementById('avatar-image');
|
||||
const avatarText = document.getElementById('avatar-text');
|
||||
avatarImage.classList.add('hidden');
|
||||
avatarText.style.display = 'flex';
|
||||
|
||||
} finally {
|
||||
// Restore button
|
||||
const uploadButton = event.target.parentElement.querySelector('button');
|
||||
uploadButton.innerHTML = originalButtonContent;
|
||||
uploadButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Password Strength Meter
|
||||
function setupPasswordStrengthMeter() {
|
||||
const newPasswordInput = document.getElementById('new-password');
|
||||
|
||||
// Create password strength indicator
|
||||
const strengthMeter = document.createElement('div');
|
||||
strengthMeter.id = 'password-strength-meter';
|
||||
strengthMeter.className = 'mt-2';
|
||||
|
||||
const strengthBar = document.createElement('div');
|
||||
strengthBar.className = 'w-full bg-gray-200 dark:bg-slate-600 rounded-full h-2';
|
||||
|
||||
const strengthIndicator = document.createElement('div');
|
||||
strengthIndicator.id = 'strength-indicator';
|
||||
strengthIndicator.className = 'h-2 rounded-full transition-all duration-300';
|
||||
|
||||
const strengthText = document.createElement('div');
|
||||
strengthText.id = 'strength-text';
|
||||
strengthText.className = 'text-xs mt-1';
|
||||
|
||||
strengthBar.appendChild(strengthIndicator);
|
||||
strengthMeter.appendChild(strengthBar);
|
||||
strengthMeter.appendChild(strengthText);
|
||||
|
||||
newPasswordInput.parentElement.appendChild(strengthMeter);
|
||||
|
||||
newPasswordInput.addEventListener('input', function() {
|
||||
const password = this.value;
|
||||
const strength = calculatePasswordStrength(password);
|
||||
updatePasswordStrengthMeter(strength);
|
||||
});
|
||||
}
|
||||
|
||||
function calculatePasswordStrength(password) {
|
||||
let score = 0;
|
||||
const checks = {
|
||||
length: password.length >= 8,
|
||||
lowercase: /[a-z]/.test(password),
|
||||
uppercase: /[A-Z]/.test(password),
|
||||
numbers: /\d/.test(password),
|
||||
special: /[!@#$%^&*(),.?":{}|<>]/.test(password)
|
||||
};
|
||||
|
||||
score = Object.values(checks).filter(Boolean).length;
|
||||
|
||||
let strength = 'weak';
|
||||
if (score >= 4) strength = 'strong';
|
||||
else if (score >= 3) strength = 'medium';
|
||||
|
||||
return { score, strength, checks };
|
||||
}
|
||||
|
||||
function updatePasswordStrengthMeter(strengthData) {
|
||||
const indicator = document.getElementById('strength-indicator');
|
||||
const text = document.getElementById('strength-text');
|
||||
|
||||
const { strength, score } = strengthData;
|
||||
const percentage = (score / 5) * 100;
|
||||
|
||||
// Update bar
|
||||
indicator.style.width = `${percentage}%`;
|
||||
|
||||
// Update colors and text
|
||||
switch (strength) {
|
||||
case 'weak':
|
||||
indicator.className = 'h-2 rounded-full transition-all duration-300 bg-red-500';
|
||||
text.textContent = 'Schwach';
|
||||
text.className = 'text-xs mt-1 text-red-500';
|
||||
break;
|
||||
case 'medium':
|
||||
indicator.className = 'h-2 rounded-full transition-all duration-300 bg-yellow-500';
|
||||
text.textContent = 'Mittel';
|
||||
text.className = 'text-xs mt-1 text-yellow-500';
|
||||
break;
|
||||
case 'strong':
|
||||
indicator.className = 'h-2 rounded-full transition-all duration-300 bg-green-500';
|
||||
text.textContent = 'Stark';
|
||||
text.className = 'text-xs mt-1 text-green-500';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced Form Validation
|
||||
function setupFormValidation() {
|
||||
// Real-time email validation
|
||||
const emailInput = document.getElementById('email');
|
||||
emailInput.addEventListener('blur', validateEmail);
|
||||
|
||||
// Phone number formatting
|
||||
const phoneInput = document.getElementById('phone');
|
||||
phoneInput.addEventListener('input', formatPhoneNumber);
|
||||
|
||||
// Name validation
|
||||
['first-name', 'last-name'].forEach(id => {
|
||||
const input = document.getElementById(id);
|
||||
input.addEventListener('input', function() {
|
||||
validateName(this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function validateEmail(event) {
|
||||
const email = event.target.value;
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (email && !emailRegex.test(email)) {
|
||||
event.target.setCustomValidity('Bitte geben Sie eine gültige E-Mail-Adresse ein');
|
||||
event.target.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
} else {
|
||||
event.target.setCustomValidity('');
|
||||
event.target.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
}
|
||||
}
|
||||
|
||||
function formatPhoneNumber(event) {
|
||||
let value = event.target.value.replace(/\D/g, '');
|
||||
|
||||
if (value.length >= 10) {
|
||||
value = value.replace(/(\d{2})(\d{3})(\d{3})(\d{4})/, '+49 $2 $3 $4');
|
||||
}
|
||||
|
||||
event.target.value = value;
|
||||
}
|
||||
|
||||
function validateName(input) {
|
||||
const nameRegex = /^[a-zA-ZäöüÄÖÜß\s-]+$/;
|
||||
const value = input.value.trim();
|
||||
|
||||
if (value && !nameRegex.test(value)) {
|
||||
input.setCustomValidity('Namen dürfen nur Buchstaben, Leerzeichen und Bindestriche enthalten');
|
||||
input.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
} else {
|
||||
input.setCustomValidity('');
|
||||
input.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle edit mode with enhanced UX
|
||||
function toggleEditMode() {
|
||||
isEditMode = !isEditMode;
|
||||
const inputs = document.querySelectorAll('#profile-form input:not(#role)');
|
||||
const editButton = document.getElementById('edit-button');
|
||||
const formActions = document.getElementById('form-actions');
|
||||
|
||||
inputs.forEach(input => {
|
||||
input.disabled = !isEditMode;
|
||||
if (isEditMode) {
|
||||
input.classList.add('focus:ring-2', 'focus:ring-blue-600', 'focus:border-blue-600');
|
||||
} else {
|
||||
input.classList.remove('focus:ring-2', 'focus:ring-blue-600', 'focus:border-blue-600');
|
||||
}
|
||||
});
|
||||
|
||||
if (isEditMode) {
|
||||
editButton.innerHTML = `
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Abbrechen
|
||||
`;
|
||||
editButton.className = 'bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-500 text-white px-4 py-2 rounded-lg transition-all duration-200';
|
||||
editButton.onclick = cancelEdit;
|
||||
formActions.classList.remove('hidden');
|
||||
} else {
|
||||
editButton.innerHTML = `
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Bearbeiten
|
||||
`;
|
||||
editButton.className = 'bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-4 py-2 rounded-lg transition-all duration-200';
|
||||
editButton.onclick = toggleEditMode;
|
||||
formActions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel edit with confirmation
|
||||
function cancelEdit() {
|
||||
// Check if there are unsaved changes
|
||||
const hasChanges = checkForUnsavedChanges();
|
||||
|
||||
if (hasChanges) {
|
||||
if (!confirm('Sie haben ungespeicherte Änderungen. Möchten Sie wirklich abbrechen?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
Object.keys(originalFormData).forEach(key => {
|
||||
const input = document.querySelector(`[name="${key}"]`);
|
||||
if (input) {
|
||||
input.value = originalFormData[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Reset avatar if changed
|
||||
if (avatarFile) {
|
||||
const avatarImage = document.getElementById('avatar-image');
|
||||
const avatarText = document.getElementById('avatar-text');
|
||||
avatarImage.classList.add('hidden');
|
||||
avatarText.style.display = 'flex';
|
||||
avatarFile = null;
|
||||
}
|
||||
|
||||
toggleEditMode();
|
||||
}
|
||||
|
||||
function checkForUnsavedChanges() {
|
||||
return Object.keys(originalFormData).some(key => {
|
||||
const input = document.querySelector(`[name="${key}"]`);
|
||||
return input && input.value !== originalFormData[key];
|
||||
}) || avatarFile !== null;
|
||||
}
|
||||
|
||||
// Enhanced profile update with loading state
|
||||
async function handleProfileUpdate(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const submitButton = event.target.querySelector('button[type="submit"]');
|
||||
const originalButtonText = submitButton.innerHTML;
|
||||
|
||||
// Show loading state
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = `
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Speichern...
|
||||
`;
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
const profileData = {};
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key !== 'avatar') {
|
||||
profileData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall('/api/user/profile', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(profileData)
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
showFlashMessage('Profil erfolgreich aktualisiert', 'success');
|
||||
storeOriginalFormData();
|
||||
toggleEditMode();
|
||||
|
||||
// Update avatar text if name changed
|
||||
const newInitial = (profileData.first_name?.[0] || profileData.email?.[0] || 'U').toUpperCase();
|
||||
document.getElementById('avatar-text').textContent = newInitial;
|
||||
|
||||
} else {
|
||||
throw new Error(response.message || 'Unbekannter Fehler');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error);
|
||||
showFlashMessage('Fehler beim Aktualisieren des Profils: ' + error.message, 'error');
|
||||
} finally {
|
||||
// Restore button
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced password change with better validation
|
||||
async function handlePasswordChange(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const submitButton = event.target.querySelector('button[type="submit"]');
|
||||
const originalButtonText = submitButton.innerHTML;
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
const newPassword = formData.get('new_password');
|
||||
const confirmPassword = formData.get('confirm_password');
|
||||
|
||||
// Enhanced password validation
|
||||
const strength = calculatePasswordStrength(newPassword);
|
||||
if (strength.score < 3) {
|
||||
showFlashMessage('Das Passwort ist zu schwach. Verwenden Sie eine Kombination aus Groß- und Kleinbuchstaben, Zahlen und Sonderzeichen.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showFlashMessage('Die Passwörter stimmen nicht überein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = `
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Ändern...
|
||||
`;
|
||||
|
||||
const passwordData = {
|
||||
current_password: formData.get('current_password'),
|
||||
new_password: newPassword
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await apiCall('/api/user/password', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(passwordData)
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
showFlashMessage('Passwort erfolgreich geändert', 'success');
|
||||
document.getElementById('password-form').reset();
|
||||
|
||||
// Reset password strength meter
|
||||
const strengthIndicator = document.getElementById('strength-indicator');
|
||||
const strengthText = document.getElementById('strength-text');
|
||||
if (strengthIndicator) {
|
||||
strengthIndicator.style.width = '0%';
|
||||
strengthText.textContent = '';
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(response.message || 'Unbekannter Fehler');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error changing password:', error);
|
||||
showFlashMessage('Fehler beim Ändern des Passworts: ' + error.message, 'error');
|
||||
} finally {
|
||||
// Restore button
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
}
|
||||
}
|
||||
|
||||
// Load user statistics with enhanced error handling
|
||||
async function loadUserStats() {
|
||||
try {
|
||||
const response = await apiCall('/api/user/stats');
|
||||
|
||||
if (response.success) {
|
||||
const stats = response.stats;
|
||||
|
||||
// Animate counter updates
|
||||
animateCounter('total-jobs', stats.total_jobs || 0);
|
||||
animateCounter('completed-jobs', stats.completed_jobs || 0);
|
||||
animateCounter('active-jobs', stats.active_jobs || 0);
|
||||
animateCounter('failed-jobs', stats.failed_jobs || 0);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading user stats:', error);
|
||||
// Keep default values (-) if loading fails
|
||||
}
|
||||
}
|
||||
|
||||
function animateCounter(elementId, targetValue) {
|
||||
const element = document.getElementById(elementId);
|
||||
const startValue = parseInt(element.textContent) || 0;
|
||||
const duration = 1500;
|
||||
const increment = (targetValue - startValue) / (duration / 16);
|
||||
let currentValue = startValue;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
currentValue += increment;
|
||||
if (increment > 0 ? currentValue >= targetValue : currentValue <= targetValue) {
|
||||
element.textContent = targetValue;
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
element.textContent = Math.floor(currentValue);
|
||||
}
|
||||
}, 16);
|
||||
}
|
||||
|
||||
// Enhanced data export with progress tracking
|
||||
async function downloadUserData() {
|
||||
try {
|
||||
// Show loading state
|
||||
const button = event.target;
|
||||
const originalButtonText = button.innerHTML;
|
||||
button.disabled = true;
|
||||
button.innerHTML = `
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 inline" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Exportiere...
|
||||
`;
|
||||
|
||||
const response = await fetch('/api/user/export', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = `meine_daten_${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
showFlashMessage('Daten erfolgreich exportiert', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error downloading user data:', error);
|
||||
showFlashMessage('Fehler beim Exportieren der Daten: ' + error.message, 'error');
|
||||
} finally {
|
||||
// Restore button
|
||||
button.disabled = false;
|
||||
button.innerHTML = originalButtonText;
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced password confirmation validation
|
||||
document.getElementById('confirm-password').addEventListener('input', function() {
|
||||
const newPassword = document.getElementById('new-password').value;
|
||||
const confirmPassword = this.value;
|
||||
|
||||
if (confirmPassword && newPassword !== confirmPassword) {
|
||||
this.setCustomValidity('Die Passwörter stimmen nicht überein');
|
||||
this.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
this.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
}
|
||||
});
|
||||
|
||||
// Warn user about unsaved changes before leaving
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (isEditMode && checkForUnsavedChanges()) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
961
templates/settings.html
Normal file
961
templates/settings.html
Normal file
@@ -0,0 +1,961 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Einstellungen - MYP Platform{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<!-- CSRF Token für AJAX-Anfragen -->
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Einstellungen</h1>
|
||||
<p class="mt-2 text-slate-600 dark:text-slate-400 transition-colors duration-300">Passen Sie die Anwendung an Ihre Bedürfnisse an</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Settings Sections (Left Side) -->
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
<!-- Appearance Settings -->
|
||||
<div class="glass-card">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Erscheinungsbild</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Theme Settings -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Farbschema
|
||||
</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="light-theme-btn" class="theme-btn active px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
|
||||
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
Hell
|
||||
</button>
|
||||
<button id="dark-theme-btn" class="theme-btn px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
|
||||
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
Dunkel
|
||||
</button>
|
||||
<button id="system-theme-btn" class="theme-btn px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
|
||||
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
System
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reduced Motion Settings -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<label for="reduced-motion" class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
Reduzierte Bewegung
|
||||
</label>
|
||||
<div class="relative inline-block w-10 mr-2 align-middle select-none">
|
||||
<input type="checkbox" id="reduced-motion" name="reduced_motion" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
|
||||
<label for="reduced-motion" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
Reduziert Animationen für bessere Barrierefreiheit
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Contrast Settings -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Kontrast
|
||||
</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="normal-contrast-btn" class="contrast-btn active px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
|
||||
Normal
|
||||
</button>
|
||||
<button id="high-contrast-btn" class="contrast-btn px-4 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white text-slate-900 dark:bg-slate-800 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-200">
|
||||
Hoher Kontrast
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Settings -->
|
||||
<div class="glass-card">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Benachrichtigungen</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Neue Aufträge</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">Benachrichtigung, wenn neue Aufträge erstellt werden</p>
|
||||
</div>
|
||||
<div class="relative inline-block w-10 mr-2 align-middle select-none">
|
||||
<input type="checkbox" id="notify-new-jobs" name="notify_new_jobs" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
|
||||
<label for="notify-new-jobs" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Auftragsaktualisierungen</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">Benachrichtigung bei Statusänderungen Ihrer Aufträge</p>
|
||||
</div>
|
||||
<div class="relative inline-block w-10 mr-2 align-middle select-none">
|
||||
<input type="checkbox" id="notify-job-updates" name="notify_job_updates" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
|
||||
<label for="notify-job-updates" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Systembenachrichtigungen</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">Wichtige Systemhinweise und Wartungsmeldungen</p>
|
||||
</div>
|
||||
<div class="relative inline-block w-10 mr-2 align-middle select-none">
|
||||
<input type="checkbox" id="notify-system" name="notify_system" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
|
||||
<label for="notify-system" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-slate-900 dark:text-white">E-Mail-Benachrichtigungen</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">Zusammenfassung per E-Mail erhalten</p>
|
||||
</div>
|
||||
<div class="relative inline-block w-10 mr-2 align-middle select-none">
|
||||
<input type="checkbox" id="notify-email" name="notify_email" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
|
||||
<label for="notify-email" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privacy & Security -->
|
||||
<div class="glass-card">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Datenschutz & Sicherheit</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Aktivitätsprotokolle</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">Protokollieren Sie Ihre Aktivitäten für erhöhte Sicherheit</p>
|
||||
</div>
|
||||
<div class="relative inline-block w-10 mr-2 align-middle select-none">
|
||||
<input type="checkbox" id="activity-logs" name="activity_logs" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/>
|
||||
<label for="activity-logs" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Zwei-Faktor-Authentifizierung</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">Zusätzliche Sicherheitsebene für Ihr Konto</p>
|
||||
</div>
|
||||
<div class="relative inline-block w-10 mr-2 align-middle select-none">
|
||||
<input type="checkbox" id="two-factor" name="two_factor" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
|
||||
<label for="two-factor" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 dark:bg-slate-700 cursor-pointer"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-slate-900 dark:text-white">Automatische Abmeldung</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">Nach einer bestimmten Zeit der Inaktivität abmelden</p>
|
||||
</div>
|
||||
<select id="auto-logout" name="auto_logout" class="form-select rounded-lg bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-700 text-slate-900 dark:text-white py-1 px-3 text-sm">
|
||||
<option value="never">Nie</option>
|
||||
<option value="30">Nach 30 Minuten</option>
|
||||
<option value="60" selected>Nach 1 Stunde</option>
|
||||
<option value="120">Nach 2 Stunden</option>
|
||||
<option value="480">Nach 8 Stunden</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-4 border-t border-gray-200 dark:border-slate-700">
|
||||
<button id="save-security-settings" class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium shadow-sm hover:shadow-md transition-all duration-300">
|
||||
Sicherheitseinstellungen speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Settings Navigation -->
|
||||
<div class="glass-card">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white transition-colors duration-300 mb-4">Einstellungen</h3>
|
||||
|
||||
<nav class="space-y-1">
|
||||
<a href="#appearance" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
|
||||
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
||||
</svg>
|
||||
Erscheinungsbild
|
||||
</a>
|
||||
|
||||
<a href="#notifications" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg text-slate-900 dark:text-white hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors duration-200">
|
||||
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
Benachrichtigungen
|
||||
</a>
|
||||
|
||||
<a href="#privacy" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg text-slate-900 dark:text-white hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors duration-200">
|
||||
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
Datenschutz & Sicherheit
|
||||
</a>
|
||||
|
||||
<a href="/user/profile" class="nav-item flex items-center px-3 py-2 text-sm font-medium rounded-lg text-slate-900 dark:text-white hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors duration-200">
|
||||
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
Profil
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- About System -->
|
||||
<div class="glass-card">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white transition-colors duration-300 mb-4">Über das System</h3>
|
||||
|
||||
<div class="space-y-3 text-sm">
|
||||
<div>
|
||||
<span class="text-slate-600 dark:text-slate-400">Version:</span>
|
||||
<div class="font-medium text-slate-900 dark:text-white">3.0.0</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-slate-600 dark:text-slate-400">Letzte Aktualisierung:</span>
|
||||
<div class="font-medium text-slate-900 dark:text-white">15.06.2024</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-slate-600 dark:text-slate-400">Support:</span>
|
||||
<div class="font-medium text-blue-600 dark:text-blue-400">
|
||||
<a href="mailto:till.tomczak@mercedes-benz.com" class="hover:underline">till.tomczak@mercedes-benz.com</a>
|
||||
<br>
|
||||
<a href="mailto:torben.haack@mercedes-benz.com" class="hover:underline">torben.haack@mercedes-benz.com</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-slate-700">
|
||||
<a href="/terms" class="text-blue-600 dark:text-blue-400 hover:underline text-sm">
|
||||
Nutzungsbedingungen
|
||||
</a>
|
||||
<span class="text-slate-400 mx-2">|</span>
|
||||
<a href="/privacy" class="text-blue-600 dark:text-blue-400 hover:underline text-sm">
|
||||
Datenschutzerklärung
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Theme Switcher
|
||||
const lightThemeBtn = document.getElementById('light-theme-btn');
|
||||
const darkThemeBtn = document.getElementById('dark-theme-btn');
|
||||
const systemThemeBtn = document.getElementById('system-theme-btn');
|
||||
const themeBtns = [lightThemeBtn, darkThemeBtn, systemThemeBtn];
|
||||
|
||||
// Initialize button states based on current theme
|
||||
const STORAGE_KEY = 'myp-dark-mode';
|
||||
const savedMode = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
if (savedMode === 'true') {
|
||||
setActiveThemeButton(darkThemeBtn);
|
||||
} else if (savedMode === 'false') {
|
||||
setActiveThemeButton(lightThemeBtn);
|
||||
} else {
|
||||
setActiveThemeButton(systemThemeBtn);
|
||||
}
|
||||
|
||||
// Theme button click handlers
|
||||
lightThemeBtn.addEventListener('click', function() {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem(STORAGE_KEY, 'false');
|
||||
setActiveThemeButton(lightThemeBtn);
|
||||
});
|
||||
|
||||
darkThemeBtn.addEventListener('click', function() {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem(STORAGE_KEY, 'true');
|
||||
setActiveThemeButton(darkThemeBtn);
|
||||
});
|
||||
|
||||
systemThemeBtn.addEventListener('click', function() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (prefersDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
setActiveThemeButton(systemThemeBtn);
|
||||
});
|
||||
|
||||
function setActiveThemeButton(activeBtn) {
|
||||
themeBtns.forEach(btn => {
|
||||
if (btn === activeBtn) {
|
||||
btn.classList.add('active');
|
||||
btn.classList.add('bg-blue-50');
|
||||
btn.classList.add('dark:bg-blue-900/20');
|
||||
btn.classList.add('text-blue-700');
|
||||
btn.classList.add('dark:text-blue-300');
|
||||
btn.classList.remove('bg-white');
|
||||
btn.classList.remove('dark:bg-slate-800');
|
||||
btn.classList.remove('text-slate-900');
|
||||
btn.classList.remove('dark:text-white');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
btn.classList.remove('bg-blue-50');
|
||||
btn.classList.remove('dark:bg-blue-900/20');
|
||||
btn.classList.remove('text-blue-700');
|
||||
btn.classList.remove('dark:text-blue-300');
|
||||
btn.classList.add('bg-white');
|
||||
btn.classList.add('dark:bg-slate-800');
|
||||
btn.classList.add('text-slate-900');
|
||||
btn.classList.add('dark:text-white');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Contrast Settings
|
||||
const normalContrastBtn = document.getElementById('normal-contrast-btn');
|
||||
const highContrastBtn = document.getElementById('high-contrast-btn');
|
||||
const contrastBtns = [normalContrastBtn, highContrastBtn];
|
||||
|
||||
normalContrastBtn.addEventListener('click', function() {
|
||||
document.documentElement.classList.remove('high-contrast');
|
||||
localStorage.setItem('myp-contrast', 'normal');
|
||||
setActiveContrastButton(normalContrastBtn);
|
||||
});
|
||||
|
||||
highContrastBtn.addEventListener('click', function() {
|
||||
document.documentElement.classList.add('high-contrast');
|
||||
localStorage.setItem('myp-contrast', 'high');
|
||||
setActiveContrastButton(highContrastBtn);
|
||||
});
|
||||
|
||||
function setActiveContrastButton(activeBtn) {
|
||||
contrastBtns.forEach(btn => {
|
||||
if (btn === activeBtn) {
|
||||
btn.classList.add('active');
|
||||
btn.classList.add('bg-blue-50');
|
||||
btn.classList.add('dark:bg-blue-900/20');
|
||||
btn.classList.add('text-blue-700');
|
||||
btn.classList.add('dark:text-blue-300');
|
||||
btn.classList.remove('bg-white');
|
||||
btn.classList.remove('dark:bg-slate-800');
|
||||
btn.classList.remove('text-slate-900');
|
||||
btn.classList.remove('dark:text-white');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
btn.classList.remove('bg-blue-50');
|
||||
btn.classList.remove('dark:bg-blue-900/20');
|
||||
btn.classList.remove('text-blue-700');
|
||||
btn.classList.remove('dark:text-blue-300');
|
||||
btn.classList.add('bg-white');
|
||||
btn.classList.add('dark:bg-slate-800');
|
||||
btn.classList.add('text-slate-900');
|
||||
btn.classList.add('dark:text-white');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save Settings Button
|
||||
const saveSecurityBtn = document.getElementById('save-security-settings');
|
||||
saveSecurityBtn.addEventListener('click', function() {
|
||||
saveAllSettings();
|
||||
});
|
||||
|
||||
// Toggle Switch Styling
|
||||
document.querySelectorAll('.toggle-checkbox').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
// Auto-save bei Änderung
|
||||
const settingName = this.name.replace('_', ' ');
|
||||
const status = this.checked ? 'aktiviert' : 'deaktiviert';
|
||||
showFlashMessage(`${settingName} wurde ${status}`, 'info');
|
||||
});
|
||||
});
|
||||
|
||||
// Sammle alle Einstellungen und speichere sie
|
||||
async function saveAllSettings() {
|
||||
const saveButton = document.getElementById('save-security-settings');
|
||||
const originalButtonText = saveButton.innerHTML;
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
saveButton.disabled = true;
|
||||
saveButton.innerHTML = `
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Speichern...
|
||||
`;
|
||||
|
||||
// Add loading state to settings cards
|
||||
const settingsCards = document.querySelectorAll('.glass-card');
|
||||
settingsCards.forEach(card => card.classList.add('settings-loading'));
|
||||
|
||||
// Erscheinungsbild-Einstellungen
|
||||
const theme = localStorage.getItem(STORAGE_KEY) === 'true' ? 'dark' :
|
||||
localStorage.getItem(STORAGE_KEY) === 'false' ? 'light' : 'system';
|
||||
const reducedMotion = document.getElementById('reduced-motion').checked;
|
||||
const contrast = localStorage.getItem('myp-contrast') || 'normal';
|
||||
|
||||
// Benachrichtigungseinstellungen
|
||||
const notifyNewJobs = document.getElementById('notify-new-jobs').checked;
|
||||
const notifyJobUpdates = document.getElementById('notify-job-updates').checked;
|
||||
const notifySystem = document.getElementById('notify-system').checked;
|
||||
const notifyEmail = document.getElementById('notify-email').checked;
|
||||
|
||||
// Datenschutz & Sicherheitseinstellungen
|
||||
const activityLogs = document.getElementById('activity-logs').checked;
|
||||
const twoFactor = document.getElementById('two-factor').checked;
|
||||
const autoLogout = document.getElementById('auto-logout').value;
|
||||
|
||||
// Validate settings
|
||||
if (!validateSettings({ theme, contrast, autoLogout })) {
|
||||
throw new Error('Ungültige Einstellungen erkannt');
|
||||
}
|
||||
|
||||
// Einstellungsobjekt erstellen
|
||||
const settings = {
|
||||
theme: theme,
|
||||
reduced_motion: reducedMotion,
|
||||
contrast: contrast,
|
||||
notifications: {
|
||||
new_jobs: notifyNewJobs,
|
||||
job_updates: notifyJobUpdates,
|
||||
system: notifySystem,
|
||||
email: notifyEmail
|
||||
},
|
||||
privacy: {
|
||||
activity_logs: activityLogs,
|
||||
two_factor: twoFactor,
|
||||
auto_logout: autoLogout
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Apply reduced motion immediately if changed
|
||||
if (reducedMotion) {
|
||||
document.documentElement.style.setProperty('--tw-transition-duration', '0s');
|
||||
} else {
|
||||
document.documentElement.style.removeProperty('--tw-transition-duration');
|
||||
}
|
||||
|
||||
// Einstellungen an den Server senden
|
||||
const response = await fetch('/api/user/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
},
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Success animation
|
||||
settingsCards.forEach(card => {
|
||||
card.classList.add('settings-saved');
|
||||
setTimeout(() => card.classList.remove('settings-saved'), 600);
|
||||
});
|
||||
|
||||
showFlashMessage('Alle Einstellungen wurden erfolgreich gespeichert', 'success');
|
||||
|
||||
// Cache settings locally for faster access
|
||||
localStorage.setItem('myp-settings-cache', JSON.stringify(settings));
|
||||
|
||||
} else {
|
||||
throw new Error(result.error || 'Unbekannter Fehler');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der Einstellungen:', error);
|
||||
showFlashMessage('Fehler beim Speichern der Einstellungen: ' + error.message, 'error');
|
||||
} finally {
|
||||
// Restore button and remove loading states
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = originalButtonText;
|
||||
|
||||
const settingsCards = document.querySelectorAll('.glass-card');
|
||||
settingsCards.forEach(card => card.classList.remove('settings-loading'));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate settings before saving
|
||||
function validateSettings(settings) {
|
||||
const validThemes = ['light', 'dark', 'system'];
|
||||
const validContrast = ['normal', 'high'];
|
||||
const validLogoutValues = ['never', '30', '60', '120', '480'];
|
||||
|
||||
return validThemes.includes(settings.theme) &&
|
||||
validContrast.includes(settings.contrast) &&
|
||||
validLogoutValues.includes(settings.autoLogout);
|
||||
}
|
||||
|
||||
// Enhanced settings loading with caching
|
||||
async function loadUserSettings() {
|
||||
try {
|
||||
// Try to load from cache first for better performance
|
||||
const cachedSettings = localStorage.getItem('myp-settings-cache');
|
||||
if (cachedSettings) {
|
||||
try {
|
||||
const cached = JSON.parse(cachedSettings);
|
||||
// Use cached settings if they're less than 5 minutes old
|
||||
if (new Date() - new Date(cached.timestamp) < 5 * 60 * 1000) {
|
||||
applySettings(cached);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
localStorage.removeItem('myp-settings-cache');
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch('/api/user/settings');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const settings = result.settings;
|
||||
applySettings(settings);
|
||||
|
||||
// Cache the loaded settings
|
||||
settings.timestamp = new Date().toISOString();
|
||||
localStorage.setItem('myp-settings-cache', JSON.stringify(settings));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Einstellungen:', error);
|
||||
// Use fallback defaults if loading fails
|
||||
applyDefaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
function applySettings(settings) {
|
||||
// Theme-Einstellungen anwenden
|
||||
if (settings.theme === 'dark') {
|
||||
localStorage.setItem(STORAGE_KEY, 'true');
|
||||
document.documentElement.classList.add('dark');
|
||||
setActiveThemeButton(darkThemeBtn);
|
||||
} else if (settings.theme === 'light') {
|
||||
localStorage.setItem(STORAGE_KEY, 'false');
|
||||
document.documentElement.classList.remove('dark');
|
||||
setActiveThemeButton(lightThemeBtn);
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
setActiveThemeButton(systemThemeBtn);
|
||||
}
|
||||
|
||||
// Apply reduced motion setting
|
||||
if (settings.reduced_motion) {
|
||||
document.documentElement.style.setProperty('--tw-transition-duration', '0s');
|
||||
document.getElementById('reduced-motion').checked = true;
|
||||
} else {
|
||||
document.documentElement.style.removeProperty('--tw-transition-duration');
|
||||
document.getElementById('reduced-motion').checked = false;
|
||||
}
|
||||
|
||||
// Weitere Einstellungen anwenden
|
||||
document.getElementById('notify-new-jobs').checked = settings.notifications?.new_jobs ?? true;
|
||||
document.getElementById('notify-job-updates').checked = settings.notifications?.job_updates ?? true;
|
||||
document.getElementById('notify-system').checked = settings.notifications?.system ?? true;
|
||||
document.getElementById('notify-email').checked = settings.notifications?.email ?? false;
|
||||
document.getElementById('activity-logs').checked = settings.privacy?.activity_logs ?? true;
|
||||
document.getElementById('two-factor').checked = settings.privacy?.two_factor ?? false;
|
||||
document.getElementById('auto-logout').value = settings.privacy?.auto_logout ?? '60';
|
||||
|
||||
// Kontrast-Einstellungen
|
||||
if (settings.contrast === 'high') {
|
||||
localStorage.setItem('myp-contrast', 'high');
|
||||
document.documentElement.classList.add('high-contrast');
|
||||
setActiveContrastButton(highContrastBtn);
|
||||
} else {
|
||||
localStorage.setItem('myp-contrast', 'normal');
|
||||
document.documentElement.classList.remove('high-contrast');
|
||||
setActiveContrastButton(normalContrastBtn);
|
||||
}
|
||||
}
|
||||
|
||||
function applyDefaultSettings() {
|
||||
// Apply safe defaults if loading fails
|
||||
setActiveThemeButton(systemThemeBtn);
|
||||
setActiveContrastButton(normalContrastBtn);
|
||||
|
||||
document.getElementById('reduced-motion').checked = false;
|
||||
document.getElementById('notify-new-jobs').checked = true;
|
||||
document.getElementById('notify-job-updates').checked = true;
|
||||
document.getElementById('notify-system').checked = true;
|
||||
document.getElementById('notify-email').checked = false;
|
||||
document.getElementById('activity-logs').checked = true;
|
||||
document.getElementById('two-factor').checked = false;
|
||||
document.getElementById('auto-logout').value = '60';
|
||||
}
|
||||
|
||||
// Auto-logout implementation
|
||||
let logoutTimer = null;
|
||||
|
||||
function setupAutoLogout() {
|
||||
const autoLogoutSelect = document.getElementById('auto-logout');
|
||||
|
||||
// Event-Listener für Änderungen der Auto-Logout-Einstellung
|
||||
autoLogoutSelect.addEventListener('change', async function() {
|
||||
const newTimeout = this.value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/user/setting', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
},
|
||||
body: JSON.stringify({ auto_logout: newTimeout })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Globales Auto-Logout-System benachrichtigen
|
||||
if (window.autoLogoutManager) {
|
||||
window.autoLogoutManager.updateSettings(newTimeout);
|
||||
}
|
||||
showFlashMessage('Auto-Logout-Einstellung aktualisiert', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Auto-Logout-Einstellung:', error);
|
||||
showFlashMessage('Fehler beim Speichern der Einstellung', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Enhanced toggle switches with keyboard support
|
||||
function enhanceToggleSwitches() {
|
||||
document.querySelectorAll('.toggle-checkbox').forEach(checkbox => {
|
||||
// Add keyboard support
|
||||
checkbox.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
this.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Enhanced change handler with debouncing
|
||||
let changeTimeout;
|
||||
checkbox.addEventListener('change', function() {
|
||||
clearTimeout(changeTimeout);
|
||||
changeTimeout = setTimeout(() => {
|
||||
const settingName = this.name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
const status = this.checked ? 'aktiviert' : 'deaktiviert';
|
||||
|
||||
// Visual feedback
|
||||
const label = this.nextElementSibling;
|
||||
if (label) {
|
||||
label.style.transform = 'scale(1.05)';
|
||||
setTimeout(() => {
|
||||
label.style.transform = '';
|
||||
}, 150);
|
||||
}
|
||||
|
||||
// Auto-save individual settings
|
||||
saveIndividualSetting(this.name, this.checked);
|
||||
|
||||
showFlashMessage(`${settingName} wurde ${status}`, 'info');
|
||||
}, 300); // Debounce to prevent spam
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Save individual settings for immediate feedback
|
||||
async function saveIndividualSetting(settingName, value) {
|
||||
try {
|
||||
const response = await fetch('/api/user/setting', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
},
|
||||
body: JSON.stringify({ [settingName]: value })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Speichern der Einstellung');
|
||||
}
|
||||
|
||||
// Update cache
|
||||
const cached = localStorage.getItem('myp-settings-cache');
|
||||
if (cached) {
|
||||
try {
|
||||
const settings = JSON.parse(cached);
|
||||
// Update the specific setting in cache
|
||||
const keys = settingName.split('.');
|
||||
let current = settings;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (!current[keys[i]]) current[keys[i]] = {};
|
||||
current = current[keys[i]];
|
||||
}
|
||||
current[keys[keys.length - 1]] = value;
|
||||
settings.timestamp = new Date().toISOString();
|
||||
localStorage.setItem('myp-settings-cache', JSON.stringify(settings));
|
||||
} catch (e) {
|
||||
localStorage.removeItem('myp-settings-cache');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der Einzeleinstellung:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Performance optimization: Intersection Observer for animations
|
||||
function setupIntersectionObserver() {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('in-view');
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
document.querySelectorAll('.glass-card').forEach(card => {
|
||||
observer.observe(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize all enhanced features
|
||||
function initializeEnhancedFeatures() {
|
||||
setupAutoLogout();
|
||||
enhanceToggleSwitches();
|
||||
setupIntersectionObserver();
|
||||
setupNavigationLinks();
|
||||
}
|
||||
|
||||
// Setup navigation links
|
||||
function setupNavigationLinks() {
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
// If it's a real link to another page, don't preventDefault
|
||||
if (this.getAttribute('href').startsWith('#')) {
|
||||
e.preventDefault();
|
||||
|
||||
// Update active state
|
||||
navItems.forEach(navItem => {
|
||||
navItem.classList.remove('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-700', 'dark:text-blue-300');
|
||||
navItem.classList.add('text-slate-900', 'dark:text-white', 'hover:bg-gray-100', 'dark:hover:bg-slate-800');
|
||||
});
|
||||
|
||||
this.classList.add('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-700', 'dark:text-blue-300');
|
||||
this.classList.remove('text-slate-900', 'dark:text-white', 'hover:bg-gray-100', 'dark:hover:bg-slate-800');
|
||||
|
||||
// Scroll to section with offset for header
|
||||
const targetId = this.getAttribute('href').substring(1);
|
||||
const targetElement = document.querySelector(`[id="${targetId}"]`) ||
|
||||
document.querySelector(`h2:contains("${targetId}")`);
|
||||
|
||||
if (targetElement) {
|
||||
const offsetTop = targetElement.offsetTop - 100; // Account for header
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to show flash messages
|
||||
function showFlashMessage(message, type = 'info') {
|
||||
// Use the global toast manager if available
|
||||
if (window.showToast) {
|
||||
window.showToast(message, type);
|
||||
} else if (window.MYP && window.MYP.UI && window.MYP.UI.ToastManager) {
|
||||
const toast = new window.MYP.UI.ToastManager();
|
||||
toast.show(message, type);
|
||||
} else {
|
||||
// Fallback to simple notification
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 p-4 rounded-lg text-white z-50 ${
|
||||
type === 'success' ? 'bg-green-500' :
|
||||
type === 'error' ? 'bg-red-500' :
|
||||
type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'
|
||||
}`;
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
setTimeout(() => document.body.removeChild(notification), 300);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize everything
|
||||
loadUserSettings();
|
||||
initializeEnhancedFeatures();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Glass Card Effect */
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(229, 231, 235, 0.8);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .glass-card {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border-color: rgba(100, 116, 139, 0.3);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dark .glass-card:hover {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Loading State Animations */
|
||||
.settings-loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: -10px 0 0 -10px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Enhanced Toggle Switches */
|
||||
.toggle-checkbox {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Toggle Switch Styling */
|
||||
.toggle-checkbox:checked {
|
||||
right: 0;
|
||||
border-color: #3b82f6;
|
||||
background-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.toggle-checkbox:checked + .toggle-label {
|
||||
background-color: #3b82f6;
|
||||
}
|
||||
|
||||
.dark .toggle-checkbox:checked + .toggle-label {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-checkbox:focus + .toggle-label {
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Button states */
|
||||
.theme-btn.active,
|
||||
.contrast-btn.active {
|
||||
box-shadow: 0 0 0 2px #3b82f6;
|
||||
}
|
||||
|
||||
/* High contrast mode styles */
|
||||
.high-contrast {
|
||||
--tw-text-opacity: 1;
|
||||
}
|
||||
|
||||
.high-contrast * {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.high-contrast button:focus,
|
||||
.high-contrast input:focus,
|
||||
.high-contrast select:focus {
|
||||
outline: 3px solid #000 !important;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.dark.high-contrast button:focus,
|
||||
.dark.high-contrast input:focus,
|
||||
.dark.high-contrast select:focus {
|
||||
outline: 3px solid #fff !important;
|
||||
}
|
||||
|
||||
/* Accessibility improvements */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.glass-card,
|
||||
.toggle-checkbox,
|
||||
.toggle-label {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.settings-loading::after {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Success feedback animation */
|
||||
.settings-saved {
|
||||
animation: settingsSaved 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes settingsSaved {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.02); background-color: rgba(34, 197, 94, 0.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Smooth section transitions */
|
||||
.settings-section {
|
||||
scroll-margin-top: 2rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
511
templates/socket_test.html
Normal file
511
templates/socket_test.html
Normal 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
282
templates/stats.html
Normal 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
356
templates/terms.html
Normal file
@@ -0,0 +1,356 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Nutzungsbedingungen - MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Hero Header mit Gradient und Animation -->
|
||||
<div class="relative overflow-hidden rounded-3xl mb-12 p-8 md:p-12 bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-slate-900 dark:via-blue-900/20 dark:to-indigo-900/20 border border-blue-200/50 dark:border-blue-700/30">
|
||||
<!-- Animated Background Pattern -->
|
||||
<div class="absolute inset-0 opacity-10 dark:opacity-5">
|
||||
<div class="absolute top-0 left-0 w-full h-full">
|
||||
<svg class="animate-pulse" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="currentColor" stroke-width="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100" height="100" fill="url(#grid)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 text-center">
|
||||
<!-- Icon -->
|
||||
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-blue-100 dark:bg-blue-900/50 mb-6">
|
||||
<svg class="w-10 h-10 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-4 tracking-tight">
|
||||
Nutzungsbedingungen
|
||||
</h1>
|
||||
<p class="text-xl text-slate-600 dark:text-slate-300 mb-6 max-w-2xl mx-auto">
|
||||
Rechtliche Grundlagen für die Nutzung der MYP Platform
|
||||
</p>
|
||||
|
||||
<!-- Meta Information -->
|
||||
<div class="inline-flex items-center gap-4 text-sm text-slate-500 dark:text-slate-400">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span>Gültig ab 15. Juni 2024</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Lesezeit: ~5 Minuten</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table of Contents -->
|
||||
<div class="mb-12">
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h2 class="text-lg font-semibold text-slate-900 dark:text-white mb-4 flex items-center gap-3">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16"/>
|
||||
</svg>
|
||||
Inhaltsverzeichnis
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<a href="#section-1" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">1</span>
|
||||
<span class="text-sm">Allgemeines</span>
|
||||
</a>
|
||||
<a href="#section-2" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">2</span>
|
||||
<span class="text-sm">Zugang</span>
|
||||
</a>
|
||||
<a href="#section-3" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">3</span>
|
||||
<span class="text-sm">Nutzungszweck</span>
|
||||
</a>
|
||||
<a href="#section-4" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">4</span>
|
||||
<span class="text-sm">Verantwortlichkeiten</span>
|
||||
</a>
|
||||
<a href="#section-5" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">5</span>
|
||||
<span class="text-sm">Einschränkungen</span>
|
||||
</a>
|
||||
<a href="#section-6" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">6</span>
|
||||
<span class="text-sm">Verfügbarkeit</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms Content -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
<!-- Sidebar Navigation (hidden on mobile) -->
|
||||
<div class="hidden lg:block">
|
||||
<div class="sticky top-8">
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-4 uppercase tracking-wider">Navigation</h3>
|
||||
<nav class="space-y-2">
|
||||
<a href="#section-1" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">1. Allgemeines</a>
|
||||
<a href="#section-2" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">2. Zugang und Berechtigung</a>
|
||||
<a href="#section-3" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">3. Nutzungszweck</a>
|
||||
<a href="#section-4" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">4. Verantwortlichkeiten</a>
|
||||
<a href="#section-5" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">5. Einschränkungen</a>
|
||||
<a href="#section-6" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">6. Verfügbarkeit</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="lg:col-span-3">
|
||||
<div class="space-y-8">
|
||||
<!-- Section 1 -->
|
||||
<section id="section-1" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
|
||||
<span class="text-blue-600 dark:text-blue-400 font-bold">1</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Allgemeines</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
Diese Nutzungsbedingungen regeln die Nutzung der Mercedes-Benz "Manage Your Printers" (MYP) Plattform. Durch die Nutzung der Plattform erklären Sie sich mit diesen Bedingungen einverstanden.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 2 -->
|
||||
<section id="section-2" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/50 flex items-center justify-center">
|
||||
<span class="text-green-600 dark:text-green-400 font-bold">2</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Zugang und Nutzungsberechtigung</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
Die MYP-Plattform steht ausschließlich Mitarbeitern der Mercedes-Benz Group AG und ihren verbundenen Unternehmen zur Verfügung. Der Zugang erfolgt über eine persönliche Benutzerkennung und ein Passwort, die nicht an Dritte weitergegeben werden dürfen.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 3 -->
|
||||
<section id="section-3" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center">
|
||||
<span class="text-purple-600 dark:text-purple-400 font-bold">3</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Nutzungszweck</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
Die MYP-Plattform dient ausschließlich der Verwaltung und Überwachung von 3D-Druckaufträgen im Rahmen der beruflichen Tätigkeit. Eine private Nutzung ist nicht gestattet.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 4 -->
|
||||
<section id="section-4" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-orange-100 dark:bg-orange-900/50 flex items-center justify-center">
|
||||
<span class="text-orange-600 dark:text-orange-400 font-bold">4</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Verantwortlichkeiten der Nutzer</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">Als Nutzer sind Sie verantwortlich für:</p>
|
||||
<div class="grid gap-3">
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Geheimhaltung Ihrer Zugangsdaten</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die ordnungsgemäße Nutzung der Geräte und Ressourcen</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Einhaltung der Unternehmensrichtlinien zum Umgang mit 3D-Druckern</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Beachtung von Urheberrechten und Schutzrechten Dritter bei der Erstellung von 3D-Modellen</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 5 -->
|
||||
<section id="section-5" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/50 flex items-center justify-center">
|
||||
<span class="text-red-600 dark:text-red-400 font-bold">5</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Einschränkungen</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">Es ist untersagt:</p>
|
||||
<div class="grid gap-3">
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Plattform für nicht-geschäftliche Zwecke zu nutzen</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Unbefugten Zugang zu verschaffen</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Sicherheitsmaßnahmen der Plattform zu umgehen</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Schädlichen Code oder Malware hochzuladen</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Plattform zu überlasten oder ihre normale Funktion zu stören</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Weitere Abschnitte analog... -->
|
||||
<section id="section-6" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center">
|
||||
<span class="text-indigo-600 dark:text-indigo-400 font-bold">6</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Verfügbarkeit und Wartung</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
Mercedes-Benz bemüht sich um eine hohe Verfügbarkeit der MYP-Plattform, kann jedoch keine ununterbrochene Verfügbarkeit garantieren. Wartungsarbeiten werden nach Möglichkeit im Voraus angekündigt.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Kompakte weitere Abschnitte -->
|
||||
<div class="grid gap-6">
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<span class="w-6 h-6 rounded-full bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400 text-sm font-bold flex items-center justify-center">7</span>
|
||||
Haftung
|
||||
</h3>
|
||||
<p class="text-slate-700 dark:text-slate-300 text-sm mb-3">Mercedes-Benz übernimmt keine Haftung für:</p>
|
||||
<ul class="text-sm text-slate-600 dark:text-slate-400 space-y-1">
|
||||
<li>• Schäden, die durch fehlerhafte Druckaufträge entstehen</li>
|
||||
<li>• Verlust von Daten oder Modellen</li>
|
||||
<li>• Ausfallzeiten der Plattform</li>
|
||||
<li>• Schäden durch unsachgemäße Verwendung der Drucker</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<span class="w-6 h-6 rounded-full bg-cyan-100 dark:bg-cyan-900/50 text-cyan-600 dark:text-cyan-400 text-sm font-bold flex items-center justify-center">8</span>
|
||||
Datenschutz
|
||||
</h3>
|
||||
<p class="text-slate-700 dark:text-slate-300 text-sm">
|
||||
Die Erhebung und Verarbeitung personenbezogener Daten erfolgt gemäß der
|
||||
<a href="/privacy" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">Datenschutzerklärung</a>.
|
||||
Die Daten werden ausschließlich zur Verwaltung und Optimierung der Druckaufträge verwendet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kontakt Section -->
|
||||
<div class="glass-card bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border border-blue-200/50 dark:border-blue-700/30 rounded-2xl p-8">
|
||||
<div class="text-center">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900/50 mb-6">
|
||||
<svg class="w-8 h-8 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-4">Haben Sie Fragen?</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-6">Bei Fragen zu diesen Nutzungsbedingungen wenden Sie sich gerne an unser Team.</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="mailto:till.tomczak@mercedes-benz.com" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Till Tomczak
|
||||
</a>
|
||||
<a href="mailto:torben.haack@mercedes-benz.com" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Torben Haack
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-end mt-12 pt-8 border-t border-slate-200 dark:border-slate-700">
|
||||
<button onclick="window.print()" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-lg transition-all duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
|
||||
</svg>
|
||||
Drucken
|
||||
</button>
|
||||
<a href="javascript:history.back()" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Zurück
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Smooth Scrolling Script -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Smooth scrolling for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetElement = document.querySelector(targetId);
|
||||
if (targetElement) {
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user