feat: Erweiterung der Druckerstatusüberprüfung durch Implementierung einer Steckdosenabfrage für Drucker. Verbesserung der Benutzeroberfläche mit optimierten Dark Mode-Elementen und neuen Statusanzeigen für Drucker. Anpassungen in den Templates zur Unterstützung dynamischer Statusinformationen und zur Verbesserung der Benutzererfahrung. Aktualisierung der CSS-Styles für ein ansprechenderes Design und bessere Benutzerinteraktion.

This commit is contained in:
Till Tomczak 2025-05-29 09:46:16 +02:00
parent e9071c7b57
commit d81149229a
13 changed files with 2138 additions and 1233 deletions

View File

@ -1110,10 +1110,10 @@ def kiosk_restart_system():
def check_printer_status(ip_address: str, timeout: int = 7) -> Tuple[str, bool]:
"""
Überprüft den Status eines Druckers über Ping mit Timeout.
Überprüft den Status eines Druckers über Steckdosenabfrage mit Timeout.
Args:
ip_address: IP-Adresse des Druckers
ip_address: IP-Adresse der Drucker-Steckdose
timeout: Timeout in Sekunden (Standard: 7)
Returns:
@ -1132,7 +1132,7 @@ def check_printer_status(ip_address: str, timeout: int = 7) -> Tuple[str, bool]:
printers_logger.warning(f"Ungültige IP-Adresse: {ip_address}")
return "offline", False
# Windows-spezifischer Ping-Befehl mit Timeout
# Zuerst prüfen, ob die Steckdose erreichbar ist
if os.name == 'nt': # Windows
cmd = ['ping', '-n', '1', '-w', str(timeout * 1000), ip_address.strip()]
else: # Unix/Linux/macOS
@ -1150,19 +1150,66 @@ def check_printer_status(ip_address: str, timeout: int = 7) -> Tuple[str, bool]:
timeout=timeout + 2 # Zusätzlicher Timeout für subprocess
)
# Erfolgreicher Ping (Return Code 0)
if result.returncode == 0:
printers_logger.debug(f"Ping erfolgreich für {ip_address}")
return "online", True
else:
# Wenn Steckdose nicht erreichbar ist, ist der Drucker offline
if result.returncode != 0:
printers_logger.debug(f"Ping fehlgeschlagen für {ip_address} (Return Code: {result.returncode})")
return "offline", False
# Jetzt den tatsächlichen Steckdosenstatus abfragen
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.plug_ip == ip_address).first()
if not printer:
printers_logger.warning(f"Kein Drucker mit Steckdosen-IP {ip_address} gefunden")
db_session.close()
return "offline", False
# Smart Plug Status prüfen
import requests
from requests.exceptions import RequestException
# Standardwerte aus der Datenbank verwenden
username = printer.plug_username
password = printer.plug_password
try:
# Für TP-Link Smart Plugs oder kompatible Steckdosen
auth = (username, password)
response = requests.get(f"http://{ip_address}/status", auth=auth, timeout=timeout)
if response.status_code == 200:
try:
status_data = response.json()
# Überprüfen ob die Steckdose eingeschaltet ist
if 'system' in status_data and 'get_sysinfo' in status_data['system']:
if status_data['system']['get_sysinfo'].get('relay_state') == 1:
printers_logger.debug(f"Steckdose {ip_address} ist eingeschaltet")
db_session.close()
return "online", True
except (ValueError, KeyError) as e:
printers_logger.debug(f"Fehler beim Parsen der Steckdosen-Antwort: {str(e)}")
# Zweiter Versuch mit einfacher GET-Anfrage
response = requests.get(f"http://{ip_address}", auth=auth, timeout=timeout)
if response.status_code == 200:
printers_logger.debug(f"Steckdose {ip_address} antwortet auf HTTP-Anfrage")
# Wenn wir hier ankommen, ist die Steckdose online, aber wir wissen nicht sicher, ob sie eingeschaltet ist
# Da wir nur die Verfügbarkeit prüfen, nehmen wir an, dass sie aktiv ist, wenn sie antwortet
db_session.close()
return "online", True
except RequestException as e:
printers_logger.debug(f"Fehler bei HTTP-Anfrage an Steckdose {ip_address}: {str(e)}")
# Wenn beide API-Anfragen fehlschlagen, können wir annehmen, dass die Steckdose nicht eingeschaltet ist
db_session.close()
return "offline", False
except subprocess.TimeoutExpired:
printers_logger.warning(f"Ping-Timeout für Drucker {ip_address} nach {timeout} Sekunden")
return "offline", False
except Exception as e:
printers_logger.error(f"Fehler beim Ping für Drucker {ip_address}: {str(e)}")
printers_logger.error(f"Fehler beim Status-Check für Drucker {ip_address}: {str(e)}")
return "offline", False
def check_multiple_printers_status(printers: List[Dict], timeout: int = 7) -> Dict[int, Tuple[str, bool]]:

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -20,22 +20,28 @@
}
.dark {
/* Dark Mode Farben - Dunkler gemacht */
--color-bg-primary: #0a0f1a; /* Dunkler als vorher */
--color-bg-secondary: #131c2e; /* Dunkler als vorher */
--color-text-primary: #f8fafc;
/* Dark Mode Farben - Noch dunkler und eleganter */
--color-bg-primary: #000000; /* Tiefes Schwarz */
--color-bg-secondary: #0a0a0a; /* Sehr dunkles Grau */
--color-text-primary: #ffffff;
--color-text-secondary: #e2e8f0;
--color-text-muted: #94a3b8;
--color-border-primary: #1e293b; /* Angepasst */
--color-accent: #f8fafc; /* Weiß statt Indigo */
--color-accent-hover: #e2e8f0;
--color-accent-text: #0f172a;
--color-shadow: rgba(0, 0, 0, 0.5); /* Dunklerer Schatten */
--color-border-primary: #1a1a1a; /* Dunkler Rahmen */
--color-accent: #ffffff; /* Reines Weiß */
--color-accent-hover: #f0f0f0;
--color-accent-text: #000000;
--color-shadow: rgba(0, 0, 0, 0.8); /* Sehr dunkler Schatten */
--mb-black: #000000; /* Mercedes-Benz Schwarz */
}
body {
@apply bg-white dark:bg-[#050505] text-slate-900 dark:text-slate-200 transition-colors duration-300;
@apply bg-white dark:bg-black text-slate-900 dark:text-white transition-colors duration-300;
position: relative;
min-height: 100vh;
}
.dark body {
background: linear-gradient(135deg, #000000 0%, #0a0a0a 50%, #000000 100%);
}
/* Navbar Styles - Glassmorphism ohne überlagerte Hintergründe */
@ -407,25 +413,98 @@
}
.dark .mercedes-background::before {
opacity: 0.02; /* Leicht reduziert für dunkleren Hintergrund */
filter: invert(1);
opacity: 0.015; /* Sehr subtil für eleganten Dark Mode */
filter: invert(1) brightness(0.3);
background-size: 150px 150px; /* Größere Sterne für bessere Sichtbarkeit */
}
/* Monochrome Button Styles */
@layer components {
/* Buttons im Light Mode Schwarz statt Blau */
/* Buttons mit verstärktem Glassmorphism */
.btn-primary {
@apply bg-black/80 hover:bg-gray-800/80 dark:bg-white/80 dark:hover:bg-gray-200/80 text-white dark:text-slate-900 px-4 py-2 rounded-lg transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 shadow-xl backdrop-blur-lg;
backdrop-filter: blur(16px) saturate(150%) brightness(110%);
-webkit-backdrop-filter: blur(16px) saturate(150%) brightness(110%);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.1);
@apply text-white dark:text-slate-900 px-4 py-2 rounded-lg transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 shadow-2xl hover:-translate-y-0.5;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(20px) saturate(150%) brightness(110%);
-webkit-backdrop-filter: blur(20px) saturate(150%) brightness(110%);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.3),
0 8px 16px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
.btn-primary:hover {
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(25px) saturate(180%) brightness(120%);
-webkit-backdrop-filter: blur(25px) saturate(180%) brightness(120%);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.4),
0 10px 20px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.dark .btn-primary {
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.2),
0 8px 16px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.8),
0 0 0 1px rgba(0, 0, 0, 0.05);
}
.dark .btn-primary:hover {
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.15);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.3),
0 10px 20px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.btn-secondary {
@apply bg-white/70 hover:bg-gray-100/70 dark:bg-slate-800/70 dark:hover:bg-slate-700/70 text-slate-900 dark:text-white border border-gray-300/60 dark:border-slate-700/60 px-4 py-2 rounded-lg transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 shadow-xl backdrop-blur-lg;
backdrop-filter: blur(16px) saturate(150%) brightness(110%);
-webkit-backdrop-filter: blur(16px) saturate(150%) brightness(110%);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.1);
@apply text-slate-900 dark:text-white px-4 py-2 rounded-lg transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 shadow-2xl hover:-translate-y-0.5;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(20px) saturate(150%) brightness(110%);
-webkit-backdrop-filter: blur(20px) saturate(150%) brightness(110%);
border: 1px solid rgba(255, 255, 255, 0.4);
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.15),
0 8px 16px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.2);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(25px) saturate(180%) brightness(120%);
-webkit-backdrop-filter: blur(25px) saturate(180%) brightness(120%);
border: 1px solid rgba(255, 255, 255, 0.6);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.2),
0 10px 20px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.dark .btn-secondary {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.3),
0 8px 16px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
.dark .btn-secondary:hover {
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.4),
0 10px 20px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.btn-outline {
@ -435,23 +514,59 @@
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(255, 255, 255, 0.05);
}
/* Glassmorphism Card mit abgerundeten Ecken */
/* Glassmorphism Card mit abgerundeten Ecken - Verstärkt */
.glass-card {
@apply bg-white/70 dark:bg-black/70 backdrop-blur-xl border border-gray-200/60 dark:border-slate-700/40 rounded-xl p-6 shadow-xl transition-all duration-300;
backdrop-filter: blur(20px) saturate(180%) brightness(110%);
-webkit-backdrop-filter: blur(20px) saturate(180%) brightness(110%);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.1);
@apply rounded-xl p-6 shadow-2xl transition-all duration-300;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(30px) saturate(200%) brightness(120%) contrast(110%);
-webkit-backdrop-filter: blur(30px) saturate(200%) brightness(120%) contrast(110%);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.15),
0 8px 16px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.1);
border-radius: var(--card-radius);
}
.dark .glass-card {
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(30px) saturate(180%) brightness(110%) contrast(120%);
-webkit-backdrop-filter: blur(30px) saturate(180%) brightness(110%) contrast(120%);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.4),
0 8px 16px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.15),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
/* Dashboard Cards mit schwarzem Hintergrund */
/* Dashboard Cards mit verstärktem Glassmorphism */
.dashboard-card {
@apply bg-white/60 dark:bg-black/80 backdrop-blur-2xl border border-gray-200/70 dark:border-slate-700/20 rounded-xl p-6 shadow-xl transition-all duration-300;
backdrop-filter: blur(24px) saturate(200%) brightness(120%);
-webkit-backdrop-filter: blur(24px) saturate(200%) brightness(120%);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.1);
@apply rounded-xl p-6 shadow-2xl transition-all duration-300 hover:-translate-y-1;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(35px) saturate(200%) brightness(125%) contrast(115%);
-webkit-backdrop-filter: blur(35px) saturate(200%) brightness(125%) contrast(115%);
border: 1px solid rgba(255, 255, 255, 0.25);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.15),
0 8px 16px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.1);
border-radius: var(--card-radius);
}
.dark .dashboard-card {
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(35px) saturate(180%) brightness(115%) contrast(125%);
-webkit-backdrop-filter: blur(35px) saturate(180%) brightness(115%) contrast(125%);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.5),
0 8px 16px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.12),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
/* Navigation Styles */
.nav-link {
@ -498,8 +613,26 @@
}
.navbar-menu {
@apply flex items-center justify-center space-x-1 md:space-x-3 lg:space-x-6 p-2 rounded-full;
background: transparent;
@apply flex items-center justify-center space-x-1 md:space-x-3 lg:space-x-6 p-3 mx-4 rounded-2xl border;
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(20px) saturate(150%) brightness(110%);
-webkit-backdrop-filter: blur(20px) saturate(150%) brightness(110%);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
.dark .navbar-menu {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(20px) saturate(150%) brightness(110%);
-webkit-backdrop-filter: blur(20px) saturate(150%) brightness(110%);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
.navbar-button {
@ -529,21 +662,81 @@
}
.menu-item {
@apply flex items-center space-x-2 px-4 py-2.5 text-slate-700 dark:text-slate-300 rounded-full hover:bg-white/50 dark:hover:bg-slate-800/50 hover:text-slate-900 dark:hover:text-white transition-all duration-300 hover:shadow-md;
@apply flex items-center space-x-2 px-4 py-2.5 text-slate-700 dark:text-slate-300 rounded-xl transition-all duration-300;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.menu-item:hover {
@apply text-slate-900 dark:text-white;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(15px) saturate(150%);
-webkit-backdrop-filter: blur(15px) saturate(150%);
border: 1px solid rgba(255, 255, 255, 0.4);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.dark .menu-item {
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.dark .menu-item:hover {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
.menu-item.active {
@apply bg-white/60 dark:bg-black/60 text-slate-900 dark:text-white font-medium shadow-sm backdrop-blur-sm;
@apply text-slate-900 dark:text-white font-medium;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.6);
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.dark .menu-item.active {
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
/* Dropdown Styles */
/* Dropdown Styles - Verstärktes Glassmorphism */
.user-dropdown {
@apply absolute right-0 mt-2 w-64 bg-white/70 dark:bg-black/80 backdrop-blur-2xl border border-gray-200/70 dark:border-slate-700/20 rounded-xl shadow-2xl z-50 overflow-hidden;
backdrop-filter: blur(24px) saturate(200%) brightness(120%);
-webkit-backdrop-filter: blur(24px) saturate(200%) brightness(120%);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1);
@apply absolute right-0 mt-2 w-64 rounded-xl shadow-2xl z-50 overflow-hidden;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(40px) saturate(200%) brightness(130%) contrast(110%);
-webkit-backdrop-filter: blur(40px) saturate(200%) brightness(130%) contrast(110%);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.25),
0 8px 16px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.1);
animation: fadeIn 0.2s ease-out forwards;
}
.dark .user-dropdown {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(40px) saturate(180%) brightness(120%) contrast(120%);
-webkit-backdrop-filter: blur(40px) saturate(180%) brightness(120%) contrast(120%);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.6),
0 8px 16px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
.dropdown-header {
@apply flex items-center p-4 border-b border-gray-200/80 dark:border-slate-700/30;
@ -581,30 +774,299 @@
width: 100% !important;
left: 0 !important;
right: 0 !important;
@apply bg-white/60 dark:bg-black/70;
backdrop-filter: blur(24px) saturate(200%);
-webkit-backdrop-filter: blur(24px) saturate(200%);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.08);
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
/* Dark Mode Toggle - Schwarz statt Blau im Light Mode */
.dark-mode-toggle {
@apply p-3 rounded-full bg-black/80 hover:bg-gray-800/80 dark:bg-white/80 dark:hover:bg-gray-200/80 text-white dark:text-slate-900 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-gray-500 shadow-xl;
backdrop-filter: blur(12px) saturate(150%);
-webkit-backdrop-filter: blur(12px) saturate(150%);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.1);
min-width: 42px;
min-height: 42px;
display: flex;
align-items: center;
justify-content: center;
.dark .navbar {
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
/* Animation für Dropdown */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
/* Neue Navbar-Komponenten */
.navbar-menu-new {
@apply flex items-center justify-center space-x-3 md:space-x-2 lg:space-x-1 xl:space-x-2;
max-width: 100%;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.dropdown-animation {
animation: fadeIn 0.2s ease-out forwards;
}
.navbar-menu-new::-webkit-scrollbar {
display: none;
}
.nav-item {
@apply flex items-center space-x-2 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-300;
color: rgba(15, 23, 42, 0.8);
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.dark .nav-item {
color: rgba(255, 255, 255, 0.8);
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.nav-item:hover {
@apply transform -translate-y-0.5;
color: rgba(15, 23, 42, 1);
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.05),
0 2px 6px rgba(0, 0, 0, 0.04),
0 0 1px rgba(0, 0, 0, 0.1);
}
.dark .nav-item:hover {
color: rgba(255, 255, 255, 1);
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.2),
0 2px 6px rgba(0, 0, 0, 0.15),
0 0 1px rgba(255, 255, 255, 0.05);
}
.nav-item.active {
color: rgba(15, 23, 42, 1);
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.25);
box-shadow:
0 10px 25px rgba(0, 0, 0, 0.08),
0 3px 8px rgba(0, 0, 0, 0.06),
0 1px 2px rgba(0, 0, 0, 0.04);
font-weight: 600;
}
.dark .nav-item.active {
color: rgba(255, 255, 255, 1);
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow:
0 10px 25px rgba(0, 0, 0, 0.3),
0 3px 8px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.06);
}
/* Dark Mode Toggle - Neues Design */
.dark-mode-toggle-new {
@apply relative p-2.5 rounded-full flex items-center justify-center transition-all duration-300 cursor-pointer;
background: rgba(241, 245, 249, 0.8);
border: 1px solid rgba(255, 255, 255, 0.7);
box-shadow:
0 2px 10px rgba(0, 0, 0, 0.05),
0 1px 2px rgba(0, 0, 0, 0.04);
color: #334155;
z-index: 100; /* Stellt sicher, dass der Button über anderen Elementen liegt */
}
.dark-mode-toggle-new:hover {
@apply transform -translate-y-0.5;
background: rgba(241, 245, 249, 0.9);
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.08),
0 2px 6px rgba(0, 0, 0, 0.06);
}
.dark-mode-toggle-new:active {
@apply transform scale-95;
transition: transform 0.1s;
}
.dark .dark-mode-toggle-new {
background: rgba(30, 41, 59, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 2px 10px rgba(0, 0, 0, 0.2),
0 1px 2px rgba(0, 0, 0, 0.1);
color: #e2e8f0;
}
.dark .dark-mode-toggle-new:hover {
background: rgba(30, 41, 59, 0.9);
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.2),
0 2px 6px rgba(0, 0, 0, 0.15);
}
/* Icon-Animation */
.dark-mode-toggle-new .sun-icon,
.dark-mode-toggle-new .moon-icon {
@apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transition-all duration-300;
}
.dark-mode-toggle-new .sun-icon:not(.hidden) {
animation: spin-in 0.5s cubic-bezier(0.25, 1, 0.5, 1) forwards;
}
.dark-mode-toggle-new .moon-icon:not(.hidden) {
animation: spin-in 0.5s cubic-bezier(0.25, 1, 0.5, 1) forwards;
}
@keyframes spin-in {
0% {
opacity: 0;
transform: translateY(10px) scale(0.7) rotate(20deg);
}
100% {
opacity: 1;
transform: translateY(0) scale(1) rotate(0);
}
}
.dark .sun-icon {
display: none;
}
.dark .moon-icon {
display: block;
}
.sun-icon {
display: block;
}
.moon-icon {
display: none;
}
/* User Menu Button - Neues Design */
.user-menu-button-new {
@apply flex items-center space-x-2 rounded-lg p-1.5 transition-all duration-300;
background: rgba(241, 245, 249, 0.6);
border: 1px solid rgba(255, 255, 255, 0.6);
box-shadow:
0 2px 10px rgba(0, 0, 0, 0.04),
0 1px 2px rgba(0, 0, 0, 0.02);
}
.user-menu-button-new:hover {
@apply transform -translate-y-0.5;
background: rgba(241, 245, 249, 0.8);
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.06),
0 2px 6px rgba(0, 0, 0, 0.04);
}
.dark .user-menu-button-new {
background: rgba(30, 41, 59, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow:
0 2px 10px rgba(0, 0, 0, 0.15),
0 1px 2px rgba(0, 0, 0, 0.1);
}
.dark .user-menu-button-new:hover {
background: rgba(30, 41, 59, 0.8);
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.15),
0 2px 6px rgba(0, 0, 0, 0.1);
}
/* User Avatar - Neues Design */
.user-avatar-new {
@apply h-8 w-8 rounded-full flex items-center justify-center text-white font-semibold text-sm shadow-md transition-all duration-300;
background: linear-gradient(135deg, #000000, #333333);
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.2),
0 1px 3px rgba(0, 0, 0, 0.1);
}
.dark .user-avatar-new {
background: linear-gradient(135deg, #f8fafc, #e2e8f0);
color: #0f172a;
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.3),
0 1px 3px rgba(0, 0, 0, 0.2);
}
/* Login Button - Neues Design */
.login-button-new {
@apply flex items-center px-4 py-2 rounded-lg text-sm font-medium shadow-sm transition-all duration-300;
background: #000000;
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 2px 10px rgba(0, 0, 0, 0.1),
0 1px 2px rgba(0, 0, 0, 0.08);
}
.login-button-new:hover {
@apply transform -translate-y-0.5;
background: #333333;
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.15),
0 3px 6px rgba(0, 0, 0, 0.1);
}
.dark .login-button-new {
background: #ffffff;
color: #000000;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
0 2px 10px rgba(0, 0, 0, 0.2),
0 1px 2px rgba(0, 0, 0, 0.15);
}
.dark .login-button-new:hover {
background: #f1f5f9;
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.25),
0 3px 6px rgba(0, 0, 0, 0.2);
}
/* Mobile Menu - Neues Design */
.mobile-menu-new {
@apply w-full overflow-hidden transition-all duration-300 z-40;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid rgba(241, 245, 249, 0.8);
max-height: 0;
opacity: 0;
}
.mobile-menu-new.open {
max-height: 500px;
opacity: 1;
border-bottom: 1px solid rgba(241, 245, 249, 0.8);
}
.dark .mobile-menu-new {
background: rgba(15, 23, 42, 0.8);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
border-bottom: 1px solid rgba(30, 41, 59, 0.8);
}
.mobile-nav-item {
@apply flex items-center space-x-3 px-4 py-3 rounded-lg text-slate-800 dark:text-slate-200 transition-all duration-300;
}
.mobile-nav-item:hover {
background: rgba(241, 245, 249, 0.8);
}
.dark .mobile-nav-item:hover {
background: rgba(30, 41, 59, 0.6);
}
.mobile-nav-item.active {
background: rgba(241, 245, 249, 0.9);
color: #000000;
font-weight: 500;
}
.dark .mobile-nav-item.active {
background: rgba(30, 41, 59, 0.8);
color: #ffffff;
}
/* Dashboard Stat Cards mit schwarzem Hintergrund im Dark Mode */
.mb-stat-card {
@ -634,12 +1096,30 @@
border-radius: var(--card-radius);
}
/* Footer Styling */
/* Footer Styling - Verstärktes Glassmorphism */
footer {
@apply bg-white/60 dark:bg-black/60 backdrop-blur-xl border-t border-gray-200/70 dark:border-slate-700/20 transition-all duration-300;
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(255, 255, 255, 0.05);
@apply transition-all duration-300;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(30px) saturate(180%) brightness(120%);
-webkit-backdrop-filter: blur(30px) saturate(180%) brightness(120%);
border-top: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 -8px 32px rgba(0, 0, 0, 0.1),
0 -2px 8px rgba(0, 0, 0, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
.dark footer {
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(30px) saturate(160%) brightness(110%);
-webkit-backdrop-filter: blur(30px) saturate(160%) brightness(110%);
border-top: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 -8px 32px rgba(0, 0, 0, 0.3),
0 -2px 8px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.1),
0 0 0 1px rgba(255, 255, 255, 0.03);
}
/* Dropdown Pfeil Animation */
@ -668,6 +1148,286 @@ footer {
}
.dark .mercedes-star-bg::after {
opacity: 0.03;
filter: invert(1);
}
opacity: 0.02;
filter: invert(1) brightness(0.4);
}
/* Zusätzliche Glassmorphism-Verbesserungen */
.glass-effect {
backdrop-filter: blur(20px) saturate(180%) brightness(110%);
-webkit-backdrop-filter: blur(20px) saturate(180%) brightness(110%);
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.dark .glass-effect {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
/* Verbesserte Hover-Effekte für alle interaktiven Elemente */
.glass-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-hover:hover {
transform: translateY(-2px);
backdrop-filter: blur(25px) saturate(200%) brightness(120%);
-webkit-backdrop-filter: blur(25px) saturate(200%) brightness(120%);
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.15),
0 8px 16px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
.dark .glass-hover:hover {
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.4),
0 8px 16px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
/* Neue verbesserte Drucker-Karten für die Drucker-Ansicht */
.printer-card-new {
@apply bg-gradient-to-br from-white/90 to-white/70 dark:from-slate-800/90 dark:to-slate-900/70 backdrop-blur-2xl rounded-xl border border-gray-200/70 dark:border-slate-700/30 p-5 shadow-2xl transition-all duration-300 hover:-translate-y-1 relative overflow-hidden;
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.08),
0 10px 20px rgba(0, 0, 0, 0.06),
0 0 0 1px rgba(255, 255, 255, 0.1);
border-radius: var(--card-radius, 1rem);
}
.dark .printer-card-new {
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.4),
0 10px 20px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
/* Online Drucker-Karten mit stärkerem visuellen Unterschied */
.printer-card-new.online {
@apply bg-gradient-to-br from-green-50/90 to-emerald-50/80 dark:from-green-900/30 dark:to-emerald-900/20 border-green-200 dark:border-green-700/50;
box-shadow:
0 20px 40px rgba(0, 122, 85, 0.08),
0 10px 20px rgba(0, 122, 85, 0.06),
0 0 0 1px rgba(209, 250, 229, 0.4);
}
.dark .printer-card-new.online {
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.3),
0 10px 20px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(16, 185, 129, 0.2);
}
/* Status-Badge mit verbesserten Styles */
.status-badge-new {
@apply px-2.5 py-1 rounded-full text-xs font-medium inline-flex items-center space-x-1 shadow-sm;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.dark .status-badge-new {
background: rgba(30, 41, 59, 0.7);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.status-badge-new.online {
@apply bg-green-100/90 text-green-800 dark:bg-green-900/60 dark:text-green-300;
}
.status-badge-new.offline {
@apply bg-red-100/90 text-red-800 dark:bg-red-900/60 dark:text-red-300;
}
/* Verbesserte Filterleiste */
.filter-bar-new {
@apply bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl rounded-lg p-1.5 border border-gray-200/60 dark:border-slate-700/30 shadow-xl;
box-shadow:
0 10px 25px rgba(0, 0, 0, 0.05),
0 5px 10px rgba(0, 0, 0, 0.03),
0 0 0 1px rgba(255, 255, 255, 0.2);
}
.dark .filter-bar-new {
box-shadow:
0 10px 25px rgba(0, 0, 0, 0.2),
0 5px 10px rgba(0, 0, 0, 0.15),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
.filter-btn-new {
@apply px-3.5 py-2 text-sm rounded-md transition-all duration-300 font-medium;
}
.filter-btn-new.active {
@apply bg-black text-white dark:bg-white dark:text-slate-900 shadow-md;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.dark .filter-btn-new.active {
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
/* Verbesserte Aktionsschaltflächen */
.action-btn-new {
@apply flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-300 shadow-md hover:-translate-y-0.5;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.action-btn-new.primary {
@apply bg-indigo-600 hover:bg-indigo-700 text-white dark:bg-indigo-600 dark:hover:bg-indigo-500;
box-shadow: 0 5px 15px rgba(79, 70, 229, 0.2);
}
.dark .action-btn-new.primary {
box-shadow: 0 5px 15px rgba(79, 70, 229, 0.3);
}
.action-btn-new.success {
@apply bg-green-600 hover:bg-green-700 text-white dark:bg-green-600 dark:hover:bg-green-500;
box-shadow: 0 5px 15px rgba(16, 185, 129, 0.2);
}
.dark .action-btn-new.success {
box-shadow: 0 5px 15px rgba(16, 185, 129, 0.3);
}
.action-btn-new.danger {
@apply bg-red-600 hover:bg-red-700 text-white dark:bg-red-600 dark:hover:bg-red-500;
box-shadow: 0 5px 15px rgba(239, 68, 68, 0.2);
}
.dark .action-btn-new.danger {
box-shadow: 0 5px 15px rgba(239, 68, 68, 0.3);
}
/* Informationszeilen in Drucker-Karten */
.printer-info-row {
@apply flex items-center gap-2 text-xs sm:text-sm text-slate-700 dark:text-slate-300 mb-1.5;
}
.printer-info-icon {
@apply w-3.5 h-3.5 sm:w-4 sm:h-4 text-slate-500 dark:text-slate-400 flex-shrink-0;
}
/* Online-Indikator mit Pulseffekt */
.online-indicator {
@apply absolute top-2.5 right-2.5 w-3 h-3 bg-green-500 rounded-full shadow-lg;
box-shadow: 0 0 0 rgba(16, 185, 129, 0.6);
animation: pulse-ring 2s cubic-bezier(0.455, 0.03, 0.515, 0.955) infinite;
}
@keyframes pulse-ring {
0% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.6);
}
70% {
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
/* Header mit verbesserten Status-Anzeigen */
.status-overview-new {
@apply flex flex-wrap gap-3 text-xs sm:text-sm p-3 bg-white/60 dark:bg-slate-800/60 backdrop-blur-xl rounded-lg border border-gray-200/60 dark:border-slate-700/30 shadow-lg;
box-shadow:
0 10px 25px rgba(0, 0, 0, 0.04),
0 5px 10px rgba(0, 0, 0, 0.02),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
.dark .status-overview-new {
box-shadow:
0 10px 25px rgba(0, 0, 0, 0.15),
0 5px 10px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(255, 255, 255, 0.03);
}
.status-dot {
@apply w-2.5 h-2.5 rounded-full;
}
.status-dot.online {
@apply bg-green-500;
animation: pulse-dot 2s cubic-bezier(0.455, 0.03, 0.515, 0.955) infinite;
}
.status-dot.offline {
@apply bg-red-500;
}
@keyframes pulse-dot {
0% {
transform: scale(0.95);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
100% {
transform: scale(0.95);
opacity: 1;
}
}
/* Verbesserte Modals mit Glassmorphism */
.modal-new {
@apply fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm;
}
.modal-content-new {
@apply bg-white/90 dark:bg-slate-800/90 backdrop-blur-2xl rounded-2xl p-6 w-full max-w-md shadow-2xl border border-gray-200/60 dark:border-slate-700/30 transform transition-all duration-300;
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.15),
0 15px 30px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(255, 255, 255, 0.1);
animation: modal-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.dark .modal-content-new {
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.4),
0 15px 30px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
@keyframes modal-in {
0% {
opacity: 0;
transform: scale(0.95) translateY(10px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* Verbesserte Input-Felder für Modals */
.input-new {
@apply w-full px-3 py-2.5 bg-white/70 dark:bg-slate-700/70 border border-gray-300/60 dark:border-slate-600/60 rounded-lg text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-black dark:focus:ring-white focus:border-transparent transition-all duration-200 backdrop-blur-lg;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.dark .input-new {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
/* Verbesserte Form-Labels */
.form-label-new {
@apply block mb-2 text-sm font-medium text-slate-900 dark:text-white;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,159 @@
/**
* Dark Mode Toggle Fix
* Diese Datei stellt sicher, dass der Dark Mode Toggle Button korrekt funktioniert
*/
document.addEventListener('DOMContentLoaded', function() {
// Dark Mode Toggle Button
const darkModeToggle = document.getElementById('darkModeToggle');
const html = document.documentElement;
// Local Storage Key
const STORAGE_KEY = 'myp-dark-mode';
/**
* Aktuellen Dark Mode Status aus Local Storage oder Systemeinstellung abrufen
*/
function isDarkMode() {
const savedMode = localStorage.getItem(STORAGE_KEY);
if (savedMode !== null) {
return savedMode === 'true';
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
/**
* Icons im Toggle-Button aktualisieren
*/
function updateIcons(isDark) {
// Finde die Icons im Button - versuche sowohl direkte Kinder als auch verschachtelte
let sunIcon = darkModeToggle.querySelector('.sun-icon');
let moonIcon = darkModeToggle.querySelector('.moon-icon');
// Wenn Icons nicht gefunden wurden, erstelle sie neu
if (!sunIcon || !moonIcon) {
console.warn('Icons nicht gefunden - erzeuge neue Icons');
// Entferne vorhandene Inhalte im Button
darkModeToggle.innerHTML = '';
// Neue Icons erstellen
sunIcon = document.createElement('svg');
sunIcon.className = 'w-5 h-5 sm:w-5 sm:h-5 sun-icon';
sunIcon.setAttribute('fill', 'none');
sunIcon.setAttribute('stroke', 'currentColor');
sunIcon.setAttribute('viewBox', '0 0 24 24');
sunIcon.setAttribute('aria-hidden', 'true');
sunIcon.innerHTML = '<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" />';
moonIcon = document.createElement('svg');
moonIcon.className = 'w-5 h-5 sm:w-5 sm:h-5 moon-icon hidden';
moonIcon.setAttribute('fill', 'none');
moonIcon.setAttribute('stroke', 'currentColor');
moonIcon.setAttribute('viewBox', '0 0 24 24');
moonIcon.setAttribute('aria-hidden', 'true');
moonIcon.innerHTML = '<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" />';
darkModeToggle.appendChild(sunIcon);
darkModeToggle.appendChild(moonIcon);
}
// Icons entsprechend dem Dark Mode Status anzeigen/verbergen
if (isDark) {
sunIcon.classList.add('hidden');
moonIcon.classList.remove('hidden');
} else {
sunIcon.classList.remove('hidden');
moonIcon.classList.add('hidden');
}
}
/**
* Dark Mode aktivieren/deaktivieren
*/
function setDarkMode(enable) {
console.log(`Setze Dark Mode auf: ${enable ? 'Aktiviert' : 'Deaktiviert'}`);
if (enable) {
html.classList.add('dark');
html.setAttribute('data-theme', 'dark');
html.style.colorScheme = 'dark';
if (darkModeToggle) {
darkModeToggle.setAttribute('aria-pressed', 'true');
darkModeToggle.setAttribute('title', 'Light Mode aktivieren');
// Button-Icons aktualisieren
updateIcons(true);
}
} else {
html.classList.remove('dark');
html.setAttribute('data-theme', 'light');
html.style.colorScheme = 'light';
if (darkModeToggle) {
darkModeToggle.setAttribute('aria-pressed', 'false');
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
// Button-Icons aktualisieren
updateIcons(false);
}
}
// Einstellung im Local Storage speichern
localStorage.setItem(STORAGE_KEY, enable.toString());
// ThemeColor Meta-Tag aktualisieren
const metaThemeColor = document.getElementById('metaThemeColor');
if (metaThemeColor) {
metaThemeColor.setAttribute('content', enable ? '#000000' : '#ffffff');
}
// Event für andere Komponenten auslösen
window.dispatchEvent(new CustomEvent('darkModeChanged', {
detail: { isDark: enable }
}));
}
// Toggle Dark Mode Funktion
function toggleDarkMode() {
const currentMode = isDarkMode();
setDarkMode(!currentMode);
}
// Event Listener für Toggle Button
if (darkModeToggle) {
// Vorherige Event-Listener entfernen, um Duplikate zu vermeiden
const newDarkModeToggle = darkModeToggle.cloneNode(true);
darkModeToggle.parentNode.replaceChild(newDarkModeToggle, darkModeToggle);
// Neuen Event-Listener hinzufügen
newDarkModeToggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation(); // Verhindere Bubbling
toggleDarkMode();
});
// Aktualisiere die Variable auf das neue Element
darkModeToggle = newDarkModeToggle;
// Initialen Status setzen
const isDark = isDarkMode();
setDarkMode(isDark);
console.log('Dark Mode Toggle Button erfolgreich initialisiert');
} else {
console.error('Dark Mode Toggle Button konnte nicht gefunden werden!');
}
// Tastaturkürzel: Strg+Shift+D für Dark Mode Toggle
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
toggleDarkMode();
e.preventDefault();
}
});
// Direkte Verfügbarkeit der Funktionen im globalen Bereich
window.toggleDarkMode = toggleDarkMode;
window.isDarkMode = isDarkMode;
window.setDarkMode = setDarkMode;
});

View File

@ -79,7 +79,7 @@
}, 100);
// Erfolgsmeldung in die Konsole
console.log(`Dark Mode ${enable ? 'aktiviert' : 'deaktiviert'}`);
console.log(`${enable ? '🌙' : '☀️'} ${enable ? 'Dark Mode aktiviert - Augenschonender Modus aktiv' : 'Light Mode aktiviert - Heller Modus aktiv'}`);
}
// Aktualisiert das Theme-Color Meta-Tag
@ -117,35 +117,38 @@
darkModeToggle.setAttribute('aria-pressed', isDark.toString());
darkModeToggle.title = isDark ? "Light Mode aktivieren" : "Dark Mode aktivieren";
// Stile aktualisieren mit Tailwind-Klassen
if (isDark) {
darkModeToggle.classList.remove('bg-indigo-600', 'hover:bg-indigo-700');
darkModeToggle.classList.add('bg-slate-800', 'hover:bg-slate-700', 'text-amber-400');
darkModeToggle.setAttribute('data-tooltip', 'Light Mode aktivieren');
} else {
darkModeToggle.classList.remove('bg-slate-800', 'hover:bg-slate-700', 'text-amber-400');
darkModeToggle.classList.add('bg-indigo-600', 'hover:bg-indigo-700');
darkModeToggle.setAttribute('data-tooltip', 'Dark Mode aktivieren');
}
// Icons aktualisieren
const sunIcon = darkModeToggle.querySelector('.sun-icon');
const moonIcon = darkModeToggle.querySelector('.moon-icon');
// Icon aktualisieren - ohne innerHTML für CSP-Kompatibilität
const icon = darkModeToggle.querySelector('svg');
if (icon) {
// Animationsklasse hinzufügen
icon.classList.add('animate-spin-once');
// Nach Animation wieder entfernen
setTimeout(() => {
icon.classList.remove('animate-spin-once');
}, 300);
const pathElement = icon.querySelector('path');
if (pathElement) {
// Sonnen- oder Mond-Symbol
if (isDark) {
pathElement.setAttribute("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");
} else {
pathElement.setAttribute("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");
if (sunIcon && moonIcon) {
if (isDark) {
sunIcon.classList.add('hidden');
moonIcon.classList.remove('hidden');
} else {
sunIcon.classList.remove('hidden');
moonIcon.classList.add('hidden');
}
} else {
// Fallback für ältere Implementierung mit einem Icon
const icon = darkModeToggle.querySelector('svg');
if (icon) {
// Animationsklasse hinzufügen
icon.classList.add('animate-spin-once');
// Nach Animation wieder entfernen
setTimeout(() => {
icon.classList.remove('animate-spin-once');
}, 300);
const pathElement = icon.querySelector('path');
if (pathElement) {
// Sonnen- oder Mond-Symbol
if (isDark) {
pathElement.setAttribute("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");
} else {
pathElement.setAttribute("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");
}
}
}
}
@ -158,6 +161,7 @@
// Wenn kein Toggle existiert, erstelle einen
if (!darkModeToggle) {
console.log('🔧 Dark Mode Toggle nicht gefunden - erstelle automatisch einen neuen Button');
createDarkModeToggle();
}
@ -165,6 +169,7 @@
if (darkModeToggle) {
darkModeToggle.addEventListener('click', function() {
const isDark = !shouldUseDarkMode();
console.log(`👆 Dark Mode Toggle: Wechsel zu ${isDark ? '🌙 dunkel' : '☀️ hell'} angefordert`);
setDarkMode(isDark);
});
}
@ -173,6 +178,7 @@
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
const isDark = !shouldUseDarkMode();
console.log(`⌨️ Tastenkombination STRG+SHIFT+D erkannt: Wechsel zu ${isDark ? '🌙 dunkel' : '☀️ hell'}`);
setDarkMode(isDark);
e.preventDefault();
}
@ -186,6 +192,7 @@
darkModeMediaQuery.addEventListener('change', function(e) {
// Nur anwenden, wenn keine benutzerdefinierte Einstellung gespeichert ist
if (localStorage.getItem(STORAGE_KEY) === null) {
console.log(`🖥️ Systemeinstellung geändert: ${e.matches ? '🌙 dunkel' : '☀️ hell'}`);
setDarkMode(e.matches);
}
});
@ -193,22 +200,27 @@
// Fallback für ältere Browser
darkModeMediaQuery.addListener(function(e) {
if (localStorage.getItem(STORAGE_KEY) === null) {
console.log(`🖥️ Systemeinstellung geändert (Legacy-Browser): ${e.matches ? '🌙 dunkel' : '☀️ hell'}`);
setDarkMode(e.matches);
}
});
}
// Initialer Zustand
setDarkMode(shouldUseDarkMode());
const initialState = shouldUseDarkMode();
console.log(`🔍 Ermittelter Ausgangszustand: ${initialState ? '🌙 Dark Mode' : '☀️ Light Mode'}`);
setDarkMode(initialState);
// Animation für den korrekten Modus hinzufügen
const animClass = shouldUseDarkMode() ? 'dark-mode-transition' : 'light-mode-transition';
const animClass = initialState ? 'dark-mode-transition' : 'light-mode-transition';
document.body.classList.add(animClass);
// Animation entfernen nach Abschluss
setTimeout(() => {
document.body.classList.remove(animClass);
}, 300);
console.log('🚀 Dark Mode Handler erfolgreich initialisiert');
}
// Erstellt ein Toggle-Element, falls keines existiert
@ -218,46 +230,61 @@
const nav = document.querySelector('nav');
const container = document.querySelector('.dark-mode-container') || header || nav;
if (!container) return;
if (!container) {
console.error('⚠️ Kein geeigneter Container für Dark Mode Toggle gefunden');
return;
}
// Toggle-Button erstellen
darkModeToggle = document.createElement('button');
darkModeToggle.id = 'darkModeToggle';
darkModeToggle.className = 'p-2 sm:p-3 rounded-full bg-indigo-600 text-white transition-all duration-300';
darkModeToggle.className = 'dark-mode-toggle-new';
darkModeToggle.setAttribute('aria-label', 'Dark Mode umschalten');
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
darkModeToggle.setAttribute('data-tooltip', 'Dark Mode aktivieren');
darkModeToggle.setAttribute('data-action', 'toggle-dark-mode');
// SVG-Icon erstellen (ohne innerHTML für Content Security Policy)
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("class", "w-4 h-4 sm:w-5 sm:h-5");
svg.setAttribute("fill", "none");
svg.setAttribute("stroke", "currentColor");
svg.setAttribute("viewBox", "0 0 24 24");
// Sonnen-Icon erstellen
const sunIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
sunIcon.setAttribute("class", "w-5 h-5 sm:w-5 sm:h-5 sun-icon");
sunIcon.setAttribute("fill", "none");
sunIcon.setAttribute("stroke", "currentColor");
sunIcon.setAttribute("viewBox", "0 0 24 24");
sunIcon.setAttribute("aria-hidden", "true");
// Path für das Icon
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("stroke-linecap", "round");
path.setAttribute("stroke-linejoin", "round");
path.setAttribute("stroke-width", "2");
path.setAttribute("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");
const sunPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
sunPath.setAttribute("stroke-linecap", "round");
sunPath.setAttribute("stroke-linejoin", "round");
sunPath.setAttribute("stroke-width", "2");
sunPath.setAttribute("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");
// Mond-Icon erstellen
const moonIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
moonIcon.setAttribute("class", "w-5 h-5 sm:w-5 sm:h-5 moon-icon hidden");
moonIcon.setAttribute("fill", "none");
moonIcon.setAttribute("stroke", "currentColor");
moonIcon.setAttribute("viewBox", "0 0 24 24");
moonIcon.setAttribute("aria-hidden", "true");
const moonPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
moonPath.setAttribute("stroke-linecap", "round");
moonPath.setAttribute("stroke-linejoin", "round");
moonPath.setAttribute("stroke-width", "2");
moonPath.setAttribute("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");
// Elemente zusammenfügen
svg.appendChild(path);
darkModeToggle.appendChild(svg);
// Screenreader-Text hinzufügen
const srText = document.createElement('span');
srText.className = 'sr-only';
srText.textContent = 'Dark Mode umschalten';
darkModeToggle.appendChild(srText);
sunIcon.appendChild(sunPath);
moonIcon.appendChild(moonPath);
darkModeToggle.appendChild(sunIcon);
darkModeToggle.appendChild(moonIcon);
// Zum Container hinzufügen
container.appendChild(darkModeToggle);
console.log('✅ Dark Mode Toggle Button erfolgreich erstellt und zur Benutzeroberfläche hinzugefügt');
}
// Sofort Dark/Light Mode anwenden (vor DOMContentLoaded)
const isDark = shouldUseDarkMode();
console.log(`🏃‍♂️ Sofortige Anwendung: ${isDark ? '🌙 Dark Mode' : '☀️ Light Mode'} (vor DOM-Ladung)`);
setDarkMode(isDark);
})();
@ -275,4 +302,5 @@ if (!document.querySelector('style#dark-mode-animations')) {
}
`;
document.head.appendChild(styleTag);
console.log('💫 Animations-Styles für Dark Mode Toggle hinzugefügt');
}

View File

@ -18,7 +18,8 @@
constructor() {
// DOM-Elemente
this.darkModeToggle = document.getElementById('darkModeToggle');
this.darkModeIcon = this.darkModeToggle ? this.darkModeToggle.querySelector('svg') : null;
this.sunIcon = this.darkModeToggle ? this.darkModeToggle.querySelector('.sun-icon') : null;
this.moonIcon = this.darkModeToggle ? this.darkModeToggle.querySelector('.moon-icon') : null;
this.html = document.documentElement;
// Local Storage Key
@ -51,6 +52,10 @@
this.updateDarkModeIcon(true);
this.darkModeToggle.setAttribute('aria-pressed', 'true');
this.darkModeToggle.setAttribute('title', 'Light Mode aktivieren');
if (this.sunIcon && this.moonIcon) {
this.sunIcon.classList.add('hidden');
this.moonIcon.classList.remove('hidden');
}
} else {
this.html.classList.remove('dark');
this.html.setAttribute('data-theme', 'light');
@ -58,6 +63,10 @@
this.updateDarkModeIcon(false);
this.darkModeToggle.setAttribute('aria-pressed', 'false');
this.darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
if (this.sunIcon && this.moonIcon) {
this.sunIcon.classList.remove('hidden');
this.moonIcon.classList.add('hidden');
}
}
// Einstellung im Local Storage speichern
@ -74,7 +83,7 @@
detail: { isDark: enable }
}));
console.log('Dark Mode set to:', enable);
console.log(`${enable ? '🌙' : '☀️'} Design umgeschaltet auf: ${enable ? 'Dark Mode' : 'Light Mode'}`);
}
/**
@ -82,18 +91,17 @@
* @param {boolean} isDark - Ob Dark Mode aktiv ist
*/
updateDarkModeIcon(isDark) {
if (!this.darkModeIcon) return;
if (!this.darkModeToggle) return;
if (isDark) {
// Sonne anzeigen (für Wechsel zum Light Mode)
this.darkModeIcon.innerHTML = `
<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" />
`;
} else {
// Mond anzeigen (für Wechsel zum Dark Mode)
this.darkModeIcon.innerHTML = `
<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" />
`;
// Stellen sicher, dass die Icons korrekt angezeigt werden
if (this.sunIcon && this.moonIcon) {
if (isDark) {
this.sunIcon.classList.add('hidden');
this.moonIcon.classList.remove('hidden');
} else {
this.sunIcon.classList.remove('hidden');
this.moonIcon.classList.add('hidden');
}
}
}
@ -102,15 +110,15 @@
*/
init() {
if (!this.darkModeToggle) {
console.error('Dark Mode Toggle Button nicht gefunden!');
console.error('⚠️ Dark Mode Toggle Button nicht gefunden! UI-Komponente nicht verfügbar.');
return;
}
console.log('Dark Mode Manager initialisiert');
console.log('🚀 Dark Mode Manager erfolgreich initialisiert');
// Event Listener für den Dark Mode Toggle Button
this.darkModeToggle.addEventListener('click', () => {
console.log('Dark Mode Toggle geklickt');
console.log('👆 Dark Mode Toggle Button angeklickt');
const newDarkModeState = !this.isDarkMode();
this.setDarkMode(newDarkModeState);
});
@ -118,7 +126,7 @@
// Alternative Event-Listener für Buttons mit data-action
document.addEventListener('click', (e) => {
if (e.target.closest('[data-action="toggle-dark-mode"]')) {
console.log('Dark Mode Toggle über data-action geklickt');
console.log('👆 Dark Mode Toggle über data-action aktiviert');
const newDarkModeState = !this.isDarkMode();
this.setDarkMode(newDarkModeState);
}
@ -144,11 +152,111 @@
// Initialisierung: Aktuellen Status setzen
const isDark = this.isDarkMode();
console.log('Initial Dark Mode Status:', isDark);
console.log(`🔍 Initialer Dark Mode Status: ${isDark ? '🌙 aktiviert' : '☀️ deaktiviert'}`);
this.setDarkMode(isDark);
}
}
/**
* Mobile Menu Manager
*/
class MobileMenuManager {
constructor() {
this.mobileMenuToggle = document.getElementById('mobileMenuToggle');
this.mobileMenu = document.getElementById('mobileMenu');
this.isOpen = false;
this.init();
}
init() {
if (!this.mobileMenuToggle || !this.mobileMenu) {
console.log(' Mobile Menu Komponenten nicht gefunden - Feature deaktiviert');
return;
}
console.log('📱 Mobile Menu Manager erfolgreich initialisiert');
// Event Listener für den Mobile Menu Toggle Button
this.mobileMenuToggle.addEventListener('click', () => {
this.toggleMenu();
});
// Event Listener für Klicks außerhalb des Menüs
document.addEventListener('click', (e) => {
if (this.isOpen && !this.mobileMenuToggle.contains(e.target) && !this.mobileMenu.contains(e.target)) {
this.closeMenu();
}
});
// Event Listener für das Schließen bei Klick auf einen Menüpunkt
this.mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
this.closeMenu();
});
});
// Beim Scrollen das Menü schließen
window.addEventListener('scroll', () => {
if (this.isOpen && window.scrollY > 50) {
this.closeMenu();
}
});
}
toggleMenu() {
if (this.isOpen) {
this.closeMenu();
} else {
this.openMenu();
}
}
openMenu() {
if (!this.mobileMenu || this.isOpen) return;
this.mobileMenu.classList.remove('hidden');
this.isOpen = true;
this.mobileMenuToggle.setAttribute('aria-expanded', 'true');
// Icon ändern
const menuIcon = this.mobileMenuToggle.querySelector('svg');
if (menuIcon) {
menuIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>';
}
// Animation - kleine Verzögerung für sanfte Animation
setTimeout(() => {
this.mobileMenu.classList.add('open');
}, 10);
// Body-Scroll verhindern (optional)
// document.body.style.overflow = 'hidden';
}
closeMenu() {
if (!this.mobileMenu || !this.isOpen) return;
this.mobileMenu.classList.remove('open');
this.isOpen = false;
this.mobileMenuToggle.setAttribute('aria-expanded', 'false');
// Icon zurücksetzen
const menuIcon = this.mobileMenuToggle.querySelector('svg');
if (menuIcon) {
menuIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>';
}
// Nach der Animation ausblenden
setTimeout(() => {
this.mobileMenu.classList.add('hidden');
}, 300);
// Body-Scroll wiederherstellen
// document.body.style.overflow = '';
}
}
/**
* User Dropdown Manager
*/
@ -992,6 +1100,9 @@
// Dark Mode Manager
window.MYP.UI.darkMode = new DarkModeManager();
// Mobile Menu Manager
window.MYP.UI.mobileMenu = new MobileMenuManager();
// User Dropdown Manager
window.MYP.UI.userDropdown = new UserDropdownManager();
@ -1025,7 +1136,7 @@
}
});
console.log('MYP UI Components initialized successfully');
console.log('✅ MYP UI Components erfolgreich initialisiert - Benutzeroberfläche bereit');
});
// Globale Variable für Toast-Funktion

View File

@ -63,6 +63,28 @@
setTimeout(function() {
document.documentElement.classList.remove('disable-transitions');
}, 10);
// Diese Funktion wird nach dem DOM-Laden ausgeführt
document.addEventListener('DOMContentLoaded', function() {
// Dark Mode Toggle Button Icons aktualisieren
const darkModeToggle = document.getElementById('darkModeToggle');
if (darkModeToggle) {
const sunIcon = darkModeToggle.querySelector('.sun-icon');
const moonIcon = darkModeToggle.querySelector('.moon-icon');
if (isDark) {
darkModeToggle.setAttribute('aria-pressed', 'true');
darkModeToggle.setAttribute('title', 'Light Mode aktivieren');
if (sunIcon) sunIcon.classList.add('hidden');
if (moonIcon) moonIcon.classList.remove('hidden');
} else {
darkModeToggle.setAttribute('aria-pressed', 'false');
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
if (sunIcon) sunIcon.classList.remove('hidden');
if (moonIcon) moonIcon.classList.add('hidden');
}
}
});
</script>
<!-- Disable Transitions Styling -->
@ -76,17 +98,17 @@
{% block head %}{% endblock %}
</head>
<body class="min-h-screen flex flex-col bg-white text-slate-900 dark:bg-[#050505] dark:text-slate-200 transition-colors duration-300 text-base mercedes-background">
<body class="min-h-screen flex flex-col bg-white text-slate-900 dark:bg-black dark:text-white transition-colors duration-300 text-base mercedes-background">
<!-- Navigation -->
<nav class="navbar">
<div class="max-w-7xl mx-auto px-2 sm:px-4 lg:px-6">
<div class="flex items-center justify-between h-14 sm:h-16 lg:h-20">
<div class="max-w-7xl mx-auto px-3 sm:px-4 lg:px-6">
<div class="flex items-center justify-between h-16 sm:h-18 lg:h-20">
<!-- Brand Section -->
<div class="flex-shrink-0">
<a href="{{ url_for('dashboard') }}" class="navbar-brand" aria-label="Zur Startseite">
<!-- Mercedes-Benz Logo -->
<div class="w-8 h-8 sm:w-10 sm:h-10 lg:w-12 lg:h-12 transition-all duration-300 group-hover:rotate-12">
<div class="w-9 h-9 sm:w-10 sm:h-10 lg:w-12 lg:h-12 transition-all duration-300 group-hover:rotate-12">
<svg class="w-full h-full text-slate-900 dark:text-white transition-colors duration-300" fill="currentColor" viewBox="0 0 80 80" aria-hidden="true">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
@ -104,11 +126,11 @@
</a>
</div>
<!-- Desktop Navigation Menu -->
<div class="hidden lg:flex flex-1 justify-center px-8">
<nav class="navbar-menu" role="navigation" aria-label="Hauptnavigation">
<!-- Desktop Navigation Menu - überarbeitetes Design -->
<div class="hidden lg:flex flex-1 justify-center">
<nav class="navbar-menu-new" role="navigation" aria-label="Hauptnavigation">
<a href="{{ url_for('dashboard') }}"
class="menu-item {{ 'text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-500' if request.endpoint == 'dashboard' else '' }}">
class="nav-item {{ 'active' if request.endpoint == 'dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"/>
</svg>
@ -116,15 +138,15 @@
</a>
<a href="{{ url_for('printers_page') }}"
class="menu-item {{ 'text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-500' if request.endpoint == 'printers_page' else '' }}">
<svg class="w-8 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
class="nav-item {{ 'active' if request.endpoint == 'printers_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
<span>3D-Drucker</span>
</a>
<a href="{{ url_for('jobs_page') }}"
class="menu-item {{ 'text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-500' if request.endpoint == 'jobs_page' else '' }}">
class="nav-item {{ 'active' if request.endpoint == 'jobs_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
@ -132,7 +154,7 @@
</a>
<a href="{{ url_for('stats_page') }}"
class="menu-item {{ 'text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-500' if request.endpoint == 'stats_page' else '' }}">
class="nav-item {{ 'active' if request.endpoint == 'stats_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
@ -141,7 +163,7 @@
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin_page') }}"
class="menu-item {{ 'text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-500' if request.endpoint == 'admin_page' else '' }}">
class="nav-item {{ 'active' if request.endpoint == 'admin_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
@ -152,47 +174,60 @@
</nav>
</div>
<!-- Right Side Controls -->
<!-- Rechte Seite der Navbar -->
<div class="flex items-center space-x-2 sm:space-x-3">
<!-- Dark Mode Toggle -->
<!-- Mobile Menu Toggle (neu) -->
<button
id="mobileMenuToggle"
class="lg:hidden p-2 rounded-full text-slate-700 dark:text-slate-300 hover:bg-slate-100/80 dark:hover:bg-slate-800/50"
aria-label="Menü öffnen"
aria-expanded="false"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<!-- Dark Mode Toggle - neues Design -->
<button
id="darkModeToggle"
class="dark-mode-toggle p-2 sm:p-3"
class="dark-mode-toggle-new"
aria-label="Dark Mode umschalten"
aria-pressed="false"
data-action="toggle-dark-mode"
title="Dark Mode aktivieren"
>
<svg class="w-6 h-6 sm:w-7 sm:h-7 transition-all duration-300 transform" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<svg class="w-5 h-5 sm:w-5 sm:h-5 sun-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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>
<svg class="w-5 h-5 sm:w-5 sm:h-5 moon-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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>
</button>
{% if current_user.is_authenticated %}
<!-- User Profile Dropdown -->
<!-- User Profile Dropdown - neues Design -->
<div class="relative" id="user-menu-container">
<button
id="user-menu-button"
class="flex items-center space-x-2 text-sm font-medium rounded-lg p-1.5 transition-all duration-300 hover:bg-blue-50 dark:hover:bg-blue-950"
class="user-menu-button-new"
aria-expanded="false"
aria-haspopup="true"
aria-label="Benutzermenu öffnen"
>
<!-- Profile Avatar -->
<div class="h-8 w-8 rounded-full flex items-center justify-center bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 font-medium transition-colors duration-300">
<div class="user-avatar-new">
{{ current_user.email[0].upper() if current_user.email else 'U' }}
</div>
<!-- User Info (hidden on mobile) -->
<div class="hidden md:block text-left ml-2">
<div class="text-sm font-medium text-slate-900 dark:text-white transition-colors duration-300">{{ current_user.email.split('@')[0] if current_user.email else 'Benutzer' }}</div>
<div class="text-sm font-semibold text-slate-900 dark:text-white transition-colors duration-300">{{ current_user.email.split('@')[0] if current_user.email else 'Benutzer' }}</div>
<div class="text-xs text-slate-600 dark:text-slate-400 transition-colors duration-300">Mercedes-Benz Mitarbeiter</div>
</div>
</button>
</div>
{% else %}
<!-- Login Button -->
<!-- Login Button - neues Design -->
<a href="{{ url_for('login') }}"
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 flex items-center">
class="login-button-new">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>
@ -203,6 +238,54 @@
</div>
</div>
</nav>
<!-- Mobile Menu (neu) -->
<div id="mobileMenu" class="mobile-menu-new hidden lg:hidden">
<nav class="flex flex-col space-y-1 px-3 py-4" role="navigation" aria-label="Mobile Navigation">
<a href="{{ url_for('dashboard') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"/>
</svg>
<span>Dashboard</span>
</a>
<a href="{{ url_for('printers_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'printers_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
<span>3D-Drucker</span>
</a>
<a href="{{ url_for('jobs_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'jobs_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
<span>Druckaufträge</span>
</a>
<a href="{{ url_for('stats_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'stats_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
<span>Statistiken</span>
</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'admin_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span>Administration</span>
</a>
{% endif %}
</nav>
</div>
<!-- Main Content -->
<main id="main-content" class="flex-grow max-w-7xl w-full mx-auto px-3 sm:px-6 lg:px-8 py-4 sm:py-8">
@ -288,6 +371,7 @@
<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/ui-components.js') }}"></script>
<script src="{{ url_for('static', filename='js/dark-mode-fix.js') }}"></script>
{% block scripts %}{% endblock %}
</body>

View File

@ -35,14 +35,23 @@
<option value="">Drucker auswählen...</option>
<!-- Wird durch JavaScript gefüllt -->
</select>
<div id="printer-status-info" class="mt-2 text-center">
<!-- Status-Info wird dynamisch gefüllt -->
</div>
<div id="printer-status-warning" class="mt-2 hidden">
<div class="flex items-center p-3 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg">
<svg class="w-5 h-5 text-orange-500 mr-2 flex-shrink-0" 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-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 class="text-sm">
<p class="font-medium text-orange-800 dark:text-orange-200">Offline-Drucker ausgewählt</p>
<p class="text-orange-700 dark:text-orange-300">Der Job wird erstellt, startet aber erst, wenn der Drucker online geht.</p>
<div class="flex items-center p-4 bg-red-50 dark:bg-red-900/20 border-2 border-red-400 dark:border-red-700 rounded-lg shadow-sm">
<div class="flex-shrink-0 w-10 h-10 mr-3 flex items-center justify-center rounded-full bg-red-100 dark:bg-red-800">
<svg class="w-6 h-6 text-red-600 dark:text-red-300" 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-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>
<div>
<p class="text-lg font-bold text-red-800 dark:text-red-200">ACHTUNG: Offline-Drucker ausgewählt!</p>
<p class="text-sm text-red-700 dark:text-red-300 mt-1">
Dieser Drucker ist derzeit <span class="font-bold">NICHT VERFÜGBAR</span>.
Der Job wird in der Warteschlange gespeichert und erst gestartet,
wenn der Drucker wieder online ist.
</p>
</div>
</div>
</div>
@ -213,20 +222,49 @@ document.addEventListener('DOMContentLoaded', function() {
loadPrinters();
loadActiveJobs();
// Event-Listener für Drucker-Auswahl (Offline-Warnung)
// Event-Listener für Drucker-Auswahl (Status-Anzeige)
const printerSelect = document.getElementById('printer_id');
const statusWarning = document.getElementById('printer-status-warning');
const statusInfo = document.getElementById('printer-status-info');
if (printerSelect && statusWarning) {
if (printerSelect && statusWarning && statusInfo) {
printerSelect.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
if (selectedOption && selectedOption.getAttribute('data-offline') === 'true') {
statusWarning.classList.remove('hidden');
} else {
if (!selectedOption || selectedOption.value === "") {
// Keine Auswahl
statusWarning.classList.add('hidden');
statusInfo.innerHTML = '';
return;
}
// Status des ausgewählten Druckers bestimmen
const isOffline = selectedOption.getAttribute('data-offline') === 'true';
const printerName = selectedOption.textContent.split('(')[0].trim().replace(/🟢|🔴/g, '').trim();
if (isOffline) {
// Offline-Drucker: Deutliche Warnung anzeigen
statusWarning.classList.remove('hidden');
statusInfo.innerHTML = `
<div class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 border border-red-200">
<span class="w-2 h-2 mr-1 bg-red-500 rounded-full"></span>
${printerName} ist OFFLINE
</div>
`;
} else {
// Online-Drucker: Positive Statusmeldung
statusWarning.classList.add('hidden');
statusInfo.innerHTML = `
<div class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 border border-green-200">
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full animate-pulse"></span>
${printerName} ist ONLINE und bereit
</div>
`;
}
});
// Initial auslösen, um den Status der Vorauswahl anzuzeigen
printerSelect.dispatchEvent(new Event('change'));
}
// Formulare initialisieren
@ -342,25 +380,50 @@ function populatePrinterSelect(printers, onlineOnly = false) {
// Bei gleichem Online-Status nach Name sortieren
return a.name.localeCompare(b.name);
});
// Zähler für Online- und Offline-Drucker
let onlineCount = 0;
let offlineCount = 0;
// Optionsgruppen für bessere visuelle Trennung erstellen
const onlineGroup = document.createElement('optgroup');
onlineGroup.label = "── ONLINE DRUCKER ──";
onlineGroup.style.fontWeight = 'bold';
onlineGroup.style.color = '#047857'; // Dunkelgrün
const offlineGroup = document.createElement('optgroup');
offlineGroup.label = "── OFFLINE DRUCKER ──";
offlineGroup.style.fontWeight = 'bold';
offlineGroup.style.color = '#b91c1c'; // Dunkelrot
// Drucker in entsprechende Gruppen einsortieren
sortedPrinters.forEach(printer => {
const option = document.createElement('option');
option.value = printer.id;
// Status-Indikator bestimmen
const isOnline = printer.status === 'available' || printer.is_online || printer.active;
let statusIcon, statusText;
let statusIcon, statusText, statusClass;
if (isOnline) {
statusIcon = '🟢';
statusText = 'Online';
option.style.color = '#059669'; // Grün für online
statusText = 'ONLINE';
statusClass = 'online';
option.style.backgroundColor = 'rgba(4, 120, 87, 0.1)'; // Leicht grüner Hintergrund
option.style.color = '#047857'; // Grün für online
option.style.fontWeight = '500';
onlineCount++;
} else {
statusIcon = '🔴';
statusText = 'Offline';
option.style.color = '#dc2626'; // Rot für offline
statusText = 'OFFLINE';
statusClass = 'offline';
option.style.backgroundColor = 'rgba(185, 28, 28, 0.05)'; // Sehr leicht roter Hintergrund
option.style.color = '#b91c1c'; // Rot für offline
option.style.fontStyle = 'italic';
option.style.fontWeight = '400';
// Offline-Drucker NICHT deaktivieren, aber kennzeichnen
option.setAttribute('data-offline', 'true');
offlineCount++;
}
// Letzter Check-Zeitstempel
@ -384,18 +447,52 @@ function populatePrinterSelect(printers, onlineOnly = false) {
// Tooltip für zusätzliche Informationen
option.title = `Standort: ${printer.location || 'Unbekannt'}\nIP: ${printer.plug_ip || printer.ip_address || 'Unbekannt'}\nStatus: ${statusText}${lastChecked}`;
printerSelect.appendChild(option);
// Zu entsprechender Gruppe hinzufügen
if (isOnline) {
onlineGroup.appendChild(option);
} else {
offlineGroup.appendChild(option);
}
});
// Hinweis für Offline-Drucker
const offlineCount = printers.filter(p => !(p.status === 'available' || p.is_online || p.active)).length;
// Status-Info als erste Option hinzufügen
const statusOption = document.createElement('option');
statusOption.disabled = true;
statusOption.className = 'status-summary';
statusOption.style.backgroundColor = '#f3f4f6';
statusOption.style.fontWeight = 'bold';
statusOption.style.textAlign = 'center';
statusOption.style.padding = '4px';
statusOption.style.marginBottom = '4px';
statusOption.style.borderBottom = '1px solid #d1d5db';
// Deutliche Status-Zusammenfassung
if (onlineCount > 0) {
statusOption.textContent = `✅ ${onlineCount} von ${printers.length} Drucker ONLINE`;
statusOption.style.color = '#047857'; // Grün
} else {
statusOption.textContent = `⚠️ ACHTUNG: Alle ${printers.length} Drucker OFFLINE`;
statusOption.style.color = '#b91c1c'; // Rot
}
printerSelect.appendChild(statusOption);
// Gruppen zum Select hinzufügen
if (onlineCount > 0) {
printerSelect.appendChild(onlineGroup);
}
if (offlineCount > 0) {
const infoOption = document.createElement('option');
infoOption.disabled = true;
infoOption.textContent = `--- ${offlineCount} Drucker offline (nicht verfügbar) ---`;
infoOption.style.fontStyle = 'italic';
infoOption.style.color = '#6b7280';
printerSelect.appendChild(infoOption);
printerSelect.appendChild(offlineGroup);
}
// Wenn online Drucker verfügbar, den ersten online Drucker vorauswählen
if (onlineCount > 0 && onlineGroup.firstChild) {
onlineGroup.firstChild.selected = true;
document.getElementById('printer-status-warning').classList.add('hidden');
} else if (offlineCount > 0 && offlineGroup.firstChild) {
// Sonst den ersten offline Drucker vorauswählen und Warnung anzeigen
offlineGroup.firstChild.selected = true;
document.getElementById('printer-status-warning').classList.remove('hidden');
}
}
@ -442,11 +539,22 @@ function loadPrintersWithLiveStatus() {
populatePrinterSelect(printers, false);
const onlineCount = printers.filter(p => p.status === 'available' || p.is_online || p.active).length;
// Verwende die vom Backend bereitgestellten Werte
const onlineCount = data.online_count || 0;
const totalCount = data.count || printers.length;
if (onlineCount > 0) {
showNotification(`${onlineCount} von ${printers.length} Drucker online (Live-Check)`, 'success');
if (onlineCount === totalCount) {
// Alle Drucker online
showNotification(`✅ OPTIMAL: Alle ${totalCount} Drucker sind ONLINE und BEREIT`, 'success');
} else {
// Einige Drucker online
const offlineCount = totalCount - onlineCount;
showNotification(`⚠️ ${onlineCount} von ${totalCount} Drucker ONLINE | ${offlineCount} Drucker OFFLINE`, 'success');
}
} else {
showNotification(`${printers.length} Drucker gefunden, aber alle offline`, 'warning');
// Kein Drucker online
showNotification(`❌ ACHTUNG: Alle ${totalCount} Drucker sind OFFLINE - Reservierte Jobs werden in Warteschlange gespeichert`, 'error');
}
})
.catch(error => {
@ -556,23 +664,29 @@ function initNewJobForm() {
// Prüfen, ob ein Offline-Drucker ausgewählt wurde
const selectedOption = document.querySelector(`#printer_id option[value="${printer_id}"]`);
if (selectedOption && selectedOption.getAttribute('data-offline') === 'true') {
const printerName = selectedOption.textContent.split(' (')[0].replace('🔴 ', '');
const printerName = selectedOption.textContent.split(' (')[0].replace(/🔴\s*/g, '').trim();
// Deutlichere Warnung mit einem ausführlicheren Dialog
const confirmOffline = confirm(
`⚠️ WARNUNG: Offline-Drucker ausgewählt!\n\n` +
`Der Drucker "${printerName}" ist derzeit OFFLINE.\n\n` +
`Wenn Sie fortfahren:\n` +
`• Der Job wird als "Wartend auf Drucker" markiert\n` +
`• Sie erhalten eine Benachrichtigung, wenn der Drucker online geht\n` +
`• Der Job startet automatisch, sobald der Drucker verfügbar ist\n\n` +
`Möchten Sie trotzdem fortfahren?`
`⛔ ACHTUNG: OFFLINE-DRUCKER AUSGEWÄHLT! ⛔\n` +
`---------------------------------------------\n\n` +
`Der Drucker "${printerName}" ist DERZEIT NICHT VERFÜGBAR!\n\n` +
`Wenn Sie trotzdem fortfahren:\n\n` +
`✓ Ihr Job wird in der WARTESCHLANGE gespeichert\n` +
`✓ Der System-Status wird regelmäßig überprüft\n` +
`✓ Sie erhalten eine BENACHRICHTIGUNG, sobald der Drucker online geht\n` +
`✓ Der Job startet AUTOMATISCH, wenn der Drucker verfügbar wird\n\n` +
`---------------------------------------------\n` +
`Möchten Sie TROTZDEM mit diesem Offline-Drucker fortfahren?`
);
if (!confirmOffline) {
showNotification('Job-Erstellung abgebrochen. Bitte wählen Sie einen Online-Drucker oder warten Sie, bis der gewünschte Drucker verfügbar ist.', 'info');
showNotification('Job-Erstellung abgebrochen - Bitte wählen Sie einen ONLINE-Drucker für sofortigen Start', 'info');
return;
}
showNotification(`Job für Offline-Drucker "${printerName}" wird erstellt. Sie werden benachrichtigt, wenn der Drucker online geht.`, 'warning');
// Spezielle Benachrichtigung für Offline-Drucker-Jobs
showNotification(`⏳ Job für OFFLINE-Drucker "${printerName}" wird in Warteschlange erstellt. Sie werden benachrichtigt, wenn der Drucker verfügbar wird.`, 'warning');
}
// Startzeit in ISO-Format konvertieren

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,12 @@
import logging
import logging.handlers
import os
from typing import Dict
import sys
import time
import platform
import socket
from typing import Dict, Optional, Any
from datetime import datetime
from config.settings import (
LOG_DIR, LOG_SUBDIRS, LOG_LEVEL, LOG_FORMAT, LOG_DATE_FORMAT,
get_log_file, ensure_log_directories
@ -10,25 +15,163 @@ from config.settings import (
# Dictionary zur Speicherung der konfigurierten Logger
_loggers: Dict[str, logging.Logger] = {}
def setup_logging():
"""Initialisiert das Logging-System und erstellt alle erforderlichen Verzeichnisse."""
# ANSI-Farbcodes für Log-Level
ANSI_COLORS = {
'RESET': '\033[0m',
'BOLD': '\033[1m',
'BLACK': '\033[30m',
'RED': '\033[31m',
'GREEN': '\033[32m',
'YELLOW': '\033[33m',
'BLUE': '\033[34m',
'MAGENTA': '\033[35m',
'CYAN': '\033[36m',
'WHITE': '\033[37m',
'BG_RED': '\033[41m',
'BG_GREEN': '\033[42m',
'BG_YELLOW': '\033[43m',
'BG_BLUE': '\033[44m'
}
# Emojis für verschiedene Log-Level und Kategorien
LOG_EMOJIS = {
'DEBUG': '🔍',
'INFO': '',
'WARNING': '⚠️',
'ERROR': '',
'CRITICAL': '🔥',
'app': '🖥️',
'scheduler': '⏱️',
'auth': '🔐',
'jobs': '🖨️',
'printers': '🔧',
'errors': '💥',
'user': '👤',
'kiosk': '📺'
}
# Prüfen, ob das Terminal ANSI-Farben unterstützt
def supports_color() -> bool:
"""Prüft, ob das Terminal ANSI-Farben unterstützt."""
if os.name == 'nt':
try:
import ctypes
kernel32 = ctypes.windll.kernel32
# Aktiviere VT100-Unterstützung unter Windows
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
return True
except:
return False
else:
return sys.stdout.isatty()
USE_COLORS = supports_color()
class ColoredFormatter(logging.Formatter):
"""Formatter, der Farben und Emojis für Logs hinzufügt."""
level_colors = {
'DEBUG': ANSI_COLORS['CYAN'],
'INFO': ANSI_COLORS['GREEN'],
'WARNING': ANSI_COLORS['YELLOW'],
'ERROR': ANSI_COLORS['RED'],
'CRITICAL': ANSI_COLORS['BG_RED'] + ANSI_COLORS['WHITE'] + ANSI_COLORS['BOLD']
}
def format(self, record):
# Basis-Format erstellen
log_fmt = LOG_FORMAT
date_fmt = LOG_DATE_FORMAT
# Emoji dem Level und der Kategorie hinzufügen
level_name = record.levelname
category_name = record.name.split('.')[-1] if '.' in record.name else record.name
level_emoji = LOG_EMOJIS.get(level_name, '')
category_emoji = LOG_EMOJIS.get(category_name, '')
# Record-Objekt modifizieren (aber temporär)
original_levelname = record.levelname
original_name = record.name
# Emojis hinzufügen
record.levelname = f"{level_emoji} {level_name}"
record.name = f"{category_emoji} {category_name}"
# Farbe hinzufügen wenn unterstützt
if USE_COLORS:
level_color = self.level_colors.get(original_levelname, ANSI_COLORS['RESET'])
record.levelname = f"{level_color}{record.levelname}{ANSI_COLORS['RESET']}"
record.name = f"{ANSI_COLORS['BOLD']}{record.name}{ANSI_COLORS['RESET']}"
# Formatieren
result = super().format(record)
# Originale Werte wiederherstellen
record.levelname = original_levelname
record.name = original_name
return result
class DebugInfoFilter(logging.Filter):
"""Filter, der Debug-Informationen zu jedem Log-Eintrag hinzufügt."""
def __init__(self, add_hostname=True, add_process_info=True):
super().__init__()
self.add_hostname = add_hostname
self.add_process_info = add_process_info
self.hostname = socket.gethostname() if add_hostname else None
self.pid = os.getpid() if add_process_info else None
def filter(self, record):
# Debug-Informationen hinzufügen
if self.add_hostname and not hasattr(record, 'hostname'):
record.hostname = self.hostname
if self.add_process_info and not hasattr(record, 'pid'):
record.pid = self.pid
# Zusätzliche Infos für DEBUG-Level
if record.levelno == logging.DEBUG:
# Funktionsname und Zeilennummer hervorheben
if USE_COLORS:
record.funcName = f"{ANSI_COLORS['CYAN']}{record.funcName}{ANSI_COLORS['RESET']}"
record.lineno = f"{ANSI_COLORS['CYAN']}{record.lineno}{ANSI_COLORS['RESET']}"
return True
def setup_logging(debug_mode: bool = False):
"""
Initialisiert das Logging-System und erstellt alle erforderlichen Verzeichnisse.
Args:
debug_mode: Wenn True, wird das Log-Level auf DEBUG gesetzt
"""
ensure_log_directories()
# Log-Level festlegen
log_level = logging.DEBUG if debug_mode else getattr(logging, LOG_LEVEL)
# Root-Logger konfigurieren
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, LOG_LEVEL))
root_logger.setLevel(log_level)
# Alle Handler entfernen
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# Formatter erstellen
formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT)
# Formatter erstellen (mit und ohne Farben)
colored_formatter = ColoredFormatter(LOG_FORMAT, LOG_DATE_FORMAT)
file_formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT)
# Filter für zusätzliche Debug-Informationen
debug_filter = DebugInfoFilter()
# Console Handler für alle Logs
console_handler = logging.StreamHandler()
console_handler.setLevel(getattr(logging, LOG_LEVEL))
console_handler.setFormatter(formatter)
console_handler.setLevel(log_level)
console_handler.setFormatter(colored_formatter)
console_handler.addFilter(debug_filter)
root_logger.addHandler(console_handler)
# File Handler für allgemeine App-Logs
@ -36,9 +179,13 @@ def setup_logging():
app_handler = logging.handlers.RotatingFileHandler(
app_log_file, maxBytes=10*1024*1024, backupCount=5
)
app_handler.setLevel(getattr(logging, LOG_LEVEL))
app_handler.setFormatter(formatter)
app_handler.setLevel(log_level)
app_handler.setFormatter(file_formatter)
root_logger.addHandler(app_handler)
# Wenn Debug-Modus aktiv, Konfiguration loggen
if debug_mode:
root_logger.debug(f"🐞 Debug-Modus aktiviert - Ausführliche Logs werden generiert")
def get_logger(category: str) -> logging.Logger:
"""
@ -60,13 +207,18 @@ def get_logger(category: str) -> logging.Logger:
# Verhindere doppelte Logs durch Parent-Logger
logger.propagate = False
# Formatter erstellen
formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT)
# Formatter erstellen (mit und ohne Farben)
colored_formatter = ColoredFormatter(LOG_FORMAT, LOG_DATE_FORMAT)
file_formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT)
# Filter für zusätzliche Debug-Informationen
debug_filter = DebugInfoFilter()
# Console Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(getattr(logging, LOG_LEVEL))
console_handler.setFormatter(formatter)
console_handler.setFormatter(colored_formatter)
console_handler.addFilter(debug_filter)
logger.addHandler(console_handler)
# File Handler für spezifische Kategorie
@ -75,7 +227,7 @@ def get_logger(category: str) -> logging.Logger:
log_file, maxBytes=10*1024*1024, backupCount=5
)
file_handler.setLevel(getattr(logging, LOG_LEVEL))
file_handler.setFormatter(formatter)
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
# Error-Logs zusätzlich in errors.log schreiben
@ -85,7 +237,7 @@ def get_logger(category: str) -> logging.Logger:
error_log_file, maxBytes=10*1024*1024, backupCount=5
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(formatter)
error_handler.setFormatter(file_formatter)
logger.addHandler(error_handler)
_loggers[category] = logger
@ -95,7 +247,100 @@ def log_startup_info():
"""Loggt Startup-Informationen."""
app_logger = get_logger("app")
app_logger.info("=" * 50)
app_logger.info("MYP (Manage Your Printers) wird gestartet...")
app_logger.info(f"Log-Verzeichnis: {LOG_DIR}")
app_logger.info(f"Log-Level: {LOG_LEVEL}")
app_logger.info("=" * 50)
app_logger.info("🚀 MYP (Manage Your Printers) wird gestartet...")
app_logger.info(f"📂 Log-Verzeichnis: {LOG_DIR}")
app_logger.info(f"📊 Log-Level: {LOG_LEVEL}")
app_logger.info(f"💻 Betriebssystem: {platform.system()} {platform.release()}")
app_logger.info(f"🌐 Hostname: {socket.gethostname()}")
app_logger.info(f"📅 Startzeit: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
app_logger.info("=" * 50)
# Hilfsfunktionen für das Debugging
def debug_request(logger: logging.Logger, request):
"""
Loggt detaillierte Informationen über eine HTTP-Anfrage.
Args:
logger: Logger-Instanz
request: Flask-Request-Objekt
"""
if logger.level > logging.DEBUG:
return
logger.debug(f"🌐 HTTP-Anfrage: {request.method} {request.path}")
logger.debug(f"📡 Remote-Adresse: {request.remote_addr}")
logger.debug(f"🧩 Inhaltstyp: {request.content_type}")
# Nur relevante Headers ausgeben
important_headers = ['User-Agent', 'Referer', 'X-Forwarded-For', 'Authorization']
headers = {k: v for k, v in request.headers.items() if k in important_headers}
if headers:
logger.debug(f"📋 Wichtige Headers: {headers}")
# Request-Parameter (max. 1000 Zeichen)
if request.args:
args_str = str(request.args)
if len(args_str) > 1000:
args_str = args_str[:997] + "..."
logger.debug(f"🔍 URL-Parameter: {args_str}")
def debug_response(logger: logging.Logger, response, duration_ms: float = None):
"""
Loggt detaillierte Informationen über eine HTTP-Antwort.
Args:
logger: Logger-Instanz
response: Flask-Response-Objekt
duration_ms: Verarbeitungsdauer in Millisekunden (optional)
"""
if logger.level > logging.DEBUG:
return
status_emoji = "" if response.status_code < 400 else ""
logger.debug(f"{status_emoji} HTTP-Antwort: {response.status_code}")
if duration_ms is not None:
logger.debug(f"⏱️ Verarbeitungsdauer: {duration_ms:.2f} ms")
content_length = response.content_length or 0
if content_length > 0:
size_str = f"{content_length / 1024:.1f} KB" if content_length > 1024 else f"{content_length} Bytes"
logger.debug(f"📦 Antwortgröße: {size_str}")
def measure_execution_time(func=None, logger=None, task_name=None):
"""
Dekorator, der die Ausführungszeit einer Funktion misst und loggt.
Args:
func: Die zu dekorierende Funktion
logger: Logger-Instanz (optional)
task_name: Name der Aufgabe für das Logging (optional)
Returns:
Dekorierte Funktion
"""
from functools import wraps
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
start_time = time.time()
result = f(*args, **kwargs)
end_time = time.time()
duration_ms = (end_time - start_time) * 1000
name = task_name or f.__name__
if logger:
if duration_ms > 1000: # Länger als 1 Sekunde
logger.warning(f"⏱️ Langsame Ausführung: {name} - {duration_ms:.2f} ms")
else:
logger.debug(f"⏱️ Ausführungszeit: {name} - {duration_ms:.2f} ms")
return result
return wrapper
if func:
return decorator(func)
return decorator