"Feature: Update admin interface design and layout"

This commit is contained in:
Till Tomczak 2025-05-29 17:36:35 +02:00
parent f29fda7075
commit 1f2405d56b
3 changed files with 908 additions and 0 deletions

View File

@ -4298,6 +4298,26 @@ if __name__ == "__main__":
atexit.register(cleanup_queue_manager)
# Robuste Drucker-Initialisierung nach Queue-Manager-Start
app_logger.info("🔄 Starte robuste Drucker-Initialisierung beim Systemstart...")
try:
# In separatem Thread ausführen, um Startzeit nicht zu blockieren
import threading
def startup_printer_init():
try:
force_load_all_printers()
app_logger.info("✅ Startup-Drucker-Initialisierung abgeschlossen")
except Exception as e:
app_logger.error(f"❌ Fehler bei Startup-Drucker-Initialisierung: {str(e)}")
init_thread = threading.Thread(target=startup_printer_init, daemon=True, name="PrinterStartupInit")
init_thread.start()
app_logger.info("🚀 Drucker-Initialisierung im Hintergrund gestartet")
except Exception as e:
app_logger.error(f"❌ Kritischer Fehler beim Starten der Drucker-Initialisierung: {str(e)}")
# System trotzdem starten, auch wenn Drucker-Init fehlschlägt
except Exception as e:
app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}")
else:
@ -4377,3 +4397,184 @@ if __name__ == "__main__":
except:
pass
sys.exit(1)
def force_load_all_printers():
"""
Lädt und initialisiert alle Drucker um jeden Preis - mit Fallback-Strategien.
Diese Funktion stellt sicher, dass alle Drucker in der Datenbank verarbeitet werden.
"""
printers_logger.info("🔄 Starte robuste Drucker-Initialisierung...")
db_session = get_db_session()
try:
# Alle Drucker aus der Datenbank laden
printers = db_session.query(Printer).all()
printers_logger.info(f"📊 {len(printers)} Drucker in der Datenbank gefunden")
if not printers:
printers_logger.warning("⚠️ Keine Drucker in der Datenbank gefunden!")
return
successful_updates = 0
failed_updates = 0
for printer in printers:
try:
# Status-Update mit Fallback-Strategie
if printer.plug_ip:
printers_logger.debug(f"🔍 Prüfe Drucker {printer.name} ({printer.plug_ip})")
try:
# Hauptstatus-Check mit Timeout
status, active = check_printer_status(printer.plug_ip, timeout=5)
# Status in Datenbank aktualisieren
old_status = printer.status
printer.status = "available" if status == "online" else "offline"
printer.active = active
printer.last_checked = datetime.now()
if old_status != printer.status:
printers_logger.info(f"✅ Drucker {printer.name}: {old_status}{printer.status}")
else:
printers_logger.debug(f"📍 Drucker {printer.name}: Status unverändert ({printer.status})")
successful_updates += 1
except Exception as status_error:
# Fallback: Drucker als offline markieren, aber trotzdem verarbeiten
printers_logger.warning(f"⚠️ Status-Check für {printer.name} fehlgeschlagen: {str(status_error)}")
printer.status = "offline"
printer.active = False
printer.last_checked = datetime.now()
failed_updates += 1
else:
# Drucker ohne IP-Adresse als offline markieren
printers_logger.warning(f"⚠️ Drucker {printer.name} hat keine IP-Adresse - als offline markiert")
printer.status = "offline"
printer.active = False
printer.last_checked = datetime.now()
failed_updates += 1
except Exception as printer_error:
# Selbst bei schwerwiegenden Fehlern: Drucker nicht ignorieren
printers_logger.error(f"❌ Schwerer Fehler bei Drucker {printer.name}: {str(printer_error)}")
try:
# Minimale Fallback-Initialisierung
printer.status = "offline"
printer.active = False
printer.last_checked = datetime.now()
failed_updates += 1
except Exception as fallback_error:
printers_logger.critical(f"💥 Kritischer Fehler bei Fallback für {printer.name}: {str(fallback_error)}")
# Änderungen in Datenbank speichern
try:
db_session.commit()
printers_logger.info(f"✅ Drucker-Status erfolgreich gespeichert")
except Exception as commit_error:
printers_logger.error(f"❌ Fehler beim Speichern der Drucker-Status: {str(commit_error)}")
db_session.rollback()
# Zusammenfassung
total_printers = len(printers)
online_printers = len([p for p in printers if p.status in ["available", "online"]])
printers_logger.info(f"📈 Drucker-Initialisierung abgeschlossen:")
printers_logger.info(f" Gesamt: {total_printers}")
printers_logger.info(f" Online: {online_printers}")
printers_logger.info(f" Offline: {total_printers - online_printers}")
printers_logger.info(f" Erfolgreich: {successful_updates}")
printers_logger.info(f" Fehlgeschlagen: {failed_updates}")
except Exception as global_error:
printers_logger.critical(f"💥 Kritischer Fehler bei Drucker-Initialisierung: {str(global_error)}")
# Trotzdem versuchen, die Session zu schließen
finally:
try:
db_session.close()
except:
pass
@app.route('/api/admin/printers/force-initialize', methods=['POST'])
@admin_required
def force_initialize_printers():
"""Startet eine robuste Drucker-Initialisierung manuell"""
try:
app_logger.info("🚀 Admin-initiierte Drucker-Initialisierung gestartet")
# Robuste Initialisierung in separatem Thread starten
import threading
def run_initialization():
try:
force_load_all_printers()
except Exception as e:
printers_logger.error(f"❌ Fehler bei Thread-initialisierter Drucker-Initialisierung: {str(e)}")
init_thread = threading.Thread(target=run_initialization, daemon=True)
init_thread.start()
return jsonify({
"success": True,
"message": "Drucker-Initialisierung gestartet",
"info": "Die Initialisierung läuft im Hintergrund. Prüfen Sie die Logs für Details."
})
except Exception as e:
app_logger.error(f"❌ Fehler beim Starten der Drucker-Initialisierung: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Starten der Initialisierung: {str(e)}"
}), 500
@app.route('/api/admin/printers/update-all', methods=['POST'])
@admin_required
def update_all_printers():
"""Aktualisiert den Status aller Drucker"""
try:
db_session = get_db_session()
printers = db_session.query(Printer).all()
updated_printers = []
for printer in printers:
if printer.plug_ip:
try:
status, active = check_printer_status(printer.plug_ip)
old_status = printer.status
printer.update_status(status, active)
updated_printers.append({
"id": printer.id,
"name": printer.name,
"old_status": old_status,
"new_status": status,
"active": active
})
except Exception as e:
printers_logger.warning(f"Fehler beim Aktualisieren von Drucker {printer.name}: {str(e)}")
db_session.commit()
db_session.close()
app_logger.info(f"Status von {len(updated_printers)} Druckern aktualisiert")
return jsonify({
"success": True,
"message": f"Status von {len(updated_printers)} Druckern aktualisiert",
"updated_printers": updated_printers
})
except Exception as e:
app_logger.error(f"Fehler beim Aktualisieren aller Drucker: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Aktualisieren: {str(e)}"
}), 500

View File

@ -601,4 +601,703 @@
--shadow-light: rgba(0, 0, 0, 0.2);
--shadow-dark: rgba(0, 0, 0, 0.5);
}
}
/* ===== MERCEDES-BENZ MYP PLATFORM - ENHANCED UI COMPONENTS ===== */
/* CSS Custom Properties für erweiterte Designkontrolle */
:root {
/* Animation Timing */
--transition-fast: 0.15s ease-out;
--transition-medium: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 0.5s cubic-bezier(0.4, 0, 0.2, 1);
/* Spacing System */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--space-2xl: 3rem;
/* Border Radius System */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-2xl: 1.5rem;
/* Shadow System */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
/* Glassmorphism */
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-backdrop: blur(20px) saturate(180%) brightness(120%);
}
.dark {
--glass-bg: rgba(0, 0, 0, 0.1);
--glass-border: rgba(255, 255, 255, 0.05);
}
/* ===== ERWEITERTE BUTTON-SYSTEME ===== */
.btn {
@apply inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed;
border-radius: var(--radius-lg);
position: relative;
overflow: hidden;
transform: translateZ(0); /* Hardware acceleration */
}
.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 var(--transition-medium);
z-index: 1;
}
.btn:hover::before {
left: 100%;
}
.btn-primary {
@apply bg-black text-white hover:bg-gray-800 focus:ring-black dark:bg-white dark:text-black dark:hover:bg-gray-100 dark:focus:ring-white;
box-shadow: var(--shadow-md);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.btn-secondary {
@apply bg-white text-gray-900 border border-gray-300 hover:bg-gray-50 focus:ring-gray-500 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700;
box-shadow: var(--shadow-sm);
}
.btn-success {
@apply bg-green-600 text-white hover:bg-green-700 focus:ring-green-500;
box-shadow: 0 4px 14px 0 rgba(34, 197, 94, 0.39);
}
.btn-success:hover {
transform: translateY(-1px);
box-shadow: 0 8px 25px 0 rgba(34, 197, 94, 0.5);
}
.btn-danger {
@apply bg-red-600 text-white hover:bg-red-700 focus:ring-red-500;
box-shadow: 0 4px 14px 0 rgba(239, 68, 68, 0.39);
}
.btn-warning {
@apply bg-amber-500 text-white hover:bg-amber-600 focus:ring-amber-500;
box-shadow: 0 4px 14px 0 rgba(245, 158, 11, 0.39);
}
.btn-glass {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
backdrop-filter: var(--glass-backdrop);
-webkit-backdrop-filter: var(--glass-backdrop);
@apply text-gray-900 dark:text-white;
box-shadow: var(--shadow-lg);
}
.btn-floating {
@apply w-14 h-14 rounded-full shadow-xl bg-black text-white dark:bg-white dark:text-black;
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 50;
transition: all var(--transition-medium);
}
.btn-floating:hover {
transform: scale(1.1) translateY(-2px);
box-shadow: var(--shadow-2xl);
}
/* Button Loading State */
.btn-loading {
@apply cursor-not-allowed;
position: relative;
}
.btn-loading::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
margin: auto;
border: 2px solid currentColor;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
}
/* ===== ERWEITERTE CARD-SYSTEME ===== */
.card {
@apply bg-white dark:bg-gray-900 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 transition-all duration-300;
position: relative;
overflow: hidden;
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-xl);
}
.card-glass {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
backdrop-filter: var(--glass-backdrop);
-webkit-backdrop-filter: var(--glass-backdrop);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
}
.card-gradient {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
@apply dark:from-gray-900 dark:to-gray-800;
}
.card-elevated {
box-shadow:
0 20px 40px -12px rgba(0, 0, 0, 0.1),
0 8px 16px -4px rgba(0, 0, 0, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.card-interactive {
@apply cursor-pointer transition-all duration-300;
transform: translateZ(0);
}
.card-interactive:hover {
transform: translateY(-4px) scale(1.02);
box-shadow: var(--shadow-2xl);
}
.card-interactive:active {
transform: translateY(-2px) scale(1.01);
}
/* ===== DASHBOARD-KOMPONENTEN ===== */
.dashboard-card {
@apply card p-6;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.dark .dashboard-card {
background: rgba(30, 41, 59, 0.8);
border-color: rgba(71, 85, 105, 0.3);
}
.stats-card {
@apply dashboard-card text-center;
position: relative;
overflow: hidden;
}
.stats-card::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: conic-gradient(from 0deg at 50% 50%, transparent 0deg, rgba(0, 115, 206, 0.1) 60deg, transparent 120deg);
animation: rotate 8s linear infinite;
z-index: -1;
}
.stats-number {
@apply text-4xl font-bold mb-2;
background: linear-gradient(135deg, #000000 0%, #374151 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: pulse 2s ease-in-out infinite;
}
.dark .stats-number {
background: linear-gradient(135deg, #ffffff 0%, #e5e7eb 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ===== FORMULARE ===== */
.form-group {
@apply mb-6;
}
.form-label {
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2;
display: flex;
align-items: center;
gap: var(--space-sm);
}
.form-input {
@apply w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-black dark:focus:ring-white focus:border-transparent transition-all duration-300;
box-shadow: var(--shadow-sm);
}
.form-input:focus {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.form-input-error {
@apply border-red-500 focus:ring-red-500;
}
.form-select {
@apply form-input 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;
}
.form-textarea {
@apply form-input resize-none;
min-height: 100px;
}
.form-checkbox {
@apply w-4 h-4 text-black bg-white border-gray-300 rounded focus:ring-black dark:focus:ring-white dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600;
}
/* ===== STATUS-BADGES ===== */
.status-badge {
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-medium;
position: relative;
overflow: hidden;
animation: fadeInScale 0.3s ease-out;
}
.status-badge::before {
content: '';
position: absolute;
top: 50%;
left: 8px;
width: 6px;
height: 6px;
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}
.status-online {
@apply bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400;
}
.status-online::before {
background-color: #22c55e;
}
.status-offline {
@apply bg-gray-100 text-gray-800 dark:bg-gray-800/30 dark:text-gray-400;
}
.status-offline::before {
background-color: #6b7280;
}
.status-printing {
@apply bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400;
}
.status-printing::before {
background-color: #3b82f6;
animation: pulse 1s ease-in-out infinite;
}
.status-warning {
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400;
}
.status-warning::before {
background-color: #eab308;
animation: pulse 1.5s ease-in-out infinite;
}
.status-error {
@apply bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400;
}
.status-error::before {
background-color: #ef4444;
animation: pulse 1s ease-in-out infinite;
}
/* ===== ERWEITERTE TABELLEN ===== */
.table-container {
@apply overflow-hidden rounded-xl border border-gray-200 dark:border-gray-700;
box-shadow: var(--shadow-lg);
}
.table {
@apply w-full divide-y divide-gray-200 dark:divide-gray-700;
}
.table-header {
@apply bg-gray-50 dark:bg-gray-800;
}
.table-header-cell {
@apply px-6 py-4 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider;
position: sticky;
top: 0;
z-index: 10;
backdrop-filter: blur(10px);
}
.table-row {
@apply bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors duration-200;
}
.table-cell {
@apply px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100;
}
.table-row-interactive {
@apply cursor-pointer;
transition: all var(--transition-medium);
}
.table-row-interactive:hover {
transform: translateX(4px);
box-shadow: inset 4px 0 0 0 #000000;
}
.dark .table-row-interactive:hover {
box-shadow: inset 4px 0 0 0 #ffffff;
}
/* ===== NAVIGATION ===== */
.nav-item {
@apply relative flex items-center px-4 py-2 text-sm font-medium rounded-lg transition-all duration-300;
position: relative;
overflow: hidden;
}
.nav-item::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1), transparent);
transition: left var(--transition-medium);
}
.nav-item:hover::before {
left: 100%;
}
.nav-item-active {
@apply bg-black text-white dark:bg-white dark:text-black;
box-shadow: var(--shadow-lg);
}
.nav-item-inactive {
@apply text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800;
}
/* ===== MODALS ===== */
.modal-overlay {
@apply fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm z-50;
animation: fadeIn 0.3s ease-out;
}
.modal-container {
@apply flex min-h-full items-center justify-center p-4;
}
.modal-content {
@apply bg-white dark:bg-gray-900 rounded-2xl shadow-2xl max-w-lg w-full p-6;
animation: slideInScale 0.3s ease-out;
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.modal-header {
@apply flex items-center justify-between mb-4;
}
.modal-title {
@apply text-lg font-semibold text-gray-900 dark:text-white;
}
.modal-close {
@apply text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors;
}
/* ===== TOOLTIPS ===== */
.tooltip {
@apply absolute z-50 px-3 py-2 text-sm text-white bg-black rounded-lg shadow-lg opacity-0 pointer-events-none transition-all duration-300;
backdrop-filter: blur(10px);
}
.tooltip.show {
@apply opacity-100 pointer-events-auto;
}
.tooltip-arrow {
@apply absolute w-2 h-2 bg-black transform rotate-45;
}
/* ===== NOTIFICATIONS/TOAST ===== */
.notification {
@apply fixed top-4 right-4 max-w-sm w-full p-4 rounded-xl shadow-xl transform transition-all duration-300 z-50;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
backdrop-filter: var(--glass-backdrop);
-webkit-backdrop-filter: var(--glass-backdrop);
animation: slideInRight 0.3s ease-out;
}
.notification-success {
@apply border-green-500 text-green-800 dark:text-green-200;
background: rgba(34, 197, 94, 0.1);
}
.notification-error {
@apply border-red-500 text-red-800 dark:text-red-200;
background: rgba(239, 68, 68, 0.1);
}
.notification-info {
@apply border-blue-500 text-blue-800 dark:text-blue-200;
background: rgba(59, 130, 246, 0.1);
}
.notification-warning {
@apply border-yellow-500 text-yellow-800 dark:text-yellow-200;
background: rgba(245, 158, 11, 0.1);
}
/* ===== LOADING STATES ===== */
.loading-skeleton {
@apply bg-gray-200 dark:bg-gray-700 rounded animate-pulse;
}
.loading-spinner {
@apply w-8 h-8 border-4 border-gray-200 border-t-black rounded-full animate-spin;
}
.dark .loading-spinner {
@apply border-gray-700 border-t-white;
}
.loading-overlay {
@apply fixed inset-0 bg-white bg-opacity-80 dark:bg-black dark:bg-opacity-80 flex items-center justify-center z-50;
backdrop-filter: blur(5px);
}
/* ===== PROGRESS BARS ===== */
.progress-container {
@apply w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden;
}
.progress-bar {
@apply h-full bg-black dark:bg-white rounded-full transition-all duration-300;
background: linear-gradient(90deg, #000000, #374151, #000000);
background-size: 200% 100%;
animation: shimmer 2s ease-in-out infinite;
}
.progress-bar-success {
@apply bg-green-500;
}
.progress-bar-warning {
@apply bg-yellow-500;
}
.progress-bar-error {
@apply bg-red-500;
}
/* ===== MICRO-INTERACTIONS ===== */
.hover-lift {
transition: transform var(--transition-medium);
}
.hover-lift:hover {
transform: translateY(-2px);
}
.hover-scale {
transition: transform var(--transition-medium);
}
.hover-scale:hover {
transform: scale(1.05);
}
.hover-glow {
transition: box-shadow var(--transition-medium);
}
.hover-glow:hover {
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.dark .hover-glow:hover {
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
}
.click-shrink {
transition: transform var(--transition-fast);
}
.click-shrink:active {
transform: scale(0.95);
}
/* ===== ANIMATIONEN ===== */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideInScale {
from {
opacity: 0;
transform: scale(0.9) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.1);
}
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* ===== RESPONSIVE DESIGN ===== */
@media (max-width: 640px) {
.card {
@apply mx-4 rounded-lg;
}
.modal-content {
@apply mx-4 p-4;
}
.btn {
@apply w-full justify-center;
}
.stats-number {
@apply text-2xl;
}
}
/* ===== ACCESSIBILITY ===== */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
.focus-visible {
@apply outline-none ring-2 ring-offset-2 ring-black dark:ring-white;
}
/* ===== PRINT STYLES ===== */
@media print {
.no-print {
display: none !important;
}
.card {
box-shadow: none !important;
border: 1px solid #000 !important;
}
}

View File

@ -237,6 +237,14 @@
Druckaufträge
</a>
<a href="{{ url_for('admin_guest_requests') }}"
class="group flex items-center px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300 {{ 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg' if active_tab == 'guest_requests' else 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:text-slate-900 dark:hover:text-white' }}">
<svg class="w-5 h-5 mr-2 {{ 'text-white' if active_tab == 'guest_requests' else 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
Gastaufträge
</a>
<a href="{{ url_for('admin_page', tab='system') }}"
class="group flex items-center px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300 {{ 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg' if active_tab == 'system' else 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:text-slate-900 dark:hover:text-white' }}">
<svg class="w-5 h-5 mr-2 {{ 'text-white' if active_tab == 'system' else 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">